やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

C#8.0のインタフェース新機能を試してみる3

 新機能というより、インタフェースの有効な使い方について。

成果物

インタフェースの特徴

  • 特定メソッドの実装を強いる
    • さもなくばコンパイルエラーになる
      • おかげで実行時エラーが起こり得ない
        • バグの早期修正ができて被害を最小限にできる
  • 呼出元はメソッドが共通である。異なる派生クラスであっても。
    • おかげでDRYに書ける
      • 呼出元は変更不要
        • 派生クラスが追加されても
        • 内部処理が変わっても
      • 呼出元は派生クラスごとに異なるコードを書かずに済む

 インタフェースを使えばメソッドは同一で済む。異なる処理とクラスを使うにも関わらず。おかげでDRYに書ける。これがインタフェースの強みなはず。

 つまり呼出側で全クラス生成をハードコーディングしていたら、インタフェースにする意義がほとんどない。メソッドが共通である長所を生かせない。

 そこで、クラス生成をファクトリー系クラスにまかせて、呼出元は全派生クラスが持っている共通メソッドを呼び出すようにする。これによって異なる実装コードの呼出をDRYに書ける。

前回

static void Main(string[] args)
{
    Console.WriteLine(new DefaultBot().Generate());
    Console.WriteLine(new FixedBot().Generate());
    Console.WriteLine(new DateTimeBot().Generate());
    Console.WriteLine(new GreetingBot().Generate());
}

 前回のIBot呼出元コード。せっかくメソッドが共通なのにDRYに書けていない。インスタンス生成と直結しているせいで。メソッド呼出をDRYに書けないならインタフェースを使う意味はない。

今回

 呼出元がシンプルに書けるようになった。IBot派生クラスの生成を外部にまかせることで。

Program.cs

static void Main(string[] args)
{
    foreach (IBot bot in AssemblyUtils.CreateInterfaceInstances<IBot>()) {
        Console.WriteLine(bot.Generate());
    }
}

 AssemblyUtilsクラスで生成する。IBotを継承する全派生クラスを。

AssemblyUtils.cs

public static class AssemblyUtils
{
    public static T[] CreateInterfaceInstances<T>() where T : class
    {
        return GetInterfaces<T>().Select(c => Activator.CreateInstance(c) as T).ToArray();
    }
}

 上記は汎用性を高めたかったので少し難しい。以下のように生成するクラスをハードコーディングで書いても良いと思う。

public static IBot[] CreateIBots()
{
    return new IBot[4] { new DefaultBot(), 
        new FixedBot(), 
        new DateTimeBot(), 
        new GreetingBot(), 
    };
}

実行結果

これは2019-10-27 07:42:06時点におけるtootです。
デフォルトのtootです。
これは固定tootです。
おはようございます。

 以下のようにクラス名の辞書順になっていた。

  1. DateTimeBot
  2. DefaultBot
  3. FixedBot
  4. GreetingBot

コードについて

 ポイントはクラス生成とメソッド呼出を別クラスに分けて実装したことである。

  • AssemblyUtils: 使用する派生クラスの選択&生成
  • Program: 派生クラスのインタフェースメソッド呼出

メリットは変更が容易になることである

 おかげで呼出元は異なる文字列生成メソッドの呼出をbot.Generate()1つに抽象化できた。異なる処理にも関わらず、呼出元は同一コード1つで書けた。

 新しいBOTを追加するときは呼出元を変更する必要が一切ない。IBotを継承する派生クラスを作ればいいだけ。よって以下のメリットが生じる。

  • 処理全体を把握する必要がない
  • 変更箇所が少なくて済む
  • 追加する機能だけに集中できる
  • まちがった箇所にまちがった修正をしてしまうリスクが減る
  • バグを作り込むリスクが減る

 これぞインタフェースの真髄だと思う。このように生成と呼出を分離することができる。おかげでクラス間の依存が疎になり、異なる処理を個別に実装できる。単一責任の原則に則って実装できる。よって、もしバグがあっても以下の通り修正しやすい。

  • バグが生じたとき箇所を特定しやすい
  • 簡単に修正しやすい
    • コードが単純化されているため
      • 1クラス1処理として

 いいことづくめ。ポリモーフィズム万歳!

おまけ

インタフェース活用を考える

 他にも抽象化できないか考えてみる。

 抽象化とは、具象化されたコードを大枠にまとめることである。よって、まずは実際に動く処理パターンが複数あることが前提となる。では、どこの処理が増やせそうか。

 文字列生成はすでに各IBot派生クラスで実装されている。これらは異なるパターンを各クラスで実装している。これと同様に、それらの呼出処理もまた、以下のように複数のパターンが考えられる。

生成 呼出
派生クラスを全て生成する それぞれ1回メソッド呼出する
派生クラスを全て生成する ランダム回(1〜10)メソッド呼出する(全派生クラス共通)
派生クラスを全て生成する ランダム回(1〜10)メソッド呼出する(派生クラスごとに異なりうる)
派生クラスを全て生成する ユーザ入力した回数だけメソッド呼出する(全派生クラス共通)
派生クラスを全て生成する ユーザ入力した回数だけメソッド呼出する(派生クラスごとに異なる回数)

 インタフェースを使うことで、これを抽象化することができるはず。

 上記はメソッド呼出する「回数」を「どうやって決定するか」という違いに注目している。

 生成する方法もまた他のパターンがある。生成する「数」に注目すると以下。

  • 派生クラスを全て生成する
  • 派生クラスを1つだけ生成する
  • 派生クラスを任意の数だけ生成する

 生成する数を「どのように決定するか」に注目すると以下。

  • アセンブリ内から全コードを取得することによって
  • ランダムによって
  • ユーザ入力によって

 これらの全パターンを抽象化すると以下。

  • 派生クラス生成する
    • 「どの派生クラスを」
      • 「どうやって」
    • 「いくつ」
      • 「どうやって」
  • メソッドを呼び出す
    • 「いくつ」
      • 「どうやって」

 これらを網羅すると以下21パターン。

生成呼出
いくつどうやっていくつどうやって
全部アセンブリ内コード取得1回ハードコーディング
全部アセンブリ内コード取得1〜10回ランダム(共通)
全部アセンブリ内コード取得1〜10回ユーザ入力(共通)
全部アセンブリ内コード取得1〜10回ランダム(個別)
全部アセンブリ内コード取得1〜10回ユーザ入力(個別)
1つアセンブリ内コードからランダムに1回ハードコーディング
1つアセンブリ内コードからランダムに1〜10回ランダム
1つアセンブリ内コードからランダムに1〜10回ユーザ入力
1つアセンブリ内コードからユーザ選択1回ハードコーディング
1つアセンブリ内コードからユーザ選択1〜10回ランダム
1つアセンブリ内コードからユーザ選択1〜10回ユーザ入力
任意数アセンブリ内コードからランダムに1回ハードコーディング
任意数アセンブリ内コードからランダムに1〜10回ランダム(共通)
任意数アセンブリ内コードからランダムに1〜10回ユーザ入力(共通)
任意数アセンブリ内コードからランダムに1〜10回ランダム(個別)
任意数アセンブリ内コードからランダムに1〜10回ユーザ入力(個別)
任意数アセンブリ内コードからユーザ選択1回ハードコーディング
任意数アセンブリ内コードからユーザ選択1〜10回ランダム(共通)
任意数アセンブリ内コードからユーザ選択1〜10回ユーザ入力(共通)
任意数アセンブリ内コードからユーザ選択1〜10回ランダム(個別)
任意数アセンブリ内コードからユーザ選択1〜10回ユーザ入力(個別)

 これを抽象化することで、様々な実行形態を実装できる。実装するときは、これらを個別のクラスにする。

 次に、どれを使用するか決定せねばならない。全パターン実行しても面倒なだけで意味がない。どれか1つだけを使うようにすべき。たとえばコマンド引数で決める。または設定ファイルによってどれか1つに決定させる。

 さらにこの他にも、機能追加することで一層パターンを増やせる。たとえばメソッド結果に何らかの文字列を「付与」したり、実行する「インターバル(時間間隔)」を与えるなどすることが考えられる。

 たとえば「付与」においては以下のようなパターンが考えられる。

  • メソッド呼出結果に何らかの文字列を付与する: IAppend
    • 末尾に: ExclamationAppend
    • 前後左右を罫線で囲む: RuledLineAppend
    • 左にを付与してフキダシっぽくする: CallOutAppend

これに価値はない

 実行形態の抽象化には価値がなさそう。

 この「おまけ」で考えたことは、BOTユーザが何をしたいかによって適切な実行形態が変わるはず。具体的なユースケースを考えないと有意義な実装にはならないし、適切な抽象化ともいいがたい。機械的に全パターン網羅するのではなく、それをする意味や価値まで考えて厳選されたものだけを用意すべき。

 今回は実行における目的について考えていない。あくまで、こういうふうに抽象化するのかな? と考えてみただけ。よって今回のような実行形態の抽象化には価値がなさそう。

対象環境

$ uname -a
Linux raspberrypi 4.19.42-v7+ #1218 SMP Tue May 14 00:48:17 BST 2019 armv7l GNU/Linux