この記事について
新人プログラマが知るべきプログラミングの原則6選!の補足記事として本記事を読むことで、より理解を深めて頂けたら幸いです。
※例外処理など一部簡略化しています。
KISS
Keep It Simple, Stupid
コードはシンプルに書きましょう。
以下は、int型の引数を渡して呼び出すと、対応する曜日が返却されるweekday()
の実装です。
public String weekday(int dayOfWeek) {
if ((dayOfWeek < 1) || (dayOfWeek > 7))
throw new IllegalArgumentException("dayOfWeek must be in range 1..7");
final String[] weekdays
= {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"};
return weekdays[dayOfWeek-1];
}
public String weekday(int dayOfWeek) {
switch(dayOfWeek) {
case 1: return "Monday";
case 2: return "Tuesday";
case 3: return "Wednesday";
case 4: return "Thursday";
case 5: return "Friday";
case 6: return "Saturday";
case 7: return "Sunday";
default: throw new IllegalArgumentException("dayOfWeek must be in range 1..7");
}
}
どちらも振舞いとしては同じですが、実装方法が異なります。
ComplexCode.java
の実装はif
文、計算処理、Exception
のスロー、配列の初期化、return
文が記述されています。
SimpleCode.java
の実装はswitch
文、return
文、Exception
のスローから成り立っており、開発者がコードを読む際に必要な「思考回数」がComplexCode.java
より少ないことが分かります。
また、SimpleCode.java
の実装では入力に対する出力が直感的に分かります。
このように「シンプルなコード」とは、難易性や複雑性を排除し、万人に読みやすく記述されたコードを指します。
DRY
Don't Repeat Yourself
同じようなコードを繰り返して書くのはよしなさい。
以下は、ファイルをコピーするcopy()
を呼び出し、その後にファイルを削除するdelete()
を呼び出す処理とその実装です。
それぞれのメソッドの中では最終更新日時を「異なる日付フォーマットで」出力しています。
copy(oldPath, newPath);
doSomething();
delete(oldPath);
void copy(Path oldPath, Path newPath) {
if (Files.exists(oldPath)) {
long lastModifiedTime = Files.getLastModifiedTime(oldPath).toMillis();
SimpleDateFormatdf sdf = new SimpleDateFormat("yyyy/MM/dd");
String formattedLastModifiedTime = sdf.format(lastModifiedTime);
System.out.println("最終更新日時:" + formattedLastModifiedTime);
Files.copy(path, newPath);
}
}
void delete(Path path) {
if (Files.exists(path)) {
long lastModifiedTime = Files.getLastModifiedTime(path).toMillis();
SimpleDateFormat sdf = new SimpleDateFormat("MM/dd/yyyy");
String formattedLastModifiedTime = sdf.format(lastModifiedTime);
System.out.println("最終更新日時:" + formattedLastModifiedTime);
Files.delete(path);
}
}
copy(oldPath, newPath);
doSomething();
delete(path);
void copy(Path oldPath, Path newPath) {
if (Files.exists(oldPath)) {
printLastModifiedTime(oldPath, new SimpleDateFormat("yyyy/MM/dd"));
Files.copy(oldPath, newPath);
}
}
void delete(Path path) {
if (Files.exists(path)) {
printLastModifiedTime(path, new SimpleDateFormat("MM/dd/yyyy"));
Files.delete(path);
}
}
void printLastModifiedTime(Path path, SimpleDateFormat sdf) {
long lastModifiedTime = Files.getLastModifiedTime(path).toMillis();
String formattedLastModifiedTime = sdf.format(lastModifiedTime);
System.out.println("最終更新日時:" + formattedLastModifiedTime);
}
WetCode.java
では最終更新日時を出力する処理がcopy()
とdelete()
にそれぞれ記述されています。
この場合、文言を「最終更新日時は:」に変更する仕様変更が入った際に、2箇所の修正が必要となります。
DryCode.java
ではそれを一つのメソッドに集約することで、修正が1箇所で済みます。
修正量、レビュー時間、テスト時間もろもろが単純計算で半分になります。
このように、同じコード、似たコードの繰り返しを排除することで、コードの保守性、複雑性、可読性が改善されることが分かります。
YAGNI
You aren't(ain't) gonna need it
不要なコードを書くのはよしなさい。
「2と3を合計した結果を返却するメソッドを作成しない」と言われたとしましょう。
どう実装するか、考えてみてください。
・
・
・
int sum(int x, int y) {
return x + y;
}
大半の方は上記のような実装を思い浮かべたのではないでしょうか。
しかしこのメソッドは無駄に汎用化された実装となっています。
要件は「2と3の合計値」なのに、「任意の二つの数字の合計値」を返却するメソッドとなってしまっています。
無駄に汎用化させてしまったことで、
・int型以外の変数を引数として渡すとコンパイルエラーとなる
・intのmax値を引数として渡すと期待する値が得られない
・テストケースの複雑化
など、様々な影響を引き起こす可能性があり、無駄な工数が発生する要因となります。
int twoPlusThree() {
return 2 + 3;
}
上記のコードは要件である「2と3の合計値」を返却するメソッドです。
引数を渡す必要が無いためバグが起こることもなく、テストも単純です。
気を利かして要件の範囲を超えたコードを記述すると、余計な作業が発生する可能性があるので、
必要最低限のコードを記述するようにしましょう。
PIE
Program intently and expressively
意図を表現したコードを書きましょう。
以下はプレイヤーのHP(ヒットポイント)が減ったかどうかを判定するメソッドです。
int check(int n, int max) {
if (n < max) {
return -1;
} else {
return 1;
}
}
第一引数に現在のHP、第二引数に最大HPを渡し、1が返却された場合は「HPが減った」ことになります。
という解説を聞いて初めて「ああそういうメソッドなのね」と理解出来るコードになっています。
以下はPIE原則に則って記述されたコードです。
boolean hasTakenDamage(Player player) {
return player.currentHealth < player.maxHealth;
}
コメントが無くても、このメソッドの意図がはっきりと分かるコードになっています。
「コンピュータが理解できるコード」を書くのは簡単です。「人間が理解できるコード」を書くのが我々のお仕事です。
SLAP
Single level of abstraction principle
抽象化レベルを統一しましょう。
以下はSet
型のEntityをList
型のDTOに変換して返却するbuildResult()
メソッドです。
public List<ResultDto> buildResult(Set<ResultEntity> resultSet) {
List<ResultDto> result = new ArrayList<>();
for (ResultEntity entity : resultSet) {
ResultDto dto = new ResultDto();
dto.setFirstName(entity.getFirstName());
dto.setLastName(entity.getLastName());
dto.setAge(computeAge(entity.getBirthday()));
result.add(dto);
}
return result;
}
このメソッドには二つの抽象化レベルが存在します。一つはresultSet
のループ処理、もう一つはループ処理内で各entity
をDTOに変換する処理です。コードの読み手はこのメソッドを読む際に、「ループ内の最初の4行でDTOに変換している」ことを読み解く必要があります。
public List<ResultDto> buildResult(Set<ResultEntity> resultSet) {
List<ResultDto> dtoList = new ArrayList<>();
for (ResultEntity entity : resultSet) {
ResultDto dto = toDto(entity);
dtoList.add(dto);
}
return dtoList;
}
private ResultDto toDto(ResultEntity entity) {
ResultDto dto = new ResultDto();
dto.setFirstName(entity.getName());
dto.setLastName(entity.getName());
dto.setAge(computeAge(entity.getBirthday()));
return dto;
}
先述のメソッドを二つのメソッドに分解したことで、各々が一つの抽象化レベルで記述されています。
各々のメソッドが独立して理解出来るようになっています。つまり、toDto()
の処理の詳細を気にする必要がない場合、buildResult()
を読むだけで(余計な情報に邪魔されることなく)処理内容を理解することが出来ます。
参考
http://principles-wiki.net/principles:keep_it_simple_stupid
https://enterprisecraftsmanship.com/2015/06/11/yagni-revisited/
https://qiita.com/kyammy/items/63db981d35886ee5806f
http://principles-wiki.net/principles:single_level_of_abstraction