LoginSignup
21
14

More than 5 years have passed since last update.

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

Posted at

動機付け

お勉強のために.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#だとだらだらと手続き的に書きづらいところが逆に良いところかなと思いました。

21
14
2

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
21
14