Help us understand the problem. What is going on with this article?

DDDを.NETでやるならドメイン層はF#が良いんじゃないか、という話

More than 1 year has passed since last update.

動機付け

お勉強のために.NETでDDDっぽく開発していたらドメイン層はF#で記述したほうが(C#より)より良いんじゃないかぁと思ったので投稿してみます。とんちんかんなこと書いてたらすいません。
以下、良いところ。

値オブジェクトの作成が便利

値オブジェクトって不変だったり等価性だったり交換可能性がないとダメじゃないですか。
C#でこれを担保しようと思うと値オブジェクトの実装に記載されているValueObjectを継承するかオレオレでEqualsをオーバーライドしないといけないから実装が億劫になるんですよね。
上記リンクに載っているAddressを例にすると

値オブジェクトの実装のValueObject引用
public abstract class ValueObject
{
    protected static bool EqualOperator(ValueObject left, ValueObject right)
    {
        if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))
        {
            return false;
        }
        return ReferenceEquals(left, null) || left.Equals(right);
    }

    protected static bool NotEqualOperator(ValueObject left, ValueObject right)
    {
        return !(EqualOperator(left, right));
    }

    protected abstract IEnumerable<object> GetAtomicValues();

    public override bool Equals(object obj)
    {
        if (obj == null || obj.GetType() != GetType())
        {
            return false;
        }

        ValueObject other = (ValueObject)obj;
        IEnumerator<object> thisValues = GetAtomicValues().GetEnumerator();
        IEnumerator<object> otherValues = other.GetAtomicValues().GetEnumerator();
        while (thisValues.MoveNext() && otherValues.MoveNext())
        {
            if (ReferenceEquals(thisValues.Current, null) ^
                ReferenceEquals(otherValues.Current, null))
            {
                return false;
            }

            if (thisValues.Current != null &&
                !thisValues.Current.Equals(otherValues.Current))
            {
                return false;
            }
        }
        return !thisValues.MoveNext() && !otherValues.MoveNext();
    }

    public override int GetHashCode()
    {
        return GetAtomicValues()
         .Select(x => x != null ? x.GetHashCode() : 0)
         .Aggregate((x, y) => x ^ y);
    }        
    // Other utilility methods
}
値オブジェクトの実装のAddress引用
public class Address : ValueObject
{
    public String Street { get; }
    public String City { get; }
    public String State { get; }
    public String Country { get; }
    public String ZipCode { get; }

    private Address() { }

    public Address(string street, string city, string state, string country, string zipcode)
    {
        Street = street;
        City = city;
        State = state;
        Country = country;
        ZipCode = zipcode;
    }

    protected override IEnumerable<object> GetAtomicValues()
    {
        // Using a yield return statement to return each element one at a time
        yield return Street;
        yield return City;
        yield return State;
        yield return Country;
        yield return ZipCode;
    }
}

これがF#だとレコードで書けるんですよ。

F#のレコード型で書いた場合
type Address =
    {
        Street: string
        City: string
        State: string
        Country: string
        ZipCode: string
    }

これをC#側で見てみると

F#で書いたAddressをC#側で参照した内容
    [CompilationMapping(SourceConstructFlags.RecordType)]
    public sealed class Address : IEquatable<Address>, IStructuralEquatable, IComparable<Address>, IComparable, IStructuralComparable
    {
        public Address(string street, string city, string state, string country, string zipCode);

        [CompilationMapping(SourceConstructFlags.Field, 0)]
        public string Street { get; }
        [CompilationMapping(SourceConstructFlags.Field, 1)]
        public string City { get; }
        [CompilationMapping(SourceConstructFlags.Field, 2)]
        public string State { get; }
        [CompilationMapping(SourceConstructFlags.Field, 3)]
        public string Country { get; }
        [CompilationMapping(SourceConstructFlags.Field, 4)]
        public string ZipCode { get; }

        [CompilerGenerated]
        public sealed override int CompareTo(Address obj);
        [CompilerGenerated]
        public sealed override int CompareTo(object obj);
        [CompilerGenerated]
        public sealed override int CompareTo(object obj, IComparer comp);
        [CompilerGenerated]
        public sealed override bool Equals(object obj, IEqualityComparer comp);
        [CompilerGenerated]
        public sealed override bool Equals(Address obj);
        [CompilerGenerated]
        public sealed override bool Equals(object obj);
        [CompilerGenerated]
        public sealed override int GetHashCode(IEqualityComparer comp);
        [CompilerGenerated]
        public sealed override int GetHashCode();
        [CompilerGenerated]
        public override string ToString();
    }

と等価性についてがっつり実装してくれてます。コンストラクタも自動で生成してくれてます。レコード型でもメソッドやプロパティを追加できるのでクラスのように使えます。

ドメイン層は関数型言語と相性が良い(気がする)

ドメイン層には業務知識を記述するんですけどたいてい、「~が~したら~する」ってなるはずなので入力と出力がしっかりしている関数型言語で記述したほうがより良いのかなぁと思いました。特に便利だと思ったのがunitignoreですね。戻り値がunitだと副作用があるとわかるしignoreが記述されていると設計が臭い気がします。

実装の教本として「リーダブルコード」「新装版 リファクタリング 既存のコードを安全に改善する」を読んでるんですが、メソッドは副作用なく簡潔で一度に一つのことをやるべき旨が書かれているので、そう書きやすい関数型言語がおすすめです。

全てがF#でなくて良い

データの入出力部や画面の入出力部のように例外処理やnullと付き合っていかないといけない場所はF#だと面倒くさいので、そこはC#なりVBで記述したほうが良いと思います。DIP(依存性逆転の原則)で作成しておけばドメイン層にはインターフェースだけ記述すれば良いわけですし。これは.NETの良いところですね。

所感

もちろんC#が駄目って話ではないです。より良いって感じです。
F#だとだらだらと手続き的に書きづらいところが逆に良いところかなと思いました。

kwhrkzk
業務系に従事。WPF,PRISM,C#,F#,.NET Core,Laravel,Vue,TypeScript,DDD,CQRS,SOLIDらへんに興味有。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away