当記事は、Qiita Advent Calendar 2018
、「C# その2」の21日目記事になります。
久々にC#を触ったので、拙い部分あると思いますが、ご意見ご指摘をコメント等いただけると、とてもうれしいです。
それではお付き合いください。
Fluentなインターフェイスとは
Fluent
は、「流暢な」、「流れるような」みたいな意味を持つ単語です。
例はリンク先を見ていただくのが良いですが、以下のような感じです。(リンク先のソースを転載)
private void makeFluent(Customer customer) { customer.newOrder() .with(6, "TAL") .with(5, "HPK").skippable() .with(3, "LGV") .priorityRush(); }
Customerのインスタンスを構築する際、メソッドを連続で(流れるように)呼び出すことで、目的の生成物を分かりやすく作ろう、というような仕組みです。
C#
だと、Linq
はメソッドを繋げていく感じで書けるので、そちらがイメージしやすいかと思います。
今回は、これを自作してみます。
目的
- インスタンスを生成する処理を、Fluentな感じで書く
- ジェネリクスを使い、型の制約を設けない
- 値型でも使えるようにする
- メソッドチェーン作成後、生成処理を呼び出すと、目的の型のインスタンスが取り出せる
作成物
GitHub
に公開しました。
最初は、当ブログにソースを記載しようとしていましたが、説明が非常に難しかったので諦めました。
使い方
詳しくは、GitHub
にあるFluentTest
にあります。きわめて単純なユースケースですが、例を載せます。
参照型の場合
[Test] public void TestObject() { var fluent = FluentFactory.Create(() => new List<string>()) .Chain(l => l.Add("a")) .Chain(l => new List<string>(l)) .Chain(l => l.Add("b")) .Chain(l => l.Add("b")); Assert.AreEqual(3, fluent.Evaluate().Count); var another = fluent.Change(l => new HashSet<string>(l)); Assert.AreEqual(2, another.Evaluate().Count); }
ファクトリは用意してあり、FluentFactory.Create
にインスタンスを生成するデリゲートを渡します。
そうすると、IFluent<List<string>>
が返ってくるので、Chain
を呼び出して、インスタンスに対する変更を加えていきます。
最終的に、IFluent.Evaluate
を呼び出すと、変更をすべて反映したインスタンスが返ってくる、というものです。
IFluent.Change
を使うと、型変換*1が行えます。
上記では、List<string>
をHashSet<string>
に変換しています。
なお、上記のように、Create
にFunc<T>
を渡すと、インスタンス生成やChain
は遅延評価されるため、Evaluate
を呼び出すたびに新規インスタンスが生成されます。
…ちょっと、面白くないですか?自分だけ?
インターフェイス定義
- IFluent.cs
using System; namespace Fluent { /// <summary> /// FluentなInterface /// </summary> /// <typeparam name="T">生成する型</typeparam> public interface IFluent<T> { /// <summary> /// インスタンスに変更を反映します。 /// </summary> /// <param name="chainer">チェーンさせる処理</param> /// <returns>チェーンが反映されたIFluentインスタンス</returns> IFluent<T> Chain(Action<T> chainer); /// <summary> /// 元の状態を元に、値を返します。 /// </summary> /// <param name="chainer">チェーンさせる処理</param> /// <returns>チェーンが反映されたIFluentインスタンス</returns> IFluent<T> Chain(Func<T, T> chainer); /// <summary> /// 評価後のインスタンスを評価する式を追加します。 /// </summary> /// <param name="check">評価値。問題ありとする場合、falseを返します。</param> /// <returns>エラーならtrue</returns> IFluent<T> Validation(Func<T, bool> check); /// <summary> /// 型を変換します。 /// </summary> /// <typeparam name="TOther">戻り値の型</typeparam> /// <param name="changer">変換メソッド</param> /// <returns>変換後の型</returns> IFluent<TOther> Change<TOther>(Func<T, TOther> changer); /// <summary> /// 評価後のインスタンスを取得します。 /// </summary> /// <returns></returns> T Evaluate(); } }
基本的な考え方として、受け取ったインスタンスに対し、IFluent<T> Chain(Action<T> chainer)
のメソッドを呼び出して変更していく、というものです。
最も単純な実装はこんな感じです。
internal class SimpleFluent<T> : IFluent<T> { private T _instance; public SimpleFluent(T instance) { _instance = instance; } public IFluent<T> Chain(Action<T> chainer) { chainer(_instance); return this; } // 以下省略 }
ただし、この実装では値型が来た場合に対応できない*2ため、Chain(Func<T, T> chainer)
を用意し、変更後の値を戻り値で返せるようにしています。
使う場合はこんな感じです。
[Test] public void TestNumber() { var fluent = FluentFactory.Create(() => 10) .Chain(val => val * 2) .Chain(val => val + 3); Assert.AreEqual(23, fluent.Evaluate()); }
値を返すことで、Evaluate
の時点で反映できます。
参考にしたもの
内部実装は、以下を参考にしました。というかほとんど一緒です。
一応、Action<T>
をFunc<T, T>
に変換する部分は、それなりに考えました。それぐらいです。
課題
このインターフェイスの起点として、処理対象のインスタンスが必要です。
その受け取りを、今は
/// <summary> /// IFluent<typeparamref name="T"/>のインスタンスを生成します。 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="factory"></param> /// <returns></returns> public static IFluent<T> Create<T>(Func<T> factory) { return new ChainFluent<T>(factory); }
というメソッドに頼っています。
しかし、これだとfactory
にはインスタンスの生成以外でも渡せてしまいます。
var list = new List<string>(); list.Add("a"); var fluent = FluentFactory.Create(() => list);
という感じです。
ちょっと悪用すると、
[Test] public void FaliedTest() { var list = new List<string>(); list.Add("a"); var fluent = FluentFactory.Create(() => list) .Chain(l => l.Add("b")); var chainedList = fluent.Evaluate(); Assert.AreEqual(2, chainedList.Count); var fluent2 = fluent.Chain(l => l.Add("c")); Assert.AreEqual(2, chainedList.Count); Assert.AreEqual(3, fluent2.Evaluate().Count); //4になってしまう }
のような感じで書いたとき、Create
の時点で引き渡したlist
の中身が変わってしまうため、意図した動作になりません。
Linq
の場合、IEnumerable
自体のコピーは簡単に作れるため問題ありませんでしたが、現状、型はなんでもよいとしてしまっているため、誤解を招きやすい挙動になっています。
用途
説明してきましたが、これを使うシーンを想定してみました。考えていませんでしたが。
- Builder後の結果を返す
- Factoryっぽく使う
- オブジェクト生成を手軽に遅延させる
IFluent
をメソッドの引数として渡して、元の型を意識させずに目的のインスタンスを取得する
などなど、でしょうか。インスタンスの遅延生成は、場合によっては使える気がします。
面白いと思う人がいたら
このブログでも、GitHub
のほうでもいいので、スター付けていただけると、大変うれしいです。
もし利用価値がある処理だ、と思っていただける方がいたら、もっと頑張ってまともなライブラリにしてみたいと考えています。
GitHub
にスターついたら、本気でいろいろ検討しますです(きっと)。
(余談)悪ふざけ
Fluent.Extension
名前空間に、とっても危険な拡張メソッドを生やしました。
- FluentExtension.cs
using System; namespace Fluent.Extension { public static class FluentExtension { public static IFluent<T> AsFluent<T>(this Func<T> builder) { return FluentFactory.Create(builder); } public static IFluent<T> AsFluent<T>(this T instance) { return FluentFactory.Create(instance); } } }
…Func<T>
とT
を、IFluent<T>
に変換してしまいます。つまり、なんでも変換します。
これを参照すると、こうなってしまいます。
相手が誰であろうと、真っ先にAsFluent
が生えてきます。恐ろしい*3。
おわりに
Python
ばっかりやっていて、静的型付言語を久々にやりたくなり、得意なC#
で遊びました。でも、案外使える処理なのでは…?と思い始めました。
そして、ジェネリクスが楽しすぎて、いろいろ作ってしまいました。
個人的には楽しかったですが、もしこれが役に立ちそうな方がいたら、もっと頑張ってちゃんと作るので、ご意見やフィードバックください。
ではでは。