依存性の逆転
新米プログラマ、太宰「うーん、依存性の逆転ってよく聞くけど、何がいいんだろうなあ」
中堅プログラマ、芥川「なるほど、それは良い質問だ。このクラスを見てみたまえ」
class Program
{
public static void Main()
{
Greeter greeter = new();
string greet = greeter.GetGreet();
Console.WriteLine(greet);
}
}
芥川「このコードを見て、君はこのクラスが何をするか分かるかね?」
太宰「Greeter って書いてあるから... ハロー・ワールド的な処理かなあ」
芥川「そうかもしれんし、そうじゃないかもしれんな。これだけを見ても、何も分からん。なのに、GetGreet
という関数を呼び出している。それが依存性だ」
太宰「分かるような分からないような」
芥川「こういうコードはみんなよく作る。なぜなら、実装中は Greet
も GetGreet
も中身も分かっているからだ。」
芥川「今見たら意味不明なコードだ。身元もよくわからん GetGreet
というコードを信用して実行している」
芥川「それがつまりこのコードを改修するための『最低知識』だということだ」
芥川「ちなみにこれは三ヶ月前、君が書いたコードだ」
太宰「なるほど、そういうこともあったかもしれない」
太宰「でもこれと、依存性の逆転がどう繋がるんだ?」
芥川「例えば、君が Program
を実装した時の考えはこうだったのではないかな」
太宰『挨拶の実装は Program
にあるのはおかしいな。Greeter
というクラスを作って、単一責任原則の原則を守ろう!』
芥川「そしてできあがったのがこの図だ」
太宰「そうかもしれない」
芥川「しかし実装の依存性がおかしい。Greeter
という存在は、Program
があったから初めて存在する」
太宰「それはそうだ」
芥川「しかし、Program
という存在が、Greeter
が無いと存在できず、逆に Greeter
は Program
が無くても存在できる」
太宰「どれどれ」
public class Hoge
{
public void Run()
{
Greeter greeter = new();
string greet = greeter.GetGreet();
Console.WriteLine(greet);
}
}
太宰「確かに、全く関係ない Hoge
がインスタンス化しても使えるな。逆に Greeter
を消したら、Program
が動かないだろう」
太宰「待てよ、そしたら Greeter
はポータビリティの高い、よくカプセル化された良い実装じゃないか」
芥川「そう信じていると、確実に地雷を踏む」
太宰「どういうことだ?」
芥川「ある日、川端というプログラマが Hoge
を修正したいと思った。このプログラムを実行すると、以下のように表示された」
ぺっぺろぱー
芥川「川端は憤慨して、Greeter
の中身を Hello World!
と出力するように変えてしまった」
太宰「良いことじゃないか。メロスも喜ぶ」
芥川「だが何を隠そう、Program
は1日1回 ぺっぺろぱー
と出力することで、核ミサイルの発射を阻止していたのだ」
芥川「Hello World!
に変わったせいで、世界は滅びてしまった」
太宰「はいはい」
芥川「まあ大分ふざけたが、結局 Greeter
は Program
に依存しているはずなのに、依存していない実装になっているのが原因でバグを起こした」
芥川「つまり、こうなっていれば防げたかもしれない、ということだ」
太宰「うーん、分かるような分からんような。そもそも現実的に可能なのか?」
芥川「現状のサンプルだとシンプルすぎるし効果がわかりにくいので、新しく『勤怠管理を行うプログラム』で、『勤怠を表示する処理』の実装を考えてみよう」
芥川「これが要件だ、いつもみたいに書いてみたまえ」
太宰「ふむ。まずはクラスを書いて、関数を書かないとな。コメントを書いて...」
public class AttendanceView
{
public void DisplayAttendance(int memberId)
{
// 勤怠をサーバから取得する
// View用のデータに加工する
// 表示する
}
}
芥川「良い感じだ。では中身を実装してみようか」
public class AttendanceView
{
public void DisplayAttendance(int memberId)
{
// 勤怠をサーバから取得する
var mysql = new MySql("..."); // connection string
IEnumerable<Row> attendance = mysql.Query(
$"from attendance select * where member_id = {memberId};"
);
// View用のデータに加工する
AttendanceDisplayConverter converter = new();
IEnumerable<AttendanceDisplayEntry> attendanceDisplayEntries = converter.Convert(attendance);
// 表示する
RenderAttendance(attendanceDisplayEntries);
}
}
public class AttendanceDisplayConverter
{
public IEnumerable<AttendanceDisplayEntry> Convert(IEnumerable<Row> attendance)
{
return attendance.Select(attendanceEntry =>
{
DateTimeOffset startAt = DateTimeOffset.FromUnixTimeSeconds(attendance["start_at"]);
DateTimeOffset endAt = DateTimeOffset.FromUnixTimeSeconds(attendance["end_at"]);
TimeSpan breakTime = TimeSpan.FromMinutes(attendance["break_time_mins"]);
return new AttendanceDisplayEntry(startAt, endAt, breakTime);
});
}
}
太宰「こんな感じかなあ。SQLの部分は余り自信はないけど」
芥川「なるほど。今回もまた単一責任の法則を意識して、AttendanceDisplayConverter
を作ったんだな」
太宰「うむ。RenderAttendance
とAttendanceDisplayEntry
はよく分からないけど、こうやって使えばいいって書いてあった」
芥川「十分だろう。これを書く上で、必要だった知識をまとめておこう」
太宰「なるほど、ちゃんと書かれてるんだな」
芥川「これはサンプルだからそうなっているが、もしこれが仕事であれば『勤怠の一覧を表示してください』と言われて終わりだろうな」
太宰「そうだな。そこからこれを調べ上げるのは...まあまあ時間がかかりそうだ」
芥川「その通りだ。つまり、このクラスを触る上で必要な知識が、先ほどの知識だということだ」
太宰「それも知らずに実装したら、バグだらけになりそうで怖いなあ。テーブルのカラム名が違ったりしたら終わりそうだ」
芥川「では依存性の逆転を使って、このコードを改善しよう」
public interface IAttendanceDataProvider
{
IEnumerable<AttendanceData> FetchAttendance(int memberId);
}
public record AttendanceData(
DateTimeOffset StartAt,
DateTimeOffset EndAt,
TimeSpan BreakTime
);
public interface IAttendanceDataToViewConverter
{
AttendanceDisplayEntry ConvertFrom(AttendanceData data);
}
public class AttendanceView
{
private readonly IAttendanceDataProvider _dataProvider;
private readonly IAttendanceDataToViewConverter _converter;
public AttendanceView(IAttendanceDataProvider dataProvider, IAttendanceDataToViewConverter converter)
{
_dataProvider = dataProvider;
_converter = converter;
}
public void DisplayAttendance(int memberId)
{
// 勤怠をサーバから取得する
IEnumerable<AttendanceData> attendance = _dataProvider.FetchAttendance(memberId);
// View用のデータに加工する
IEnumerable<AttendanceDisplayEntry> attendanceDisplayEntries = attendance.Select(attendanceEntry =>
{
return _converter.ConvertFrom(attendanceEntry);
});
// 表示する
RenderAttendance(attendanceDisplayEntries);
}
}
太宰「あれ、データ取得と変換部分がない」
芥川「そうだな、一旦は置いておこう。とりあえずこれは以下のコードを追加した。」
IAttendanceDataProvider
AttendanceData
IAttendanceDataToViewConverter
芥川「これらはあくまで『AttendanceView
を実装する上で必要な知識』だ」
太宰「前もそうじゃないのか?」
芥川「例えば、勤怠の一覧を表示するときに、データベースが MySQL だろうが PostgreSQL だろうが大きくは変わらないだろう」
芥川「しかし IAttendanceDataProvider
、つまりデータを取得しないといけない、ということは絶対に知る必要がある」
芥川「勤怠のデータを取得せず、勤怠を表示することはできないからな」
太宰「くどいな」
芥川「新しく追加されたのは以下の情報だ」
芥川「これらはあくまで、AttendanceView
を実装する上で知らなければいけない最低知識であることは分かるだろう」
太宰「そうだな、テーブルのカラムみたいな細かい情報はない。しかも AttendanceData
を使って必要な情報を自分で定義しているな」
太宰「しかも、このコードが動いていれば、少なくとも各インターフェースの実装自体が正しいことも納得できそうだ」
芥川「うむ。我々が AttendanceView
を触る上で必要な知識は以下のように変わった」
前:
今:
芥川(なお View 都合の情報はいずれにしても知る必要があるので、上記から省いた)
芥川「一番の違いは、AttendanceView
の実装者は、外部実装について何も知らなくてよくなったということだ」
芥川「この実装ならデータベースが MySQL なのか、PostgreSQL なのか分からなくてもいいし、SQLの使い方も分からなくていい」
芥川「では、このコードに『メモを追加で表示してほしい』という要件が来たとしよう。実装してみなさい」
太宰「はあ。まずは、DTOに追加が必要かな」
public record AttendanceData(
DateTimeOffset StartAt,
DateTimeOffset EndAt,
TimeSpan BreakTime,
+ string Memo
);
太宰「む。何か例外が出たぞ。どうやら、IAttendanceDataProvider
を継承するクラスから出ているようだ」
public class AttendanceDataProvider : IAttendanceDataProvider
{
public IEnumerable<AttendanceData> FetchAttendance(int memberId)
{
var mysql = new MySql("..."); // connection string
IEnumerable<Row> attendance = mysql.Query(
$"from attendance select * where member_id = {memberId};"
);
return attendance.Select(MapRow);
}
private AttendanceData MapRow(Row row)
{
DateTimeOffset startAt = DateTimeOffset.FromUnixTimeSeconds(attendance["start_at"]);
DateTimeOffset endAt = DateTimeOffset.FromUnixTimeSeconds(attendance["end_at"]);
TimeSpan breakTime = TimeSpan.FromMinutes(attendance["break_time_mins"]);
return new AttendanceData(
StartAt: startAt,
EndAt: endAt,
BreakTime: breakTime
// ここで解析エラー
);
}
}
太宰「あ、さっきのコードだ。なるほど、ここに追加しないといけないんだな。テーブルの定義は...」
+ string memo = attendance["memo"];
return new AttendanceData(
StartAt: startAt,
EndAt: endAt,
- BreakTime: breakTime
+ BreakTime: breakTime,
+ Memo: memo
);
太宰「これでビルドは通るけど、このメモを参照しないとレンダリングできないかな」
public class AttendanceDataToViewConverter : IAttendanceDataToViewConverter
{
public AttendanceDisplayEntry ConvertFrom(AttendanceData data)
{
return new AttendanceDisplayEntry
{
StartAt: data.StartAt,
EndAt: data.EndAt,
- BreakTime: data.BreakTime
+ BreakTime: data.BreakTime,
+ Memo: data.Memo
}
}
}
太宰「これでいいかな」
芥川「いいな。今回は新しくデータを取得する必要があったから、全知識の動員が必要だった」
太宰「そうだったな」
芥川「なので、こういう修正を行うと、ただ手間が増えたように感じる」
太宰「実際複数のファイルをまたいで大変だったぞ」
芥川「では今度は、メモの表示の前に『メモ』とつけてくれ、と依頼が来た場合どうなる」
太宰「そしたら、コンバータの変更だけでいいかな」
public class AttendanceDataToViewConverter : IAttendanceDataToViewConverter
{
public AttendanceDisplayEntry ConvertFrom(AttendanceData data)
{
return new AttendanceDisplayEntry
{
StartAt: data.StartAt,
EndAt: data.EndAt,
BreakTime: data.BreakTime,
- Memo: data.Memo
+ Memo: $"メモ {data.Memo}"
}
}
}
芥川「そうだ。AttendanceDataToViewConverter
を触る上では、もはやデータベースを知る必要が無い」
太宰「それはそうだけど、私はもうデータベースについて知っているぞ」
芥川「それはたまたまなんだ。もし君がこのチームに配属されてすぐに『メモを追加して』と言われて、以下のコードをみたとしよう」
public class AttendanceDisplayConverter
{
public IEnumerable<AttendanceDisplayEntry> Convert(IEnumerable<Row> attendance)
{
return attendance.Select(attendanceEntry =>
{
DateTimeOffset startAt = DateTimeOffset.FromUnixTimeSeconds(attendance["start_at"]);
DateTimeOffset endAt = DateTimeOffset.FromUnixTimeSeconds(attendance["end_at"]);
TimeSpan breakTime = TimeSpan.FromMinutes(attendance["break_time_mins"]);
return new AttendanceDisplayEntry(startAt, endAt, breakTime);
});
}
}
芥川「君はまず、Row
ってなんだ? AttendanceDisplayEntry
ってなんだ? と考え、探し当てなければならない」
芥川「今回はまだ参照がないかもしれないが、Row
を渡せるならなんでもいいなんて状況だ」
芥川「参照が2,3になっていたら、もはやこの修正が怖くてできないだろう。もしかしたら場合によっては Row
の中身が違うなんてざらにあるぞ」
太宰「ひい...」
芥川「でも若造はだいたい怖くないからな、だいたい変更を入れて障害を起こす」
太宰「まだ起こしてないよ」
芥川「ともかく、私たちは依存の逆転を使うことで、『クラスの修正にあたって必要な知識を削減』することができるのだ」
前:
今:
芥川「全体で見ると増えているように見えるが、小さい知識の集合体にまとまっている」
芥川「全部を常に修正しないといけない、なんてことはない。大体のプロジェクトではViewとModelは分かれていて、大体修正が必要なのはViewだ」
芥川「ほかにも、フィルタを追加するだけならModelの修正だけで済む。ViewとModelを両方修正することは、特に保守になったら希だ」
太宰「なるほどなあ」
太宰「今まで『事前に時間をかけることで将来の保守を軽くする』なんて聞いても、正直半信半疑だったけど、ちゃんと理由があったんだなあ」
芥川「その通りだ」
太宰「そういえば、最初のぺっぺろぱーの件はどうなるんだろう?」
芥川「ああ、そうだったな、最初のプログラムも修正しようか」
interface IGreeter
{
string GetGreet();
}
class Program
{
private readonly IGreeter _greeter;
public Program(IGreeter greeter) => _greeter = greeter;
public static void Main()
{
string greet = _greeter.GetGreet();
Console.WriteLine(greet);
}
}
class Hoge
{
private readonly IGreeter _greeter;
public Hoge(IGreeter greeter) => _greeter = greeter;
public void Run()
{
Greeter greeter = new();
string greet = greeter.GetGreet();
Console.WriteLine(greet);
}
}
太宰「これはでも、IGreeter
の中身をいじってしまったらバグになるよね?」
芥川「それは当然だ。だが Greeter
が露出されているのと違って、バグっていれば IGreeter
を実装するクラスを新しく作れば良い」
太宰「なるほどね。練習してみるかなあ」
芥川「励みたまえ」
Discussion