LoginSignup
0
0

More than 3 years have passed since last update.

Rubyで値オブジェクトの親クラスを作ってみた

Last updated at Posted at 2020-07-12

ドメイン駆動設計にかぶれて値オブジェクトを作ってみたんですが、結構面倒くさかったんで、親クラスを作ってみました。
(あくまでも個人開発で開発スピードを早めるために作っていますので、製品でそのまま使えるかは保証しません)

値オブジェクトとは

https://qiita.com/kichion/items/151c6747f2f1a14305cc
に書いてあるような特徴を持つオブジェクトです。

Rubyで書いてみると↓みたいなオブジェクトになります。この例はユーザの名前を入れる値オブジェクトです。
(正統な定義をするならequal的なものを定義したほうが良いんでしょうが、あまり使わないので今は定義していません。)

domain/user/name.rb
module Domain::User
  class Name
    attr_reader :first_name, :last_name

    def initialize(first_name, last_name)
      @first_name = first_name
      @last_name = last_name
    end

    def update_first_name(first_name)
      self.new(first_name, @last_name)
    end

    def update_last_name(last_name)
      self.new(@first_name, last_name)
    end

    def fullname
      "#{@last_name} #{@first_name}"
    end
  end
end

使うのには便利なんですが、いちいちこのお決まりのフィールドのゲッター、セッターの何行かを書くのが面倒くさいんですよね。
なので、これらを勝手に定義してくれる親クラスを作ってみました。

値オブジェクトの親クラス

domain/base/value_object.rb
module Domain::Base
  class ValueObject
    attr_reader :changed_fields

    FIELDS = []

    class << self
      # 次のようなメソッドを定義 new_date = date.update_month(11)
      def attr_updater(*attrs)
        attrs.map(&:to_sym).each do |defining_attr|

          define_method("update_#{defining_attr}") do |defining_value|
            values = attrs.map { |attribute| attribute == defining_attr ? defining_value : instance_variable_get("@#{attribute}")}
            changed_fields = @changed_fields.include?(defining_attr) ? @changed_fields : @changed_fields + [defining_attr]
            self.class.new(*values, changed_fields: changed_fields)
          end

        end
      end
    end

    # NOTE
    # - 基本的に値オブジェクト側ではFIELDSとアクセサだけ定義させる
    # - initializeは親のこのクラスの物を使わせる。
    # - initializeの独自実装をするのは可能。しかしその場合、値オブジェクト側からこの親クラスのinitializeを使うだけで、値オブジェクト使用者にはこのクラスのinitializeを直接は使わせない
    #   - changed_fieldsなどの変更は内部でのみ行う
    #   - 値オブジェクト自体の初期化引数はできるだけ単純になるようにケア
    def initialize(*field_values, fields: self.class::FIELDS, changed_fields: [])
      define_fields(fields, field_values)
      @changed_fields = changed_fields
    end

    private

    def define_fields(fields, field_values)
      fields.zip(field_values).each do |field, field_value|
        instance_variable_set("@#{field}", field_value)
      end
    end
  end
end

これを使うと以下の書き方で、元々のメソッドが提供できます。

domain/user/name.rb
module Domain::User
  class Name < ::Domain::Base::ValueObject
    FIELDS = %I(first_name last_name)
    attr_reader *FIELDS
    attr_updater*FIELDS

    def fullname
      "#{@last_name} #{@first_name}"
    end
  end
end

備考

  • メタプログラミングの黒魔術を使いすぎると、追えなくなる(grepでも目視でも)ので、複雑なメソッドは定義していません
  • attr_readerとかも自動化できるけど、attr_readerとかがあると読み手に負荷をかけないので、基本のRubyの書き方ができるところは踏襲しています。
  • この書き方だと、initialize時にFILEDどおりの順番で引数を入れることを暗黙に示していており、それを明示していないんですがが、自分ではその問題をわかっているので、今の所運用で回避しています。
  • changed_fieldsはDB更新時に更新があったフィールドだけ更新したかったので好みで作りました。
  • イミュータブルなオブジェクトで作ることは色々メリットがあるような気がしますが、一般ピーポープログラマとしてはまだそんなメリットを享受できず、YAGNIな感じがしています。並行処理とかを行うわけではないので。
0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0