読者です 読者をやめる 読者になる 読者になる

No more Death March

あるSEのチラシの裏 C# WPF

インターフェースを考える(8)判定処理と拡張メソッド

ISpecificationの組み合わせ方について
これまで以下のような実装をしていました。

using System.Linq;
using System.Collections.Generic;

namespace Nmdm.Specifications
{
    public sealed class AndSpecification<T> : ISpecification<T>
    {
        public AndSpecification(IEnumerable<ISpecification<T>> specs)
        {
            this.Specifications = specs;
        }

        private IEnumerable<ISpecification<T>> Specifications { get; }

        public bool IsSatisfiedBy(T obj)
        {
            if (obj == null) return false;
            if (this.Specifications == null) return false;
            if (this.Specifications.Count() <= 0) return false;
            foreach (var specification in this.Specifications)
            {
                if (!specification.IsSatisfiedBy(obj)) return false;
            }
            return true;
        }
    }
}

が、この実装だと気になる点がいくつかある。
・処理が妙にくどい、複雑
・クライアントコードにListやArrayの使用を強制してしまう。たった二つの評価であってもコレクションにISpecificationを設定する必要がある。
・DDDのSpecificationパターンのように流れるような記述(A And B みたいな)が出来ず可読性が損なわれる。

かといってDDDのようにクラスを継承させる形はとりたくない。

すこし手直ししてみる。
まずはこのクラス自体、コンストラクタでIEnumerableを受け取らず、シンプルに二つのISpecificationの実装クラスを受け取るようにする。

namespace Nmdm.Specifications
{
    public sealed class AndSpecification<T> : ISpecification<T>
    {
        public AndSpecification(ISpecification<T> one,ISpecification<T> other)
        {
            this.One = one ?? new SpecificationIsNotSatisfied<T>();
            this.Other = other ?? new SpecificationIsNotSatisfied<T>();
        }

        private ISpecification<T> One { get; }
        private ISpecification<T> Other { get; }

        public bool IsSatisfiedBy(T obj)
        {
            return this.One.IsSatisfiedBy(obj) && this.Other.IsSatisfiedBy(obj);
        }
    }
}

コンストラクタ内ではIsSatisfiedByメソッド内でのnullチェックを排除するためnullが渡されたらSpecificationIsNotSatisfiedを代入、
実装は以下の通り。

namespace Nmdm.Specifications
{
    public sealed class SpecificationIsNotSatisfied<T> : ISpecification<T>
    {
        public bool IsSatisfiedBy(T obj)
        {
            return false;
        }
    }
}

OrSpecificationクラスも同様に手直し

namespace Nmdm.Specifications
{
    public sealed class OrSpecification<T> : ISpecification<T>
    {
        public OrSpecification(ISpecification<T> one, ISpecification<T> other)
        {
            this.One = one ?? new SpecificationIsNotSatisfied<T>();
            this.Other = other ?? new SpecificationIsNotSatisfied<T>();
        }

        private ISpecification<T> One { get; }
        private ISpecification<T> Other { get; }

        public bool IsSatisfiedBy(T obj)
        {
            return this.One.IsSatisfiedBy(obj) || this.Other.IsSatisfiedBy(obj);
        }
    }
}

以上でAnd、Orの論理演算を実現するクラスは出来たけども、
じゃあこれを使う時に毎回New AndSpecificationなんていう記述はしたくない、
ただのAnd演算をするだけなのに毎回20文字以上ソースコードが増えてしまう。

そこでISpecificationインターフェースの拡張メソッドを用意する。

namespace Nmdm.Specifications
{
    public static class AndSpecificationExtension
    {
        public static ISpecification<T> And<T>(this ISpecification<T> one, ISpecification<T> other)
        {
            return new AndSpecification<T>(one, other);
        }
    }
}
namespace Nmdm.Specifications
{
    public static class OrSpecificationExtension
    {
        public static ISpecification<T> Or<T>(this ISpecification<T> one, ISpecification<T> other)
        {
            return new OrSpecification<T>(one, other);
        }
    }
}

これで毎回New AndSpecificationとかNew OrSpecificationという記述をしなくても、
ISpecificationを実装したクラスであればAndまたはOrという記述でインスタンス同士をつなげることが出来、
冒頭で触れたListなど余計な変数を登場させる必要はなくなる。

ただ、拡張メソッドを用いるという解決策で考えなければいけないことがある。
・拡張メソッドはあくまでStaticメソッドのシンタックスシュガーであり、特定の実装クラスに依存することで処理の組み換えが出来なくなる。
・同じシグネチャで解決されるインスタンスメソッドがあればそちらが優先される。そのため、第三者に提供される型に対して拡張メソッドを追加するとライブラリの更新により意図しない動作に代わる可能性がある。
・安易に多用するとインテリセンスに毎回出てきてうっとおしい。

この実装に限って言うと、
・AndやOrといった論理演算が非常に汎用性が高い。
・ISpecification自体は自分で実装したインターフェースである。
・これらを除けば依存しているのはISpecificationインターフェースであり、実装に対する依存が無い。
ということになるので、これはこれでありなんじゃないかなぁ、と思う。