[開発図書]Clean Code::15章-JUnit


JUnitを作成し、テストコードで再パッケージします.
*JUnit:Javaプログラミング言語用ユニットテストフレームワーク

📕 JUnitフレームワーク


サンプルコードは、2つの文字列を受信し、差異を返すコードです.
💻 15-1 ComparisonCompactorTest.java
次のコードは、Comparison Compactorモジュールのオーバーライド率を分析するテストケースです.
*コードオーバーライド率:ソフトウェアテストケースがどの程度の要件を満たしているかを測定する指標であり、テスト時に「コード自体がどのくらい実行されているか」
package junit.tests.framework;

import junit.framework.ComparisonCompactor;
import junit.framework.TestCase;

public class ComparisonCompactorTest extends TestCase {

    public void testMessage() {
        String failure = new ComparisonCompactor(0, "b", "c").compact("a");
        assertTrue("a expected:<[b]> but was:<[c]>".equals(failure));
    }

    public void testStartSame() {
        String failure = new ComparisonCompactor(1, "ba", "bc").compact(null);
        assertEquals("expected:<b[a]> but was:<b[c]>", failure);
    }

    public void testEndSame() {
        String failure = new ComparisonCompactor(1, "ab", "cb").compact(null);
        assertEquals("expected:<[a]b> but was:<[c]b>", failure);
    }

    public void testSame() {
        String failure = new ComparisonCompactor(1, "ab", "ab").compact(null);
        assertEquals("expected:<ab> but was:<ab>", failure);
    }

    public void testNoContextStartAndEndSame() {
        String failure = new ComparisonCompactor(0, "abc", "adc").compact(null);
        assertEquals("expected:<...[b]...> but was:<...[d]...>", failure);
    }

    public void testStartAndEndContext() {
        String failure = new ComparisonCompactor(1, "abc", "adc").compact(null);
        assertEquals("expected:<a[b]c> but was:<a[d]c>", failure);
    }

    public void testStartAndEndContextWithEllipses() {
        String failure = new ComparisonCompactor(1, "abcde", "abfde").compact(null);
        assertEquals("expected:<...b[c]d...> but was:<...b[f]d...>", failure);
    }

    public void testComparisonErrorStartSameComplete() {
        String failure = new ComparisonCompactor(2, "ab", "abc").compact(null);
        assertEquals("expected:<ab[]> but was:<ab[c]>", failure);
    }

    public void testComparisonErrorEndSameComplete() {
        String failure = new ComparisonCompactor(0, "bc", "abc").compact(null);
        assertEquals("expected:<[]...> but was:<[a]...>", failure);
    }

    public void testComparisonErrorEndSameCompleteContext() {
        String failure = new ComparisonCompactor(2, "bc", "abc").compact(null);
        assertEquals("expected:<[]bc> but was:<[a]bc>", failure);
    }

    public void testComparisonErrorOverlappingMatches() {
        String failure = new ComparisonCompactor(0, "abc", "abbc").compact(null);
        assertEquals("expected:<...[]...> but was:<...[b]...>", failure);
    }

    public void testComparisonErrorOverlappingMatchesContext() {
        String failure = new ComparisonCompactor(2, "abc", "abbc").compact(null);
        assertEquals("expected:<ab[]c> but was:<ab[b]c>", failure);
    }

    public void testComparisonErrorOverlappingMatches2() {
        String failure = new ComparisonCompactor(0, "abcdde", "abcde").compact(null);
        assertEquals("expected:<...[d]...> but was:<...[]...>", failure);
    }

    public void testComparisonErrorOverlappingMatches2Context() {
        String failure = new ComparisonCompactor(2, "abcdde", "abcde").compact(null);
        assertEquals("expected:<...cd[d]e> but was:<...cd[]e>", failure);
    }

    public void testComparisonErrorWithActualNull() {
        String failure = new ComparisonCompactor(0, "a", null).compact(null);
        assertEquals("expected:<a> but was:<null>", failure);
    }

    public void testComparisonErrorWithActualNullContext() {
        String failure = new ComparisonCompactor(2, "a", null).compact(null);
        assertEquals("expected:<a> but was:<null>", failure);
    }

    public void testComparisonErrorWithExpectedNull() {
        String failure = new ComparisonCompactor(0, null, "a").compact(null);
        assertEquals("expected:<null> but was:<a>", failure);
    }

    public void testComparisonErrorWithExpectedNullContext() {
        String failure = new ComparisonCompactor(2, null, "a").compact(null);
        assertEquals("expected:<null> but was:<a>", failure);
    }

    public void testBug609972() {
        String failure = new ComparisonCompactor(10, "S&P500", "0").compact(null);
        assertEquals("expected:<[S&P50]0> but was:<[]0>", failure);
    }
}
上記のテストケースでコードオーバーライド率分析を行い、100%に達することができます.これは、テスト・ケースがすべてのロー、すべてのif、for文を実行することを意味します.したがって,モジュールは正常に動作していると考えられる.
💻 15-2 ComparisonCompactor.java
次のコードはComparison Compactorモジュールです.
コードの分離が適切で,構造が簡単である.
public class ComparisonCompactor {

    private static final String ELLIPSIS = "...";
    private static final String DELTA_END = "]";
    private static final String DELTA_START = "[";

    private int fContextLength;
    private String fExpected;
    private String fActual;
    private int fPrefix;
    private int fSuffix;

    public ComparisonCompactor(int contextLength, String expected, String actual) {
        fContextLength = contextLength;
        fExpected = expected;
        fActual = actual;
    }

    public String compact(String message) {
        if (fExpected == null || fActual == null || areStringsEqual()) {
            return Assert.format(message, fExpected, fActual);
        }

        findCommonPrefix();
        findCommonSuffix();
        String expected = compactString(fExpected);
        String actual = compactString(fActual);
        return Assert.format(message, expected, actual);
    }

    private String compactString(String source) {
        String result = DELTA_START + source.substring(fPrefix, source.length() - fSuffix + 1) + DELTA_END;
        if (fPrefix > 0) {
            result = computeCommonPrefix() + result;
        }
        if (fSuffix > 0) {
            result = result + computeCommonSuffix();
        }
        return result;
    }

    private void findCommonPrefix() {
        fPrefix = 0;
        int end = Math.min(fExpected.length(), fActual.length());
        for (; fPrefix < end; fPrefix++) {
            if (fExpected.charAt(fPrefix) != fActual.charAt(fPrefix)) {
                break;
            }
        }
    }

    private void findCommonSuffix() {
        int expectedSuffix = fExpected.length() - 1;
        int actualSuffix = fActual.length() - 1;
        for (; actualSuffix >= fPrefix && expectedSuffix >= fPrefix; actualSuffix--, expectedSuffix--) {
            if (fExpected.charAt(expectedSuffix) != fActual.charAt(actualSuffix)) {
                break;
            }
        }
        fSuffix = fExpected.length() - expectedSuffix;
    }

    private String computeCommonPrefix() {
        return (fPrefix > fContextLength ? ELLIPSIS : "") + fExpected.substring(Math.max(0, fPrefix - fContextLength), fPrefix);
    }

    private String computeCommonSuffix() {
        int end = Math.min(fExpected.length() - fSuffix + 1 + fContextLength, fExpected.length()); // 경계조건
        return fExpected.substring(fExpected.length() - fSuffix + 1, end) + (fExpected.length() - fSuffix + 1 < fExpected.length() - fContextLength ? ELLIPSIS : ""); // 경계조건
    }

    private boolean areStringsEqual() {
        return fExpected.equals(fActual);
    }
}
上のコードも良いコードですが、童子軍のルールに従ってもっときれいなコードを作らなければなりません.
*童子軍ルール:チェックアウト時よりも良いコードをチェックインします.つまり、コードを整理すればするほど、より良いコードになるはずです.
より良いコードのために、次のルールを適用します.
1.符号化回避(403)
名前にタイプ情報や範囲情報を表す接頭辞を付ける必要はありません.
したがって、メンバー変数の前のプレフィックスfを削除する.
private int contextLength;
private String expected;
private String actual;
private int prefix;
private int suffix;
2.条件をカプセル化する.(403)
次のcompact関数の先頭には、カプセル化されていない条件文が表示されます.
public String compact(String message) {
    if (expected == null || actual == null || areStringsEqual()) { // 이부분
        return Assert.format(message, expected, actual);
    }

    findCommonPrefix();
    findCommonSuffix();
    String expected = compactString(this.expected);
    String actual = compactString(this.actual);
    return Assert.format(message, expected, actual);
}
意図を明確に表現するために,条件文をカプセル化する過程.すなわち,条件文をメソッドとして抽出して名前を付け,以下のように変更する.
public String compact(String message) {
    if (shouldNotCompact()) {
        return Assert.format(message, expected, actual);
    }

    findCommonPrefix();
    findCommonSuffix();
    String expected = compactString(this.expected);
    String actual = compactString(this.actual);
    return Assert.format(message, expected, actual);
}

private boolean shouldNotCompact() { // 조건문을 함수로 뽑아낸다.
    return expected == null || actual == null || areStringsEqual();
}
3.できるだけ標準ネーミング法を使用する(402)
compact関数で使用されるthis.期待とこれ実際には地域変数もあるのでよくありません.これは,fをfExpectedから削除した結果である.(接頭辞を削除中に問題が発生)
メンバー変数と同じ名前の変数に異なる機能がある場合は、その名前を付けます.
String compactExpected = compactString(expected);
String compactActual = compactString(actual);
4.不正を避ける(389)
否定文は肯定文よりも理解しにくい.したがって,第1文ifを肯定,反条件文とする.
public String compact(String message) {
    if (canBeCompacted()) { // if (shouldNotCompact()) {
        findCommonPrefix();
        findCommonSuffix();
        String compactExpected = compactString(expected);
        String compactActual = compactString(actual);
        return Assert.format(message, compactExpected, compactActual);
    } else {
        return Assert.format(message, expected, actual);
    }       
}

private boolean canBeCompacted() {
    return expected != null && actual != null && !areStringsEqual();
    // return expected == null || actual == null || areStringsEqual();
}
5.付随効果を名前で説明する(404)
compact関数は文字列を圧縮するための関数ですが、canbecompactがfalseの場合は圧縮されません.このように付加段階の名前を隠すのはよくない.名前は、関数、変数、クラスを含むすべての作業に適用されます.
また、単純な圧縮文字列ではなくフォーマットを持つ文字列を返すため、実際にはformatCompactedComparisonという名前を使用する必要があります.
public String formatCompactedComparison(String message) { 
// public String compact(String message) {
6.1つの関数しか作成できません(389)
formatCompactedComparison内のif文で、予想文字列と実際の文字列を圧縮します.このセクションを分離して圧縮を実行するcompactExpectedAndActualメソッドとして作成し、formatCompactedComparison関数ではフォーマット操作のみが許可されます.
これにより、各関数は1つの機能しか実行されません.
...

private String compactExpected; 
private String compactActual;

...

public String formatCompactedComparison(String message) {
    if (canBeCompacted()) {
        compactExpectedAndActual();
        return Assert.format(message, compactExpected, compactActual);
    } else {
        return Assert.format(message, expected, actual);
    }       
}

private compactExpectedAndActual() {
    findCommonPrefix();
    findCommonSuffix();
    compactExpected = compactString(expected);
    compactActual = compactString(actual);
}
7.一貫性の欠如(376)
前にCompactExpectedとCompactActualをメンバー変数に変更しました.
compactExpectedAndActual関数の最後の2行は変数を返しますが、1行目と2行目は値を返しません.(一貫性に欠けている)したがって、findCommonPrefixとfindCommonSuffixを変更し、関数をvoidからinに変更し、戻り値を追加できます.
private compactExpectedAndActual() {
    prefixIndex = findCommonPrefix(); // findCommonPrefix();
    suffixIndex = findCommonSuffix(); // findCommonSuffix();
    compactExpected = compactString(expected);
    compactActual = compactString(actual);
}

...

private int findCommonPrefix() // void
	return prefixIndex;
    
private int findCommonSuffix() // void
	return expected.length() - expectedSuffix;

...
8.記述名の使用(376)
メンバー変数はインデックスの位置を表すため、より明確な意味を与えるためにprefix、suffixと宣言されたものをprefixIndex、suffixIndexに変更します.
private int prefixIndex;
private int suffixIndex;
9.非表示の視覚結合(390)
findCommonSuffixでは非表示の時間結合が存在する.すなわちfindCommonSuffixはfindCommonPrefixがプレフィックスインデックスを計算する事実に依存する.順序が間違っている場合は、絶え間ないデバッグが必要です.そこで,時間結合を外部に露出させるためにfindCommonPrefixを修正し,prefixIndexを仁秀に渡した.
*直感的な結合を表します.関数を記述するときは、関数呼び出しの順序を明確に表示するために、関数パラメータを適切に配置します.
private compactExpectedAndActual() {
    prefixIndex = findCommonPrefix();
    suffixIndex = findCommonSuffix(prefixIndex); // 인수로 넘기기
    String compactExpected = compactString(expected);
    String compactActual = compactString(actual);
}

private int findCommonSuffix(int prefixIndex) { // 인수로 넘기기
    int expectedSuffix = expected.length() - 1;
    int actualSuffix = actual.length() - 1;
    for (; actualSuffix >= prefixIndex && expectedSuffix >= prefix; actualSuffix--, expectedSuffix--) {
        if (expected.charAt(expectedSuffix) != actual.charAt(actualSuffix)) {
            break;
        }
    }
    return expected.length() - expectedSuffix;
}
10.一貫性の維持(391)
コード構造を記述する際には原因を考慮し,コード構造が原因を明確に表現できることを確保する.このようにしてこそ一貫した構造を形成することができる.
前のコードのようにprefixIndexを引数として渡すと、関数呼び出しの順序が明確になりますが、引数が必要な理由は不明です.
したがって、findCommonPrefixとfindCommonSuffixを元の名前に戻し、findCommonPrefixAndSuffixをfindCommonPrefixAndSuffixに名前を変更し、findCommonPrefixAndSuffixがfindCommonPrefixを最初に呼び出します.この場合、呼び出しの順序は、前に変更したコードよりも明確になります.
private compactExpectedAndActual() {
    findCommonPrefixAndSuffix();
    String compactExpected = compactString(expected);
    String compactActual = compactString(actual);
}

private void findCommonPrefixAndSuffix() {
    findCommonPrefix();
    int expectedSuffix = expected.length() - 1;
    int actualSuffix = actual.length() - 1;
    for (; actualSuffix >= prefixIndex && expectedSuffix >= prefix; actualSuffix--, expectedSuffix--) {
        if (expected.charAt(expectedSuffix) != actual.charAt(actualSuffix)) {
            break;
        }
    }
    suffixIndex = expected.length() - expectedSuffix;
}

private void findCommonPrefix() {
    int prefixIndex = 0;
    int end = Math.min(expected.length(), actual.length());
    for (; prefixIndex < end; prefixIndex++) {
        if (expected.charAt(prefixIndex) != actual.charAt(prefixIndex)) {
            break;
        }
    }
}
次に、findCommanPrefixAndsuffix関数を整理します.アレンジの過程で接尾辞Indexは実際のコードの1から始まるため,Indexよりも長さに適していると考えられるため,変数名を接尾辞Lengthに変更する.(実際には、Lengthは接尾辞の長さよりも既存の接尾辞インデックスに適しています)
private void findCommonPrefixAndSuffix() {
    findCommonPrefix();
    int suffixLength = 1;
    for (; suffixOverlapsPrefix(suffixLength); suffixLength++) {
        if (charFromEnd(expected, suffixLength) != charFromEnd(actual, suffixLength)) {
            break;
        }
    }
    suffixIndex = suffixLength;
}

private char charFromEnd(String s, int i) {
    return s.charAt(s.length() - i);
}

private boolean suffixOverlapsPrefix(int suffixLength) {
    return actual.length() = suffixLength < prefixLength || expected.length() - suffixLength < prefixLength;
}
11.パッケージ境界条件(392)
境界条件は1つの場所で個別に処理されます.すなわち,コードは+1または−1をあちこちに分散しない.(392例)
この要件を満たすためにコードに変更しましょう.
ソースコードを表示すると、ComputeCommSuffixの+1の部分が表示されます.(境界条件の探索)したがって、この部分を除去するために、接尾辞長=1は1ではなく0に初期化され、接尾辞長+1部分は接尾辞長に変更される.このため、charFromEndに-1を追加し、suffixOverlapsPrefixでは<=ではなく<=を使用します.
public class ComparisonCompactor {
    ...
    private int suffixLength;
    ...

    private void findCommonPrefixAndSuffix() {
        findCommonPrefix();
        suffixLength = 0;  // 수정
        for (; suffixOverlapsPrefix(suffixLength); suffixLength++) {
            if (charFromEnd(expected, suffixLength) != charFromEnd(actual, suffixLength)) {
                break;
            }
        }
    }

    private char charFromEnd(String s, int i) {
        return s.charAt(s.length() - i - 1);  // 수정
    }

    private boolean suffixOverlapsPrefix(int suffixLength) {
        return actual.length() = suffixLength <= prefixLength || expected.length() - suffixLength <= prefixLength;  // 수정
    }

    ...
    private String compactString(String source) {
        String result = DELTA_START + source.substring(prefixLength, source.length() - suffixLength) + DELTA_END;
        if (prefixLength > 0) {
            result = computeCommonPrefix() + result;
        }
        if (suffixLength > 0) {
            result = result + computeCommonSuffix();
        }
        return result;
    }

    ...
    private String computeCommonSuffix() {
        int end = Math.min(expected.length() - suffixLength + contextLength, expected.length()); // 수정
        return expected.substring(expected.length() - suffixLength, end) + (expected.length() - suffixLength < expected.length() - contextLength ? ELLIPSIS : "");  // 수정
    }
}
しかし、私たちはここでこの部分を見てみましょう.
if (suffixLength > 0) {
suffixLengthは0に初期化すると1が減少するので、>演算子を>=演算子に変更する必要があります.そうすると、このif文はずっと本物なので、意味のない条件文になります!
12.デッドコード(376)
デッドコードとは、実行しないコードです.たとえば、throw文のtry catch文や、見たばかりの不要なif文がありますか.
システムから削除したほうがいいです.したがってifゲートを除去し、構造を調整します.
private String compactString(String source) {
        return computeCommonPrefix() + DELTA_START 
        +  source.substring(prefixLength, source.length() - suffixLength)
        + DELTA_END + computeCommonSuffix();
    }
これにより、CompactString関数は、文字列フラグメントのみを結合する関数に変更できます.
これらのプロセスによって生成される最終コードを以下に示す.
💻 15-5 ComparsionCompactor.java
package junit.framework;

public class ComparisonCompactor {

    private static final String ELLIPSIS = "...";
    private static final String DELTA_END = "]";
    private static final String DELTA_START = "[";

    private int contextLength;
    private String expected;
    private String actual;
    private int prefixLength;
    private int suffixLength;

    public ComparisonCompactor(int contextLength, String expected, String actual) {
        this.contextLength = contextLength;
        this.expected = expected;
        this.actual = actual;
    }

    public String formatCompactedComparison(String message) {
        String compactExpected = expected;
        String compactactual = actual;
        if (shouldBeCompacted()) {
            findCommonPrefixAndSuffix();
            compactExpected = comapct(expected);
            compactActual = comapct(actual);
        }         
        return Assert.format(message, compactExpected, compactActual);      
    }

    private boolean shouldBeCompacted() {
        return !shouldNotBeCompacted();
    }

    private boolean shouldNotBeCompacted() {
        return expected == null && actual == null && expected.equals(actual);
    }

    private void findCommonPrefixAndSuffix() {
        findCommonPrefix();
        suffixLength = 0;
        for (; suffixOverlapsPrefix(suffixLength); suffixLength++) {
            if (charFromEnd(expected, suffixLength) != charFromEnd(actual, suffixLength)) {
                break;
            }
        }
    }

    private boolean suffixOverlapsPrefix(int suffixLength) {
        return actual.length() = suffixLength <= prefixLength || expected.length() - suffixLength <= prefixLength;
    }

    private void findCommonPrefix() {
        int prefixIndex = 0;
        int end = Math.min(expected.length(), actual.length());
        for (; prefixLength < end; prefixLength++) {
            if (expected.charAt(prefixLength) != actual.charAt(prefixLength)) {
                break;
            }
        }
    }

    private String compact(String s) {
        return new StringBuilder()
            .append(startingEllipsis())
            .append(startingContext())
            .append(DELTA_START)
            .append(delta(s))
            .append(DELTA_END)
            .append(endingContext())
            .append(endingEllipsis())
            .toString();
    }

    private String startingEllipsis() {
        prefixIndex > contextLength ? ELLIPSIS : ""
    }

    private String startingContext() {
        int contextStart = Math.max(0, prefixLength = contextLength);
        int contextEnd = prefixLength;
        return expected.substring(contextStart, contextEnd);
    }

    private String delta(String s) {
        int deltaStart = prefixLength;
        int deltaend = s.length() = suffixLength;
        return s.substring(deltaStart, deltaEnd);
    }
    
    private String endingContext() {
        int contextStart = expected.length() = suffixLength;
        int contextEnd = Math.min(contextStart + contextLength, expected.length());
        return expected.substring(contextStart, contextEnd);
    }

    private String endingEllipsis() {
        return (suffixLength > contextLength ? ELLIPSIS : "");
    }
}
このように整理されたモジュールは文字列解析関数と組合せ関数に分けられる.また,各関数を使用後にソート定義し,解析関数を最初に出現させ,組合せ関数をその後に出現させる.
これにより,コードがあるレベルに達する前に,何度も試行を繰り返すことで,より良いコードを生成することができる.

📚 Reference

  • Clean Code:爱子日软件达人精神
  • JUnit : https://ko.wikipedia.org/wiki/JUnit
  • コードオーバーライド率:https://woowacourse.github.io/tecoble/post/2020-10-24-code-coverage/2