LoginSignup
8
4

More than 5 years have passed since last update.

プログラミングの原則6(5)選の具体事例

Last updated at Posted at 2018-06-14

この記事について

新人プログラマが知るべきプログラミングの原則6選!の補足記事として本記事を読むことで、より理解を深めて頂けたら幸いです。
※例外処理など一部簡略化しています。

KISS

Keep It Simple, Stupid
コードはシンプルに書きましょう。

以下は、int型の引数を渡して呼び出すと、対応する曜日が返却されるweekday()の実装です。

ComplexCode.java
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];
}
SimpleCode.java
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()を呼び出す処理とその実装です。
それぞれのメソッドの中では最終更新日時を「異なる日付フォーマットで」出力しています。

WetCode.java
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);
    }
}
DryCode.java
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を合計した結果を返却するメソッドを作成しない」と言われたとしましょう。
どう実装するか、考えてみてください。


Sum.java
int sum(int x, int y) {
    return x + y;
}

大半の方は上記のような実装を思い浮かべたのではないでしょうか。
しかしこのメソッドは無駄に汎用化された実装となっています。
要件は「2と3の合計値」なのに、「任意の二つの数字の合計値」を返却するメソッドとなってしまっています。
無駄に汎用化させてしまったことで、
・int型以外の変数を引数として渡すとコンパイルエラーとなる
・intのmax値を引数として渡すと期待する値が得られない
・テストケースの複雑化
など、様々な影響を引き起こす可能性があり、無駄な工数が発生する要因となります。

Sum.java
int twoPlusThree() {
    return 2 + 3;
}

上記のコードは要件である「2と3の合計値」を返却するメソッドです。
引数を渡す必要が無いためバグが起こることもなく、テストも単純です。

気を利かして要件の範囲を超えたコードを記述すると、余計な作業が発生する可能性があるので、
必要最低限のコードを記述するようにしましょう。

PIE

Program intently and expressively
意図を表現したコードを書きましょう。

以下はプレイヤーのHP(ヒットポイント)が減ったかどうかを判定するメソッドです。

UglyCode.java
int check(int n, int max) {
    if (n < max) {
        return -1;
    } else {
        return 1;
    }
}

第一引数に現在のHP、第二引数に最大HPを渡し、1が返却された場合は「HPが減った」ことになります。
という解説を聞いて初めて「ああそういうメソッドなのね」と理解出来るコードになっています。

以下はPIE原則に則って記述されたコードです。

AwesomeCode.java
boolean hasTakenDamage(Player player) {
    return player.currentHealth < player.maxHealth;
}

コメントが無くても、このメソッドの意図がはっきりと分かるコードになっています。
「コンピュータが理解できるコード」を書くのは簡単です。「人間が理解できるコード」を書くのが我々のお仕事です。

SLAP

Single level of abstraction principle
抽象化レベルを統一しましょう。

以下はSet型のEntityをList型のDTOに変換して返却するbuildResult()メソッドです。

NonSlapCode.java
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に変換している」ことを読み解く必要があります。

SlapCode.java
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

8
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
4