เรื่องของ Function: ทำงานอย่างเดียว
ทำงานเพียงอย่างเดียว (Do One Thing )
public static String testableHtml(PageData pageData, boolean includeSuiteSetup) throws Exception {
WikiPage wikiPage = pageData.getWikiPage();
StringBuffer buffer = new StringBuffer();
if (pageData.hasAttribute("Test")) {
if (includeSuiteSetup) {
WikiPage suiteSetup =
PageCrawlerImpl.getInheritedPage(
SuiteResponder.SUITE_SETUP_NAME, wikiPage
);
if (suiteSetup != null) {
WikiPagePath pagePath =
suiteSetup.getPageCrawler().getFullPath(suiteSetup);
String pagePathName = PathParser.render(pagePath);
buffer.append("!include -setup .").append(pagePathName).append("\n");
}
}
WikiPage setup =
PageCrawlerImpl.getInheritedPage("SetUp", wikiPage);
if (setup != null) {
WikiPagePath setupPath =
wikiPage.getPageCrawler().getFullPath(setup);
String setupPathName = PathParser.render(setupPath);
buffer.append("!include -setup .").append(setupPathName).append("\n");
}
}
buffer.append(pageData.getContent());
if (pageData.hasAttribute("Test")) {
WikiPage teardown =
PageCrawlerImpl.getInheritedPage("TearDown", wikiPage);
if (teardown != null) {
WikiPagePath tearDownPath =
wikiPage.getPageCrawler().getFullPath(teardown);
String tearDownPathName = PathParser.render(tearDownPath);
buffer.append("\n").append("!include -teardown .").append(tearDownPathName).append("\n");
}
if (includeSuiteSetup) {
WikiPage suiteTeardown =
PageCrawlerImpl.getInheritedPage(
SuiteResponder.SUITE_TEARDOWN_NAME,
wikiPage
);
if (suiteTeardown != null) {
WikiPagePath pagePath =
suiteTeardown.getPageCrawler().getFullPath (suiteTeardown);
String pagePathName = PathParser.render(pagePath);
buffer.append("!include -teardown .").append(pagePathName).append("\n");
}
}
}
pageData.setContent(buffer.toString());
return pageData.getHtml();
}
โค้ดข้างบนยาวไหมครับ ถ้าดูจริงๆมันยาวมากกกกกกและถ้าสามารถทนอ่านได้จะพบว่าโค้ดชุดนี้ทำงานหลายอย่างมากตั้งแต่ สร้าง buffers, fetchingpages, searching สำหรับ inherited pages, rendering paths,appending arcane strings, และ สร้าง HTML,
และอีกหลายๆอย่างและเราจะเห็นว่ามันจะยุ่งมากเราจะดูแลรักษามันยากมากดังนั้นถ้้าเราพยายามแยกการทำงานออกมาใหม่ยกตัวอย่างเช่น
public static String renderPageWithSetupsAndTeardowns(
PageData pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}
จะเห็นได้ว่าฟังก์ชั่นนี้ทำงานอย่างเดียวและอ่านง่ายกว่ามาก ดังนั้นจากตัวอย่างที่แสดงให้ดูเราเห็นสมควรว่า
FUNCTIONS SHOULD DO ONE THING. THEY SHOULD DO IT WELL. THEY SHOULD DO IT ONLY.
แต่ปัญหาของการพยายามเขียนฟังก์ชั่นให้ทำงานหนึ่งอย่างคือเราจะรู้ได้อย่างไรว่าไอ้ “หนึ่งอย่าง” ที่ว่าคืออะไรเรามาดูตามตัวอย่างที่สั้นๆกัน
1. ตรวจสอบดูว่า page นี้ถูกทดสอบแล้วหรือยัง
2. ถ้าไม่ให้ include การ setup และ teardowns ลงไป
3. หลังจากนั้นให้ Render HTML Page ออกไป
อ่าวแล้วไอ้ฟังก์ชั่นนี้มันทำงานกี่อย่าง หนึ่งอย่างหรือสามอย่างกันแน่แต่ถ้าเรานั่งพิจรณาสักพักจะพบว่ามันเป็นขั้นตอนการทำงานสามลำดับที่ทำงานที่ระดับเดียวกันภายใต้ชื่อฟังก์ชั่น ซึ่งเราสามารถอธิบายการทำงานของฟังก์ชั่นสามารถอธิบายได้ด้วยวิธีการที่เรียกว่า TO
“TO RenderPageWithSetupsAndTeardowns, we check to see whether the page is a test page and if so, we include the setups and teardowns. In either case we render the page in HTML.”
ถ้าฟังก์ชั่นทำงานตามที่เรากำไว้เหมือนกับชื่อของมันนั่นแปลว่ามันทำงาน “อย่างเดียว” เนื่องจากว่าสาเหตุหลักของการสร้างฟังก์ชั่นคือการแตกความซับซ้อนขนาดใหญ่ๆออกเป็นกระบวนการทำงานเล็กๆที่เหมาะสมซึ่งถ้าเราดูตัวหย่างยาวๆเราจะเห็นว่ากรทำงานของมันมีหลายขั้นตอนมากจนเกินไปและทำงานในหลายระดับจนเกินไป จะเห็นว่าจริงๆแล้วเราสามารถแตกฟังก์ชั่นออกมาได้อีกแต่ถ้าเราแตกมันออกมาแล้วระดับของการทำงานมันไม่เปลี่ยนมันก็ไม่มีประโยชน์ที่เราจะแยกมันออกมาอีกเช่นจะแยก if ออกมา (ดูไร้สาระมาก)
แล้วมีเทคนิคอื่นไหม
Sections within Functions
เรามาดูอีกฟังก์ชั่นกันที่ยาวเหมือนกันแต่มันทำงานอย่างเดียว
import java.util.*;
public class GeneratePrimes{
public static int[] generatePrimes(int maxValue){
if (maxValue >= 2){
// declarations
int s = maxValue + 1; // size of array
boolean[] f = new boolean[s];
int i;
// initialize array to true.
for (i = 0; i < s; i++)
f[i] = true;
// get rid of known non-primes
f[0] = f[1] = false;
// sieve
int j;
for (i = 2; i < Math.sqrt(s) + 1; i++){
if (f[i]){
for (j = 2 * i; j < s; j += i)
f[j] = false; // multiple is not prime
}
}
// how many primes are there?
int count = 0;
for (i = 0; i < s; i++){
if (f[i])
count++; // bump count.
}
int[] primes = new int[count];
// move the primes into the result
for (i = 0, j = 0; i < s; i++){
if (f[i]) // if prime
primes[j++] = i;
}
return primes; // return the primes
}
else // maxValue < 2
return new int[0]; // return null array if bad input.
}
}
generatePrimes ฟังก์ชั่นสามารถแตกเป็นขั้นตอนต่างๆได้หลายขั้นตอนเช่น declarations, initializations, และ sieve. จะเห็นว่าฟังก์ชั่นนี้สามารถแยกเป็น section ได้ทำให้เราได้อีกนิยามว่าฟังก์ชั่นที่สามารถถูกแยกเป็น section ได้ก็สมควรจะถูกแยกเป็นฟังก์ชั่นใหม่ได้เช่นกัน
One Level of Abstraction per Function
อีกเทคนิคที่เราสามารถนำมาพิจรณาได้คือ Level Of Abstraction เราต้องแน่ใจได้ว่าฟังก์ชั่นของเราทำงานอยู่บน Level Of Abstraction เดียวกัน ถ้าเรากลับไปดูตัวอย่างยาวๆอันแรกเราจะเห็นว่า Level มันพิศดารมากมันมีตั้งแต่ Level ระดับสูงอย่าง getHtml() และ Level ต่ำๆอย่าง String pagePathName = PathParser.render(pagePath); และ Level ที่ตำ่มากๆอีกเช่น .append(“\n”).
ดังนั้นอย่าพยายามให้ฟังก์ชั่นทำอะไรหลายอย่างอย่าพยายามยัดการทำงานที่อยู่ต่าง Level Of Abstraction เข้าไปด้วยกัน