概要
半年前とかに読んだJava言語で学ぶデザインパターン入門第3版 感想まとめ
サンプルコードは書き慣れたTSで記述している(Javaは書き慣れていないので)
参考: https://github.com/torokmark/design_patterns_in_typescript
Adapterパターン
クラスのインターフェイスを、クライアントが期待する別のインターフェイスへ変換を実現するパターン
互換性のないインターフェースのためにできなかったクラス同士の連携が可能になる
Singletonパターン
インスタンスが1つしか存在しないことを保証するパターン
サンプルコード
https://github.com/torokmark/design_patterns_in_typescript/tree/main/singleton
Singletonパターンの定義
- コンストラクタは外部に公開しない
new Singleton
が外部から不可能になる
- インタンス取得用のメソッドのみ外部公開する
- 内部実装的にはFactory Methodのようになっている
- インスタンスがすでに作成済み確認する
1namespace SingletonPattern { 2 export class Singleton { 3 private static singleton: Singleton; 4 private constructor() {} 5 public static getInstance(): Singleton { 6 if (!Singleton.singleton) { 7 Singleton.singleton = new Singleton(); 8 } 9 return Singleton.singleton; 10 } 11 } 12}
呼び出し
1namespace SingletonPattern { 2 export namespace Demo { 3 export function show(): void { 4 const singleton1 = SingletonPattern.Singleton.getInstance(); 5 const singleton2 = SingletonPattern.Singleton.getInstance(); 6 if (singleton1 === singleton2) { 7 console.log('two singletons are equivalent'); 8 } else { 9 console.log('two singletons are not equivalent'); 10 } 11 } 12 } 13}
Prototypeパターン
新しいインスタンスを作る際にクラスではなく既存のインスタンスから複製するパターン
サンプルコード
https://github.com/torokmark/design_patterns_in_typescript/tree/main/prototype
Prototypeパターンの定義
- 複製対象クラスは自身を複製できるcloneメソッドをインターフェースとして持っている
- Builderクラスに複製対象クラスを表す文字列(ex. c1)を渡すと透過的に複製できる
1namespace PrototypePattern { 2 export interface Prototype { 3 clone(): Prototype; 4 toString(): string; 5 } 6 7 export class Concrete1 implements Prototype { 8 clone(): Prototype { 9 return new Concrete1(); 10 } 11 12 toString(): string { 13 return 'This is Concrete1'; 14 } 15 } 16 17 export class Concrete2 implements Prototype { 18 clone(): Prototype { 19 return new Concrete2(); 20 } 21 22 toString(): string { 23 return 'This is Concrete2'; 24 } 25 } 26 27 export class Builder { 28 private prototypeMap: { [s: string]: Prototype } = {}; 29 30 constructor() { 31 this.prototypeMap['c1'] = new Concrete1(); 32 this.prototypeMap['c2'] = new Concrete2(); 33 } 34 createOne(s: string): Prototype { 35 console.log(s); 36 return this.prototypeMap[s].clone(); 37 } 38 } 39}
呼び出し
1namespace PrototypePattern { 2 export namespace Demo { 3 export function show(): void { 4 var builder: PrototypePattern.Builder = new PrototypePattern.Builder(); 5 var i = 0; 6 for (i = 1; i <= 2; i += 1) { 7 console.log(builder.createOne('c' + i).toString()); 8 } 9 } 10 } 11}
所感
このコードでは複製時にnew
キーワード使っているので、生成コストは同じだと思った。
要件次第ではディープコピーを採用することになるので、そこのロジックは考えないといけないと思う。
また、生成時に複数のパラメータが必要な場合、生成コストはそこまで変わらないし、むしろ上がるのではと思う。
ただ、APIレスポンスをパラメータとしている場合は、複製する方がコスト低い(APIリクエストが発生しない)ので複製した方が良さそう。
Abstract Factoryパターン
関連したオブジェクトの集まりを具象クラスを指定することなく生成できるようにしたパターン
サンプルコード
https://github.com/torokmark/design_patterns_in_typescript/tree/main/abstract_factory
Abstract Factoryパターンの定義
基本的にAbstract.*
はインターフェースとして定義されており、メンバの集合体となっている
Concreate.*
はAbstract.*
のインターフェースの中身を定義している
Abstract.*
では最終的に生成するオブジェクトの形式だけを決めておいて*、*Concreate.*
でオブジェクトの実際の中身を返している
1namespace AbstractFactoryPattern { 2 export interface AbstractProductA { 3 methodA(): string; 4 } 5 export interface AbstractProductB { 6 methodB(): number; 7 } 8 9 export interface AbstractFactory { 10 createProductA(param?: any): AbstractProductA; 11 createProductB(): AbstractProductB; 12 } 13 14 export class ProductA1 implements AbstractProductA { 15 methodA = () => { 16 return 'This is methodA of ProductA1'; 17 }; 18 } 19 export class ProductB1 implements AbstractProductB { 20 methodB = () => { 21 return 1; 22 }; 23 } 24 25 export class ProductA2 implements AbstractProductA { 26 methodA = () => { 27 return 'This is methodA of ProductA2'; 28 }; 29 } 30 export class ProductB2 implements AbstractProductB { 31 methodB = () => { 32 return 2; 33 }; 34 } 35 36 export class ConcreteFactory1 implements AbstractFactory { 37 createProductA(param?: any): AbstractProductA { 38 return new ProductA1(); 39 } 40 41 createProductB(param?: any): AbstractProductB { 42 return new ProductB1(); 43 } 44 } 45 export class ConcreteFactory2 implements AbstractFactory { 46 createProductA(param?: any): AbstractProductA { 47 return new ProductA2(); 48 } 49 50 createProductB(param?: any): AbstractProductB { 51 return new ProductB2(); 52 } 53 } 54 55 export class Tester { 56 private abstractProductA: AbstractProductA; 57 private abstractProductB: AbstractProductB; 58 59 constructor(factory: AbstractFactory) { 60 this.abstractProductA = factory.createProductA(); 61 this.abstractProductB = factory.createProductB(); 62 } 63 public test(): void { 64 console.log(this.abstractProductA.methodA()); 65 console.log(this.abstractProductB.methodB()); 66 } 67 } 68}
呼び出し
ConcreteFactory1
やConcreteFactory2
はどちらもAbstractFactory
型なので、結局どのFactoryなのかクライアント側で気にする必要はない- 気にする必要があるのは、生成されたオブジェクトが
test
メソッドを持つということだけ - 生成ロジックは隠蔽されている
1namespace AbstractFactoryPattern { 2 export namespace Demo { 3 export function show() { 4 // Abstract factory1 5 var factory1: AbstractFactoryPattern.AbstractFactory = 6 new AbstractFactoryPattern.ConcreteFactory1(); 7 var tester1: AbstractFactoryPattern.Tester = 8 new AbstractFactoryPattern.Tester(factory1); 9 tester1.test(); 10 11 // Abstract factory2 12 var factory2: AbstractFactoryPattern.AbstractFactory = 13 new AbstractFactoryPattern.ConcreteFactory2(); 14 var tester2: AbstractFactoryPattern.Tester = 15 new AbstractFactoryPattern.Tester(factory2); 16 tester2.test(); 17 } 18 } 19}
所感
単一責任の原則に従っているし、クライアントと生成ロジックが密結合しないのはメリットだと感じた
ただ、関連するオブジェクトの集まりが少ない場合は、普通にFactory Method
パターンで良いと思った
似たUIが複数生成する必要がある場合は、このパターンを採用しても良さそう
仮にこのパターンを採用する場合、コードが複雑になるのは避けられないので、トレードオフを丁寧に評価する必要がある
Bridgeパターン
抽象部分と実装部分を切り離し、両者を独立して変更できるようにしたパターン
サンプルコード
https://github.com/torokmark/design_patterns_in_typescript/blob/main/bridge/bridge.ts
Bridgeパターンの定義
Abstraction
は実装から独立した操作を定義したクラスImplementator
への参照を保持し、メソッドを定義
RefinedAbstraction.*
はAbstraction
クラスの具体的なサブクラスAbstraction
クラスで定義したメソッドの具体的な動作を定義
Implementor
は抽象化から独立した操作を定義したクラスConcreteImplementor.*
はImplementor
クラスの具体的なサブクラスImplementor
クラスで定義したメソッドの具体的な動作を定義
1namespace BridgePattern { 2 export class Abstraction { 3 implementor: Implementor; 4 constructor(imp: Implementor) { 5 this.implementor = imp; 6 } 7 public callIt(s: String): void { 8 throw new Error('This method is abstract!'); 9 } 10 } 11 12 // ... 以下のコードは省略 ... 13}
呼び出し
1namespace BridgePattern { 2 export namespace Demo { 3 export function show(): void { 4 var abstractionA: BridgePattern.Abstraction = 5 new BridgePattern.RefinedAbstractionA( 6 new BridgePattern.ConcreteImplementorA(), 7 ); 8 var abstractionB: BridgePattern.Abstraction = 9 new BridgePattern.RefinedAbstractionB( 10 new BridgePattern.ConcreteImplementorB(), 11 ); 12 13 abstractionA.callIt('abstractionA'); 14 abstractionB.callIt('abstractionB'); 15 } 16 } 17}
所感
ここまでやるかは別として、抽象と実装を切り分けることで、コアロジックと詳細なロジックを分離できる点はメリットだと思う それにより変更容易性がある程度担保されている デザインパターンは単一責任の原則が色濃く出ているなと思う
Strategyパターン
同じインターフェースだが振る舞い(アルゴリズム)をユースケースごとに分離して実行できるようにしたパターン
サンプルコード
https://github.com/torokmark/design_patterns_in_typescript/tree/main/strategy
Strategyパターンの定義
- それぞれの振る舞い(アルゴリズム)を
Strategy.*
クラスに分離する Strategy.*
クラスはStrategy
インターフェースを持つContext
クラスは渡されたStrategy.*
クラスを呼び出す
1namespace StrategyPattern { 2 export interface Strategy { 3 execute(): void; 4 } 5 6 export class ConcreteStrategy1 implements Strategy { 7 public execute(): void { 8 console.log('`execute` method of ConcreteStrategy1 is being called'); 9 } 10 } 11 12 export class ConcreteStrategy2 implements Strategy { 13 public execute(): void { 14 console.log('`execute` method of ConcreteStrategy2 is being called'); 15 } 16 } 17 18 export class ConcreteStrategy3 implements Strategy { 19 public execute(): void { 20 console.log('`execute` method of ConcreteStrategy3 is being called'); 21 } 22 } 23 24 export class Context { 25 private strategy: Strategy; 26 27 constructor(strategy: Strategy) { 28 this.strategy = strategy; 29 } 30 public executeStrategy(): void { 31 this.strategy.execute(); 32 } 33 } 34}
呼び出し
1namespace StrategyPattern { 2 export namespace Demo { 3 export function show(): void { 4 var context: StrategyPattern.Context = new StrategyPattern.Context( 5 new StrategyPattern.ConcreteStrategy1(), 6 ); 7 8 context.executeStrategy(); 9 10 context = new StrategyPattern.Context( 11 new StrategyPattern.ConcreteStrategy2(), 12 ); 13 context.executeStrategy(); 14 15 context = new StrategyPattern.Context( 16 new StrategyPattern.ConcreteStrategy3(), 17 ); 18 context.executeStrategy(); 19 } 20 } 21}
所感
個人的にはかなり現実的なパターンだと考えている。
生成ロジックの分離に重きを置いたPrototypeやFactory、Bridgeパターンより、Strategyパターンのような振る舞いを分離する方がコスパよく変更の影響範囲を最小化できそうな気がしている。
最初からPrototypeやFactoryパターンなどを実装しているプロジェクトは少ないと思っていて、そうなるとStrategyパターンで振る舞いを分離して置いた上で他のデザインパターンを適用していくという考え方もあるのでは。
まとめ
デザインパターンを愚直に適用するのは良くないけれど、考え方自体を知っておくのはとても有用だと思う。
残りのデザインパターンは別記事にまとめる。
- Compositeパターン
- Visitorパターン
- Facadeパターン
- Mediatorパターン
- Proxyパターン
- Commandパターン
- Interpreterパターン