ساختار داخلی JUnit
فریمورک JUnit
JUnit نویسندگان زیادی داشته است، اما آغاز آن با Kent Beck و Erich Gamma در یک پرواز به آتلانتا بوده است. Kent میخواست Java یاد بگیرد و Erich میخواست دربارهٔ فریمورک تستنویسی Smalltalk متعلق به Kent بیشتر بداند.
«چه چیزی برای دو گیک در فضایی تنگ طبیعیتر است از اینکه لپتاپهایشان را بیرون بیاورند و شروع به کدنویسی کنند؟»
پس از سه ساعت کار در ارتفاع بالا، آنها توانستند اصول پایهٔ JUnit را بنویسند.
ماژولی که به آن خواهیم پرداخت، بخش هوشمندانهای از کد است که به شناسایی خطاهای مقایسهٔ رشتهها کمک میکند.
این ماژول ComparisonCompactor
نام دارد. وقتی دو رشته متفاوت به آن داده شود، مانند ABCDE
و ABXDE،
تفاوت را با تولید رشتهای مانند <...B[X]D...>
نمایان میسازد.
میتوانم آن را بیشتر توضیح دهم، اما تستکیسها این کار را بهتر انجام میدهند.
پس به لیستینگ 15-1 نگاهی بیندازید و نیازمندیهای این ماژول را بهخوبی درک خواهید کرد.
در حین بررسی، ساختار تستها را نیز نقد کنید. آیا میتوان آنها را سادهتر یا واضحتر نوشت؟
Listing 15-1 -- ComparisonCompactorTest.java
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 testComparisonErrorOverlapingMatches() {
String failure =
new ComparisonCompactor(0, "abc", "abbc").compact(null);
assertEquals("expected:<...[]...> but was:<...[b]...>", failure);
}
public void testComparisonErrorOverlapingMatchesContext() {
String failure =
new ComparisonCompactor(2, "abc", "abbc").compact(null);
assertEquals("expected:<ab[]c> but was:<ab[b]c>", failure);
}
public void testComparisonErrorOverlapingMatches2() {
String failure =
new ComparisonCompactor(0, "abcdde", "abcde").compact(null);
assertEquals("expected:<...[d]...> but was:<...[]...>", failure);
}
public void testComparisonErrorOverlapingMatches2Context() {
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);
}
}
من یک تحلیل پوشش کد (Code Coverage) روی ماژول ComparisonCompactor
با استفاده از این تستها انجام دادم.
کد بهطور کامل پوشش داده شده است؛ هر خط کد، هر دستور if
و هر حلقه for
توسط تستها اجرا میشود.
این موضوع به من اطمینان زیادی میدهد که کد بهدرستی کار میکند و احترام زیادی برای مهارت نویسندگان آن ایجاد میکند.
کد مربوط به ComparisonCompactor
در لیستینگ 15-2 قرار دارد.
کمی وقت بگذارید و این کد را مرور کنید. فکر میکنم آن را خوشساخت، نسبتاً گویا، و از نظر ساختار ساده خواهید یافت.
وقتی مرور آن را تمام کردید، با هم وارد جزئیات دقیقتر خواهیم شد.
Listing 15-2 -- ComparisonCompactor.java (Original)
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 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
عجیب و چیزهایی از این دست وجود دارد.
اما در کل، این ماژول نسبتاً خوب است.
در هر صورت، ممکن بود شبیه لیستینگ 15-3 باشد.
Listing 15-3 -- ComparisonCompator.java (defactored)
package junit.framework;
public class ComparisonCompactor {
private int ctxt;
private String s1;
private String s2;
private int pfx;
private int sfx;
public ComparisonCompactor(int ctxt, String s1, String s2) {
this.ctxt = ctxt;
this.s1 = s1;
this.s2 = s2;
}
public String compact(String msg) {
if (s1 == null || s2 == null || s1.equals(s2))
return Assert.format(msg, s1, s2);
pfx = 0;
for (; pfx < Math.min(s1.length(), s2.length()); pfx++) {
if (s1.charAt(pfx) != s2.charAt(pfx))
break;
}
int sfx1 = s1.length() - 1;
int sfx2 = s2.length() - 1;
for (; sfx2 >= pfx && sfx1 >= pfx; sfx2--, sfx1--) {
if (s1.charAt(sfx1) != s2.charAt(sfx2))
break;
}
sfx = s1.length() - sfx1;
String cmp1 = compactString(s1);
String cmp2 = compactString(s2);
return Assert.format(msg, cmp1, cmp2);
}
private String compactString(String s) {
String result = "[" + s.substring(pfx, s.length() - sfx + 1) + "]";
if (pfx > 0)
result = (pfx > ctxt ? "..." : "")
+ s1.substring(Math.max(0, pfx - ctxt), pfx) + result;
if (sfx > 0) {
int end = Math.min(s1.length() - sfx + 1 + ctxt, s1.length());
result = result
+ (s1.substring(s1.length() - sfx + 1, end)
+ (s1.length() - sfx + 1 < s1.length() - ctxt ? "..."
: ""));
}
return result;
}
}
با اینکه نویسندگان این ماژول را در وضعیت بسیار خوبی باقی گذاشتهاند،
Boy Scout Rule به ما میگوید که باید آن را تمیزتر از زمانی که آن را یافتیم، ترک کنیم.
پس، چگونه میتوانیم کد اصلی در لیستینگ 15-2 را بهبود دهیم؟
اولین چیزی که برایم خوشایند نیست، پیشوند f
برای متغیرهای عضو است [N6]
.
امروزه محیطهای توسعه این نوع کدگذاری دامنه را غیرضروری کردهاند.
بنابراین بیایید تمام f
ها را حذف کنیم.
private int contextLength;
private String expected;
private String actual;
private int prefix;
private int suffix;
مورد بعدی، یک شرط بدون انکپسولهشده در ابتدای تابع compact
است [G28]
.
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();
}
من زیاد از استفاده از this.expected
و this.actual
در تابع compact
خوشم نمیآید.
این مورد وقتی اتفاق افتاد که نام fExpected
به expected
تغییر یافت.
چرا در این تابع متغیرهایی با همان نام متغیرهای عضو وجود دارد؟
آیا آنها نمایانگر چیز دیگری نیستند؟ [N4]
باید نامها را واضحتر کنیم.
String compactExpected = compactString(expected);
String compactActual = compactString(actual);
جملات منفی کمی سختتر از جملات مثبت قابلدرکاند [G29]
.
پس بیایید شرط را برعکس کرده و معنای آن را تغییر دهیم.
public String compact(String message) {
if (canBeCompacted()) {
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();
}
نام این تابع کمی عجیب است [N7]
.
اگرچه رشتهها را فشرده میکند، اما ممکن است اصلاً فشردهسازی انجام ندهد اگر canBeCompacted
مقدار false
بازگرداند.
پس نام تابع compact
اثر جانبی بررسی خطا را پنهان میکند.
همچنین توجه کنید که این تابع یک پیام قالببندیشده بازمیگرداند، نه فقط رشتههای فشردهشده.
پس نام تابع باید واقعاً formatCompactedComparison
باشد.
این نام با توجه به آرگومان آن، خوانایی را بسیار بیشتر میکند:
public String formatCompactedComparison(String message)
بدنهی if جایی است که فشردهسازی واقعی رشتههای expected
و actual
انجام میشود.
باید آن را به متدی با نام compactExpectedAndActual
استخراج کنیم.
با این حال، میخواهیم تابع formatCompactedComparison
تمام فرمتبندی را انجام دهد.
تابع compact...
نباید کاری جز فشردهسازی انجام دهد [G30]
.
پس بیایید آن را به این شکل تقسیم کنیم:
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 void compactExpectedAndActual() {
findCommonPrefix();
findCommonSuffix();
compactExpected = compactString(expected);
compactActual = compactString(actual);
}
توجه کنید که مجبور شدیم متغیرهای compactExpected
و compactActual
را به متغیرهای عضو ارتقا دهیم.
از اینکه دو خط آخر تابع جدید مقدار بازمیگردانند ولی دو خط اول اینگونه نیستند، خوشم نمیآید.
آنها از قرارداد یکسانی استفاده نمیکنند [G11]
.
پس باید findCommonPrefix
و findCommonSuffix
را تغییر دهیم تا مقدار prefix
و suffix
را بازگردانند.
private void compactExpectedAndActual() {
prefixIndex = findCommonPrefix();
suffixIndex = findCommonSuffix();
compactExpected = compactString(expected);
compactActual = compactString(actual);
}
private int findCommonPrefix() {
int prefixIndex = 0;
int end = Math.min(expected.length(), actual.length());
for (; prefixIndex < end; prefixIndex++) {
if (expected.charAt(prefixIndex) != actual.charAt(prefixIndex))
break;
}
return prefixIndex;
}
private int findCommonSuffix() {
int expectedSuffix = expected.length() - 1;
int actualSuffix = actual.length() - 1;
for (; actualSuffix >= prefixIndex && expectedSuffix >= prefixIndex;
actualSuffix--, expectedSuffix--) {
if (expected.charAt(expectedSuffix) != actual.charAt(actualSuffix))
break;
}
return expected.length() - expectedSuffix;
}
باید نام متغیرهای عضو را نیز کمی دقیقتر کنیم [N1]
؛
در نهایت، هر دو متغیر، اندیس هستند.
بررسی دقیق findCommonSuffix
یک وابستگی زمانی پنهان را نشان میدهد [G31]
؛
این تابع به این وابسته است که prefixIndex
قبلاً توسط findCommonPrefix
محاسبه شده باشد.
اگر این دو تابع به ترتیب اشتباهی فراخوانی شوند، دیباگ کردن بسیار دشوار خواهد شد.
پس برای نمایان کردن این وابستگی زمانی، بیایید findCommonSuffix
را طوری تغییر دهیم که prefixIndex
را بهعنوان آرگومان دریافت کند.
private void compactExpectedAndActual() {
prefixIndex = findCommonPrefix();
suffixIndex = findCommonSuffix(prefixIndex);
compactExpected = compactString(expected);
compactActual = compactString(actual);
}
private int findCommonSuffix(int prefixIndex) {
int expectedSuffix = expected.length() - 1;
int actualSuffix = actual.length() - 1;
for (; actualSuffix >= prefixIndex && expectedSuffix >= prefixIndex;
actualSuffix--, expectedSuffix--) {
if (expected.charAt(expectedSuffix) != actual.charAt(actualSuffix))
break;
}
return expected.length() - expectedSuffix;
}
از این موضوع واقعاً خوشم نمیآید.
ارسال آرگومان prefixIndex
کمی دلبخواهی است [G32]
.
این کار ترتیب فراخوانی را تضمین میکند اما دلیلی برای نیاز به این ترتیب ارائه نمیدهد.
برنامهنویس دیگری ممکن است آنچه را انجام دادهایم، بازگرداند چون هیچ نشانهای از ضروری بودن آن پارامتر وجود ندارد.
پس بیایید مسیر متفاوتی را انتخاب کنیم.
private void compactExpectedAndActual() {
findCommonPrefixAndSuffix();
compactExpected = compactString(expected);
compactActual = compactString(actual);
}
private void findCommonPrefixAndSuffix() {
findCommonPrefix();
int expectedSuffix = expected.length() - 1;
int actualSuffix = actual.length() - 1;
for (; actualSuffix >= prefixIndex && expectedSuffix >= prefixIndex;
actualSuffix--, expectedSuffix--) {
if (expected.charAt(expectedSuffix) != actual.charAt(actualSuffix))
break;
}
suffixIndex = expected.length() - expectedSuffix;
}
private void findCommonPrefix() {
prefixIndex = 0;
int end = Math.min(expected.length(), actual.length());
for (; prefixIndex < end; prefixIndex++)
if (expected.charAt(prefixIndex) != actual.charAt(prefixIndex))
break;
}
ما findCommonPrefix
و findCommonSuffix
را به حالت قبلی بازگرداندیم،
نام findCommonSuffix
را به findCommonPrefixAndSuffix
تغییر دادیم و آن را طوری نوشتیم که قبل از انجام هر کار دیگری findCommonPrefix
را فراخوانی کند.
این کار ماهیت زمانی بین این دو تابع را بهشکل خیلی واضحتری نشان میدهد نسبت به راهحل قبلی.
همچنین مشخص میکند که findCommonPrefixAndSuffix
چقدر زشت است.
بیایید آن را تمیزتر کنیم.
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 < prefixIndex ||
expected.length() - suffixLength < prefixIndex;
}
این بسیار بهتر است.
نشان میدهد که suffixIndex
در واقع طول پسوند است و نامگذاری خوبی ندارد.
همین موضوع در مورد prefixIndex
نیز صدق میکند، هرچند در آنجا «اندیس» و «طول» مترادف هستند.
با این حال، استفاده از length
از نظر مفهومی منسجمتر است.
مشکل این است که متغیر suffixIndex
از صفر شروع نمیشود؛ مقدار آن از یک شروع میشود و بنابراین یک طول واقعی نیست.
این همان دلیلی است که باعث شده در تابع computeCommonSuffix
از آن +1ها استفاده شود [G33]
.
پس بیایید آن را اصلاح کنیم.
نتیجه در لیستینگ 15-4 آمده است.
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
: "");
}
}
ما علامتهای +1
را در تابع computeCommonSuffix
با یک -1 در تابع charFromEnd
جایگزین کردیم، جایی که کاملاً منطقی است، و همچنین از دو عملگر <= در تابع suffixOverlapsPrefix
استفاده کردیم، که در آنجا هم کاملاً منطقی هستند. این تغییر به ما اجازه داد تا نام متغیر suffixIndex
را به suffixLength
تغییر دهیم، که خوانایی کد را به طرز چشمگیری افزایش داد.
با این حال، یک مشکل وجود دارد. هنگامی که داشتم علامتهای +1
را حذف میکردم، به خط زیر در تابع compactString
برخورد کردم:
if (suffixLength > 0)
نگاهی به آن در لیست 15-4 بیندازید. از نظر منطقی، حالا که مقدار suffixLength
یک واحد کمتر از مقدار قبلیاش است، باید عملگر > را به >= تغییر دهم. اما این کار بیمعناست. حالا منطقی است! این بدان معناست که قبلاً بیمعنا بوده و احتمالاً یک باگ بوده است.
البته نه دقیقاً یک باگ. با بررسی بیشتر میبینیم که این دستور شرطی اکنون از اضافه شدن یک پسوند با طول صفر جلوگیری میکند. قبل از اینکه این تغییر را ایجاد کنیم، آن دستور شرطی بیاثر بود، چون مقدار suffixIndex
هیچگاه کمتر از یک نمیتوانست باشد!
این مسئله هر دو دستور شرطی موجود در تابع compactString
را زیر سؤال میبرد! به نظر میرسد که هر دوی آنها را میتوان حذف کرد. پس بیایید آنها را کامنت کنیم و تستها را اجرا کنیم. همهی تستها پاس شدند! حالا بیایید تابع compactString
را بازسازی کنیم تا دستورات شرطی اضافی را حذف کرده و تابع را بسیار سادهتر کنیم [G9]
.
private String compactString(String source) {
return
computeCommonPrefix() +
DELTA_START +
source.substring(prefixLength, source.length() - suffixLength) +
DELTA_END +
computeCommonSuffix();
}
این خیلی بهتر است! حالا میبینیم که تابع compactString
به سادگی در حال کنار هم گذاشتن قطعههاست. احتمالاً میتوانیم این را حتی واضحتر کنیم. در واقع، پاکسازیهای کوچکی زیادی هست که میتوان انجام داد. اما به جای اینکه شما را درگیر بقیه تغییرات کنم، فقط نتیجه نهایی را در لیست 15-5 نشان میدهم.
Listing 15-5 -- ComparisonCompactor.java (final)
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 = compact(expected);
compactActual = compact(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++) {
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() {
return actual.length() - suffixLength <= prefixLength
|| expected.length() - suffixLength <= prefixLength;
}
private void findCommonPrefix() {
prefixLength = 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() {
return prefixLength > 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 : "");
}
}
این در واقع خیلی زیباست. ماژول به گروهی از توابع تحلیلی و گروه دیگری از توابع ترکیبی تقسیم شده است. این توابع بهصورت توپولوژیکی مرتب شدهاند بهطوری که تعریف هر تابع درست بعد از استفاده آن قرار میگیرد. تمام توابع تحلیلی اول آمدهاند و تمام توابع ترکیبی آخر.
اگر با دقت نگاه کنید، متوجه خواهید شد که چند تصمیمی که قبلاً در این فصل گرفتم را معکوس کردهام. به عنوان مثال، بعضی از متدهای استخراجشده را دوباره در تابع formatCompactedComparison
گنجاندم و حس عبارت shouldNotBeCompacted
را تغییر دادم. این کاملاً طبیعی است. اغلب یک بازسازی منجر به بازسازی دیگری میشود که منجر به برگرداندن تغییرات اول میشود. بازسازی یک فرایند تکراری است که پر از آزمایش و خطاست و بهطور اجتنابناپذیری به چیزی میرسد که احساس میکنیم شایسته یک حرفهای است.
نتیجهگیری
و به این ترتیب ما قانون پیشخدمت اسکات را رعایت کردهایم. این ماژول را کمی تمیزتر از آنچه که پیدا کردیم، رها کردهایم. نه اینکه قبلاً تمیز نبوده باشد. نویسندگان کار فوقالعادهای با آن انجام داده بودند. اما هیچ ماژولی از بهبود مصون نیست و هرکدام از ما مسئولیت داریم که کد را کمی بهتر از آنچه که پیدا کردهایم، ترک کنیم.