Breadth-first หรือ depth-first
ส่วนต่อไปที่เราจะทำคือเราจะหากระบวนท่าในการจัดการปัญหาที่เราเผชิญหน้าอยู่ ดังนั้นเราควรจะย้อนวันเวลากลับไป สมัยเป็นเกรียนมหาลัย ตอนที่เรานั่งหลับในวิชา Data Structure ถ้ายังจำได้เราจะพบว่าการ transverse tree สามารถทำได้มากกว่า 1 วิธีคือ breadth-first และ depth-first วิธีการนี้ใช้ได้กับ TDD เช่นกันตามรูป 2.3 และ 2.4 คือ
ถ้าเราตัดสินใจแก้ปัญหาด้วยกระบวนท่า breadth-first ผลคือเราจะเขียน test เพื่อทดสอบการทำงานของ interface ของคลาส Template โดยเราจะ fake การทำงานข้างในมันทั้งหมดและหลัจากนั้นเราจะค่อยๆเขียน test เพื่อทดสอบการ ทำงานของ function ข้างในทีละตัวไปตามภาพ 2.3 มองจากซ้ายไปขวา
ภาพ 2.3
ภาพ 2.4
ในทางกลับกันถ้าเราเลือกไปท่า depth-first เราจะ test-implement จากระดับล่างมาก่อนดังนั้น เราจะต้องเขียนรายละเอียด ของ template parsing logic ก่อนแล้วตามมาด้วยการเขียน rendering แล้วสุดท้ายจะกระโดดขึ้นไปเขียน Template Engine เป็นที่สุดท้าย
ในกรณีการทำงานของเราเรื่อง template engine ที่ต้องจัดการกับ “Hello ${name}” นั้นเราสามารถที่จะ fake การทำงานระดับล่าง(parsing template) ไว้ได้ก่อนแล้วหลังจากนั้นเราค่อยๆเพิ่มความสามารถของมันเข้าไปทุกครั้งที่ เราเพิ่ม test เข้าไปดังนั้นเราเลือก breadth-first แล้วจะทำยังไงล่ะนี่
Faking details a little longer
สิ่งที่เราต้องทำคือเราจะ Fake รายละเอียดดังนั้น อันดับแรกเราต้องหาที่สำหรับเก็บ Template และ ค่าตัวแปรก่อน สิ่งที่เราต้องทำคือหาวิธีแทนค่าเพื่อให้ผ่าน test ไปก่อนโดยเราจะทำได้ตามนี้
public class Template {
private String variableValue;
private String templateText;
public Template(String templateText) {
this.templateText = templateText;
}
public void set(String variable, String value) {
this.variableValue = value;
}
public String evaluate() {
return templateText.replaceAll("\\$\\{name\\}", variableValue);
}
}
เมื่อเราใส่ test นี้เข้าไปแน่นอนว่า แดง อีกแล้วเพราะอะไรเราคงเห็นกันจะจะอยุ่ว่า Reg-Ex ของเรามันจ้องจะหา ${name} อย่างเดียวถ้าเจออย่างอื่นมันจะออกอาการไม่ตอบสนองทันที ดังนั้นคำถามต่อไปคือเราจะรับมือกับโจทย์ข้อนี้ยังไง
ทางออกแรกที่คิดออกคือเขียนแบบ search-and-replace ไล่ไปเป็นคู่ๆ แต่การเขียนแบบนี้อาจมีปัญหาที่ บางอย่างคือมันอาจจะช้าและอาจมีปัญหากับตัวแปรที่เราต้องการเข้าไปแทนค่าที่มีค่าเป็น ${variable} แต่คงยังไม่มีทางออกไหนที่ดีไปกว่านี้อีกแล้ว ณ ตอนนี้เราจะเลือกทางนี้ไปก่อนเพราะเราต้องการเดินไปข้างหน้า แต่อย่างไรก็ตามเพื่อให้แน่ใจว่าสิ่งที่เราจะ test นั้นสามารถทำงานได้ถูกต้องเราสามารถเขียนออกมาเป็นประโยคได้ดังนี้
Evaluate template “${one}, ${two}, ${three}” with values “1”, “${foo}”, and “3”, respectively, and verify that the template engine renders the result as “1, ${foo}, 3”.
การเขียนออกมาเป็นประโยคแบบนี้จะช่วยให้เราสามารถมีสมาธิอยู่กับสิ่งที่เราทำความคิดเราจะไม่กระโดด ข้ามไปข้ามมา โดยผลของการใช้วิธีการ search-and-replace จะทำให้เราได้โค้ดออกมาใหม่เป็นแบบนี้
New and improved details
import java.util.Map;
import java.util.HashMap;
import static java.util.Map.Entry;
public class Template {
private Map<String, String> variables;
private String templateText;
public Template(String templateText) {
this.variables = new HashMap<String, String>();
this.templateText = templateText;
}
public void set(String name, String value) {
this.variables.put(name, value);
}
public String evaluate() {
String result = templateText;
for (Entry<String, String> entry : variables.entrySet()) {
String regex = "\\$\\{" + entry.getKey() + "\\}";
result = result.replaceAll(regex, entry.getValue());
}
return result;
}
}
และแน่นอนว่าเรา test ผ่านแล้ว และสิ่งต่อไปที่เราจะทำคือการ Refactor แต่อย่างไรก็ตาม เรายังไม่เห็นว่าโค้ดจะสามารถถูกปรับไปได้มากกว่านี้ ดังนั้นจะขอข้ามไปทำอย่างอื่นก่อนจะดีกว่า
■ เราต้อง evaluate template “Hello, ${name}” ด้วยการไม่ส่งค่า name มาให้และเราควรจะได้รับ MissingValueError
■ เราต้อง evaluate template “Hello, ${name}” ด้วยการส่งค่า “Hi” และ “Reader” แต่เนื่องจากเราส่งค่าตัวแปรไปเกินสิ่งที่เราควรได้รับกลับมาคือ “Hello Reader” เท่านั้น
■ ข้อนี้เป็นข้อที่เราจดไว้ก่อนเขียนโค้ด search-and-replace โดยเราจะ evaluate template ที่เป็น
“${one}, ${two}, ${three}” ด้วยค่า “1”, “${foo}”, and “3” เราก็ควรจะได้ “1”, “${foo}”, and “3”
Testing for a special case
โค้ดส่วนต่อไปคือเราจะ test สามรายการด้านบนที่เรา list ไว้โดยหน้าตา test จะเป็นแบบนี้
@Test
public void unknownVariablesAreIgnored() throws Exception {
Template template = new Template("Hello, ${name}");
template.set("name", "Reader");
template.set("doesnotexist", "Hi");
assertEquals("Hello, Reader", template.evaluate());
}
และแน่นอนผ่าน ฉลุย แต่หน้าที่ของเรายังไม่จบและสิ่งที่เราต้องทำต่อไปคือ Refactoring นั่นเอง
รอตอนใหม่มานาน ขอบคุณครับที่มาเขียนต่อ
อยากอ่านเรื่อง refactoring ครับ จะรอนะครับ
ขอบคุณมาก
กำลังปั่นอยู่ครับว่าจะออกอาทิตย์ละตอน
Refactor เขียนเป็นตอนยาวๆได้อีก 4 ตอนครับ
ขอบคุณล่วงหน้าครับ