ピンポイントタイム散布(PTS)でのバックエンド改善の軌跡 ~7. Entityのインスタンス生成ロジックを文脈で変える ~

こんにちは、農業系プロダクトの開発を担当している糸井です。

弊社ではピンポイントタイム散布(以下、PTS)という防除のデジタル化サービスを展開しております。

www.optim.co.jp

PTSシステムのバックエンドではSpringBootを使用しております。
立ち上げ期より、既存の農業系プロダクトの課題感をもとに改善を進めてきましたので、その軌跡をご紹介します。

前回までの記事はこちら


前回の記事では、メンバー変数への再代入禁止に対する事例をご紹介しました。
今回はEntityのインスタンス生成ロジックについての改善事例をご紹介します。

7. Entityのインスタンス生成ロジックを文脈で変える

本題に入る前に、前提とPublicSetter撲滅運動の期待結果について触れておきます。

前提とPublicSetter撲滅運動の期待結果


Entityのインスタンス生成ロジックを文脈で変える必要性については、ドメイン駆動設計 モデリング/実装ガイド をご参照ください。

PTSではコーディング量を減らすための現実解として、 別の文脈(Domain) でも同じ Class(Entity) を使用するという方針を定めました。



上記を踏まえ、2つのルールを作りました。

  1. 境界付けられたコンテキストを適用するために文脈によってConstructorやインスタンス生成メソッドを書き分けること。
  2. ConstructorはMethodと違って意図や文脈を命名で伝えられないので、どの文脈で呼び出されるべきConstructorなのかをJavadocで必ず書くこと。

期待していたサンプル実装は以下のとおりです。

package entity;

@AllArgsConstructor
@Getter
public class User {
  private final String ulid;
  private final String name;
  private final List<MailAddress> mailAddressList;

  /**
   * メールアドレスを更新する場合に使用する
   *
   * @param beforeUpdateUser          DBから取得した更新前のUserインスタンス
   * @param mailAddressList           更新後のメールアドレス
   * @throws IllegalArgumentException beforeUpdateUserがnullの場合。mailAddressListが空配列の場合。
   */
  public User(
    final User beforeUpdateUser,
    final List<MailAddress> mailAddressList
  ) {
    if (Objects.isNull(beforeUpdateUser)) {
      throw new IllegalArgumentException("DBから取得した更新前のUserインスタンスを渡してください。")
    }

    if (mailAddressList.isEmpty()) {
      throw new IllegalArgumentException("mailAddressListは空に更新できません。")
    }

    this.ulid = beforeUpdateUser.getUlid();
    this.name = beforeUpdateUser.getName();
    this.mailAddressList = mailAddressList;
  }
}

PublicSetter撲滅運動から新たに発生した問題

DomainのEntityでConstructorやインスタンス生成メソッドを実装するのが手間なので、
Usecase側からAllArgsConstructorにnullを渡してEntityのインスタンスを生成するという実装が増えていきました。


上記の簡潔なサンプル実装からでは想像しにくいですが、実際はこんな感じです。

package entity;

@AllArgsConstructor
public class Hoge {
    // テーブルと一致するフィールド
    private BigInteger id;
    private String aaa;
    private BigInteger bbb;
    private Integer ccc;
    private String ddd;
    private BigInteger eee;
    private LocalDateTime fff;
    private BigInteger ggg;
    private LocalDateTime hhh;

    private LocalDate iii;

    private String jjj;
    private String kkk;
    private BigDecimal lll;
    private BigDecimal mmm;
    private String nnn;

    private List<Fuga> ooo;
package usecase;

public class Usecase1 {
  public void usecase1() {

    ...省略

    final var hoge = new Hoge(
        id,
        aaa,
        bbb,
        ccc,
        null, null, null, null, null, null, null, null, null, null, null, null
    );

    ...省略

  }
}


ドメイン貧血症を防ぐという目的を達成するための手段としてPublicSetter撲滅運動を展開しましたが、
目的を伝えきれておらず、手段が目的化してしまいました。
上記のような実装に対してはコーディングルールに沿って修正してもらうようにソースコードレビューで対処しています。

つづきはこちら (2025/4/04 追記)

ピンポイントタイム散布(PTS)でのバックエンド改善の軌跡 ~8. Testable ~ - OPTiM TECH BLOG

最後に

OPTiMではチームで協力し、難しく大きな課題を楽しみながらチャレンジしていきたいというメンバーを大募集しております。 ご興味がありましたら、下記フォームよりご応募ください。

www.optim.co.jp