新機能というより、インタフェースの有効な使い方について。
成果物
インタフェースの特徴
- 特定メソッドの実装を強いる
- さもなくばコンパイルエラーになる
- おかげで実行時エラーが起こり得ない
- バグの早期修正ができて被害を最小限にできる
- おかげで実行時エラーが起こり得ない
- さもなくばコンパイルエラーになる
- 呼出元はメソッドが共通である。異なる派生クラスであっても。
- おかげでDRYに書ける
- 呼出元は変更不要
- 派生クラスが追加されても
- 内部処理が変わっても
- 呼出元は派生クラスごとに異なるコードを書かずに済む
- 呼出元は変更不要
- おかげで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です。 おはようございます。
以下のようにクラス名の辞書順になっていた。
- DateTimeBot
- DefaultBot
- FixedBot
- 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ユーザが何をしたいかによって適切な実行形態が変わるはず。具体的なユースケースを考えないと有意義な実装にはならないし、適切な抽象化ともいいがたい。機械的に全パターン網羅するのではなく、それをする意味や価値まで考えて厳選されたものだけを用意すべき。
今回は実行における目的について考えていない。あくまで、こういうふうに抽象化するのかな? と考えてみただけ。よって今回のような実行形態の抽象化には価値がなさそう。
対象環境
- Raspbierry pi 3 Model B+
- Raspbian stretch 9.0 2018-11-13 ※
- bash 4.4.12(1)-release ※
- SQLite 3.29.0 ※
- C# dotnet 3.0.100
$ uname -a Linux raspberrypi 4.19.42-v7+ #1218 SMP Tue May 14 00:48:17 BST 2019 armv7l GNU/Linux