<書評> Java言語で学ぶデザインパターン入門第3版 第1弾

published_at: 2023-08-11

概要

半年前とかに読んだ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}

呼び出し

  • ConcreteFactory1ConcreteFactory2はどちらも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パターンの定義

image of bridge pattern

  • 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パターン