インターフェースを考える(14)考えを整理(出来ていない)
C#実践開発手法のシングルメソッドインターフェースで色々書いてきたけども、
途中途中浮かんだ考えをだぁーっと箇条書きにしてみる。
C#実践開発手法 デザインパターンとSOLID原則によるアジャイルなコーディング
- 作者: ゲイリーマクリーンホール
- 出版社/メーカー: 日経BP社
- 発売日: 2015/06/25
- メディア: Kindle版
- この商品を含むブログを見る
- オブジェクト指向や〇〇パターンとかはツールであって目的ではないということ。
- 処理は小さく、抽象化されており、組み換え可能であることが望ましい。
- シンプルな処理であればあるほど、コードを修正する可能性は低くなる。
- 小さいクラスは設計・開発・テストのサイクルがシンプルで作業を理解しやすい。
- コードに修正の余地が無ければ、たった一つでも仕事をしていれば資産と言える。
- コードを修正して機能を増やすことよりクラスを増やして機能を増やしていく。
- クラス数が増えるのは問題ではない、増えたクラスがライブラリや名前空間で整理されていないのが問題。
- 複数の処理を持っているクラスはその分コードを修正する可能性が高くなる。
- 修正の可能性が低いコードほど堅牢で修正の可能性が高いコードほど脆弱なコードになる。
- 10の機能を持った1つのクラスを作るなら、1つの機能を持ったクラスを10個つくれば良い。
- 具体的な処理は不変であり、コラボレーション部分のみが修正の対象となれば良い。
- 分岐を目的としたパラメータを持つメソッドはパターン毎にメソッドを分割出来るはず。
- 条件分岐はパターン毎の処理を実行するという責務と、パターン毎に振り分けを行うという責務に分離する。
- シングルメソッドインターフェースは構造化プログラミングとOOPのポリモーフィズムのおいしいとこ取りをしている。
- 複数の処理を持ちたければ振る舞いクラスに委譲すれば良い。
- インターフェースのみに依存する場合、またはクラスに依存していても行き着く依存先がインターフェースのみであれば静的クラスの利用も悪くない。
- 何かを呼び出して戻り値を受け取る処理はまた別ななにかを呼び出して依存する。戻り値が必要無いように組み替えれば余計な依存関係は生まれない。
- プログラムの仕様変更が入った時のことを想像する。それがそのまま責務を分割する糸口になる場合がある。
- 思い切ってクラスの継承は無いものと考えた方が設計が捗る。
インターフェースを考える(13)コレクションとインターフェース
IActionやISpecificationなど、単一の要素に対するインターフェースでしたが、
実際にアプリケーションを組むとなると必ずコレクションに対する操作が必要になると思います。
今回は拡張メソッドを使って、都度コーディングをしなくてもこれらインターフェースの処理を使い回す方法を考えます。
まずIActionについて、以下のような拡張メソッドを用意しました。
public static void DoEach<T>(this IEnumerable<T> actors,IAction<T> action) { new List<T>(actors).ForEach(x => x.Do(action)); } public static IEnumerable<T> DoEachFluent<T>(this IEnumerable<T> actors, IAction<T> action) { actors.DoEach(action); return new List<T>(actors); }
やっていることは単純でListクラスのForEachメソッドにIActionを実行する匿名メソッドを指定しているだけです。
同じようにISpecificationを使った拡張メソッドをIEnumerableに追加します。
using System.Collections.Generic; namespace Nmdm.Specifications { public static class ISpecificationExtension { public static IEnumerable<T> FindAll<T>(this IEnumerable<T> collection,ISpecification<T> spec) { return new List<T>(collection).FindAll(x => spec.IsSatisfiedBy(x)); } } }
こちらも同じ要領でListのFindAllメソッドにboolを返す匿名メソッドを指定しているだけ。
どちらもやっていることはデリゲートを使うメソッドをインターフェースに適用させてるだけですね。
C#実践開発手法でもコードに適用力を持たせるならデリゲートよりクラスですよと書いてあるけど、
それなら標準のデリゲートと同じようにインターフェースも用意してあれば良かったのになぁ・・・
インターフェースを考える(12)ActionとActor
IActionインターフェースについて思いついたことがあるので書いてみます。
まずIActionインターフェース
namespace Nmdm.Actions { public interface IAction<TContext> { void Do(TContext context); } }
機能面だけで見ると一つの引数を受け取って戻り値の無いメソッドを実行するインターフェースですが。
これを少し見方を変えて「TContextが行う動作」とすると、以下の拡張メソッドでIActionをTContextのインスタンスメソッドのように呼び出せる。
namespace Nmdm.Actions { public static class IActionExtension { public static void Do<T>(this T actor, IAction<T> action) { action.Do(actor); } } }
これのクライアントコードの例は以下の通り
// IActionのパラメータになるクラス var actor = new object(); // IActionのNullオブジェクト var action = new NullAction<object>(); // IActionにパラメータを渡して実行する場合。 action.Do(actor); // actorのインスタンスメソッドに見せかけて実行する場合。 actor.Do(action);
メリット(?)を考える。
・あたかもactorが主体となって処理を実行するので視覚的にオブジェクト指向チックに見える。
・actorの振る舞いを別クラスで実装出来るのでactor自身の肥大化が防げる。
(ただし、actorの状態自体を書き換えるのであればactor自身にSetterのようなものが必要)
・IAction自体は組み換えしやすい。特に複雑な動作が求められる場合に有利?
うーん・・・処理とふるまいが一体化するっていうところからかなり逸脱してるけども・・・
とにかくクラスを細分化するっていうところだけ突き詰めていくとこういう書き方もありなのか・・・?
これをもう少し手を入れて「流暢なインターフェース」にするのであれば、
拡張メソッドにactor自身を返すメソッドDoFluentを追加する。
namespace Nmdm.Actions { public static class IActionExtension { public static void Do<T>(this T actor, IAction<T> action) { action.Do(actor); } public static T DoFluent<T>(this T actor, IAction<T> action) { actor.Do(action); return actor; } } }
このDoFluentの呼び出し例は以下の通り。
// 流暢な記述
actor
.DoFluent(action)
.DoFluent(action)
.DoFluent(action);
インターフェースを考える(11)IActionを手直し
ISpecification同様IActionを手直し+拡張メソッドを導入
まずCompositeActionクラス
namespace Nmdm.Actions { public sealed class CompositeAction<TContext> : IAction<TContext> { public CompositeAction(IAction<TContext> one, IAction<TContext> other) { this.One = one ?? new NullAction<TContext>(); this.Other = other ?? new NullAction<TContext>(); } private IAction<TContext> One { get; } private IAction<TContext> Other { get; } public void Do(TContext context) { this.One.Do(context); this.Other.Do(context); } } }
これの拡張メソッドが以下の通り
namespace Nmdm.Actions { public static class CompositeActionExtension { public static IAction<TContext> Add<TContext>(this IAction<TContext> one,IAction<TContext> other) { return new CompositeAction<TContext>(one, other); } } }
考え方はISpecificationで実装した拡張メソッドと同じか、
前にCompositeActionを書いた時は無理やり不変+流暢にするため、CompositeActionにAddメソッドを持たせたけど、
やはりあの書き方だとクラスが無駄に複雑になってましたね。
あくまで「書き方」に留まるレベルなら無理にクラスの実装をこねくり回すより、
拡張メソッドに委譲した方が楽ですね。
インターフェースを考える(10)ISpecificationのテスト
この記事では以下の記事で実装したクラスをテストしてみて、クライアントコードの具合を考えてみます。
記事の順序やらテストのタイミングがぐちゃぐちゃですが、チラシの裏ブログなのでご容赦を・・・
nomoredeathmarch.hatenablog.com
まず、最初のテストコードは以下の通り
using Nmdm.Specifications; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Nmdm.SpecificationsTests { [TestClass] public class SpecificationsTest { [TestMethod] public void TestMethod() { var satisfiedSpec = new SpecificationIsSatisfied<object>(); Assert.IsTrue(satisfiedSpec.IsSatisfiedBy(new object())); var notSatisfiedSpec = new SpecificationIsNotSatisfied<object>(); Assert.IsFalse(notSatisfiedSpec.IsSatisfiedBy(new object())); } } }
固定でtrueやfalseを返すSpecificationクラスの動作を確認。
型パラメータはなんでも良いのでobjectクラスを指定する。
で、続いてAndSpecificationの動作確認、まずは拡張メソッドを使わないで書くと↓のような具合
// AndSpecificationをNewして書くと。 ISpecification<object> spec1 = new AndSpecification<object>(satisfiedSpec, satisfiedSpec); Assert.IsTrue(spec1.IsSatisfiedBy(new object())); ISpecification<object> spec2 = new AndSpecification<object>(satisfiedSpec, notSatisfiedSpec); Assert.IsTrue(spec1.IsSatisfiedBy(new object()));
これだとAndやOrが続いた時にnew AndSpecification(new AndSpecification(…))とネストしてってわかりにくい。
拡張メソッドを使って書くと。
// AndSpecificationを拡張メソッドで書く Assert.IsTrue(satisfiedSpec.And(satisfiedSpec).IsSatisfiedBy(new object())); Assert.IsFalse(satisfiedSpec.And(notSatisfiedSpec).IsSatisfiedBy(new object()));
という具合に数珠つなぎに書けると…
うーん、
これってITaskやIAction等、汎用的なインターフェースと拡張メソッドを駆使すれば、
個々のドメインモデルの実装を省コード化出来るような気がする・・・
ドメインモデルはデータ構造を提供し、個々の処理やチェック処理は小さいIActionやISpecificationを組み合わせて実現すると・・・
ただそれをやってしまうと、ドメインモデルがただの入れ物クラスになってしまうんだよなぁ・・・
良し悪しはともかく、引き続きインターフェースをこねくりまわすと良い考えが浮かぶかもしれない・・・
インターフェースを考える(9)インターフェースの組み合わせ
前回ISpecificationでAndやOrといった論理演算を拡張メソッドで簡単に記述出来るようにしましたが、
思うにこれって他の汎用的なインターフェースにすべて適用出来ますね。
以下前回の記事
nomoredeathmarch.hatenablog.com
ただ適用する際の注意点としては
・拡張メソッド内で具体的な処理(AndやOrといった汎用性の高いものは別として)に依存させない。
・拡張メソッドのシグネチャのいずれかに、自身が提供する型が含まれる。
今回はこれを引数、戻り値の無い、ITaskに適用してみます。
まずITaskの復習
namespace Nmdm.Tasks { public interface ITask { void Do(); } }
うん、シンプル、なにかを受け取らず、なにも返さず、ただDoメソッドの呼び出しをトリガーになんらかの処理を実行する。
このITaskを連続で実行するためのクラスとして用意したのがCompositeTask、
前回のAndSpecificationと同様に手直ししたものが以下の通り。
namespace Nmdm.Tasks { public sealed class CompositeTask:ITask { public CompositeTask(ITask one,ITask other) { this.One = one ?? new NullTask(); this.Other = other ?? new NullTask(); } private ITask One { get; } private ITask Other { get; } public void Do() { this.One.Do(); this.Other.Do(); } } }
これを毎回New CompositeTaskを呼ばなくて良いように拡張メソッドを実装する。
namespace Nmdm.Tasks { public static class CompositeTaskExtension { public static ITask Add(this ITask one,ITask other) { return new CompositeTask(one, other); } } }
これでクライアント側はITaskを実装したすべてのクラスについて(ITask).Add(ITask).Do()という具合に記述出来るようになると。
インターフェースを考える(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
・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インターフェースであり、実装に対する依存が無い。
ということになるので、これはこれでありなんじゃないかなぁ、と思う。