インターフェースを考える(6)IEvent
以前書いたドメインイベントの実装について、
インターフェースの側から考える。
nomoredeathmarch.hatenablog.com
nomoredeathmarch.hatenablog.com
まず作成したインターフェースについて復習
まず、イベントを表すクラスのインターフェースIEvent
namespace Nmdm.Events { public interface IEvent { } }
あくまで印をつけるためのマーカー
続いてそのインターフェースを受け取るIHandler
namespace Nmdm.Events { public interface IHandler { void Handle(IEvent value); bool CanHandle(IEvent value); } }
こちらはパブリッシャーからの受信機構となるメソッド。
でここからが今回の本題、
Publisherクラスをstaticで実装したけども、これもインターフェースで抽象化してみる。
以下がインターフェースを抜き出したIPublisher
namespace Nmdm.Events { public interface IPublisher { void Publish(IEvent e); void Subscribe(IHandler handler); void Unsubscribe(IHandler handler); } }
staticじゃなくなったことで単体テストがしやすくなったけど、
半面、どこからでもイベント通知、購読することが出来なくなったわけだ。
ただ、staticクラスのように振舞わせたければシングルトンなりこのクラスをキャッシュするためだけのスタティッククラスを作れば良いわけだし、
それであれば通常のクラスとして設計すれば良いとも思える。
これを以前作ったPublisherクラスに実装させる。
using System; using System.Collections.Generic; using System.Threading; namespace Nmdm.Events { public class Publisher:IPublisher { private static Mutex Mutex { get; } = new Mutex(); private static List<WeakReference<IHandler>> Subscribers { get; } = new List<WeakReference<IHandler>>(); public void Publish(IEvent e) { try { Mutex.WaitOne(); if (e == null) throw new ArgumentNullException("e"); var list = new List<IHandler>(); list.AddRange(Subscribers.ConvertAll<IHandler>(x => ToStrong(x))); list.RemoveAll(x => x == null || !x.CanHandle(e)); list.ForEach(x => x.Handle(e)); Subscribers.RemoveAll(x => ToStrong(x) == null); } finally { Mutex.ReleaseMutex(); } } private static IHandler ToStrong(WeakReference<IHandler> obj) { IHandler ret; obj.TryGetTarget(out ret); return ret; } public void Subscribe(IHandler subscriber) { try { Mutex.WaitOne(); if (subscriber == null) throw new ArgumentNullException("subscriber"); if (Subscribers.Contains(new WeakReference<IHandler>(subscriber))) return; Subscribers.Add(new WeakReference<IHandler>(subscriber)); } finally { Mutex.ReleaseMutex(); } } public void Unsubscribe(IHandler subscriber) { try { Mutex.WaitOne(); if (subscriber == null) throw new ArgumentNullException("subscriber"); Subscribers.RemoveAll(x => subscriber.Equals(ToStrong(x))); } finally { Mutex.ReleaseMutex(); } } } }
インターフェースを考える(5)IAction その2
前回に続きIActionについて考える。
IPredicateやITaskと同じように組み合わせ用にCompositeActionクラスを作る。
using System; using System.Collections.Generic; namespace Nmdm.Actions { public sealed class CompositeAction<TContext> : IAction<TContext> { public CompositeAction(IEnumerable<IAction<TContext>> actions) { if (actions == null) throw new ArgumentNullException("actions"); this.Actions = actions; } private IEnumerable<IAction<TContext>> Actions { get; } public void Do(TContext context) { foreach(var obj in this.Actions) { obj.Do(context); } } } }
IPredicateの時も気になったけど、このクラスを使う時にList型の変数を使ってIActionをコレクションに入れてからコンストラクタを呼び出すのかって考えるといまいちな感じがする。
かといって、クラスは基本不変にしたい・・・・
ということで↓へ改造
using System; using System.Collections.Generic; namespace Nmdm.Actions { public sealed class CompositeAction<TContext> : IAction<TContext> { public CompositeAction() { this.Actions = new IAction<TContext>[] { }; } private CompositeAction(IEnumerable<IAction<TContext>> actions) { if (actions == null) throw new ArgumentNullException("actions"); this.Actions = actions; } private IEnumerable<IAction<TContext>> Actions { get; } public CompositeAction<TContext> Add(IAction<TContext> action) { if (action == null) throw new ArgumentNullException("actions"); var actions = new List<IAction<TContext>>(this.Actions); actions.Add(action); return new CompositeAction<TContext>(actions); } public void Do(TContext context) { foreach(var obj in this.Actions) { obj.Do(context); } } } }
コンストラクタをプライベートにしてAddメソッドを追加、メソッドの戻り値でprivateなコンストラクタで生成した結果を返して不変かつ流暢なインターフェースに。
このクラス自体がIActionを実装しているからAddRangeメソッドはいらないと思う。
ついでにIActionの受け口をAddメソッドだけにしたから個々の要素に対してもnullチェックが出来ている。
元のソースと比べると複雑さが増しているけどIAction以外への依存はなく変更の可能性が低いから良いかと思います。
==2017.03.14追記===
やっぱり改造やめ、読みにくいから、というならまだしも、
書きにくいから、という理由でメソッドを増やすのに少し抵抗を感じる。
インターフェースを考える(4)IAction
C#実践開発手法よりIActionインターフェースです。
namespace Nmdm.Actions { public interface IAction<TContext> { void Do(TContext context); } }
単一の引数を受け取る戻り値の無いインターフェースで、
引数は型パラメータで指定出来る・・・と
使いどころはどこなんだろう。
こないだ書いたITaskに引数が一つ付いただけなわけですが・・・
nomoredeathmarch.hatenablog.com
手段と目的がごっちゃになってるけど何かスマートな使い方は出来ないものか・・・
たとえば、ドメインイベントのハンドラにイベントの具体的な処理を着脱できるようにするとか?
まずIHandlerインターフェス(前の記事ではISubscriberだったけどC#実践開発手法の表記に合わせた。)
namespace Nmdm.Events { public interface IHandler { void Handle(IEvent value); bool CanHandle(IEvent value); } }
こいつの実装クラスとして、着脱可能なアクションをコンストラクタで受け取るHandlerクラス
using System; using Nmdm.Actions; namespace Nmdm.Events { public sealed class Handler<T> : IHandler where T:IEvent { public Handler(IAction<T> action) { if (action == null) throw new ArgumentNullException("action"); this.Action = action; } private IAction<T> Action { get; } public bool CanHandle(IEvent value) { if (value == null) return false; return value is T; } public void Handle(IEvent value) { if (!this.CanHandle(value)) return; this.Action.Do((T)value); } } }
コンストラクタでIAction
あとはHandlerクラスがドメインイベントの通知を受け取る度にIAction
ドメインイベントとは違うけど、MVVMのメッセージ通知とかに使えるかな?
イベントでメッセージの内容を受け取って、アクション側でメッセージ表示の具体的な方法を実装する。
最初は標準のメッセージボックスを表示してたけど、そのうちカスタマイズしたメッセージボックスにすげ変えると・・・
パブリッシャに予約してなければ単体テストもそのままで良さそう。
イベント通知される場面なら↓みたいなIActionのモックと入れ替えても良いのか。
namespace Nmdm.Actions { public sealed class ActionMock<T> : IAction<T> { public void Do(T context) { } } }
「イベントの通知を受け取る」っていう仕事と「イベントの情報を読み込んで処理を実行する。」っていう仕事を分離して実装出来ますね。
WPF IValueConverter小メモ
DateTime向けのコンバーターでDateTime以外の入力があったら例外投げていたけど、
どうもnullで飛んでくるタイミングがあって例外発生してたもよう。
変換に失敗したら例外投げるより既定値で渡した方が良い?
そのうち検証しよう。
インターフェースを考える(3)IPredicate
今回もC#実践開発手法から、IPredicateを題材にします。
まずはインターフェース
namespace Nmdm.Predicates { public interface IPredicate { bool Test(); } }
ITaskインターフェース同様引数はありませんが、戻り値がbool型です。
戻り値をジェネリックにしてIFunc
条件判定という目的に特化している。と考えたら別のインターフェースにした方がシンプルかな。
ITaskの時と同じようにモックを作るのですが、bool型を返すのでtrueを返すモックとfalseを返すモックの2パターンを作ります。
まずは常にtrueを返すTruePredicateクラス
namespace Nmdm.Predicates { public sealed class TruePredicate : IPredicate { public bool Test() { return true; } } }
反対に常にfalseを返すFalsePredicateクラス
namespace Nmdm.Predicates { public sealed class FalsePredicate : IPredicate { public bool Test() { return false; } } }
IPredicateを組み立てるためのクラスですが、ここでは一旦置いておき、
ITaskとの連携を考えてみます。
まずはTrueかFalseか判定して処理を分岐させるクラスとしてTestedTaskを作ってみました。
C#実践開発手法の中で紹介されている分岐デコレータと同じですね。
using Nmdm.Tasks; namespace Nmdm.Predicates { public sealed class TestedTask : ITask { public TestedTask(IPredicate predicate,ITask trueTask,ITask falseTask) { this.Predicate = predicate; this.TrueTask = trueTask; this.FalseTask = falseTask; } private IPredicate Predicate { get; } private ITask TrueTask { get; } private ITask FalseTask { get; } public void Do() { if(this.Predicate.Test()) { this.TrueTask.Do(); return; } this.FalseTask.Do(); } } }
条件判定と処理を組み合わせて処理の分岐を表現しているわけですね。なるほど・・・
記事が長くなりそうなのでこのへんで一端切ります。
インターフェースを考える(2)ITaskインターフェース
C#実践開発手法で紹介されているもっとも単純なインターフェースITaskを作ってみます。
namespace Nmdm.Tasks { public interface ITask { void Do(); } }
引数も無く、戻り値もなく、最もシンプルなインターフェースですね。
こういうインターフェースを作るとテストの時のモックが必要になるだろう。
というわけでITaskを実装したNullオブジェクト、NullTaskを作りました。
namespace Nmdm.Tasks { public sealed class NullTask : ITask { public void Do() { } } }
じゃあ今度はITaskを組み合わせて連続でDoメソッドを実行出来るクラスCompositeTaskクラスを作ってみます。
using System; using System.Collections.Generic; namespace Nmdm.Tasks { public sealed class CompositeTask:ITask { public CompositeTask(IEnumerable<ITask> tasks) { if (tasks == null) throw new ArgumentNullException("tasks"); this.TaskList.AddRange(tasks); this.TaskList.RemoveAll(x => x == null); } private List<ITask> TaskList { get; } = new List<ITask>(); public void Do() { this.TaskList.ForEach(x => x.Do()); } } }
nullチェックのタイミングとかnullを無視する処理の書き方はともかく、
コンストラクタでIEnumerable
条件判定も何もないから、Compositeタスクさえあれば組み合わせるのには十分なような気がするけど、
書きながら思いついたのはTry-CatchやTry-Fillalyを仕込みたい時はどうするんだろう?という話
例えばDBへの書き込み処理なんかだとアプリケーション側で
try { Hoge.BeginTransaction(); // 永続化ロジックはここ Hoge.Commit(); } finally { Hoge.RollBack(); }
みたいな具合の処理を書きたくなる。
Begin⇒永続化⇒Commitの流れはCompositeTaskに分解したタスクを渡せば連続で実行出来るけどRollBackはそうはいかんですね。
トランザクション処理に特化したクラスを作るのであればITaskを実装したTransactionクラス的なのにITaskを実装した永続化ロジックを渡してやれば実現出来るでしょうけど、
あえてITaskだけに依存した方法で書くことを考えてみましょう。
まずはcatchを無視して、tryブロックとfinallyブロックに分けてITaskを実行するTryFinallyTaskを組んでみます。
using System; namespace Nmdm.Tasks { public sealed class TryFinallyTask : ITask { public TryFinallyTask(ITask tryTask,ITask finallyTask) { if (tryTask == null) throw new ArgumentNullException("tryTask"); if (tryTask == null) throw new ArgumentNullException("finallyTask"); this.TryTask = tryTask; this.FinallyTask = finallyTask; } private ITask TryTask { get; } private ITask FinallyTask { get; } public void Do() { try { this.TryTask.Do(); } finally { this.FinallyTask.Do(); } } } }
というわけで実装してみました。
コンストラクタでtryブロックで実行する処理とfinallyブロックで実行する処理を受け取りプロパティに格納する。
Doメソッドでtry-finallyを使ってそれぞれのITaskを実行すると・・・
もう一つのtry-catchの方はどうなるだろう。
通常try-catchでやることと言えば例外をキャッチしてログを出力したり、
ファイルIOなんかの外部リソース周りの例外をキャッチして自作のクラスで利用者に通知を出したりかなぁ、
単にExceptionをキャッチするだけじゃなく具体的な例外をキャッチすることを考えるとこのインターフェースで表現出来る???
とりあえずITaskについて考えるのはこのくらいにしときます。
うーん、プログラムは小さくてシンプルな方が良いとは思うけど、やりすぎ感がプンプンしますねー。
インターフェースを考える(1)はじめに
オブジェクト指向プログラミングを勉強していると
実装ではなく抽象に依存する
というフレーズを目にすることがあります。
このカテゴリの記事では私自身の勉強のために、
とことんインターフェースを使ってみたらどうなるか、
実際にプログラミングしながら考察していこうかと思います。