https://zenn.dev/msksgm/articles/20220617-go-is-incompatible-to-ddd
以前、「ドメイン駆動設計入門」という本に掲載されている、戦術的 DDD の内容を C#から Go に置き換えることで戦術的 DDD を学んでいました。 色々、実装してみて戦術的 DDD のミニマムな実装(エンティティ、値オブジェクトなど)において「Go と DDD の相性が悪い」という考えが生まれたので、理由を記述していきます。 自分自身は凄腕 Gopher でもなければ、大規模サービスに DDD を導入した経験もないので、考察の浅い意見が含まれている可能性があります。 是非、コメントにてご指摘のほどお願いします。
先に結論を書くと、 「DDD で守りたい原則を Go の言語仕様で守ってくれる部分が少なく人の目で守らなければならないことが多い」 に尽きると考えています。 具体的な理由を記述します。
DDD では、値オブジェクトやエンティティを生成する際に、コンストラクタでプロパティのバリデーション(いわゆる完全コンストラクタ)を記述します。 生成した時点でオブジェクトがドメインルールを守ったオブジェクトであることが保証され、凝集度が高いコードになります。
public class UserName {
private String name;
public UserName(String name) {
if (name.trim().length() == 0) {
throw new IllegalArgumentException("名前を入力してください");
}
if (name.trim().length() > 10) {
throw new IllegalArgumentException("名前は10文字以内で入力してください");
}
this.name = name;
}
}
Go で同様のことを実装すると、コンストラクタを実装する必要があり、一般的には以下のような書き方になります。
type UserName struct {
Name string
}
func NewUserName(name string) ( *UserName, error) {
if utf8.RuneCountInString(strings.Trim(name, " ")) == 0 {
return nil, fmt.Errorf("名前を入力してください")
}
if utf8.RuneCountInString(strings.Trim(name, " ")) > 10 {
return nil, fmt.Errorf("名前は10文字以内で入力してください")
}
return UserName{Name: name}, nil
}
結果、Go でもコンストラクタを使えば、ドメインルールを守ったドメインオブジェクトを生成できます。
// 以下だったらOK
userName1, err := NewUserName("hoge")
if err != nil {
// ドメインルールを守っていないと、エラーハンドリング
}
しかし、コンストラクタの実装自体は言語で強制されているわけではなく、あくまで開発者が一般的に実装しがちなコンストラクタです。 そのため、以下みたいに直接構造体を生成したり、他のコンストラクタを定義することで、ドメインルールが破綻したドメインオブジェクトを生成できてしまいます。
他のコンストラクタを作成すること自体は Go 特有の問題ではありませんが、言語が用意しているコンストラクタが存在しないことから指摘しています。 自分も、リポジトリからドメインオブジェクト生成用のコンストラクタはバリデーションがないことは割と普通だと考えています。 その場合、Java(or Kotlin)であれば、リポジトリからドメインオブジェクト生成用のコンストラクタをユースケース層で使わせないために、ArchiUnit でチェックできます。しかし、Go で同様のことができるか確認できていないです(これは自身の調査不足です)。
簡単にドメインルールを破ったドメインオブジェクトを生成できてしまうので、DDD の原則に沿わなくなってしまいます。 結局、人の目で確認して「コンストラクタを使っていない」と指摘するしかありません。
理由 1 と関連しますが、コンストラクタのプロパティはほとんどの場合final
で値を固定にするかセッターを実装しません。
これは、ドメインオブジェクトのドメインルールを破られる可能性がなくすためのもので、開発者は安心して開発を進められます。
public class UserName {
private String name;
public UserName(String name) {
if (name.trim().length() == 0) {
throw new IllegalArgumentException("名前を入力してください");
}
if (name.trim().length() > 10) {
throw new IllegalArgumentException("名前は10文字以内で入力してください");
}
this.name = name;
}
// ゲッターは設定しても良い
public String Name() {
return this.name
}
// セッターは実装しないか、privateにして自己カプセル化で実装する(自己カプセル化もまずやらない)
// public void setName() {
// this.name = name
// }
}
しかし、Go ではfinal
といった変数を不変にする手段もなければ、メソッドを通さずに直接更新できます。
具体的にドメインルールを無視する過程をソースコードで確認します。以下は Go にセッターを実装していない実装です。