はじめに
ソフトウェア開発の世界では、問題解決に悩むことがよくあります。その際、他のプログラマーがすでに遭遇し、解決した問題の経験を共有できれば、開発プロセスはよりスムーズに進むことでしょう。ここでデザインパターンが重要な役割を果たします。
デザインパターンは、ソフトウェアの品質向上、保守性の確保、柔軟性の向上など、さまざまな利点を提供します。これはある種のプログラミングの「ベストプラクティス」や「標準的なソリューション」であり、ソフトウェア開発者が問題に効果的に対処するためのガイドラインを提供します。
デザインパターンは以下に示す様に依然として価値あるトピックです。
- 普遍的な問題解決手法: デザインパターンは、特定のプログラミング言語やフレームワークに依存せず、普遍的な問題に対する解決手法を提供します。そのため、プログラミングの基礎を学ぶ上で非常に有益です。
- 保守性と拡張性: デザインパターンは、ソフトウェアの保守性と拡張性を向上させるのに役立ちます。これにより、変更やアップグレードが容易になり、ソフトウェアの寿命を延ばすことができます。
- 設計の理解: デザインパターンは、良い設計の原則を学ぶのに役立ちます。これにより、初心者から中級者、上級者まで、あらゆる経験レベルのプログラマーがより洗練されたソフトウェアを開発できるようになります。
- 業界標準: デザインパターンは、多くの場合、業界標準として採用されています。プロジェクトやチームの中で共通の言語やアーキテクチャを共有するため、コミュニケーションの円滑化に寄与します。
総じて、デザインパターンはプログラマーのスキルセットにおいて依然として価値があり、学ぶことでより良いコードを書くための手段を提供しています。プログラミングの基本的な原則は時代を超えて通用するものであり、デザインパターンはその一環です。
Gang of Four(Gof)と呼ばれる4人の著者によってまとめられ、広く使われている23のデザインパターンが、「オブジェクト指向における再利用のためのデザインパターン」として知られています。その中から、特に一般的に使用されると考えられるデザインパターンを7つピックアップしました。
- 1. Singleton(シングルトン)
- よく使用されます。唯一のインスタンスが必要な場合や、リソースの共有が必要な場合に適しています。
- 2. Factory Method(ファクトリーメソッド)
- よく使用され、オブジェクトの生成プロセスをサブクラスに委任することで柔軟性を提供します。
- 3. Observer(オブザーバ)
- イベント駆動アーキテクチャやMVC(Model-View-Controller)パターンなどで頻繁に使用されます。
- 4. Strategy(ストラテジー)
- アルゴリズムの交換が可能な場合に有用で、動的な振る舞いをサポートします。
- 5. Adapter(アダプター)
- 既存のクラスを別のインタフェースに適合させるために使用され、既存のコードとの統合で重宝されます。
- 6. Decorator(デコレータ)
- オブジェクトに新しい機能を追加する際に使用され、柔軟な拡張が可能です。
- 7. Command(コマンド)
- 操作の要求、キュー、操作の履歴など、コマンドの表現と実行を分離する必要がある場合に使用されます。
~~~それぞれ、例え話と簡単なJavaプログラムソースと共に解説していきます。~~~
1. Singleton(シングルトン)
シングルトン(Singleton)は、デザインパターンの一つで、特定のクラスがインスタンスを単一のインスタンスに制限することを目的としています。具体的には、そのクラスのインスタンスがプログラム内で唯一であり、それにアクセスするためのグローバルなポイントを提供します。これにより、特定のクラスの単一のオブジェクトがアプリケーション全体で共有され、一貫性が保たれます。
シングルトンパターンは、以下の要素を持っています:
- プライベートなコンストラクタ (Private Constructor): シングルトンクラスのコンストラクタはプライベートにされ、外部からのインスタンス化が制限されます。
- プライベートな静的変数 (Private Static Instance): シングルトンクラス内部に唯一のインスタンスを保持するための静的な変数があります。
- 公開された静的メソッド (Public Static Method): シングルトンクラスのインスタンスにアクセスするための公開された静的メソッドがあります。通常、このメソッドはクラス自体のインスタンスを生成し、初回呼び出し時にのみインスタンスを生成して以降は同じインスタンスを返します。
例え話:
ある会社に、熱心な秘書「ログマスター」がいます。ログマスターの仕事は、会社で起こる重要な出来事や決定を記録することです。何か注目すべきことが起こると、従業員たちはその詳細をログマスターに提供し、ログマスターは時間を記録とともに丹念にメモを取ります。ログマスターは非常に整頓された人物で、すべての記録を綺麗に整理してファイルに保管しています。
プログラミングの世界では、このログマスターが Logger クラスを表しています。このクラスは、プログラムの実行中に発生する重要なイベントや決定を記録またはログに残す責任を負います。プログラムの異なる部分が記録を作成する場合、それらの部分は Logger とコミュニケートし、情報が適切に文書化されるようにします。
Logger クラスは、まるでログマスターのように、ログが体系的に保持されることを確実にし、後でデバッグ、分析、またはモニタリングのためにこれらのログを確認できるようにします。アプリケーションの起動時刻、重要な決定、またはエラーなど、Logger はすべてを忠実に記録し、開発チームの貴重なメンバーとなります。
import java.util.Date;
// Loggerクラス(ログを記録する人)
public class Logger {
// 唯一のインスタンス
private Logger() {
// インスタンス生成のための初期化コード
}
// インスタンスを取得するメソッド
public static Logger getInstance() {
// ConfigInstanceHolder内部クラスを経由して唯一のインスタンスを取得
return ConfigInstanceHolder.instance;
}
// ログを記録するメソッド
public void log(String message) {
// タイムスタンプ付きでメッセージを出力
System.out.println(new Date() + ": " + message);
}
// ConfigInstanceHolder 内部クラス
private static class ConfigInstanceHolder {
// 唯一のインスタンスを生成
private static final Logger instance = new Logger();
}
}
// AnotherClassクラス(別の部署で何かを実行する人)
public class AnotherClass {
public static void doSomething() {
// ログを記録する人(Logger)のインスタンスを取得
Logger logger = Logger.getInstance();
// 別の部署でも同じログを記録する人を使用して、何かを実行します。
logger.log("別の部署で何かを実行しました。");
}
}
// Clientクラス(主要な業務を担当する人)
public class Client {
public static void main(String[] args) {
// ログを記録する人(Logger)のインスタンスを取得
Logger logger = Logger.getInstance();
// 主要な業務を担当する人がログを記録する人を使用して、アプリケーションを起動します。
logger.log("アプリケーションを起動しました。");
// 別の部署でも同じログを記録する人を使用して何かを実行します。
AnotherClass.doSomething();
}
}
あるいは、上記例のLoggerクラスは、下記のような場合もあり、こちらの方がイメージしやすいかもしれません。
//Loggerクラス(シングルトン)
public class Logger {
// 唯一のインスタンス
private static Logger instance;
// プライベートなコンストラクタ(外部からのインスタンス生成を禁止)
private Logger() {
// 初期化のためのコード
}
// インスタンスを取得するメソッド
public static Logger getInstance() {
if (instance == null) {
// インスタンスがまだ作成されていない場合は新しく作成
instance = new Logger();
}
// すでに存在する場合は既存のインスタンスを返す
return instance;
}
// ログを記録するメソッド
public void log(String message) {
// タイムスタンプ付きでメッセージを出力
System.out.println(new Date() + ": " + message);
}
}
2. Factory Method(ファクトリーメソッド)
Factory Method(ファクトリーメソッド)は、Creational(生成)パターンに分類されます。このパターンは、オブジェクトの生成に関する責任をサブクラスに委任することで、具体的なクラスの選択をクライアントから分離することを目的としています。これにより、クライアントは具体的なクラスのインスタンスを直接生成するのではなく、生成するべきクラスを指定するための抽象的なインターフェースを持つことができます。
以下は、Factory Method パターンの主要な要素と特徴です:
- Product(製品): インターフェースまたは抽象クラスで、生成される具体的な製品(オブジェクト)の共通のインターフェースを定義します。
- ConcreteProduct(具体的な製品): Product インターフェースを実装した具体的なクラスで、生成される具体的な製品です。
- Creator(生成者): 抽象クラスまたはインターフェースで、Factory Method を宣言します。このメソッドは具体的な製品の生成を行いますが、その具体的な実装はサブクラスに委任されます。
- ConcreteCreator(具体的な生成者): Creator を実装した具体的なクラスで、具体的な製品の生成方法を実装します。これにより、生成の詳細がサブクラスに移譲され、クライアントは具体的な生成方法を気にせずに製品を取得できます。
例え話:
工場の社員管理
ある大きな工場があり、その工場では様々な社員が働いています。工場は製品を作るために社員を雇っており、その社員たちは仕事をこなしています。
最初は、仕事があまり得意でない社員(うまくできない社員)がいます。彼らは指示された仕事をこなすことができません。工場は彼らに指示を与え、製品を作らせるのですが、なかなか期待通りの結果が得られません。
しかし、工場は柔軟で、うまくできる社員(うまくできる社員)に変更することができます。ここでFactory Methodが登場します。工場は社員を生成する特別なメソッドを持ち、このメソッドを変更することで異なる種類の社員を作ることができます。
うまくできない社員の場合
// インターフェース: 仕事をする社員を表す
interface Employee {
void doWork();
}
// うまくできない社員のクラス
class IncompetentEmployee implements Employee {
@Override
public void doWork() {
System.out.println("仕事がうまくできません。");
}
}
// 工場クラス: 社員の生成を行う
class Factory {
// Factory Method: 社員を生成するメソッド
public Employee createEmployee() {
return new IncompetentEmployee();
}
}
// メインクラス
public class Main {
public static void main(String[] args) {
// 工場を生成
Factory factory = new Factory();
// 社員を生成して仕事をさせる
Employee employee = factory.createEmployee();
employee.doWork();
}
}
上記のプログラムでは、IncompetentEmployee クラスがうまく仕事をできない社員を表しています。そして、Factory クラスの createEmployee メソッドがFactory Methodとして、この社員を生成しています。
次に、うまくいく社員に置き換える場合の例を提示します。うまくいく社員のクラスと工場クラスの一部だけが変更されています。
// うまくいく社員のクラス
class CompetentEmployee implements Employee {
@Override
public void doWork() {
System.out.println("仕事がうまくできます。");
}
}
// 工場クラス: 社員の生成を行う
class Factory {
// Factory Method: 社員を生成するメソッド
public Employee createEmployee() {
return new CompetentEmployee();
}
}
これにより、CompetentEmployee クラスによってうまく仕事をできる社員を生成できます。Factory Method パターンにより、Factory クラス内の変更のみで、メインクラス側には影響がないことが確認できます。
3. Observer(オブザーバ)
Observer(オブザーバ)は、あるオブジェクト(Subject)の状態が変化したときに、それに依存する他のオブジェクト(Observer)に自動的に通知を送るための方法を提供します。これにより、オブジェクト間の疎結合性(loose coupling)を実現し、変更が容易になります。
以下にObserverパターンの主な要素と動作を説明します:
-
Subject(被験者): 状態を保持するオブジェクト。このオブジェクトの状態が変化すると、それに依存するObserverたちに通知を送ります。
SubjectはObserverを登録・削除するメソッドを提供します。 -
Observer(観察者): Subjectの状態変化を監視し、それに応じて特定のアクションを実行するオブジェクト。
Observerは通知を受け取るための更新(update)メソッドを実装します。 - ConcreteSubject(具体的な被験者): Subjectの具体的な実装。状態の変化があると、登録されたObserverたちに対して通知を送ります。
-
ConcreteObserver(具体的な観察者): Observerの具体的な実装。Subjectの変化に対する具体的な反応を実装します。
ConcreteObserverはSubjectに登録され、Subjectの状態変化時に通知を受け取ります。
Observerパターンの利点は、SubjectとObserverがお互いに疎結合であることです。SubjectはObserverの存在を知らずに通知を送り、ObserverはSubjectの詳細に依存せずに通知を受け取ります。これにより、システム全体が柔軟で拡張可能な構造を持ち、変更が発生した際に影響が少なくなります。
Observerパターンは、GUIフレームワークやイベント処理、モデル-ビュー-コントローラ(MVC)アーキテクチャなどで広く利用されています。
Observer(オブザーバ)デザインパターンは、オブジェクト間の一対多の依存関係を定義し、あるオブジェクトが変化するとその他の依存するオブジェクトに通知が行くようにします。これは、製品開発の組織においても例えることができます。
例え話:
会社の部門や協力会社は製品の進捗に興味があり、製品が変化するとその情報を受け取りたいと考えているとしましょう。この場合、製品が変更された際にそれに依存する各組織や協力会社に通知が行く仕組みがObserverパターンに相当します。つまり、製品が進捗するたびに関連する各組織や協力会社に通知が行き、最新の情報を得ることができます。
import java.util.ArrayList;
import java.util.List;
// 製品を表すクラス
class Product {
private String name;
private List<Observer> observers = new ArrayList<>();
public Product(String name) {
this.name = name;
}
// 製品が変更されたときに通知を受け取るObserverを追加
public void addObserver(Observer observer) {
observers.add(observer);
}
// 製品が変更されたときに通知を送る
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(name + " の進捗が変更されました");
}
}
// 製品の進捗を変更する
public void progress() {
// 製品の進捗が変更されたときに通知を送る
notifyObservers();
}
}
// Observerを表すインターフェース
interface Observer {
void update(String message);
}
// 部門や協力会社を表すクラス
class Organization implements Observer {
private String name;
public Organization(String name) {
this.name = name;
}
// 製品の進捗が変更されたときに通知を受け取る
@Override
public void update(String message) {
System.out.println(name + ": " + message);
}
}
public class ObserverPatternExample {
public static void main(String[] args) {
// 製品のインスタンスを生成
Product product = new Product("新製品");
// Observerとして登録する部門や協力会社を生成
Organization departmentA = new Organization("開発部門A");
Organization departmentB = new Organization("開発部門B");
Organization partnerCompany = new Organization("協力会社");
// Observerを製品に登録
product.addObserver(departmentA);
product.addObserver(departmentB);
product.addObserver(partnerCompany);
// 製品の進捗が変更されたときにObserverに通知
product.progress();
}
}
4. Strategy(ストラテジー)
Strategy(ストラテジー)」は、アルゴリズムを定義し、それをカプセル化し、相互に交換可能にすることで、クライアントからアルゴリズムの変更を簡単にすることを目的としています。これは、アルゴリズムの変更がクライアントコードに影響を与えず、再利用性を高めるための手法です。
以下は、Strategyパターンの要素とその動作の概要です。
-
コンテキスト(Context): コンテキストは、アルゴリズムを適用するクラスであり、ストラテジーのインターフェースを保持します。
コンテキストはストラテジーオブジェクトと連携し、具体的なアルゴリズムの実装の詳細を知りません。 -
ストラテジー(Strategy): ストラテジーは、同じコンテキスト内で交換可能なアルゴリズムを定義するインターフェースまたは抽象クラスです。
具体的なストラテジークラスは、このインターフェースを実装し、異なるアルゴリズムを提供します。 -
具体的なストラテジークラス(Concrete Strategy): 具体的なストラテジークラスは、ストラテジーインターフェースを具体的なアルゴリズムで実装します。
コンテキストはこれらの具体的なストラテジークラスのインスタンスを持ち、必要に応じてこれを切り替えて利用します。
Strategyパターンのメリットは、アルゴリズムの変更がコンテキストから分離され、新しいアルゴリズムの追加や既存のアルゴリズムの変更が容易であることです。これにより、柔軟性が向上し、コードの再利用性が増します。
Strategy(ストラテジー)パターンを製品開発組織に例えると、異なる戦略やアプローチを適用することで、同じ製品やサービスを提供する場面で有用です。
例え話:
製品開発組織と戦略の適用
会社Xは新しい製品を開発することになりました。製品の開発は部門A(内製)、部門B(協力会社1)、部門C(協力会社2)が担当することになりました。各部門は異なる戦略を持ち、製品を効果的に開発するためには、部門ごとに異なる戦略を取る必要があります。
これをStrategyパターンに置き換えると、部門が具体的な実装を提供し、組織全体でインターフェースを共有することで、異なる戦略を簡単に切り替えることができます。
// Strategy インターフェース
interface DevelopmentStrategy {
void executeDevelopment();
}
// 部門A: 内製戦略
class InHouseDevelopment implements DevelopmentStrategy {
@Override
public void executeDevelopment() {
System.out.println("内製: 部門A が製品を開発中");
// 内製の具体的な開発処理
}
}
// 部門B: 協力会社1戦略
class OutsourcedDevelopment1 implements DevelopmentStrategy {
@Override
public void executeDevelopment() {
System.out.println("協力会社1: 部門B が製品を開発中");
// 協力会社1の具体的な開発処理
}
}
// 部門C: 協力会社2戦略
class OutsourcedDevelopment2 implements DevelopmentStrategy {
@Override
public void executeDevelopment() {
System.out.println("協力会社2: 部門C が製品を開発中");
// 協力会社2の具体的な開発処理
}
}
// 製品開発担当クラス
class ProductDevelopment {
private DevelopmentStrategy developmentStrategy;
public ProductDevelopment(DevelopmentStrategy strategy) {
this.developmentStrategy = strategy;
}
public void setDevelopmentStrategy(DevelopmentStrategy strategy) {
this.developmentStrategy = strategy;
}
public void executeDevelopment() {
developmentStrategy.executeDevelopment();
}
}
// メインクラス
public class Main {
public static void main(String[] args) {
// 部門A: 内製戦略で開発
ProductDevelopment productA = new ProductDevelopment(new InHouseDevelopment());
productA.executeDevelopment();
// 部門B: 協力会社1戦略に切り替えて開発
productA.setDevelopmentStrategy(new OutsourcedDevelopment1());
productA.executeDevelopment();
// 部門C: 協力会社2戦略に切り替えて開発
productA.setDevelopmentStrategy(new OutsourcedDevelopment2());
productA.executeDevelopment();
}
}
このサンプルコードでは、製品開発担当クラス(ProductDevelopment)が異なる戦略を持つ部門と連携し、戦略の切り替えが容易にできるようになっています。
5. Adapter(アダプター)
Adapter(アダプター)は、特定のインターフェースを持つクラスを、別のインターフェースを持つクラスに変換するためのパターンです。これにより、互換性のないクラス同士を協調させることができます。Adapterパターンは、既存のクラスと新しいクラスが協力して動作できるようにするために使用されます。
以下は、Adapterパターンの主な要素とその動作の説明です:
- Target(ターゲット): これは、クライアントが期待するインターフェースを定義する抽象クラスまたはインターフェースです。クライアントはこのターゲットを介して操作を行います。
- Adaptee(適応される側): これは、既存のクラスで、クライアントが直接利用したいのではなく、変換が必要なクラスです。
- Adapter(アダプター): これは、Targetのインターフェースを実装し、Adapteeのインスタンスを保持します。Adapterはクライアントの要求を適切な形式でAdapteeに委譲し、変換の役割を果たします。
例え話:
ある会社で、社員Aは「やってください」という指示でしか動きません。しかし、ある日、新しい部長が就任し、「やれよ」と指示してくるようになりました。社員Aはこの新しい指示に対応できず、仕事が進まなくなってしまいました。
そこで、社員Bが登場しました。社員Bは、社員Aと部長のコミュニケーションを円滑にするためのアダプターとして機能します。社員Bは部長からの「やれよ」の指示を「やってください」という形に変換し、社員Aに指示することができるのです。
// Adapter(社員B)のインターフェース
interface EmployeeAdapter {
void yareyo();
}
// 社員Aのクラス
class EmployeeA {
void yattekudasai() {
System.out.println("仕事をやっています");
}
}
// Adapter(社員B)の実装クラス
class EmployeeBAdapter implements EmployeeAdapter {
private EmployeeA employeeA;
public EmployeeBAdapter(EmployeeA employeeA) {
this.employeeA = employeeA;
}
@Override
public void yareyo() {
// 社員Aのメソッドを変換して呼び出す
employeeA.yattekudasai();
}
}
// 部長のクラス
class Manager {
void instruct(EmployeeAdapter employee) {
System.out.print("部長の指示: ");
employee.yareyo();
}
}
public class AdapterPatternExample {
public static void main(String[] args) {
// 社員Aのインスタンス
EmployeeA employeeA = new EmployeeA();
// Adapter(社員B)のインスタンスを作成し、社員Aを渡す
EmployeeAdapter employeeBAdapter = new EmployeeBAdapter(employeeA);
// 部長の指示を呼ぶ
Manager manager = new Manager();
manager.instruct(employeeBAdapter);
}
}
社員A(EmployeeA)が「やってください」メソッドを持ち、社員B(EmployeeBAdapter)が「やれよ」メソッドを「やってください」に変換するアダプターとして機能しています。最後に、部長が社員Bに対して指示を行います。
ちょっと休憩:Adapter(アダプター) のイメージはこんな感じかなぁ。
6. Decorator(デコレータ)
Decorator(デコレータ)は、構造型パターンに分類されます。このパターンは、既存のクラスに新しい機能や責務を追加するための柔軟な方法を提供します。Decoratorパターンは、継承を使用せずにクラスを拡張する手段を提供し、オブジェクトの機能を動的に変更することができます。
以下は、Decoratorパターンの主な要素と構造です。
- Component(コンポーネント): デコレータパターンで拡張される具象コンポーネントの基本インターフェースです。通常、インターフェースや抽象クラスとして定義されます。
- ConcreteComponent(具象コンポーネント): Componentの具象実装です。拡張される機能がまだない基本のオブジェクトを表します。
- Decorator(デコレータ): Componentの実装を拡張するための基本抽象クラスであり、Componentを継承します。また、内部にComponentへの参照を保持します。
- ConcreteDecorator(具象デコレータ): 実際に機能を追加するためのクラスで、Decoratorを継承します。ConcreteDecoratorは追加の機能を提供し、それが必要な場合にComponentをラップします。
デコレータパターンの利点は以下の通りです:
- 柔軟性と拡張性: クラスの変更を最小限に抑えつつ、新しい機能を追加できます。
- 再利用性: デコレータは個別の機能を提供し、これらの機能は他のオブジェクトでも再利用できます。
- 単一責任の原則の遵守: クラスは一つの責務だけを持ち、それが達成されます。
例え話:
社員(クラス): 会社の基本的な業務を担当する社員。
役職(デコレータ): 社員に新しい責任や機能を追加するデコレータ。例えば、管理職や開発リーダーがこれに相当。
仕事内容(メソッド): 社員が実際に行う業務。例えば、コーヒーを淹れる、書類を整理するなど。
ある会社には様々な仕事があり、それにはそれぞれの社員が担当しています。社員は基本的な業務をこなす力を持っています。しかし、ある社員には追加の責任やスキルが必要な場合があります。
例えば、ある社員が優秀であるため、その社員にプロジェクトのリーダーとしての役割を与えたいとします。この場合、その社員を新たな責任を持つ「リーダー社員」として位置付け、元の社員に新しい役割を追加したと言えます。
同様に、もう一人の社員には技術的なスキルがあり、その社員に技術リーダーとしての責任を与えたい場合もあります。これもデコレータパターンを利用して、元の社員に新しい責任を追加する形となります。
このように、元の社員(クラス)に対して役職(デコレータ)を追加することで、新しい責任やスキルを持った社員を作り上げることができます。デコレータパターンは柔軟で再利用可能な機能の追加を可能にする点が、この組織の例えに当てはまります。
// 社員を表すインターフェース
interface Employee {
void doTask();
}
// 基本の社員クラス
class BasicEmployee implements Employee {
@Override
public void doTask() {
System.out.println("基本の業務をこなします。");
}
}
// 役職(Decorator)の基底クラス
abstract class PositionDecorator implements Employee {
protected final Employee decoratedEmployee;
public PositionDecorator(Employee employee) {
this.decoratedEmployee = employee;
}
@Override
public void doTask() {
decoratedEmployee.doTask();
}
}
// リーダー社員を表すデコレータ
class TeamLeaderDecorator extends PositionDecorator {
public TeamLeaderDecorator(Employee employee) {
super(employee);
}
@Override
public void doTask() {
super.doTask();
leadTeam();
}
private void leadTeam() {
System.out.println("チームをリードします。");
}
}
// 技術リーダー社員を表すデコレータ
class TechLeaderDecorator extends PositionDecorator {
public TechLeaderDecorator(Employee employee) {
super(employee);
}
@Override
public void doTask() {
super.doTask();
provideTechnicalLeadership();
}
private void provideTechnicalLeadership() {
System.out.println("技術的なリーダーシップを提供します。");
}
}
// メインクラス
public class Main {
public static void main(String[] args) {
// 基本の社員を雇う
Employee basicEmployee = new BasicEmployee();
System.out.println("基本の社員:");
basicEmployee.doTask();
// リーダー社員を雇う
Employee teamLeader = new TeamLeaderDecorator(basicEmployee);
System.out.println("\nリーダー社員:");
teamLeader.doTask();
// 技術リーダー社員を雇う
Employee techLeader = new TechLeaderDecorator(basicEmployee);
System.out.println("\n技術リーダー社員:");
techLeader.doTask();
// リーダー社員に技術リーダーの役割を追加する
Employee teamTechLeader = new TechLeaderDecorator(new TeamLeaderDecorator(basicEmployee));
System.out.println("\nチームリーダーかつ技術リーダー社員:");
teamTechLeader.doTask();
}
}
このプログラムでは、基本の社員に対してリーダーの役職と技術リーダーの役職を追加するデコレータを定義しています。そして、それぞれの役職を持った社員を雇用し、役職に応じた業務を実行することができます。デコレータを組み合わせて、柔軟かつ再利用可能な機能の追加が行われています。
7. Command(コマンド)
Command(コマンド)は、オブジェクト指向プログラミングにおいて操作をオブジェクトとして表現し、それをカプセル化して他のオブジェクトとの間での通信を可能にするためのパターンです。このパターンの目的は、コマンドの実行を要求するオブジェクト(クライアント)と、そのコマンドを処理するオブジェクト(リサイバー)を分離し、柔軟性と拡張性を高めることにあります。
以下は、Commandパターンの主要な要素と役割です。
-
Command(コマンド): コマンドとは、特定の操作を表すオブジェクトです。このオブジェクトは、実際の操作を実行するメソッド(例: execute())を持っています。
コマンドはインタフェースまたは抽象クラスとして定義され、具体的なコマンドクラスがそれを実装します。 - ConcreteCommand(具体コマンド): 実際の操作を具体的に実装するクラスです。これはCommandインタフェースを実装し、実際の処理を execute() メソッドに持っています。
- Client(クライアント): コマンドを生成し、必要な操作を要求するオブジェクトです。クライアントはどのコマンドが実行されるかを知る必要はありません。
-
Invoker(起動者): コマンドを実行するオブジェクトで、具体的なコマンドクラスの存在を知っています。
インボーカーは、クライアントからの要求に応じて適切なコマンドを呼び出し、それを実行します。 - Receiver(リサイバー): 実際の操作を実行するオブジェクトです。具体的なコマンドがこのリサイバーのメソッドを呼び出して実際の処理が行われます。
Commandパターンを使用することで、新しいコマンドを追加したり、既存のコマンドを変更したりする際に、クライアントやインボーカーを変更せずにシステムを拡張できます。これにより、柔軟性や保守性が向上し、オブジェクト間の結合が低減されます。
例え話:
社長(Invoker): 会社の経営者であり、具体的な仕事内容は知らなくてもよい。プロジェクトを進めたいという要望を部長(ConcreteCommand)に伝え、期待通りの成果を得る。
部長(ConcreteCommand): 具体的な仕事内容を知っており、社長(Invoker)からの要望を遂行可能な形に変換する。各社員に具体的な仕事(Command)を割り振り、必要に応じて変更や追加を行う。
社員(Receiver): 部長(ConcreteCommand)が割り振った仕事(Command)を実際に遂行する。各社員は自分が担当する仕事に集中し、それを実行する。
シナリオ:
社長(Invoker): 「部長、新しいプロジェクトを始めることにした。それぞれの部署に仕事を割り振ってくれ。」
部長(ConcreteCommand): 「了解しました、社員たちにそれぞれのタスクを割り振ります。」
社員(Receiver): 各社員は自分に割り当てられたタスクを実行する。例えば、プログラミング、デザイン、テストなど。
この例え話では、社長がInvoker、部長がConcreteCommand、社員がReceiverとなり、プロジェクトの進行やタスクの遂行が柔軟になります。社長は具体的な作業内容を知らなくても、部長を通じて適切な指示を行えるようになり、部長は社員に具体的な仕事を割り振りながら柔軟に変更できます。
// Command インターフェース
interface Command {
void execute();
}
// ConcreteCommand クラス
class ProjectAssignmentCommand implements Command {
private Employee employee;
private String task;
public ProjectAssignmentCommand(Employee employee, String task) {
this.employee = employee;
this.task = task;
}
@Override
public void execute() {
employee.performTask(task);
}
}
// Receiver クラス
class Employee {
private String name;
public Employee(String name) {
this.name = name;
}
public void performTask(String task) {
System.out.println(name + " is performing task: " + task);
}
}
// Invoker クラス
class Manager {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void instruct() {
command.execute();
}
}
// Client クラス
public class Main {
public static void main(String[] args) {
// Employee インスタンスを作成
Employee programmer = new Employee("John");
// ConcreteCommand インスタンスを作成し、Receiver を設定
Command assignmentCommand = new ProjectAssignmentCommand(programmer, "Develop a new feature");
// Invoker インスタンスを作成し、ConcreteCommand を設定
Manager manager = new Manager();
manager.setCommand(assignmentCommand);
// Instruct メソッドを呼び出すことで、対応する ConcreteCommand が実行される
manager.instruct();
}
}
このプログラムでは、ManagerがInvoker、ProjectAssignmentCommandがConcreteCommand、EmployeeがReceiverとなっています。ManagerがProjectAssignmentCommandを通じてEmployeeに具体的な仕事を割り振り、それが実行される構造となっています。
おわりに
この記事は、私の記憶とAIのテキスト生成をもとに作成しました。私の記憶の根底には、様々なウェブサイトの記事や書籍がありますが、具体的なサイトのURLなどはもう覚えていません。
手元にある書籍としては、以下の1冊があります。
『増補改訂版 Java言語で学ぶデザインパターン入門』
著者: 結城 浩
ISBN: 4-7973-2703-0
本記事においては、この書籍もネットの記事も引用していません。
今回解説した7つのパターンは、オブジェクト指向プログラミングにおいて非常に重要であり、実際の開発で頻繁に遭遇する事例です。しかし、これに限らず、Gang of Fourが提案したデザインパターンには合計23個ものパターンが存在します。
今回解説していない残りの16個のパターンも、それぞれ異なる問題に対する優れた解決策を提供しています。これらを学ぶことで、より幅広い視野を得て、より洗練されたプログラミングスキルを身につけることができるでしょう。
デザインパターンの理解と実践は、あなたの開発者キャリアを飛躍的に向上させる手助けとなります。新しいパターンを学び、それを実際のプロジェクトで適用することで、柔軟性と効率性を兼ね備えたコードを生み出すことができるでしょう。