8.簡潔なコードで再包装


1.小型リフォーム


再構築とは,既存の機能を維持しながらコードの下位構造を変更することである.

1.再構築の機会

public class Profile {

   private Map<String,Answer> answers = new HashMap<>();
   // ...

   private int score;
   private String name;

   public Profile(String name) {
      this.name = name;
   }
   
   public String getName() {
      return name;
   }

   public void add(Answer answer) {
      answers.put(answer.getQuestionText(), answer);
   }
   
   public boolean matches(Criteria criteria) {
      score = 0;
      
      boolean kill = false;
      boolean anyMatches = false;
      for (Criterion criterion: criteria) {
         Answer answer = answers.get(
               criterion.getAnswer().getQuestionText());
         boolean match = 
               criterion.getWeight() == Weight.DontCare ||
               answer.match(criterion.getAnswer());
         if (!match && criterion.getWeight() == Weight.MustMatch) {
            kill = true;
         }
         if (match) {
            score += criterion.getWeight().getValue();
         }
         anyMatches |= match;
         // ...
      }
      if (kill)
         return false;
      return anyMatches;
   }

   public int score() {
      return score;
   }

   public List<Answer> classicFind(Predicate<Answer> pred) {
      List<Answer> results = new ArrayList<Answer>();
      for (Answer answer: answers.values())
         if (pred.test(answer))
            results.add(answer);
      return results;
   }
   
   @Override
   public String toString() {
     return name;
   }

   public List<Answer> find(Predicate<Answer> pred) {
      return answers.values().stream()
            .filter(pred)
            .collect(Collectors.toList());
   }
}
Profileクラスのmatches()メソッドから見ると,条件文は複雑である.したがって、matches()メソッドの複雑さを低減して、コードが何を担当しているのかをよりよく理解したい場合があります.

2.方法抽出:2番目の重要な再構築友人

boolean match = 
    criterion.getWeight() == Weight.DontCare ||
    answer.match(criterion.getAnswer());
このセクションを個別の方法として抽出し、複雑さを解消します.では、繰り返し文には単純な宣言しか残っていません.match変数は,条件が答えに合致するか否かのみを表す.
public boolean matches(Criteria criteria) {
    score = 0;
      
    boolean kill = false;
    boolean anyMatches = false;
    for (Criterion criterion: criteria) {
        Answer answer = answers.get(
            criterion.getAnswer().getQuestionText());
        boolean match = matches(criterion, answer);

        if (!match && criterion.getWeight() == Weight.MustMatch) {
           kill = true;
        }
        if (match) {
           score += criterion.getWeight().getValue();
        }
        anyMatches |= match;
     }
    if (kill)
        return false;
    return anyMatches;
 }

private boolean matches(Criterion criterion, Answer answer) {
    return criterion.getWeight() == Weight.DontCare ||
        answer.match(criterion.getAnswer());
   }

2.方法のためにもっと良い家を探す


新しい抽出されたマッチング()メソッドは、Profileオブジェクトに関係なく、AnswerクラスとCriterionクラスに関連しています.したがって,matches()メソッドをCriterionクラスに移動する.CriterionオブジェクトはAnswerオブジェクトを知っているが,このドメインは成り立たないからである.つまり,AnswerクラスはCriterionクラスに依存しない.matches()メソッドをAnswerクラスに移動した場合は、双方向依存関係になります.
public class Criterion implements Scoreable {
   // ... 
   private Weight weight;
   private Answer answer;
   private int score;

   public Criterion(Answer answer, Weight weight) {
      this.answer = answer;
      this.weight = weight;
   }
   
   public Answer getAnswer() { return answer; }
   public Weight getWeight() { return weight; }
   
   public void setScore(int score) { this.score = score; }
   public int getScore() { return score; }

   public boolean matches(Answer answer) {
      return getWeight() == Weight.DontCare ||
          answer.match(getAnswer());
   }
}
移動後、Profileクラスの繰り返し文は次のとおりです.
for (Criterion criterion: criteria) {
    Answer answer = answers.get(
        criterion.getAnswer().getQuestionText());
    boolean match = criterion.matches(answer);

    if (!match && criterion.getWeight() == Weight.MustMatch) {
        kill = true;
    }
    if (match) {
        score += criterion.getWeight().getValue();
    }
    anyMatches |= match;
}
また,応答領域変数に割り当てられた文はかなり長く複雑である.
Answer answer = answers.get(
    criterion.getAnswer().getQuestionText());
上のコードは「Demeter法則」(要約すると他のオブジェクトに伝播する連鎖メソッド呼び出しを避けるべき)に違反しており,簡潔ではない.
この点を改善する最初のステップは、答え割り当て文の右側を新しいメソッド「答え生成」()に抽出することです.
public boolean matches(Criteria criteria) {
    score = 0;

    boolean kill = false;
    boolean anyMatches = false;
    for (Criterion criterion: criteria) {
        Answer answer = answerMatching(criterion);
        boolean match = criterion.matches(answer);

        if (!match && criterion.getWeight() == Weight.MustMatch) {
           kill = true;
        }
        if (match) {
           score += criterion.getWeight().getValue();
        }
        anyMatches |= match;
    }
    if (kill)
        return false;
    return anyMatches;
}

private Answer answerMatching(Criterion criterion) {
    return answers.get(criterion.getAnswer().getQuestionText());
}
一時変数の使い方は多種多様である.一時変数として、コストの高い計算値をキャッシュに入れるか、メソッド本体で変更された内容を収集します.また、一時変数は、コードの意図を明確にするために使用することができる.

3.自動と手動で再包装


ここで,答え領域変数はコードの明確性を増やさずに1回のみ使用する.変数を除去し、「応答一致」を埋め込みます.
for (Criterion criterion: criteria) {
    boolean match = criterion.matches(answerMatching(criterion));

    if (!match && criterion.getWeight() == Weight.MustMatch) {
        kill = true;
    }
    if (match) {
        score += criterion.getWeight().getValue();
    }
    anyMatches |= match;
}
また、ほとんどのIDEは内部再構築を自動化しています.
matches()メソッドの詳細は削除されたので、高度なポリシーを簡単に理解できます.したがって,メソッドのコアターゲットを区別することができる.
  • 一致条件の重み値を加算してスコアを計算します.
  • 必須項目が基本情報の答えと一致しない場合はfalseを返します.
  • そうでなければ、マッチングがあればtrueを返し、マッチングがなければfalseを返します.
  • matches()メソッドでどのプロファイルが一致するかを決定する部分を再設計します.
     public boolean matches(Criteria criteria) {
         score = 0;
    
         boolean kill = false;
         for (Criterion criterion: criteria) {
             boolean match = criterion.matches(answerMatching(criterion));
    
             if (!match && criterion.getWeight() == Weight.MustMatch) {
                 kill = true;
             }
             if (match) {
                 score += criterion.getWeight().getValue();
             }
         }
         if (kill)
             return false;
         return anyMatches(criteria);
    }
    
    private boolean anyMatches(Criteria criteria) {
        boolean anyMatches = false;
        for (Criterion criterion: criteria)
            anyMatches = criterion.matches(answerMatching(criterion));
        return anyMatches;
    }
    相互に分離したコードを新しいメソッドとして抽出すると自動化できないため,手動で再包装する必要がある.次のテストに失敗しました.
    @Test
    public void matchAnswersTrueWhenAnyOfMultipleCriteriaMatch() {
        profile.add(answerThereIsRelocation);
        profile.add(answerDoesNotReimburseTuition);
        criteria.add(new Criterion(answerThereIsRelocation, Weight.Important));
        criteria.add(new Criterion(answerReimbursesTuition, Weight.Important));
    
        boolean matches = profile.matches(criteria);
    
        assertTrue(matches);
    }
    この解決策は、anyMatches値を更新するときに複合割り当て演算子(|=)を使用することです.
     private boolean anyMatches(Criteria criteria) {
         boolean anyMatches = false;
         for (Criterion criterion: criteria)
             anyMatches |= criterion.matches(answerMatching(criterion));
         return anyMatches;
    }

    4.大げさな再構築?


    同様に、一致するすべての総重み付けを計算するコードを抽出する.
     public boolean matches(Criteria criteria) {
         calculateScore(criteria);
          
         boolean kill = false;
         for (Criterion criterion: criteria) {
             boolean match = criterion.matches(answerMatching(criterion));
             if (!match && criterion.getWeight() == Weight.MustMatch) {
                 kill = true;
             }
        }
        if (kill)
            return false;
        return anyMatches(criteria);
    }
    
    private void calculateScore(Criteria criteria) {
        score = 0;
        for (Criterion criterion: criteria) 
            if (criterion.matches(answerMatching(criterion))) 
                score += criterion.getWeight().getValue();
    }
    最後に,不整合の有無を決定する必須条件の論理を抽出する.
    public boolean matches(Criteria criteria) {
        calculateScore(criteria);
        if (doesNotMeetAnyMustMatchCriterion(criteria))
            return false;
        return anyMatches(criteria);
    }
    
    private boolean doesNotMeetAnyMustMatchCriterion(Criteria criteria) {
        for (Criterion criterion: criteria) {
            boolean match = criterion.matches(answerMatching(criterion));
            if (!match && criterion.getWeight() == Weight.MustMatch) 
                return true;
        }
        return false;
    }
    現在、新しい方法と重複文はそれぞれ3つになっています.パフォーマンスのメリットを分析してみましょう.

    1.奨励:明確かつテスト可能な単位


    matches()メソッドは,アルゴリズム全体がきちんと整理されていることをすぐに理解できるようになった.現在のコードは、次の順序のアルゴリズムに従います.
  • によって与えられた条件に基づいてスコアを計算する.
  • プロファイルは必要な条件に合致せずfalseを返します.
  • そうでなければ、条件に合致するかどうかを返します.
  • アルゴリズムの3つのステップの各実装の詳細は、CalculateScore()、IsoMeetAnyMustMatchCriterion()およびAnyMatches()のアシスタントメソッドに隠されている.各アシスタントメソッドは明確で孤立した方法でよく表現されている.

    2.性能の心配:心配する必要はない


    matches()メソッドを再パッケージすると、AnyteMatches()、CalculateScore()、donotmeetAnyMustMatchCriterion()メソッドの各メソッドに標準条件の重複文があります.3つの新しい重複文マッチング()メソッドの潜在的な実行時間は、元の4倍です.
    パフォーマンスに直ちに問題が発生しない場合は、最適化に時間を費やすのではなく、コードを清潔に保つ必要があります.最適化されたコードは多くの点で問題があります.通常、コードの可読性が低く、メンテナンスコストが高く、設計も柔軟ではありません.
    逆に、簡潔な設計は、パフォーマンスを最適化するときにすぐに応答することができます.シンプルな設計は、モバイルコードの柔軟性を提供し、他のアルゴリズムの適用も容易です.
    パフォーマンスに直ちに問題が発生した場合、他のことをする前に、まず問題の深刻さを測定し、その後、以前のコードの速度がどれだけ速いかを決定するために小さなテストコードを作成し、再コンパイル後のコードを判断し、比較します.
    リファレンス
  • JavaとJUnitによる実用主義ユニットテスト
  • https://github.com/gilbutITbook/006814