LoginSignup
13
6

More than 1 year has passed since last update.

理解するのが難しいシステムをドメインモデリングによって生産性をあげた話

Posted at

Azit株式会社でバックエンドエンジニアとして機能開発の他にDDDの導入やリファクタリングをしている鈴木です。

私が弊社で仕事をするようになってから半年たちましたので、ミノ駆動さんとQiitaさんのキャンペーンを利用して、私がおこなったことを振り返りたいと思います。

システムの説明

最初に開発しているシステムについて説明をします。
ラストワンマイルの配送に特化しているシステムを開発しています。
荷物の配送を実施してくださる配達パートナーと、配送の依頼をしてくださるクライアントという2つの種類のユーザーを扱う配送サービスです。

配達パートナーさんは以下の3種類にわかれます

  • ギグワーカーとして単発の案件を実施してくださる方々
  • 私たちと連携してクライアントさん専属のシフトに入ってくださる方々
  • クライアントさんが直接管理しておられ我々のシステムを通して稼動している方々

また、配送については以下のようなフィルタリングがあります。

  • クライアントさんが自社で管理されている方々と弊社からの専属の方々両方に送りたいといった場合や、逆に自社で管理されておられる方のみに送りたいといった、「誰に送りたいか」といったグループでのフィルタリング
  • 「冷蔵可能」のような特定のスキルを持った方にだけ送りたいといった配達パートナーの方のスキルでのフィルタリング

他にもルートを最適化して配送するなどの複雑な機能があります。
このため、ドメインモデリングをしながら、DDDの考え方を生かした実装をしていくやりかたにマッチする複雑度が高いシステムと言えると思います。

今回実施した内容について

今回は次の3つのことを実施しました

  • 用語を理解するのが難しかったのでユビキタス言語を定着させた
  • 何をするのかが名前からでは理解できないFormクラスのリファクタリング
  • システムの全体を理解できるようにするためにコンテキストマップを作成した

次にそれぞれの詳細です。

用語を理解するのが難しかったのでユビキタス言語を定着させた

おこなった理由

私たちが開発しているシステムは、配送システムの中でも配達パートナーとクライアントの間の配送というラストワンマイルに特化した配送プラットフォームなのでシステム特有の用語があります。
例えば、配送料金の体型を表す時間立て時間立や件立てなどの普段聞かない名前や停車時間という一般的な名前ですが、いろいろな意味が含まれているものもありました。
そのため、開発に参加したときはシステムのキャッチアップが大変でした。

解決策

システムにに関する用語を理解するには、ビジネスチームとエンジニア間で共通で使用するユビキタス言語を見て理解するのがいいと思いますので、ユビキタス言語集をみてましたがそのユビキタス言語集は以前作成したことはあるが、放置されている状態でした。

そこで、システムの理解と並行する形でユビキタス言語をアップデートすることになりました。

ユビキタス言語は放置されているものの、プロジェクトに関する仕様書を作成するという文化が私がチームに加わる前からしっかりとありました。

仕様書をみながら用語をまとめていき、コードには書いてあるがドキュメントには書いていないことは、ビジネスチームと頻繁にやりとりをしているCTOに質問しながら用語集を作成していき一通り必要なことを網羅してある用語集を作成することができました。
このユビキタス言語集を作成していく中で、システムに関する知識ができたのも非常に大きいと思います。

作成したユビキタス言語は以下のようにNotion上にまとめてあります。
ユビキタス言語.png

ユビキタス言語を作成していくのと並行して、会社のビジネスモデルを理解するために部署間のリーダー同士のMTGにも参加して、ビジネスについても理解することができたので会社で使われている用語について理解することができました。

ユビキタス言語の更新をしたことで、エンジニア間で話すときでもユビキタス言語で話をして、ビジネスサイドが話していることもそのまま理解できたのがシステム開発をするときにすごい役に立っています。

何をするのかが名前からでは理解できないFormクラスのリファクタリング

おこなった理由

Formクラスを使用しているが、すでにメリットがほぼない状態

Formクラスはフロントエンドも含めてRails内で完結しているときは、すごい有用なパターンだと思います。
しかし、私がチームに参加したときはすでにReactを使用してる状況でした。
ReactのフォームとRailsのフォームクラスでは乖離が発生している状態でした。
そのため、Formクラスを使用していることのメリットが薄かったです。

Formクラスの名前から何をしているのかが理解することはできない

Formクラスの命名パターンはActiveRecordを継承しているクラスのインナークラスとして定義してCRUDでの名前をつけるということが習慣化されていました。
例えば、配達業務を開始するときに実行するのが、DeliveryEntry::CreateForm です。
そのため、Formクラスがクラス名から何をしているのかがわからない状態でした。
ミノ駆動さんがおっしゃっている目的駆動名前設計からは程遠い状態でした。

Railsに依存していたのを修正

ドメインモデリングをしていることもあり、コードもドメインモデリングに適した形のDDDの戦術パターンのドメインサービスクラスにできるだけ近づけていこうという話をチームで決定しました。
しかし、FormクラスはRailsに依存していたのでリファクタリングをしました。

解決策

配達業務を開始するFormクラスのリファクタリング前のクラス

DeliveryEntoryは配達パートナーが配達している状態を表しているテーブルです。
保存する処理は代わりのコードで記述させていただきます。

Formクラス

class DeliveryEntry
  class CreateForm
    extend ActiveModel::Callbacks

    attr_reader :delivery_entry

    def initialize(delivery_partner)
      @delivery_partner = delivery_partner
    end

    define_model_callbacks :save

    after_save AfterSaveCallbacks

    def save!
      run_callbacks :save do
        ActiveRecord::Base.transaction do
		  save_hoge
          save_fuga
          save_piyo
        end

        delivery_entry
      end
    end

AfterSaveCallbacks

class DeliveryEntry
  class CreateForm
    class AfterSaveCallbacks
      class << self
        def after_save(create_form)
          new(create_form).after_save
        end
      end

      private_class_method :new

      def initialize(create_form)
        @create_form = create_form
      end

      def after_save
        メールなどに通知するジョブをスケジューリングする処理
      end
    end
  end
end

上記のコードは以下の問題点があると思います。

  • DeliveryEntry::CreateFormだと単純にDeliveryEntryを作成しているだけのようにに見える
    • しかし実際は集荷処理をするための準備をするという処理
  • AfterSaveCallbacksが何をしているのかが一切わからない

そのため、以下のリファクタリングをしました。

  • 配達ドメインの中の集荷(Pickup)の準備をするというユビキタス言語やドメインモデルに沿った名前にした
  • RailsのCallbacksクラスの依存をやめて、after_saveが実行する順番を明示的にした
module DeliveryDomain
  class GoingToPickupPreparer
    attr_reader :delivery_entry

    class << self
      def execute!(delivery_entry)
        new(create_form).after_save
      end
    end

    private_class_method :new

    def initialize(delivery_entry)
      @delivery_entry = delivery_entry
    end

    def execute!
      ActiveRecord::Base.transaction do
		  save_hoge
          save_fuga
          save_piyo
      end

      NotificationJobsScheduler.execute(self)

      delivery_entry
    end

このようなFormクラスが、多くあるため優先度の高い順からひとつずつリファクタリングをしていきました。これにより、クラスとユビキタス言語、ドメインモデリングが関連することになったので、クラス名をみることで、ドメインがなにをするのかがわかりやすくなったと思います。
(まだ、道半ばですが

システムの全体を理解できるようにするためにコンテキストマップを作成した

おこなった理由

開発しているシステムの機能は多いため、システムに存在するドメインがどれぐらいあるのかもチームに参加したてのころはわからなかったです。
また、それぞれどのような関係があるのかを理解した状態での優先づけしてリファクタリングすることができていなかったです。

解決策

実践ドメイン駆動設計の勉強会でコンテキストマップを作成しました

弊社では、ソフトウェア設計に関する読書会をしていて、ちょうど改善したいと思ったタイミングで、実践ドメイン駆動設計の読書会をおこないながらDDDの理解を深めておりました。
実践ドメイン駆動設計の章末の問題で自分たちが開発しているシステムのドメインを考えたり、コンテキストマップを作成するという問題がありチームで挑戦することをしました。

その中で作成したコンテキストマップです
コンテキストマップ.png

コンテキストマップを作成していく中で思ったこと

図を作成していく事自体も有益ですが、チームでディスカッションをしながらお互いの認識を揃ることができたので、ドメインについて同じ認識を持っていることが設計について話すときもコードレビューをするときに役立っています。

コンテキストマップに従ってドメインモジュールを作成しています

ビジネスに沿ったドメインモデルに従うためには作成したコンテキストマップに従ってモジュールを作っていったほうがいいと判断したため、現在はドメインクラスを使用したドメインモジュールに移行するリファクタリングに挑戦しております。

Domainクラスは、DomainのしたにContextを定義して、そのクラスにコンテキスト内にドメイン知識の実装をしています。
ActiveRecordはドメインの知識をもたないRDB上のテーブルについての知識や全ドメインに共通することだけの知識を持つようにしています

ディレクトリ構成は以下のようになります

domains
  delivery_domain
    delivery_context POROとして構成されているドメインクラス
    match_context (PORO)

models
  delivery_partner (ActiveRecordを継承したクラス)
  delivery_entry (ActiveRecordを継承したクラス)

最後に

今のチームに私が参加してから、おこなった設計の改善点は次の3つです。

  • 用語を理解するのが難しかったのでユビキタス言語を定着させた
  • 何をするのかが名前からでは理解できないFormクラスのリファクタリング
  • システムの全体を理解できるようにするためにコンテキストマップを作成した

これのことをしたことにより、要件定義や設計と実際のコードでユビキタス言語を意識した開発をすることができています。
システムの特性上より多くのクライアントのニーズに答えていく必要がありますが、ビジネスモデリングをしたうえで設計、実装をすることで後々大きな問題になる負債を抱えずに開発していくことができると思っています。

CTOが書いた記事も読んでいただけると改善してきたことについてより知ることができますので、弊社での開発についてもっと知りたい方はよんでいただけると嬉しいです。

記事を最後までみていただきありがとうございました!

13
6
0

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
13
6