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

流れるようなインターフェイス

なんか単に this を返せばいいって思っている人も多いようけど、ただ this を返せばそれが使いやすいかって言われると、正直微妙。
例えば、

public static class StringUtil
{
    public static SubstrInfo Substr(this string str)
    {
        return new SubstrInfo(str);
    }
    public sealed class SubstrInfo
    {
        public SubstrInfo From(int from) { ... return this; }
        public SubstrInfo To(int to) { ... return this; }
        public SubstrInfo Length(int length) { ... return this; }
    }
}

なんてクラスは、

"hogepiyofoobar".Substr().To(2).From(1);
"hogepiyofoobar".Substr().From(1).To(3).Length(4);
"hogepiyofoobar".Substr().From(1).From(2).From(1);

こんな感じに、

  • 自然ではない順番で指定ができてしまう
  • To と Length はどちらか一方を指定したいのに制限できない
  • 何回も指定できてしまう
  • 補完候補に必要のないものまでずらずらと出てきてしまう

などと言った欠点がある。


なので、this を返すのはおすすめしない。
使いやすさを高めるためには、専用のオブジェクトを返すといい。
以下その例。

// 珍しくC#3.0

// CommonsのStringUtilsのsubstring系メソッドを参考
// ただし、全く同じ動作ではないので注意が必要
public static class StringUtil
{
    // 入り口
    public static SubstrContext Substr(this string str)
    {
        return new SubstrContext(str);
    }
    
    public sealed class SubstrContext
    {
        internal readonly string Str;
        internal SubstrContext(string str) { Str = str; }
        
        // 開始位置を指定する場合に使用
        // この呼出しの後に終了位置か長さを指定する
        public SubstrFromContext From(int from)
        {
            return new SubstrFromContext(this, from);
        }
        
        // 開始位置を指定せず、終了位置を使用する場合に使用
        public string To(int to)
        {
            return Substr(new SubstrFromContext(this, 0), to);
        }
        
        // 開始位置を文字列として指定する場合に使用
        public string After(string separator)
        {
            int sepIdx = Str.IndexOf(separator);
            if (sepIdx == -1)
                return "";
            
            return From(sepIdx + separator.Length).ToLast;
        }
        
        // Afterのバリエーション
        public string AfterLast(string separator)
        {
            int sepIdx = Str.LastIndexOf(separator);
            if (sepIdx == -1)
                return "";
            
            return From(sepIdx + separator.Length).ToLast;
        }
        
        // 終了位置を文字列として指定する場合に使用
        public string Before(string separator)
        {
            int sepIdx = Str.IndexOf(separator);
            if (sepIdx == -1)
                return Str;
            
            return From(0).To(sepIdx);
        }
        
        // Beforeのバリエーション
        public string BeforeLast(string separator)
        {
            int sepIdx = Str.LastIndexOf(separator);
            if (sepIdx == -1)
                return Str;
            
            return From(0).To(sepIdx);
        }
        
        // 開始位置と終了位置を文字列として指定する場合に使用
        public string Between(string tag)
        {
            int tagIdx = Str.IndexOf(tag);
            if (tagIdx == -1)
                return "";
            
            int from = tagIdx + tag.Length;
            return From(from).To(Str.IndexOf(tag, from));
        }
        
        // Betweenのバリエーション
        public string Between(string open, string close)
        {
            int openIdx = Str.IndexOf(open);
            if (openIdx == -1)
                return "";
            
            int from = openIdx + open.Length;
            int closeIdx = Str.IndexOf(close, from);
            if (closeIdx == -1)
                return "";
            
            return From(from).To(closeIdx);
        }
    }
    
    // Fromにより開始位置を指定した場合に使用するクラス
    public sealed class SubstrFromContext
    {
        internal readonly string Str;
        internal readonly int From;
        internal SubstrFromContext(SubstrContext info, int from)
        {
            Str = info.Str;
            From = from;
        }
        // 長さを指定する場合に使用
        public string Length(int length)
        {
            return Substr(this, length);
        }
        // 終了位置を指定する場合に使用
        public string To(int to)
        {
            return Substr(this, to - From);
        }
        // 終了位置を末尾の場合に使用
        public string ToLast
        {
            get { return Substr(this, Str.Length - From); }
        }
    }
    
    internal static string Substr(SubstrFromContext context, int length)
    {
        string target = context.Str;
        int from = context.From;
        return target.Substring(from, length);
    }
}

こんな感じに、単に this を返さないことで、

"hogepiyofoobar".Substr().To(2).From(1);
"hogepiyofoobar".Substr().From(1).To(3).Length(4);
"hogepiyofoobar".Substr().From(1).From(2).From(1);

これらは全部コンパイルできない。
そもそも、IDE の補完候補に出てこないのでそんなコードを書くこともないだろうし。


欠点としては、面倒な点、これに尽きると思う。
流れるようなインターフェイスをサポートするような言語仕様をもった言語ってたぶんまだ存在しないんだけど、あってもいいよなぁ、とは思う。
自動生成でもいいんだけど、やっぱり言語内で完結してる方が望ましいというか何というか。


あとは、プロパティが拡張できればいいんだけどなぁ・・・
プロパティが拡張できれば、

"hogepiyofoobar".Substr().From(2).To(5);

ではなくて、

"hogepiyofoobar".Substr.From(2).To(5);

って記述が出来るのに・・・