デザインパターンはJavaなどのオブジェクト指向言語で使われる設計パターンのことです。特に有名なGoFのパターンでは、数々のエンジニアが工夫を重ねてきた設計が23種類にパターン分けされまとめられています。
レビューの際に読みづらいといつも指摘される
保守、改修、機能拡張に手間と時間ばかりかかる
などでお困りでしたら、この記事をぜひご覧になってください。
- デザインパターンとは
- デザインパターン一覧
- 入門者向けオススメの書籍
デザインパターンの基本からおすすめな情報についても解説していきます。
なお、Javaの記事については、こちらにまとめています。
デザインパターンとは
デザインパターンはJava、C++、C#などのオブジェクト指向言語でよく使われる設計をパターン化したものです。1995年に発刊された「オブジェクト指向における再利用のためのデザインパターン」という本で広く知られるようになりました。
この本は4人の著者がよく使われる設計を23パターンに分けて説明しているので、「GoFの23パターン」と呼ばれ特に有名です。23種のデザインパターンを取り入れることは、主に以下の3つのメリットがあります。
- 設計が効率化できる
- 数々のエンジニアが試行錯誤してきた結果を少ない時間で利用、体得できる
- 再利用しやすいコードを書ける
- コードが読みやすくなり、修正したり拡張したりする際のメンテナンス性が高まる
- エンジニア同士の共通言語として使える
デザインパターンを取り入れたコードは他のエンジニアにとっても読みやすく、意思の疎通が図りやすい。
この記事では「GoFの23パターン」を「オブジェクトの生成」「プログラムの構造」「オブジェクトの振る舞い」の3つに分けて、それぞれの特長を説明します。また23種類すべてを解説するには紙面が足りませんので、代表的なパターンのサンプルコードをご紹介します。
なお、サンプルコードについてはJavaで記述しています。Javaにおけるオブジェクト指向については、こちらで詳しく解説しています。
ぜひ参考にしてください。
デザインパターン一覧
23種のデザインパターンを「オブジェクトの生成」「プログラムの構成」「オブジェクトの振る舞い」に分けて、それぞれの特長を説明します。
オブジェクトの生成に関するパターン
まずはオブジェクトの生成に関する5パターンについて説明します。
パターン名 | 説明 |
---|---|
Abstract Factory | 関連する部品(オブジェクト群)をまとめて一つのシステムを生成するための手順を抽象化 |
Builder | 元となる専用のクラスを用意し、複数のオブジェクトを生成 |
Factory Method | インスタンス生成のための枠組みと、インスタンス生成のクラスを分離 クラスを増やし、機能を拡張する際に使用 |
Prototype | インスタンスをコピーして新しいインスタンスを作成(クローンの作成) 同じクラスで複数のオブジェクトを生成する際に作業を効率化 |
Singleton | 1つのクラスから1つのインスタンスだけを生成するように制限 |
それでは、オブジェクトの生成に関するパターンの中で代表的なFactory Methodパターンについてサンプルコードでみていきましょう。
// 抽象的な枠組み(製品) abstract class Product { public abstract void method(); } // 抽象的な枠組み(製品の作成) abstract class Creator { protected abstract Product factoryMethod(String str); public final Product create(String str) { Product p = factoryMethod(str); return p; } } // 具体的な機能拡張(製品) class ConcreteProduct extends Product { private String str; public ConcreteProduct(String str) { this.str = str; } //@Override public void method() { System.out.println("Hello " + str + "!"); } } // 具体的な機能拡張(製品の作成) class ConcreteCreator extends Creator { //@Override protected Product factoryMethod(String str) { return new ConcreteProduct(str); } } public class User { public static void main(String[] args) { // 製品の作成者を生成 Creator creator = new ConcreteCreator(); // 製品の作成 Product java = creator.create("Java"); Product cpp = creator.create("C++"); Product cs = creator.create("C#"); // 処理の呼び出し java.method(); cpp.method(); cs.method(); } }
実行結果:
Hello Java! Hello C++! Hello C#!
このサンプルプログラムでは、製品とその製品を作成する抽象的な枠組みをProductクラス、Creatorクラスとしています。そして、具体的な機能の定義をConcreteProductクラス、ConcreteCreatorクラスで行っています。
Userクラスのmainメソッドで利用しているのは、抽象的な枠組みを表現したCreatorクラスとProductクラスのみです。mainメソッドから具体的な機能を定義しているConcreteProductクラス、ConcreteCreatorクラスは使用していません。
このようにFactory Methodパターンではクラスの宣言とインスタンスの生成が分離され、さらに枠組みと機能の実装が分離され、メンテナンスしやすい構成になっていることが特長になります。
プログラムの構造に関するパターン
続いてプログラムの構成に関する7パターンについて説明します。
パターン名 | 説明 |
---|---|
Adaptor | 本来関連のないクラス同士を関連付ける 「すでに提供されているもの」と「必要なもの」との間のズレを埋める |
Bridge | 機能を拡張するクラスと実装するクラスを分離し、その間の橋渡しを行う 継承元のスーパークラスの抽象メソッドを適切に実装したり、サブクラスで機能を拡張する際に利用 |
Composite | フォルダ内のサブフォルダとファイルの関係のように再帰的な構造を作る際に利用 |
Decorator | 機能を次々に追加する |
Facade | 複数のクラスを適切に制御するための「窓口」役のクラスを作成し、その「窓口」から各クラスへ指令を出す |
Flyweight | 小さなオブジェクトを数多くロードする必要がある場合にインスタンスを可能な限り共有させて、無駄にインスタンスを作成せずに効率的に処理を行う |
Proxy | 「代理」に処理を肩代わりさせ、重い処理は必要な時点まで遅らせることが可能 |
それでは、プログラムの構造に関するパターンの中で代表的なDecoratorパターンについてサンプルコードでみていきましょう。
// 機能を追加するときの核 abstract class Component { public abstract int getColumns(); public abstract int getRows(); public abstract String getRowText(int row); public final void show() { for(int i = 0; i < getRows(); i++) { System.out.println(getRowText(i)); } } } // Componentの実装 class ConcreteComponent extends Component { private String string; public ConcreteComponent(String string) { this.string = string; } //@Override public int getColumns() { return string.getBytes().length; } //@Override public int getRows() { return 1; } //@Override public String getRowText(int row) { if(row == 0) { return string; } else { return null; } } } // 装飾者 abstract class Decorator extends Component { protected Component component; protected Decorator(Component component) { this.component = component; } } // 具体的な装飾者 class ConcreteSideDecorator extends Decorator { private char borderChar; public ConcreteSideDecorator(Component component, char ch) { super(component); this.borderChar = ch; } //@Override public int getColumns() { return 1 + component.getColumns() + 1; } //@Override public int getRows() { return component.getRows(); } //@Override public String getRowText(int row) { return borderChar + component.getRowText(row) + borderChar; } } // 具体的な装飾者 class ConcreteFullDecorator extends Decorator { public ConcreteFullDecorator(Component component) { super(component); } //@Override public int getColumns() { return 1 + component.getColumns() + 1; } //@Override public int getRows() { return 1 + component.getRows() + 1; } //@Override public String getRowText(int row) { if(row == 0 || row == component.getRows() + 1) { return "+" + makeLine('-', component.getColumns()) + "+"; } else { return "|" + component.getRowText(row - 1) + "|"; } } private String makeLine(char ch, int count) { StringBuffer buf = new StringBuffer(); for(int i = 0; i < count; i++) { buf.append(ch); } return buf.toString(); } } public class User { public static void main(String[] args) { // 核の作成と装飾の追加 Component comp1 = new ConcreteComponent("Hello Java!"); Component comp2 = new ConcreteSideDecorator(comp1, '#'); Component comp3 = new ConcreteFullDecorator(comp2); // 処理の呼び出し comp1.show(); comp2.show(); comp3.show(); } }
実行結果:
Hello Java! #Hello Java!# +-------------+ |#Hello Java!#| +-------------+
このサンプルコードでは、まず機能を追加するときの核となるComponentクラスを宣言しています。このComponentクラスをConcreteComponentクラスで実装しています。
そして、装飾を加えて機能を追加するDecoratorクラスを宣言しています。このDecoratorクラスをConcreteSideDecoratorとConcreteFullDecoratorで実装しています。
UserクラスのmainメソッドではConcreteComponentクラス、ConcreteSideDecoratorクラス、ConcreteFullDecoratorクラスのインスタンスを順に生成しています。
実行結果を確認すると、順に装飾が追加されていることが分かります。
オブジェクトの振る舞いに関するパターン
最後にオブジェクトの振る舞いに関する11パターンについて説明します。
パターン名 | 説明 |
---|---|
Chain of Responsibility | あるクラスのオブジェクトが処理可能なら処理を行い、処理不可の場合は他のクラスのオブジェクトに送って処理を実行 |
Command | 「マウスをクリック」「キーを押す」のような命令をインスタンスという「モノ」で管理 |
Interpreter | プログラムをミニ言語に分け、そのミニ言語を「通訳」するプログラムを作成 変更が必要な場合はミニ言語を書き換える |
Iterator | 複数のオブジェクトを順番に指し示し、全体をスキャンしていく処理を行う |
Mediator | 「相談役」が複雑なオブジェクトの状態を把握し、適切な判断と支持を行う |
Memento | インスタンスの状態を表す役割を設け、インスタンスの状態の保存と復元を行う |
Observer | if文を利用することなく、状態変化に対応した処理を実行 |
State | 状態をクラスとして表現し、クラスを切り替えることで状態の変化を表す |
Strategy | アルゴリズムの実装部分が交換可能で、変更が容易 |
Template Method | スーパークラスで処理の枠組みを定め、サブクラスでその具体的な内容を定義する |
Visitor | データ構造と処理を分離 データ構造を渡り歩く「訪問者」を表すクラスを用意し、そのクラスが処理を実施 新しい処理を追加したい場合は新しい「訪問者」を作成 データ構造側は必要に応じて「訪問者」を受け入れる |
それでは、プログラムの構造に関するパターンの中で代表的なTemplate Methodパターンについてサンプルコードでみていきましょう。
// 抽象クラス abstract class AbstractClass { protected abstract void method(); public final void templateMethod() { method(); } } // 具象クラス class ConcreteJavaClass extends AbstractClass { //@Override protected void method() { System.out.println("Hello Java!"); } } // 具象クラス class ConcreteCppClass extends AbstractClass { //@Override protected void method() { System.out.println("Hello C++!"); } } public class User { public static void main(String[] args) { // インスタンスの生成 AbstractClass java = new ConcreteJavaClass(); AbstractClass cpp = new ConcreteCppClass(); // 処理の呼び出し java.templateMethod(); cpp.templateMethod(); } }
実行結果:
Hello Java! Hello C++!
このサンプルコードでは、抽象クラスAbstractClassで処理の枠組みを定め、サブクラスConcreteJavaClass、ConcreteCppClassでその具体的な内容を定義しています。UserクラスのmainメソッドでConcreteJavaClass、ConcreteCppClassのインスタンスを生成し、AbstractClassのtemplateMethodメソッドを呼び出しています。
このサンプルコードの内容はポリモーフィズムの実装によく似ています。ポリモーフィズムでは抽象クラスのmethodを使用しますが、Template Methodパターンではmethodを直接使用せずに、templateMethodを使用します。
ポリモーフィズムについてはこちらで詳しく解説していますので、ぜひ参考にしてください。
入門者向けオススメの書籍
この記事では紙面の都合上、ごく一部しかご紹介できませんでした。
デザインパターンについて入門者にもわかりやすく解説している書籍をご紹介しておきます。
どれか一冊手元に置いておくと調べる際に効率的ですし、設計の際の不安も解消されるでしょう。
増補改訂版Java言語で学ぶデザインパターン入門
入門者向けの参考書の中で、最も有名な書籍です。
入門書としてオススメです。
独習デザインパターン
事例がたくさん盛り込まれた入門書です。
オブジェクト指向における再利用のためのデザインパターン
GoFのデザインパターンについての原著を日本語訳した書籍です。
まとめ
ここではデザインパターンについて説明しました。
デザインパターンには設計を短時間に効率化できる、再利用しやすいコードを書ける、エンジニア同士の共通言語として使えるなどのメリットがあります。
使いこなすことができるように、この記事を何度も参考にして下さいね!