Tbpgr Blog

Employee Experience Engineer tbpgr(てぃーびー) のブログ

if文がネストするような条件をどうシンプルにするか?

概要

if文がネストするような条件をどうシンプルにするか?

問題

チェックロジック系など、条件文が多くなってくるとif文やswitch文の
記述が長くなりわかりにくくなる。
実際に仕事でもよく遭遇するタイプのコード。

例題

超人の必須チェック処理を想定。

超人種別がない場合はその時点でエラー。
正義超人は必須チェック不要。
悪魔超人は名前と必殺技と担当するミート君の体の部位が必須。
完璧超人(旧)は名前と必殺技必須。
完璧超人(真)は名前と必殺技と異名必須。

必須チェック表

超人種別有無 超人種別 名前 必殺技 ミート部位 異名 エラー内容 備考
× null 超人種別必須エラー 超人種別必須エラー時は他のパラメータの内容に関わらずエラー
悪魔超人 false × 正常 異名以外は必須
悪魔超人 false × × 名前必須エラー 異名以外は必須
悪魔超人 false × × × 名前必須エラー 異名以外は必須
悪魔超人 false × × × 名前必須エラー 異名以外は必須
悪魔超人 false × × × × 名前必須エラー 異名以外は必須
悪魔超人 false × × 必殺技必須エラー 異名以外は必須
悪魔超人 false × × × 必殺技必須エラー 異名以外は必須
悪魔超人 false × × ミート部位必須エラー 異名以外は必須
完璧超人 false × × 正常 異名以外は必須
完璧超人 false × × × 名前必須エラー 名前と必殺技は必須
完璧超人 false × × × × 名前必須エラー 名前と必殺技は必須
完璧超人 false × × × 必殺技必須エラー 名前と必殺技は必須
完璧超人 true × 正常 ミート部位以外は必須
完璧超人 true × × 名前必須エラー ミート部位以外は必須
完璧超人 true × × × 名前必須エラー ミート部位以外は必須
完璧超人 true × × × 名前必須エラー ミート部位以外は必須
完璧超人 true × × × × 名前必須エラー ミート部位以外は必須
完璧超人 true × × 必殺技必須エラー ミート部位以外は必須
完璧超人 true × × × 必殺技必須エラー ミート部位以外は必須
完璧超人 true × × 異名必須エラー ミート部位以外は必須

※以下のサンプルコードはあくまで4パターンの観点を比較することを目的としています。
個別コードの記述作法などは最適化されていません。

共通利用コード

public enum SuperManEnum {
  JUSTICE("1"), DEVIL("2"), PERFECT("3");

  @SuppressWarnings("unused")
  private String type;

  private SuperManEnum(final String type) {
    this.type = type;
  }
}
public class SuperMan {
  public SuperManEnum superManType;
  public boolean isReal;
  public String name;
  public String specialMoves;
  public String meatBodyParts;
  public String nickname;

  public SuperMan(SuperManEnum superManType, boolean isReal, String name, String specialMoves,
      String meatBodyParts, String nickname) {
    this.superManType = superManType;
    this.isReal = isReal;
    this.name = name;
    this.specialMoves = specialMoves;
    this.meatBodyParts = meatBodyParts;
    this.nickname = nickname;
  }
}

実装パターン1

ベタ書き版
条件が多くなると、if文が大量になりコードの見通しが悪くなる。
派遣系下請会社の有象無象が集まるような現場だと誰もが理解できるこの構造がベストに
なってしまうのか?
で、現状は実際こうなっている。

public class ComplexConditionSample1 {

  public ComplexConditionSample1() {
  }

  /**
   * オーソドックス。
   *
   * @param args
   */
  public static void main(String[] args) {
    ComplexConditionSample1 complexConditionSample1 = new ComplexConditionSample1();
    SuperMan kinNikuMan = new SuperMan(SuperManEnum.JUSTICE, false, "キン肉マン", "マッスルスパーク", null, null);
    System.out.println(complexConditionSample1.checkError(kinNikuMan));

    SuperMan stekaseKing = new SuperMan(SuperManEnum.DEVIL, false, "ステカセキング", "超人大全集", "胴体", null);
    System.out.println(complexConditionSample1.checkError(stekaseKing));
    stekaseKing.meatBodyParts = null;
    System.out.println(complexConditionSample1.checkError(stekaseKing));

    SuperMan neptuneMan = new SuperMan(SuperManEnum.PERFECT, false, "ネプチューンマン", "喧嘩ボンバー", null, null);
    System.out.println(complexConditionSample1.checkError(neptuneMan));
    neptuneMan.specialMoves = null;
    System.out.println(complexConditionSample1.checkError(neptuneMan));

    SuperMan strongTheBudo = new SuperMan(SuperManEnum.PERFECT, true, "ストロング・ザ・ブドー", "竹刀攻撃", null, "完武");
    System.out.println(complexConditionSample1.checkError(strongTheBudo));
    strongTheBudo.nickname = null;
    System.out.println(complexConditionSample1.checkError(strongTheBudo));

    SuperMan human = new SuperMan(null, false, null, null, null, null);
    System.out.println(complexConditionSample1.checkError(human));

  }

  private boolean checkError(SuperMan superMan) {
    if (superMan.superManType == null) {
      return false;
    }

    if (superMan.superManType == SuperManEnum.DEVIL) {
      if (superMan.name == null || superMan.specialMoves == null || superMan.meatBodyParts == null) {
        return false;
      }
    } else if (superMan.superManType == SuperManEnum.PERFECT) {
      if (superMan.isReal) {
        if (superMan.name == null || superMan.specialMoves == null || superMan.nickname == null) {
          return false;
        }
      } else {
        if (superMan.name == null || superMan.specialMoves == null) {
          return false;
        }
      }
    }
    return true;
  }
}

実装パターン2

必須チェック表の内容をテーブルで表現。
条件が増えた場合は正解テーブルのみ更新すればよい。
しかし、これってオブジェクト指向的にはトリッキーな部類に入るのでは?
また、ベタ書き版に比べて不要な真偽判定が実行される。

import java.util.ArrayList;
import java.util.List;

public class ComplexConditionSample2 {

  public ComplexConditionSample2() {
  }

  /**
   * テーブル比較。
   *
   * @param args
   */
  public static void main(String[] args) {
    ComplexConditionSample2 complexConditionSample2 = new ComplexConditionSample2();
    SuperMan kinNikuMan = new SuperMan(SuperManEnum.JUSTICE, false, "キン肉マン", "マッスルスパーク", null, null);
    System.out.println(complexConditionSample2.checkError(kinNikuMan));

    SuperMan stekaseKing = new SuperMan(SuperManEnum.DEVIL, false, "ステカセキング", "超人大全集", "胴体", null);
    System.out.println(complexConditionSample2.checkError(stekaseKing));
    stekaseKing.meatBodyParts = null;
    System.out.println(complexConditionSample2.checkError(stekaseKing));

    SuperMan neptuneMan = new SuperMan(SuperManEnum.PERFECT, false, "ネプチューンマン", "喧嘩ボンバー", null, null);
    System.out.println(complexConditionSample2.checkError(neptuneMan));
    neptuneMan.specialMoves = null;
    System.out.println(complexConditionSample2.checkError(neptuneMan));

    SuperMan strongTheBudo = new SuperMan(SuperManEnum.PERFECT, true, "ストロング・ザ・ブドー", "竹刀攻撃", null, "完武");
    System.out.println(complexConditionSample2.checkError(strongTheBudo));
    strongTheBudo.nickname = null;
    System.out.println(complexConditionSample2.checkError(strongTheBudo));

    SuperMan human = new SuperMan(null, false, null, null, null, null);
    System.out.println(complexConditionSample2.checkError(human));
  }

  private boolean checkError(SuperMan superMan) {
    List<CheckTable> resultTable = getResultList();
    CheckTable superManTable = this.new CheckTable((superMan.superManType != null), superMan.superManType,
        superMan.isReal, (superMan.name != null), (superMan.specialMoves != null), (superMan.meatBodyParts != null),
        (superMan.nickname != null));

    // 結果テーブルと比較して一致するならエラー
    for (CheckTable check : resultTable) {
      if ((check.isSuperMan == superManTable.isSuperMan && (check.superManCls == superManTable.superManCls) && (check.isReal == superManTable.isReal))
          && (check.hasName == superManTable.hasName)
          && (check.hasSpecialMoves == superManTable.hasSpecialMoves)
          && (check.hasMeatBodyParts == superManTable.hasMeatBodyParts)
          && (check.hasNickName == superManTable.hasNickName)) {
        return false;
      }
    }
    return true;
  }

  // 個別の比較要素をフィールドにしたクラス
  class CheckTable {
    public CheckTable(boolean isSuperMan, SuperManEnum superManCls, boolean isReal, boolean hasName,
        boolean hasSpecialMoves, boolean hasMeatBodyParts, boolean hasNickName) {
      super();
      this.isSuperMan = isSuperMan;
      this.superManCls = superManCls;
      this.isReal = isReal;
      this.hasName = hasName;
      this.hasSpecialMoves = hasSpecialMoves;
      this.hasMeatBodyParts = hasMeatBodyParts;
      this.hasNickName = hasNickName;
    }

    boolean isSuperMan = true;
    SuperManEnum superManCls = null;
    boolean isReal = false;
    boolean hasName = false;
    boolean hasSpecialMoves = false;
    boolean hasMeatBodyParts = false;
    boolean hasNickName = false;
  }

  // 比較用結果データ生成メソッド
  private List<CheckTable> getResultList() {
    // nullの場合false
    CheckTable checkTable1 = this.new CheckTable(false, null, false, false, false, false, false);
    CheckTable checkTable2 = this.new CheckTable(true, SuperManEnum.DEVIL, false, false, true, true, false);
    CheckTable checkTable3 = this.new CheckTable(true, SuperManEnum.DEVIL, false, false, false, true, false);
    CheckTable checkTable4 = this.new CheckTable(true, SuperManEnum.DEVIL, false, false, true, false, false);
    CheckTable checkTable5 = this.new CheckTable(true, SuperManEnum.DEVIL, false, false, false, false, false);
    CheckTable checkTable6 = this.new CheckTable(true, SuperManEnum.DEVIL, false, true, false, true, false);
    CheckTable checkTable7 = this.new CheckTable(true, SuperManEnum.DEVIL, false, true, false, false, false);
    CheckTable checkTable8 = this.new CheckTable(true, SuperManEnum.DEVIL, false, true, true, false, false);
    CheckTable checkTable9 = this.new CheckTable(true, SuperManEnum.PERFECT, false, false, true, false, false);
    CheckTable checkTable10 = this.new CheckTable(true, SuperManEnum.PERFECT, false, false, false, false, false);
    CheckTable checkTable11 = this.new CheckTable(true, SuperManEnum.PERFECT, false, true, false, false, false);
    CheckTable checkTable12 = this.new CheckTable(true, SuperManEnum.PERFECT, true, false, true, false, true);
    CheckTable checkTable13 = this.new CheckTable(true, SuperManEnum.PERFECT, true, false, false, false, true);
    CheckTable checkTable14 = this.new CheckTable(true, SuperManEnum.PERFECT, true, false, true, false, false);
    CheckTable checkTable15 = this.new CheckTable(true, SuperManEnum.PERFECT, true, false, false, false, false);
    CheckTable checkTable16 = this.new CheckTable(true, SuperManEnum.PERFECT, true, true, false, false, true);
    CheckTable checkTable17 = this.new CheckTable(true, SuperManEnum.PERFECT, true, true, false, false, false);
    CheckTable checkTable18 = this.new CheckTable(true, SuperManEnum.PERFECT, true, true, true, false, false);
    List<CheckTable> resultTable = new ArrayList<CheckTable>();
    resultTable.add(checkTable1);
    resultTable.add(checkTable2);
    resultTable.add(checkTable3);
    resultTable.add(checkTable4);
    resultTable.add(checkTable5);
    resultTable.add(checkTable6);
    resultTable.add(checkTable7);
    resultTable.add(checkTable8);
    resultTable.add(checkTable9);
    resultTable.add(checkTable10);
    resultTable.add(checkTable11);
    resultTable.add(checkTable12);
    resultTable.add(checkTable13);
    resultTable.add(checkTable14);
    resultTable.add(checkTable15);
    resultTable.add(checkTable16);
    resultTable.add(checkTable17);
    resultTable.add(checkTable18);
    return resultTable;
  }
}

実装パターン3

必須チェック表の内容をマップで表現。
パターン2の完全上位版か。2の場合は値の一致のために
リスト数と項目数分の一致チェックが必要だが
Mapの場合は、keyで一発ヒットなので処理が軽い。
実装パターン2と同様トリッキーであることがデメリットか。
また、ベタ書き版に比べて不要な真偽判定が実行される。

import java.util.HashMap;
import java.util.Map;

public class ComplexConditionSample3 {

  public ComplexConditionSample3() {
  }

  /**
   * マップ比較。
   *
   * @param args
   */
  public static void main(String[] args) {
    ComplexConditionSample3 complexConditionSample2 = new ComplexConditionSample3();
    SuperMan kinNikuMan = new SuperMan(SuperManEnum.JUSTICE, false, "キン肉マン", "マッスルスパーク", null, null);
    System.out.println(complexConditionSample2.checkError(kinNikuMan));

    SuperMan stekaseKing = new SuperMan(SuperManEnum.DEVIL, false, "ステカセキング", "超人大全集", "胴体", null);
    System.out.println(complexConditionSample2.checkError(stekaseKing));
    stekaseKing.meatBodyParts = null;
    System.out.println(complexConditionSample2.checkError(stekaseKing));

    SuperMan neptuneMan = new SuperMan(SuperManEnum.PERFECT, false, "ネプチューンマン", "喧嘩ボンバー", null, null);
    System.out.println(complexConditionSample2.checkError(neptuneMan));
    neptuneMan.specialMoves = null;
    System.out.println(complexConditionSample2.checkError(neptuneMan));

    SuperMan strongTheBudo = new SuperMan(SuperManEnum.PERFECT, true, "ストロング・ザ・ブドー", "竹刀攻撃", null, "完武");
    System.out.println(complexConditionSample2.checkError(strongTheBudo));
    strongTheBudo.nickname = null;
    System.out.println(complexConditionSample2.checkError(strongTheBudo));

    SuperMan human = new SuperMan(null, false, null, null, null, null);
    System.out.println(complexConditionSample2.checkError(human));
  }

  private boolean checkError(SuperMan superMan) {
    Map<String, Boolean> resultMap = getResultMap();
    String key = "" + (superMan.superManType != null) + superMan.superManType + superMan.isReal
        + (superMan.name != null) + (superMan.specialMoves != null) + (superMan.meatBodyParts != null)
        + (superMan.nickname != null);
    Boolean result = resultMap.get(key);
    if (result == null) {
      return true;
    }
    return result;

  }

  // 比較用結果データ生成メソッド
  private Map<String, Boolean> getResultMap() {
    Map<String, Boolean> map = new HashMap<String, Boolean>();
    // nullの場合false
    map.put("falsenullfalsefalsefalsefalsefalse", false);
    map.put("trueDEVILfalsefalsetruetruefalse", false);
    map.put("trueDEVILfalsefalsefalsetruefalse", false);
    map.put("trueDEVILfalsefalsetruefalsefalse", false);
    map.put("trueDEVILfalsefalsefalsefalsefalse", false);
    map.put("trueDEVILfalsetruefalsetruefalse", false);
    map.put("trueDEVILfalsetruefalsefalsefalse", false);
    map.put("trueDEVILfalsetruetruefalsefalse", false);
    map.put("truePERFECTfalsefalsetruefalsefalse", false);
    map.put("truePERFECTfalsefalsefalsefalsefalse", false);
    map.put("truePERFECTfalsetruefalsefalsefalse", false);
    map.put("truePERFECTtruefalsetruefalsetrue", false);
    map.put("truePERFECTtruefalsefalsefalsetrue", false);
    map.put("truePERFECTtruefalsetruefalsefalse", false);
    map.put("truePERFECTtruefalsefalsefalsefalse", false);
    map.put("truePERFECTtruetruefalsefalsetrue", false);
    map.put("truePERFECTtruetruefalsefalsefalse", false);
    map.put("truePERFECTtruetruetruefalsefalse", false);
    return map;
  }
}

実装パターン4

チェックをStrategyパターンによる多様性で表現。
個人的にはオブジェクト指向ならこれかな、と思うが現場では
NGが出るのが見え見えであるため提案はしないでおこう。
デメリットはオブジェクト指向に慣れてない人には受け入れがたい。
クラス数が増えるのを嫌うタイプの現場だと受け入れられない。

public class ComplexConditionSample4 {

  public ComplexConditionSample4() {
  }

  /**
   * 多様性+Strategy。
   *
   * @param args
   */
  public static void main(String[] args) {
    ComplexConditionSample4 complexConditionSample1 = new ComplexConditionSample4();
    SuperMan kinNikuMan = new SuperMan(SuperManEnum.JUSTICE, false, "キン肉マン", "マッスルスパーク", null, null);
    System.out.println(complexConditionSample1.checkError(kinNikuMan));

    SuperMan stekaseKing = new SuperMan(SuperManEnum.DEVIL, false, "ステカセキング", "超人大全集", "胴体", null);
    System.out.println(complexConditionSample1.checkError(stekaseKing));
    stekaseKing.meatBodyParts = null;
    System.out.println(complexConditionSample1.checkError(stekaseKing));

    SuperMan neptuneMan = new SuperMan(SuperManEnum.PERFECT, false, "ネプチューンマン", "喧嘩ボンバー", null, null);
    System.out.println(complexConditionSample1.checkError(neptuneMan));
    neptuneMan.specialMoves = null;
    System.out.println(complexConditionSample1.checkError(neptuneMan));

    SuperMan strongTheBudo = new SuperMan(SuperManEnum.PERFECT, true, "ストロング・ザ・ブドー", "竹刀攻撃", null, "完武");
    System.out.println(complexConditionSample1.checkError(strongTheBudo));
    strongTheBudo.nickname = null;
    System.out.println(complexConditionSample1.checkError(strongTheBudo));

    SuperMan human = new SuperMan(null, false, null, null, null, null);
    System.out.println(complexConditionSample1.checkError(human));

  }

  private boolean checkError(SuperMan superMan) {
    if (superMan.superManType == null) {
      return false;
    }
    CheckStrategy checkStrategy = CheckStrategy.getInstance(superMan.superManType);
    return checkStrategy.check(superMan);
  }
}
abstract class CheckStrategy {
  protected CheckStrategy() {
  };

  public static CheckStrategy getInstance(SuperManEnum superManEnum) {
    if (superManEnum == SuperManEnum.JUSTICE) {
      return new JusticeCheckStrategy();
    } else if (superManEnum == SuperManEnum.DEVIL) {
      return new DevilCheckStrategy();
    } else if (superManEnum == SuperManEnum.PERFECT) {
      return new PerfectCheckStrategy();
    } else {
      throw new IllegalArgumentException();
    }
  }

  public boolean check(SuperMan superMan) {
    if (!checkCommon(superMan)) {
      return false;
    }
    if (!checkEach(superMan)) {
      return false;
    }
    return true;
  }

  protected abstract boolean checkEach(SuperMan superMan) ;

  protected boolean checkCommon(SuperMan superMan) {
    if (superMan.name == null || superMan.specialMoves == null) {
      return false;
    }
    return true;
  }
}
public class JusticeCheckStrategy extends CheckStrategy {
  @Override
  protected boolean checkCommon(SuperMan superMan) {
    return true;
  }

  @Override
  protected boolean checkEach(SuperMan superMan) {
    return true;
  }
}
class DevilCheckStrategy extends CheckStrategy {
  @Override
  protected boolean checkEach(SuperMan superMan) {
    if (superMan.meatBodyParts == null) {
      return false;
    }
    return true;
  }
}
class PerfectCheckStrategy extends CheckStrategy {
  @Override
  protected boolean checkEach(SuperMan superMan) {
    if (superMan.isReal && (superMan.nickname == null)) {
      return false;
    }
    return true;
  }
}

共通実行結果

true
true
false
true
false
true
false
false

感想

一番いいかな、と思っているパターン4を現場では使えないのが悲しい。
複雑なValidatorなどをエレガントに実装する方法が欲しい・・・