SE(たぶん)の雑感記

一応SEやっている筆者の思ったことを書き連ねます。会計学もやってたので、両方を生かした記事を書きたいと考えています。 でもテーマが定まってない感がすごい。

ジェネリクスを活用して、FluentなインターフェイスをC#で実装してみた

当記事は、Qiita Advent Calendar 2018、「C# その2」の21日目記事になります。

qiita.com

久々にC#を触ったので、拙い部分あると思いますが、ご意見ご指摘をコメント等いただけると、とてもうれしいです。

それではお付き合いください。

Fluentなインターフェイスとは

bliki-ja.github.io

Fluentは、「流暢な」、「流れるような」みたいな意味を持つ単語です。

例はリンク先を見ていただくのが良いですが、以下のような感じです。(リンク先のソースを転載)

private void makeFluent(Customer customer) {
    customer.newOrder()
            .with(6, "TAL")
            .with(5, "HPK").skippable()
            .with(3, "LGV")
            .priorityRush();
}

Customerのインスタンスを構築する際、メソッドを連続で(流れるように)呼び出すことで、目的の生成物を分かりやすく作ろう、というような仕組みです。

C#だと、Linqはメソッドを繋げていく感じで書けるので、そちらがイメージしやすいかと思います。

今回は、これを自作してみます。

目的

  • インスタンスを生成する処理を、Fluentな感じで書く
  • ジェネリクスを使い、型の制約を設けない
  • 値型でも使えるようにする
  • メソッドチェーン作成後、生成処理を呼び出すと、目的の型のインスタンスが取り出せる

作成物

github.com

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>に変換しています。

なお、上記のように、CreateFunc<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の時点で反映できます。

参考にしたもの

内部実装は、以下を参考にしました。というかほとんど一緒です。

Enumerable.cs

一応、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>に変換してしまいます。つまり、なんでも変換します。

これを参照すると、こうなってしまいます。

f:id:hiroronn:20181221222925p:plain
AsFluentが生えてくる

相手が誰であろうと、真っ先にAsFluentが生えてきます。恐ろしい*3

おわりに

Pythonばっかりやっていて、静的型付言語を久々にやりたくなり、得意なC#で遊びました。でも、案外使える処理なのでは…?と思い始めました。

そして、ジェネリクスが楽しすぎて、いろいろ作ってしまいました。

個人的には楽しかったですが、もしこれが役に立ちそうな方がいたら、もっと頑張ってちゃんと作るので、ご意見やフィードバックください。

ではでは。

*1:既存の型から、新しい型のインスタンスを生成する

*2:値型は常に新たな値を返す

*3:Fluent.Extensionを参照しなければよい