きり丸の技術日記

技術検証したり、資格等をここに残していきます。

SLAP(抽象度統一の原則)を覚えてリファクタリングをしよう

SLAPという抽象度統一の原則をご存じでしょうか。
エンジニアであれば、覚えておくと1つレベルアップできるキーワードとなりますので、覚えておくと便利です。

ぜひ、この記事で覚えていってください。


この内容は以下のLTの中で、リファクタリングの一環として発表しようかと思いました。

しかし、5分間だと話し切れない可能性が高いので、ブログに残します。


なお、SLAPで検索すると良く出てくるこちらの本は読んだことありません。
直接他のエンジニアに教えてもらったので、原著と違う点があったらコメント及びマサカリ願います。

SLAPとは

SLAPとはSingle Level of Abstraction Principleの略です。

プログラムにて、記載している内容のレベルを揃えることを意味します。

  • 処理としてやりたいこと。
  • やりたいことを達成するために、条件やループを使う。

上記が混ざっていると、コードが読みづらくなってしまいます。

正直、これだけ見ても意味が分からないと思うので、例でわかっていただければと思います。

※ javaは雰囲気でお願いします。

やりたいこと

あなたは学園の生徒です。  
あなたの学年を元に、どの県から何人在籍しているかを調べてください。

処理の流れ

  1. パラメータにあなた(生徒)を指定する。
  2. あなたの学年を取得する。
  3. あなたの学年から、生徒たちを取得する。
  4. 生徒たちから出身を取得する。
  5. 出身からどの県に何人いるかを数える。

何も考えずにコードを組むとなると以下のようになると思います。

/**
 * 所属する学年の生徒の出身地を調べる。
 */
public 出身地と生徒数 findBirthPlaceForClassmate(生徒 you) {
    // 2. 学年を取得
    学年 grade = you.get学年();
    // 3. 学年の生徒を取得
    List<生徒> students = grade.find生徒たち();

    List<出身地と生徒数> birthPlaceList = new ArrayList();
    for (生徒 student: students) {
        // 4. 生徒から出身を取得する
        出身地 birthPlace = student.get出身地();
        // 5. 出身からどの県に何人いるかを数える
        出身地と生徒数 tmp = birthPlaceList.findBirthPlace(birthPlace);
        // 5-1. 処理中に出身地を設定していなければ新しく設定する。
        if (tmp == null) {
            出身地と生徒数 tmp2 = new 出身地と生徒数(birthPlace, 1);
            birthPlaceList.add(tmp2);
        } else {
            // 5-2. 既に設定していた場合は、生徒数を加算する。
            tmp.生徒数 += 1;
        }
    }
    return birthPlaceList;
}

public class 出身地と生徒数 {
    private 出身地 出身地;
    private int 生徒数;
}

この時点で、だいぶ見づらいと思います。
特に分割もしていないので、コメントも蛇足になりがちです。

ここで、どの県の出身者が多いのかを計算するロジックを別関数にしてみましょう。

※コメントが蛇足になるので、削除します。
public 出身地と生徒数 findBirthPlaceForClassmate(生徒 you) {
    学年 grade = you.get学年();
    List<生徒> students = grade.find生徒たち();
    return 出身地と生徒数を計算する(students);
}

private 出身地と生徒数を計算する(List<生徒> students) {
    List<出身地と生徒数> birthPlaceList = new ArrayList();
    for (生徒 student: students) {
        出身地 birthPlace = student.get出身地();
        出身地と生徒数 tmp = birthPlaceList.findBirthPlace(birthPlace);
        if (tmp == null) {
            出身地と生徒数 tmp2 = new 出身地と生徒数(birthPlace, 1);
            birthPlaceList.add(tmp2);
        } else {
            tmp.生徒数 += 1;
        }
    }
    return birthPlaceList;
}

※出身地と生徒数クラスは割愛

どうでしょうか。
publicメソッドを見ると、たった3行の処理になっています。

ただ、これだけでは分割しただけで、大きなメリットはないように思えます。

なので、NullPointerException(NPE)を回避する防御的なロジックを入れてみましょう。

public 出身地と生徒数 findBirthPlaceForClassmate(生徒 you) {
    学年 grade = you.get学年();
    if (grade == null) {
        throw new RuntimeException("卒業生?");
    }
    List<生徒> students = grade.find生徒たち();
    if (students == null) {
        throw new RuntimeException("あなたの学年が異常");
    }

    return 出身地と生徒数を計算する(students);
}

※出身地と生徒数を計算するメソッドは割愛

ここで、nullを回避するロジックを入れてしまったことで、記載しているレベルが混ざってしまいました。
異常系のハンドリングは大事ですが、処理の流れを追いたい時には蛇足でしかありません。

なので、別関数にしてしまいましょう。

public 出身地と生徒数 findBirthPlaceForClassmate(生徒 you) {
    学年 grade = 学年を取得(you);
    List<生徒> students = 生徒を取得(grade);
    return 出身地と生徒数を計算する(students);
}

private 学年 学年を取得(生徒 student){
    学年 grade = you.get学年();
    if (grade == null) {
        throw new RuntimeException("卒業生?");
    }
    return grade;
}

private List<生徒> 生徒を取得(学年 grade){
    List<生徒> students = grade.find生徒たち();
    if (students == null) {
        throw new RuntimeException("あなたの学年が異常");
    }
    return students;
}

こうしてみるとどうでしょうか。
public関数は綺麗なコードを維持できるので、後から非常に読みやすくなります。

また、privateに切り出した単位が表現したい単位になりますので、ロングメソッドになりづらいです。
もし、ロングメソッドになった場合は、表現したいことが複数混ざっている可能性があるので、思い切って分けてしまいましょう。

プログラムの実行速度は落ちますが、大した変化はありません。
理解しやすくする方が大事です。
明示的に速度のためであれば、なぜ処理を分けていないのか(why not)をコメントで残しましょう。

なお、上記内容であれば、IDEで自動メソッド切り出しできるので、工数はほぼ0でリファクタリングできます。

終わりに

当然、ifやforや == があっても、コードは普通に読めます。
ifを使ったら、別関数にしなさい、という訳でもありません。

あなたが何気なく書いたifは、本当に表現したいことですか?
エラーハンドリングのために仕方なく書いたものではないですか?

この記事の最初の方に書いたこちらが非常に大事になります。

  • 処理としてやりたいこと。
  • やりたいことを達成するために、条件やループを使う。

コードを通して、何を表現したいのか。
自問自答していくことで、エンジニアとしてのレベルアップは図れますので、ぜひ覚えていってください。

参考