⚖️

リッチドメインモデルではrequiredを使うべきか?

に公開

はじめに

English version below.

C# 11でrequiredというキーワードが追加されました。オブジェクトをインスタンス化する時、requiredと決められたプロパティやフィールドに、値を入れない場合、コンパイラがエラーを出してくれます。

オブジェクトが正しく動作するために、必要な値を忘れないようにするための、とても良い言語機能です。しかし、ドメインモデルで使うにはあまり良くないと思って、振る舞いがほとんどなくデータだけを持つアネミックドメインモデルになりやすいためです。

使い方の例

class Person
{
    public required string Name { get; init; }
}

new Person(); // Nameをセットしなかったので、エラーになります

[Required]属性との違い

requiredキーワードは、System.Text.JsonASP.NETモデルバインディングで使われている[Required]属性と異なります。そのような属性は、普通は、コンパイル時よりも実行時に効果があって、使い方も異なります。この記事の後半でもっと説明します。

オブジェクト初期化子の問題は何ですか?

requiredのコンパイラ規則は、コンストラクタを使う時なく、オブジェクト初期化子を使う時だけで適用します。なぜこれが重要なのでしょうか?このコードを考えてみてください:

// 初期化子を使うバージョン
class Person
{
    public required string Name { get; init; }
    public required int Age { get; init; }
}

var tanaka = new Person()
{
    Name = "Tanaka",
    Age = 40
};

// コンストラクタを使うバージョン
class Person
{
    private readonly string _name;
    private readonly int _age;

    public Person(string name, int age)
    {
        _name = name;
        _age = age;
    }
}

// 初期化が終わると、明示的に公開されていない限り、
// Personオブジェクトのメンバーにはアクセスできません
var tanaka = new Person(name: "Tanaka", age: 40);

プロパティをrequiredにすると、突然それらのプロパティを公開にしなければならなくなります。公開に値を設定するためにセッターが必要なので、ゲッターも必ず必要になります。これはカプセル化を壊してしまいます。

非公開のセッターが使えないため、開発者は次のどちらかを選ばなければなりません:

  1. 変更可能なsetを使って、永遠にカプセル化を犠牲にする
  2. 将来に、特定の文脈で値を変更したくなるかもしれないがinitを使う

モデルはアネミックになってしまって、リッチドメインモデルとは逆です。DTOに似てきました。責任は曖昧です。

文脈こそが重要

文脈は主な点です。データの使い方や制限されることを知らないので、この状況でrequiredを使うのは駄目だと思います。ドメインのことを深く考えるのをやめさせるためです。

ドメインの値を別々に考えさせてしまいます。特定の値は意味がわかるために他の値が必要です。例えば、開始日と終了日です。requiredを使うと、その値たちは設定しなければなりませんが、値の関係や守るべきルールについては何も説明していません。理想的には、この場合、代わりに手作りのDateRangeという値オブジェクトを渡すようにすべきです。一緒に扱うべき値は、独自の概念としてモデル化したほうが良いと思います。

何を設定するかは文脈次第です。

解決策

ドメインモデルでは、このような場合、コンストラクタやファクトリーメソッドは役に立ちます。必ず渡す値や関連のある値を明確に決めることができます。requiredを使うのは良いと思いますが、DTOのような単純なタイプに使う場合に限ります。

個人的には、コンパイラは常に開発者にプログラムの全ての値を初期化させるべきです。C#で値を初期化するデフォルトルールは悪いと思っていて、危険な状況や状態を招くと感じます。その話題については、他の記事でさらに詳しく説明します。値を初期化するのを忘れやすく、よくバグが起こってしまいます。

私はnullableメンバーもデフォルトのままにせず、requiredにします。なぜか?そう、文脈のためです。開発者として、初期化時に何を設定するかは文脈によって決まると思います。何も言わずに値がデフォルトで設定されたり無視されたりすると、判断する力を失われてしまいます。時にはdefaultnullでいい場合もありますが、いつもそうとは限りません。DTOは単純であるべきなので、「適切なデフォルト値」を決めるのはDTOではなく、ドメインなど別の場所の役割だと思います。

requiredとコンストラクタを同時に使う

requiredとコンストラクタの両方を一緒に使うことも問題になります。以下のコードを考えてみてください:

class Person
{
    public required string Name { get; init; }
    public Person(string name) => Name = name;
}

new Person("Tanaka"); // CS9035

オブジェクトを作成すると、「CS9035 Required member 'Person.Name' must be set in the object initializer or attribute constructor」というエラーが発生されます。コンパイラはコンストラクタがすでに必須メンバーを初期化していることを認識していないため、このような状態になります。

コンストラクタに「SetsRequiredMembers」属性を付けると、コンパイラに「必須メンバーの初期化をコンストラクタに任せる」ことを通知します。しかし、その方法だと、コンパイラは参照型以外のメンバーの確認を行わないため、クラスに新しい必須メンバーを追加しても検出されません。参照型のメンバーを追加した場合は、コンパイラが以下の警告を出します:「warning CS8618: Non-nullable property 'AnotherProperty' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable」。この警告は、新しい値を初期化しなければならないことを示しています。一方、値型のメンバーの場合は警告が発生しません。

そのため、私の意見としては、この属性を使わないことをおすすめします。

すでにある仕組みと組み合わせること

一部のフレームワークは、実行時にコードを検査し、requiredの使用を独自の「Required」属性に変換するため注意が必要です。例えば、あるプロパティがコンパイル時に必須として設定されてから、System.Text.Jsonは何もを示せずに、[JsonRequired]属性を追加します。それで、デシリアライズ時に、ペイロードにそのプロパティが含まれていなければ、その仕組みが例外を発生させます。

私にはそれが設計の間違いかもしれません。なぜなら、requiredキーワードは値の初期化をコンパイル時に安全に保証するためのものであり、さまざまな仕組みの扱いや動作に影響を与えないほうが望ましいからです。将来の記事でこれについて触れるかもしれません。

幸い、EF Coreのエンティティでrequiredを使っても安全なはずです。EFの内部で何かをこっそり変更することもなく、データベースのカラムがNULL許容かどうかを決めるものでもなく、その設定はメンバーの定義によって決まります。EFはメンバーをリフレクションで設定し、それらがrequiredとマークされているかどうかを考慮しません。Microsoft Learnの公式ドキュメントでも、requiredを使うほうが推奨され、普通のnull!やその他の厄介なデフォルト値の問題を解決します。

ただし、この意見は、ドメインモデルとデータモデルを分けているときだけで、しかもデータモデルがアネミックな場合に限ります。最近、EF Coreは、requiredに頼らなくてもよいDDD式のカプセル化モデル作りを十分に支援しています。

まとめ

requiredキーワードは、システム内のデータのつながりと正しさを守る「赤い糸」のように役立ちますが、厳しいドメインモデルには適しません。オブジェクト初期化子の使用を促すため、アネミックモデルやカプセル化の欠如を招きやすいからです。

それに、既存の仕組みと併用しにくく、追加設定も必要なため、開発者は採用を迷うでしょう。

DTOではrequiredを使うのが望ましく、ドメインモデルではコンストラクタを使うのがおすすめです。


Should you use the required keyword in your domain objects?

Introduction

C# 11 introduced the required keyword which forces you to initialize a field or property when creating the object, else you will get a compiler error. This is a good thing, as it ensures you don’t forget to initialize data that’s important for the object to function correctly. However, using it in your rich domain model is in my opinion not a great idea as it leads to an anemic model.

Example usage

class Person
{
    public required string Name { get; init; }
}

new Person(); // Fails to compile, Name must be set

Comparison with the [Required] attribute

The required keyword is notably different from the [Required] attribute offered by various frameworks such as System.Text.Json and ASP.NET model binding. These attributes typically enforce their rules at runtime as opposed to compile time, and they have different use cases than the required keyword. We’ll talk more about these later in this article.

The problem with object initializers

The required keyword is only enforced when using an object initializer, which is not the same as a constructor. Why does this matter? Consider this code:

// Version using object initializer
class Person
{
    public required string Name { get; init; }
    public required int Age { get; init; }
}

var tanaka = new Person()
{
    Name = "Tanaka",
    Age = 40
};

// Version using constructor
class Person
{
    private readonly string _name;
    private readonly int _age;

    public Person(string name, int age)
    {
        _name = name;
        _age = age;
    }
}

// After initialization, we have no access to anything
// on the Person object unless explicitly allowed
var tanaka = new Person(name: "Tanaka", age: 40);

If we mark our properties as required, suddenly these properties must be made public. Because the properties must have a setter in order to be set from outside the class, they must also have a getter. This breaks encapsulation as we might not want these properties accessible to the outside world.

Because you can’t use a private setter, you are forced to decide between having a mutable setter which completely disregards any attempt at encapsulation, forever, or you have to use init even if you’d like to be able to change the value in certain contexts.

Your model has become anemic, the opposite of a rich domain model. It’s starting to look more like a DTO. The lines are blurred.

Context is King

Context is the keyword here. Because you don’t know how the data is meant to be used or constrained, using the required keyword in this way is not a good idea. It discourages thinking in terms of the domain.

It encourages reasoning about values in isolation. Some values depend on others being present for the concept to make sense, e.g. a start and an end date. Having these properties as required means they must be set, sure, but it says nothing about how they relate to each other or what rules and invariants they must follow. Ideally in this case you would pass in a custom DateRange value object instead. Values that belong together are often better off being modelled as their own concept.

It also encourages not thinking about what data makes sense in what context. Sometimes you only want to initialize certain data, but not some other data. If you have to initialize everything, you lose this contextualization.

Solution

For this reason I opt towards using constructors and factory methods instead. This way I’m able to clearly decide what should be mandatory or even relevant in each specific context. Using required is great (and in my opinion essential), but only for simpler types like DTOs.

If it was up to me everything would be required by the compiler by default, because it reduces risk, and I consider C#’s defaulting behavior to be an anti-pattern (I’ll write more on this topic in another post). Forgetting to initialize a value is more often than not going to lead to a bug, and it’s extremely easy to forget to do so.

I even mark nullable members as required instead of letting them default. Why? You guessed it: context. Allowing any value to be silently ignored during construction takes power away from me as the developer to decide what should be set in this context. Sometimes it’s default or null, but not always. DTOs are meant to be simple — deciding on "reasonable defaults" and such belongs somewhere else, such as in the domain, in my opinion.

Using required and constructors together

Using both required and constructors together also poses a problem. Consider this code:

class Person
{
    public required string Name { get; init; }
    public Person(string name) => Name = name;
}

new Person("Tanaka"); // CS9035

Trying to construct the object will lead to the error CS9035 Required member 'Person.Name' must be set in the object initializer or attribute constructor. This happens because the compiler doesn’t know that the constructor has initialized the required property already.

You can add the [SetsRequiredMembers] attribute to the constructor to signal to the compiler that the constructor is now responsible for setting the required members. This is however not validated, so if you add another required member the compiler will not cover you unless it’s a reference type. If it is, you will get a warning warning CS8618: Non-nullable property 'AnotherProperty' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. If the member is a value type, you get no warning at all.

I would therefore advise against using required and constructors together.

Integration with existing frameworks

It’s important to note that some frameworks inspect your code at runtime and transform your use of required to additionally use their own internal [Required] attributes. This may lead to unexpected behavior. For example, when deserializing a payload with System.Text.Json where a member is marked as required, internally it will be marked as [JsonRequired], and if the value is not present, it will throw an exception.

In my opinion, this is a design mistake, because the required keyword is meant to be a compile time safety measure that affects how you instantiate your types. It should have no bearing on how the framework functions. I might talk more about this in a future post.

Luckily, using required in your Entity Framework Core entities should be safe. It forces you to set the values at compile time, but it doesn’t determine whether the column in the database should be nullable or not — that’s decided by the member’s nullability. EF sets the members using reflection and won’t care if they’re marked as required or not. The documentation even states that it’s preferable to use required because it solves the ugly defaulting with null! and the like.

This argument really only applies if you’re using separate domain- and persistence models, where the persistence model is anemic, however. EF Core has decent support for DDD style encapsulated models where one does not need to rely on the required keyword.

Conclusion

The required keyword is a great tool to keep the red thread between data flowing through your system intact, but it does not fit into a strict domain model. It encourages the use of object initializers which leads to anemic models and a lack of encapsulation.

It also doesn’t play nice with some existing frameworks without additional configuration, which I surmise is why developers are hesitant to adopt it.

Use required in your DTOs and use constructors in your domain objects.

Discussion