カラリアを支える「同梱」の仕組みを統一した話

こんにちは。ロジスティクス開発のかんたろうです。

この記事では、カラリアを支えている多様な同梱の仕組みを、物流管理システム側で統一的に扱えるようにした事例を紹介します。

既存の仕組みを統合するためにRubyメタプログラミングなどの柔軟性を活用しているので、興味がある方はご一読ください。

カラリアにおける多様な「同梱」

カラリアにはいくつかの同梱の仕組みがあり、それらが最終的にお客様にお届けする物を決定しています。

参考までに、カラリアの香りの定期便で発送されるものをFAQから引用します。

香りの定期便新規ご注文の場合

【香水のアトマイザーをご注文の場合】

  • ご注文商品のアトマイザー(香水が入ったスプレー状の入れ物)
  • アトマイザーケース1本
  • 紙筒(2item・3itemプランの方のみ)
  • アイテムカード
  • 使用上の注意
  • その他キャンペーンチラシ

定期便や単品購入時のお届け内容について教えてください。 – カラリア よくある質問

引用元では、この内容以外に定期便を更新した場合、単品購入した場合、小分けルームフレグランスを購入した場合と説明が続いています。

これらを見てみると、注文の内容によって発送されるものが細かく違うことが分かりますが、それらはこれから説明する同梱の仕組みで実現されています。

物流管理システムによって管理されている同梱に絞ると、大きく分けて以下の三つに絞られます。

  • キャンペーンなどの施策による同梱
  • 資材の同梱
  • セット売りのための同梱

キャンペーンなどの施策による同梱

特定のキャンペーンに該当する注文に対する同梱です。

カラリアの物流システムでは、キャンペーンに対応する同梱物をデータで管理しており、それを参照して同梱を行なっています。

資材の同梱

カラリアでは、注文品のほかに使用上の注意やアトマイザーを収納する紙製のケースなどを発送しています。これらはECサービス側では管理されておらず、物流システム側で同梱の設定が行われています。

この設定はデータ化されておらず、コード上にロジックが実装されています。

セット売りのための同梱

カラリアでは香水を注文されたお客様にアイテムカードを発送しています。このアイテムカードと香水の対応関係はデータ上で表現されており、本記事では「販売セット」と呼びます。

これは仕組みとして異なるので資材同梱と区別されていますが、同梱の性質としては資材とあまり変わりがありません。同梱の設定をデータとして持っておくことでメンテナンス性が高まる場合はこちらの仕組みを使うというケースが多いです。

ここまでに紹介した三つの同梱の仕組みは、それぞれ受注モジュール内で分散していました。

次章ではそれらを一つにまとめる方法について解説します。

同梱をまとめて扱う仕組み

これらの仕組みをまとめて扱うために、標準的な同梱物の決定プロセスを定義しました。

既存の仕組みを精査した結果、同梱物を決定するために必要な要素は以下のようになることが分かりました。

  • 注文された商品
  • 商品の販売形態
  • 注文について有効なキャンペーン

商品の販売形態には、それが定期便によって受注したものなのか、単品注文によって受注したものなのか、「オプション」機能によって受注したものなのかという情報が入っています。これは販売形態によって同梱する資材が変わる場合があるため必要になる情報ですが、詳細は割愛します。

上記で得られた情報をもとに、同梱を扱うより抽象的なクラスを作成しました。

注文内容から同梱物を判断して発送物に加える「同梱ルール」と、複数の同梱ルールをまとめる「バンドラー」です。

バンドラーと同梱ルールの関係

画像には例示として「使用上の注意」と「紙筒」の同梱ルールを記載しましたが、同梱ルールはこのように商材ごとに作っています。

こうすることで、特定の商材の同梱条件を追加・変更・削除する場合に、変更範囲を一つの同梱ルールに留めることができます。

既存の仕組みを同梱ルールに適合させる

バンドラーと同梱ルールの導入によって、同梱を一元的に扱う準備が整ったので、既存のロジックをこの仕組みに押し込んでいきます。

一番最初に取り組んだのは「資材の同梱」です。これらは以前から注文内容をチェックして同梱を行うように実装されていたので、受注サービス内に存在していたそのロジックを同梱ルールに書き換えます。

これにより、受注サービス内にあった同梱用のプライベートメソッドが、それぞれ1つのクラスになり、適切な凝集性が確保されました。

次に「キャンペーン施策による同梱」をこの仕組みに乗せます。これまでは施策ごとに設定された同梱物のデータを参照して同梱物を決めていましたが、そのロジックを同梱ルールとして実装し直す必要があります。

しかし、キャンペーン施策に対応する同梱物のセットはデータとして存在するため、キャンペーンごとに同梱ルールの実装を行うのは筋が良くありません。キャンペーンのデータが増えるごとに実装を増やしていてはキャンペーン実施のスピードが落ちてしまいます。これではデータでキャンペーンを表現して管理画面からの操作を可能にした意味が失われてしまいます。

そこで、キャンペーンに対応する同梱物を表現するActiveRecordモデルから、同梱ルールの実装を生成することにしました。

具体的には、Class#new を利用することで同梱ルールのサブクラスを生成しています。

module Logistics
    module Bundle
        class BundleUnit < Logistics::ApplicationRecord
            # 省略

            def bundle_rule
              bundle_unit = self
              Class.new(::Logistics::Bundle::BundleRule) do
                define_method :apply do |_order, bundle_items: []|
                  bundle_unit.bundle_unit_details.each do |bundle_unit_detail|
                    bundle_items << ::Logistics::Bundle::BundleItem.new(sku: bundle_unit_detail.sku, quantity: bundle_unit_detail.quantity)
                  end
                  bundle_items
                end
              end
            end
        end
    end
end

バンドラーは同梱ルールの配列から初期化されるので、その配列にBundleUnit から生成される同梱ルールを加えてあげることで、新しい同梱の仕組みに統合することができます。

module Logistics
  module Bundle
    class Bundler
            # 省略

      def self.build(bundle_units = [])
        Bundler.new(
          [
            PrecautionBundleRule.new,
                        # 中略
            *bundle_units.map { |bundle_unit| bundle_unit.bundle_rule.new }
          ]
        )
      end
        end
    end
end

このようにして、キャンペーン施策による同梱も新しい仕組みに乗せることができました。

最後に「セット売りのための同梱」を同梱ルールの仕組みに統合します。

こちらは「販売ユニット」というモデルによって管理されていると述べましたが、この仕組みを維持するのは難しいという判断を下したため、これを解体するところから始めました。

まず「販売ユニット」のレコード数は膨大に存在しましたが、それらを分類すると数種類しか存在しないことが分かりました。これらはルールベースで表現できるものだったので、同梱ルールの一つとして実装し直しました。

しかし、中には同梱ルールとして不安定で、クラスとして実装してしまうと今後のメンテナンスコストが増加する恐れのあるものも存在していました。商材が次々に追加されていく環境では、このような水物の仕様が現れるのはよくあることなので、それらをエンジニアリングリソースなしで管理する方法が必要だと判断しました。

その結果、特定の商材に対してこの商材を同梱するというルールをデータとして保持することになりました。

このルールも同梱ルールの仕組みに統合する必要がありますが、先ほど紹介した「キャンペーン施策による同梱」と同様に、同梱ルールの実装を動的に生成することで実現しました。

「同梱ルール」に移行して得られたメリット

この仕組みに移行したことでいくつかのメリットが得られました。

受注サービスが同梱に対する変更の影響を受けなくなった

同梱の責務を同梱ルールに委譲したことにより、受注サービスが同梱に関する変更の影響を受けなくなりました。

これにより同梱に対する変更の心理的な安全性が上がり、開発メンバーとしては助かっています。

また同梱への変更は受注への変更よりも比較的頻度が高いため、適切な凝集度に近づいたと考えています。

同梱要件の追加が容易になった

同梱に必要な情報を整理して標準的な形式を生み出したことで、同梱の要件が増えた場合の対応が比較的容易になりました。

この記事では三種類の同梱の統合を一度に紹介しましたが、実際には時間軸上の幅があります。特に「販売ユニット」の解体は、この仕組みを導入してから長い期間を経てからのことです。

販売ユニットを解体する際、それらが持っていた同梱の仕組みをどこに収めるかが議論の焦点でしたが、拡張に対して開いている仕組みがあったおかげで、スムーズに実装の方針を決めることができました。

これらのメリットの根底には、各コンポーネントの適切な凝集度とオープン・クローズドの原則があると思います。これらの概念を意識して設計できていたわけではありませんが、事後的に「なぜ良かったか」を振り返ると、そこに古典的なノウハウが見えてくるのはジュニアエンジニアとして興味深いです。

おわりに

今回は、カラリアの物流システムにバラバラに存在した同梱の仕組みをまとめた話を紹介しました。

ECと物流の全体最適化の問題に社内で取り組めるのは、物流管理システムを自社開発している環境のメリットだと思います。内製の理由については以下の記事で紹介されているので、読んでいただけると嬉しいです。

tech.high-link.co.jp

herp.careers