開発ノート@HarikoApps
HarikoApps: https://hariko.sprkls.me
2024-09-21[Rails]ActiveModel::Attributesを用いてモデルのコンポジションを行うためにひとまずカスタムタイプで対応する #20
Railsプロジェクトにて、ActiveModel::AttributesはDBのテーブルにデータを永続化しないモデルを作成するのにとても便利なのですが、オブジェクト指向設計でいうところのコンポジション(合成)のための機能は現状(v7.2.1)では備わっていません。Rails本体のIssueでこの機能のための作業が行われていてリリースが楽しみなのですが、最近このコンポジションを使いたい場面が出てきたので、Rails本体でのリリースまでどう乗り切ろうか調べていたところ、カスタムのタイプ(ActiveModel::Type::Valueクラスに類するクラス)を使えば望んでいることができそうなことがわかりました。
ActiveModel::Attributesではここでタイプの登録が行われており、attributeメソッドにはシンボル以外のタイプを渡すことが可能です。この仕組みを用いてカスタムタイプを用いるために、下記のようにActiveModel::ComposableTypeと名付けた小さなモジュールを定義しました。
ActiveModel::Attributesではここでタイプの登録が行われており、attributeメソッドにはシンボル以外のタイプを渡すことが可能です。この仕組みを用いてカスタムタイプを用いるために、下記のようにActiveModel::ComposableTypeと名付けた小さなモジュールを定義しました。
module ActiveModel module ComposableType def self.included(base) base.extend(ClassMethods) end module ClassMethods def type = :composable_type def [] item_class = self Class.new(ArrayType) do self.item_class = item_class end end def cast(value) if value.nil? nil elsif value.instance_of?(self) value else new(value) end end def assert_valid_value(_); end end end module ComposableType class ArrayType < Array include ActiveModel::ComposableType class << self attr_accessor :item_class def type = :composable_array_type end def initialize(items) super(items.map { |item| self.class.item_class.cast(item) }) end end end end
このモジュールを下記のようにカスタムタイプとしてふるまわせたいモデルクラスにincludeして使用します。
class Cat include ActiveModel::Model include ActiveModel::Attributes include ActiveModel::ComposableType # <= attribute :name, :string attribute :age, :integer end class Address include ActiveModel::Model include ActiveModel::Attributes include ActiveModel::ComposableType # <= attribute :country, :string attribute :city, :string end class Person include ActiveModel::Model include ActiveModel::Attributes attribute :name, :string attribute :address, Address, default: Address.new(country: "Unknown", city: "Unknown") attribute :pets, Cat[], default: [] end alice = Person.new( name: "Alice", address: { country: "USA", city: "NY" }, pets: [ { name: "Mimi", age: 3 }, { name: "Kitty", age: 2 } ] ) alice.pets.first #=> #<Pet:... alice.pets.first.name #=> "Mimi"
あくまでRails本体の機能がリリースされるまでのつなぎではあるのですが、少ない行数で実装できたのでしばらくはこれで乗り切れそうです。
Updated at