はじめに
こんにちは、Tochiです。
デザインパターンを暗記レベルでインプットしたかったので ざっくりと自分なりまとめました。
Factory Methodパターン
概要
Factory Method (ファクトリー・メソッド) は、 スーパークラスでオブジェクトを作成するためのインターフェースが決まっているが、 サブクラスでは作成されるオブジェクトの型を変更することができます。
- メリット
- クリエーターと具象プロダクトとの密な結合を回避
- 単一責任の原則: プロダクト作成コードがプログラム中の一箇所にまとめられ、 保守が容易。
- 開放閉鎖の原則: プロダクトの新しい型をプログラムに導入しても、 既存のクライアント・コードの機能に影響しない。
- デメリット
- 多数の新規サブクラス導入の必要があり、 コードの複雑化の恐れあり。 既存クリエーター・クラスの階層にこのパターンを適用する場合、 最善の結果が得られる。
実装例
class Creator def factory_method raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" end def some_operation # Call the factory method to create a Product object. product = factory_method # Now, use the product. "Creator: The same creator's code has just worked with #{product.operation}" end end class ConcreteCreator1 < Creator def factory_method ConcreteProduct1.new end end class ConcreteCreator2 < Creator def factory_method ConcreteProduct2.new end end class Product def operation raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" end end class ConcreteProduct1 < Product # @return [String] def operation '{Result of the ConcreteProduct1}' end end class ConcreteProduct2 < Product # @return [String] def operation '{Result of the ConcreteProduct2}' end end def client_code(creator) print "Client: I'm not aware of the creator's class, but it still works.\n"\ "#{creator.some_operation}" end puts 'App: Launched with the ConcreteCreator1.' client_code(ConcreteCreator1.new) puts "\n\n" puts 'App: Launched with the ConcreteCreator2.' client_code(ConcreteCreator2.new)
Abstract Factoryパターン
概要
関連したオブジェクトの集りを、 具象クラスを指定することなく生成することを可能とする
- メリット
- ファクトリーから得られる製品同士は、 互換であることが保証される
- 具象製品とクライアント側コードの密結合を防止できる
- 単一責任の原則: 製品作成コードが一箇所にまとめられ、 保守が容易になる
- 開放閉鎖の原則: 製品の新しい変種を導入しても、 既存クライアント側コードは動作する
- デメリット
- パターンの使用に伴い、 多数の新規インターフェースやクラスが導入され、 コードが必要以上に複雑になる可能性あり
実装例
class AbstractFactory # @abstract def create_product_a raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" end # @abstract def create_product_b raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" end end class ConcreteFactory1 < AbstractFactory def create_product_a ConcreteProductA1.new end def create_product_b ConcreteProductB1.new end end class ConcreteFactory2 < AbstractFactory def create_product_a ConcreteProductA2.new end def create_product_b ConcreteProductB2.new end end class AbstractProductA # @abstract # # @return [String] def useful_function_a raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" end end # Concrete Products are created by corresponding Concrete Factories. class ConcreteProductA1 < AbstractProductA def useful_function_a 'The result of the product A1.' end end class ConcreteProductA2 < AbstractProductA def useful_function_a 'The result of the product A2.' end end class AbstractProductB # Product B is able to do its own thing... def useful_function_b raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" end def another_useful_function_b(_collaborator) raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" end end class ConcreteProductB1 < AbstractProductB # @return [String] def useful_function_b 'The result of the product B1.' end def another_useful_function_b(collaborator) result = collaborator.useful_function_a "The result of the B1 collaborating with the (#{result})" end end class ConcreteProductB2 < AbstractProductB # @return [String] def useful_function_b 'The result of the product B2.' end def another_useful_function_b(collaborator) result = collaborator.useful_function_a "The result of the B2 collaborating with the (#{result})" end end def client_code(factory) product_a = factory.create_product_a product_b = factory.create_product_b puts product_b.useful_function_b puts product_b.another_useful_function_b(product_a) end # The client code can work with any concrete factory class. puts 'Client: Testing client code with the first factory type:' client_code(ConcreteFactory1.new) puts "\n" puts 'Client: Testing the same client code with the second factory type:' client_code(ConcreteFactory2.new)
参考: Factory MethodパターンとAbstract Factoryパターンの違い
こちらが参考になりました。 kanae.dev
実装例を比較すると、Factory MethodパターンはFactory Methodと密結合になってます。 (Createrクラスのsome_operationメソッドの部分)
一方Abstract FactoryパターンはFactoryのインタフェースを呼び出しているだけなので、疎結合が保たれていますので Abstract Factoryパターンの方がインスタンス生成のロジックが再利用性が高いことがわかります。 ただし、 使うコード量は増えるのでおいそれと使うわけにはいけなさそうです
Builderパターン
概要
複雑なオブジェクトを段階的に構築できる生成に関するデザインパターンです。 このパターンを使用すると、 同じ構築コードを使用して異なる型と表現のオブジェクトを生成することが可能。
- メリット
- 段階的にオブジェクトを作成したり、 構築ステップを遅延させたり、 再帰的にステップを実行することが可能
- プロダクトの様々な表現の作成に際して、 同じ構築コードの再利用が可能
- 単一責任の原則: 複雑な構築用コードを、 プロダクトのビジネス・ロジックから分離可能。
- デメリット
- 本パターンでは、 複数の新規クラス作成の必要があるため、 コードの全体的な複雑さが増加
実装例
class Builder # @abstract def produce_part_a raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" end # @abstract def produce_part_b raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" end # @abstract def produce_part_c raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" end end class ConcreteBuilder1 < Builder # A fresh builder instance should contain a blank product object, which is # used in further assembly. def initialize reset end def reset @product = Product1.new end def product product = @product reset product end def produce_part_a @product.add('PartA1') end def produce_part_b @product.add('PartB1') end def produce_part_c @product.add('PartC1') end end class Product1 def initialize @parts = [] end # @param [String] part def add(part) @parts << part end def list_parts print "Product parts: #{@parts.join(', ')}" end end class Director # @return [Builder] attr_accessor :builder def initialize @builder = nil end def builder=(builder) @builder = builder end # The Director can construct several product variations using the same # building steps. def build_minimal_viable_product @builder.produce_part_a end def build_full_featured_product @builder.produce_part_a @builder.produce_part_b @builder.produce_part_c end end director = Director.new builder = ConcreteBuilder1.new director.builder = builder puts 'Standard basic product: ' director.build_minimal_viable_product builder.product.list_parts puts "\n\n" puts 'Standard full featured product: ' director.build_full_featured_product builder.product.list_parts puts "\n\n" # Remember, the Builder pattern can be used without a Director class. puts 'Custom product: ' builder.produce_part_a builder.produce_part_b builder.product.list_parts
◎ 参考になる記事
Prototypeパターン
概要
既存オブジェクトのコピーをそのクラスに依存することなく可能とする。
- メリット
- 具象クラスと密に結合せずにオブジェクトのクローンが可能。
- 構築済みのプロトタイプのクローン作成を使うことにより、 初期化コードの重複を削減。
- 複雑なオブジェクトの生成がより便利。
- 複雑なオブジェクトに対する構成の事前設定を扱う上で継承に代わる方法を提供。 -デメリット
- 循環参照のある複雑なオブジェクトのクローン作成は一筋縄ではいかない場合あり。
実装例
# The example class that has cloning ability. We'll see how the values of field # with different types will be cloned. class Prototype attr_accessor :primitive, :component, :circular_reference def initialize @primitive = nil @component = nil @circular_reference = nil end # @return [Prototype] def clone @component = deep_copy(@component) # Cloning an object that has a nested object with backreference requires # special treatment. After the cloning is completed, the nested object # should point to the cloned object, instead of the original object. @circular_reference = deep_copy(@circular_reference) @circular_reference.prototype = self deep_copy(self) end # deep_copy is the usual Marshalling hack to make a deep copy. But it's rather # slow and inefficient, therefore, in real applications, use a special gem. private def deep_copy(object) Marshal.load(Marshal.dump(object)) end end class ComponentWithBackReference attr_accessor :prototype # @param [Prototype] prototype def initialize(prototype) @prototype = prototype end end # The client code. p1 = Prototype.new p1.primitive = 245 p1.component = Time.now p1.circular_reference = ComponentWithBackReference.new(p1) p2 = p1.clone if p1.primitive == p2.primitive puts 'Primitive field values have been carried over to a clone. Yay!' else puts 'Primitive field values have not been copied. Booo!' end if p1.component.equal?(p2.component) puts 'Simple component has not been cloned. Booo!' else puts 'Simple component has been cloned. Yay!' end if p1.circular_reference.equal?(p2.circular_reference) puts 'Component with back reference has not been cloned. Booo!' else puts 'Component with back reference has been cloned. Yay!' end if p1.circular_reference.prototype.equal?(p2.circular_reference.prototype) print 'Component with back reference is linked to original object. Booo!' else print 'Component with back reference is linked to the clone. Yay!' end
結果↓
Primitive field values have been carried over to a clone. Yay! Simple component has been cloned. Yay! Component with back reference has been cloned. Yay! Component with back reference is linked to the clone. Yay!
◎参考記事
プロトタイプベース | TypeScript入門『サバイバルTypeScript』
Singletonパターン
概要
クラスが一つのインスタンスのみを持つことを保証するとともに、 このインスタンスへの大域アクセス・ポイントを提供します。 Singleton パターンは、 単一責任の原則に違反しますが、 二つの問題を同時に解決します: - クラスのインスタンスが一つだけであるであることを保証 - そのインスタンスへの大域アクセス・ポイントを提供する
- メリット
- クラスにはインスタンスが一つしかないことが保証可能。
- そのインスタンスへの大域アクセス・ポイントが得られる。
- シングルトンのオブジェクトは、 初回要求時にのみ初期化。
- デメリット
- 単一責任の原則に違反。 このパターンは、 二つの問題点を同時に解決しようとするため。
- Singleton パターンは、 設計上の欠陥を隠蔽。 たとえば、 プログラム中のコンポーネント同士が互いの詳細を知りすぎるなど。
- このパターンの使用にあたっては、 マルチスレッド環境において、 複数のスレッドがシングルトン・オブジェクトを複数回生成しないように特別な処理が必要。
- 多くのテスト・フレームワークがモック・オブジェクトの生成において継承に依存しているため、 シングルトンのクライアント・コードは、 ユニット・テストが困難。 シングルトンのクラスのコンストラクターは非公開のため、 ほとんどの言語で静的メソッドを上書きすることが不可能。 シングルトンのモックを行うには、 巧妙な方法を考える必要あり。 またはテストを放棄。 または Singleton パターンの使用をあきらめる。
実装例
# The Singleton class defines the `intance` method that lets clients access the # unique singleton instance. class Singleton attr_reader :value @instance_mutex = Mutex.new private_class_method :new def initialize(value) @value = value end # The static method that controls the access to the singleton instance. # # This implementation let you subclass the Singleton class while keeping just # one instance of each subclass around. def self.instance(value) return @instance if @instance @instance_mutex.synchronize do @instance ||= new(value) end @instance end # Finally, any singleton should define some business logic, which can be # executed on its instance. def some_business_logic # ... end end # @param [String] value def test_singleton(value) singleton = Singleton.instance(value) puts singleton.value end # The client code. puts "If you see the same value, then singleton was reused (yay!)\n"\ "If you see different values, then 2 singletons were created (booo!!)\n\n"\ "RESULT:\n\n" process1 = Thread.new { test_singleton('FOO') } process2 = Thread.new { test_singleton('BAR') } process1.join process2.join
結果↓
If you see the same value, then singleton was reused (yay!) If you see different values, then 2 singletons were created (booo!!) RESULT: FOO FO
◎ 参考になる記事
[Ruby] SingletonなLoggerクラスを作成する