Tagged: Function

เรื่องของ 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 เข้าไปด้วยกัน