はじめに
「読みやすいコードのガイドライン」という本を読んでいる中で、
YAGNI
という原則が紹介されていましたので、記事にしてみたいと思います。
なお今回も前回の「ボーイスカウト・ルール」の記事の時と同様、
Kotlinで書かれた本著の内容をDartに置き換えて書いてみました。
YAGNIとは
YAGNI1とは
「そんなの必要にならないよ」
という英文の頭文字であり、
エクストリーム・プログラミング(XP)の原則の1つです。
含みからすると、
「それって今、必要かな?」
「また後で、必要になった時に実装した方が(結果的に)良いんじゃない?」
こんな感じになりますかね。
先々のことを色々と考え、
「こんな機能やコードを用意しておいた方がいいかも〜🤔」
「ここはこうしておいた方が後々便利になるかも〜🤔」
などと色々と考え、良かれと思って気を利かせてしまったがばっかりに、
結局使われることのないコードを残してしまうことになってしまいます...
そればかりか、そのコードによって機能の拡張が難しくなったり、
可読性やメンテナンス性も下がってしまって、
欠陥を生みやすい土壌に繋がってしまうことにもなってしまいます...😨
そうならないためにも、
「機能の実装は、その機能が必要になったタイミングで行った方が良い」
といった原則です。
具体例を書いてみた
これもコードに置き換えた方が分かりやすいかと思いますので、
具体例を書いてみたいと思います。
例えばこんな座標クラス(座標を示すモデルクラス)があったとします。
class Coodinate {
final int x;
final int y;
const Coodinate(this.x, this.y);
}
このCoodinate
クラスは
ビューの表示位置など、UIに関する座標をピクセル単位で表示するためのクラスです。
この状態であればとてもシンプルですが、
ここで、
「ポイント(1/72インチ)でも座標を指定できるようにした方がいいかなぁ〜」
と気を利かせて、そのように実装したとします。
enum UnitType {
PIXEL,
POINT,
}
class Coodinate {
final double x;
final double y;
final UnitType unitType;
const Coodinate(this.x, this.y, this.unitType);
}
確かにそんな局面もあるかと思いますし、
単位はUnitType
によって指定されているため、
ミリメートルなどの新しい単位が必要になったとしても列挙子を追加するだけなので、
一見するとこの時点では拡張性もある良いコードのように感じられますね。。。
困ったことに...
ただ、結局ポイントによる座標指定が使われることはなく、
代わりにCoodinate
クラスを使った座標の加減算が必要になったといたします。
確かに、描画範囲を計算したり、UI要素のサイズや要素間の余白の計算をしたりなど、
そんな場合には座標の加減算も必要になりそうですね。
しかしここで困ったことになります...
enum UnitType {
PIXEL,
POINT,
}
/// YAGNIではないバッドコードな座標クラス
class Coodinate {
final double x;
final double y;
final UnitType unitType;
const Coodinate(this.x, this.y, this.unitType);
static const int POINTS_PER_INCH = 72;
Coodinate plus(
Coodinate that, DisplayEnvironment environment, UnitType resultUnitType) {
final newX = _convertType(x, unitType, resultUnitType, environment) +
_convertType(that.x, that.unitType, resultUnitType, environment);
final newY = _convertType(y, unitType, resultUnitType, environment) +
_convertType(that.y, that.unitType, resultUnitType, environment);
return Coodinate(newX, newY, resultUnitType);
}
double _convertType(double value, UnitType from, UnitType to,
DisplayEnvironment environment) {
if (from == to) {
return value;
}
switch (to) {
case UnitType.PIXEL:
return value * environment.pixelPerInch / POINTS_PER_INCH;
case UnitType.POINT:
return value * POINTS_PER_INCH / environment.pixelPerInch;
}
}
}
class DisplayEnvironment {
final int pixelPerInch;
const DisplayEnvironment(this.pixelPerInch);
}
上記のコードを見ていただくと感じていただきやすいかと思われますが、
座標の加減算をしたいのにもかかわらず、本来なら不必要な処理が色々と入っていますね...
そのせいで全体がゴチャゴチャとしており、
パッと見て何をしているのかを直感的に把握しづらくてなっています...
ただの座標の加減算が色々なコンテキストに依存してしまっているばっかりに、
とても回りくどく、ややこしいことになっていますね...😱
本来なら...
もし上記のような先回りをしなければ、本当は...
/**
* YAGNIを踏まえた座標クラス
*/
class Coodinate {
final int x;
final int y;
const Coodinate(this.x, this.y);
Coodinate plus(Coodinate that) {
return Coodinate(x + that.x, y + that.y);
}
}
こんなにもシンプルに書けたんです。。。
先々のことを色々と深く考えすぎてしまわずに、
ここは愚直にシンプルに書けばよかったんですね😅
注意点
ここで注意点があります。
YAGNIはあくまでも「現在使っていない機能の実装」に焦点を当てているので、
その機能が必要かどうかといった議論や検証そのものや、
必要になった時の設計や実装方法までは否定はしてはいません。
便利な原則であるがゆえに、ここを誤解してしまい、
本当は必要な上記のプロセスまでも省略しないように気をつけないといけないですね💦
参照
-
You ain't gonna need itの略です。 ↩