コツコツエンジニアの日常🐻

Webエンジニアとして働く中で得た学びを消化せず、コツコツと学びを積み上げていきます

【Ruby】ざっくり学ぶデザインパターン(生成関連)

はじめに

こんにちは、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

◎ 参考になる記事

Builder design pattern in Ruby. Builder pattern in Ruby is useful when… | by Krzysztof Kempiński | kkempin’s dev blog | Medium

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クラスを作成する

参考記事

refactoring.guru