Template Engine

  โดย  ประดับเก่ง

 
เกริ่นนำ
ถ้าใครเคยอ่านตัวอย่างโค๊ดของ Servlet ที่อยู่ใน tutorial ต่าง ๆ ไม่ว่าจะเป็นตามหนังสือหรือตามเวปก็ตาม เรามักจะเห็นว่าหลังจากที่ Servlet ทำการประมวลผลที่มาจาก client แล้ว ท้ายสุดตัว Servlet นั้นจะต้องทำการสร้าง html เพจกลับไปให้ client ดังกล่าวโดยวิธีการสร้าง html ก็ทำกันแบบง่าย ๆ คือการใส่ html โค๊ดเข้าไปอยู่รวมกับส่วนที่เป็น java โค๊ดของคลาส Servlet นั้น ๆ เลย ตัวอย่างโค๊ดในลักษณะนี้ก็อาจเป็นแบบ code snippet ข้างล่าง

public class MyServlet extends HttpServlet  {
...
 public void doGet(HttpServletRequest request, HttpServletResponse response) 
    throws IOException, ServletException { 
 ...
    sb.append("<TR ");
    sb.append(CCStyles.LGREY);
    sb.append("><TD align=\"center\" valign=\"middle\">");
    sb.append(spellitem.id);
    sb.append("</TD><TD valign=\"middle\" ");
    sb.append(CCStyles.DGREY);
    sb.append(">");
    sb.append(spellitem.word);
    sb.append("</TD><TD valign=\"bottom\">");
    sb.append(to_continue);
    sb.append(spellitem.text);
    sb.append("</TD><TD valign=\"bottom\">");
    sb.append(spellitem.suggestions);
    sb.append("</TD></TR>\n"); 
... 
    response.setContentType("text/html"); 
    PrintWriter out = response.getWriter(); 
    out.print(sb.toString());
    out.close();
    } 

สำหรับเพจที่ไม่ซับซ้อนมากนัก การใส่ html โค๊ดเข้าไปรวมกับ java โค๊ดก็คงไม่มีปัญหาอะไร แต่ถ้าสมมุติว่า html เพจนั้นมี layout ที่ซับซ้อน ผลที่จะตามมาก็คือจำนวนโค๊ด html ที่มากขึ้น ซึ่งถ้าเราต้องเอา html โค๊ดดังกล่าวใส่เข้าไปรวมกับ java โค๊ดใน Servlet แล้ว เราก็มักจะพบกับปัญหาที่ตามมามากมาย

ปัญหาที่เกิดขึ้น
ปัญหาแรกที่เห็นได้ชัดคือ ความซับซ้อนที่มากขึ้นของระบบ ปัญหานี้มักถูกมองข้ามในกรณีที่เวปไซด์ที่ใช้ Servlet ในการสร้างเพจมีขนาดเล็ก ขนาดเล็กในที่นี้อาจเปรียบเทียบได้กับจำนวนเพจที่ตัว Servlet สามารถสร้างขึ้นมาได้ อย่างไรก็ตามสำหรับเวปไซด์ที่มีขนาดใหญ่ตั้งแต่ 10 เพจจนถึงร้อย ๆ เพจ การใส่ html เข้าไปรวมกับ java โค๊ดมักจะเป็นสิ่งที่่น่าปวดหัวสำหรับนักพัฒนาอย่างเรา ๆ เป็นอย่างมาก

ปัญหาถัดมาคือ ความเร็วในการพัฒนาระบบ นักพัฒนาไม่สามารถที่จะพัฒนาระบบได้เร็วมากนัก ด้วยเหตุผลที่ว่านักพัฒนาจะต้องเสียเวลากับการแก้ไขและตรวจสอบ html ต่าง ๆ ที่อยู่ใน java โค๊ดให้ถูกต้อง ซึ่งผลที่เห็นอย่างชัดเจนอย่างหนึ่งก็คือ แทนที่นักพัฒนาจะเสียเวลากับการ debug ตัว java โค๊ดแต่เพียงอย่างเดียว นักพัฒนายังจะต้องเสียเวลากับการ debug ส่วนที่เป็น html ต่าง ๆ ด้วย

ปัญหาที่เห็นได้ชัดอีกอย่างหนึ่งก็คือ ปัญหาที่เกี่ยวกับการดูแลรักษา เพจที่ถูกสร้างขึ้นโดย Servlet ลักษณะนี้จะยากต่อการดูแลและเปลี่ยนแปลงเป็นอย่างมาก เมื่อใดก็ตามที่เวปไซด์มีความจำเป็นในการที่จะต้องเปลี่ยนหน้าตา นักพัฒนาก็มักจะปวดหัวกับการแก้ไข html โค๊ดต่าง ๆ เสมอ

ปัญหาสุดท้ายที่อาจจะไม่เห็นเด่นชัดมากนักแต่มักเป็นปัญหาสำคัญที่มักเกิดขึ้นในโครงการที่มีขนาดใหญ่คือ ปัญหาการขาดการพัฒนาโครงการอย่างเป็นระบบ สำหรับระบบใหญ่ ๆ แล้ว html เพจต่าง ๆ มักจะถูกสร้างขึ้นโดยกลุ่มบุคคลสามกลุ่มใหญ่ ๆ คือ นักพัฒนาจาว่า (Java Developer), นักพัฒนาเวป (Web Developer) และนักออกแบบเวป (Web Designer)

- นักพัฒนาจาว่า คือบุคคลที่มีหน้าที่รับผิดชอบเกี่ยวกับการเขียนโค๊ดสำหรับส่วนที่เป็น Server Side โดยเฉพาะซึ่งโดยทั่วไปมักจะเป็นการเขียน Servlet, JSP, JDBC หรือ EJB
- นักพัฒนาเวป คือบุคคลที่อยู่กึ่งกลางระหว่างนักพัฒนาจาว่าและนักออกแบบเวปซึ่งหน้าที่ส่วนมากมักจะเป็นการเขียน html ให้ตรงกับ design หรือ layout ที่ได้มาจากนักออกแบบเวปรวมไปถึงการเขียน Script ที่ใช้รันที่ web browser (Client Side) ยกตัวอย่างเช่น JavaScript หรือ VBScript เป็นต้น
- นักออกแบบเวป คือบุคคลที่ทำหน้าที่ในการออกแบบหน้าตาของ html เพจต่าง ๆ ให้ออกมาตามคอนเซ็ปของเวปไซด์นั้น ๆ ซึ่งสำหรับระบบเล็ก ๆ แล้วนักออกแบบเวปมักจะทำหน้าที่เป็นนักพัฒนาเวปไปในตัวอีกด้วย 

การใส่ html โค๊ดเข้าไปใน java จะทำให้การทำงานของกลุ่มคนสามกลุ่มนี้มีความล่าช้า เพราะนักพัฒนาจาว่าจะต้องรอจนกระทั่งนักออกแบบเวปทำการออกแบบเพจจนเสร็จ ตลอดจนรอกระทั่งนักพัฒนาเวปทำการเขียน html ให้เสร็จก่อนที่ตัวเองจะนำ html โค๊ดที่เขียนเสร็จแล้วมาใส่ใน Servlet ซึ่งหลายคนอาจจะคิดว่าทำไมนักพัฒนาจาว่าถึงไม่ทำการเขียนเพจแบบง่าย ๆ ใส่เข้าไปใน Servlet ก่อน คำตอบก็คือหลักจากที่ html ที่เป็นตัวจริงถูกเขียนเสร็จแล้ว นักพัฒนาจาว่าก็จะต้องเสียเวลาอีกครั้งหนึ่งในการใส่ html อันใหม่นี้เข้าไปใน java โค๊ดแทน
จากปัญหาข้างต้นที่เกิดขึ้นเสมอ ๆ ในการพัฒนาเวปไซด์ วงการ Server Side Application จึงได้มีการคิดค้นเทคโนโลยีต่าง ๆ ขึ้นมาเพื่อแก้ไขปัญหาเหล่านี้ เทคโนโลยีอันหนึ่งที่ถูกสร้างขึ้นและนิยมใช้กันมากในกลุ่มนักพัฒนา Servlet คือเทคโนโลยีที่เรียกว่า Template Engine

Template Engine
Template Engine เป็นคอนเซ็ปง่าย ๆ ที่นักพัฒนา(จาว่า)นิยมใช้ในการแยกโค๊ดส่วน html ซึ่งมักจะถูกเรียกว่า Presentation Layer ออกจากส่วนที่เป็นข้อมูล โดยตัว Servlet จะเป็นแต่เพียงตัวที่ใช้ในการคำนวณและสร้างข้อมูลต่าง ๆ ขึ้นมา แล้วให้ส่วนที่เรียกว่า Template Engine นี้ทำการแปะข้อมูลต่าง ๆ ใส่เข้าไปใน html เพจแทน

Template Engine เป็นตัวช่วยในการแก้ไขปัญหาต่าง ๆ ที่เกิดขึ้นจากการรวม html โค๊ดเข้ากับส่วนที่ java โค๊ดได้อย่างมีประสิทธิภาพ นักพัฒนาสามารถทำการพัฒนาระบบไปพร้อม ๆ กับการที่นักออกแบบเวปทำการพัฒนาหน้าตาของเพจ โดยระหว่างที่นักออกแบบเวปทำการออกแบบเพจ นักพัฒนาก็จะทำการสร้างเพจปลอม ๆ ขึ้นมาแล้วใช้เพจนั้นเทสระบบที่ตนเองเขียนขึ้น ซึ่งหลังจากที่นักพัฒนาเวปทำการเขียน html โค๊ดเสร็จแล้ว นักพัฒนาก็จะนำข้อมูล*ที่ถูกใส่ไว้ในเพจปลอม ๆ นั้นไปใส่ไว้ใน html เพจที่ถูกเขียนขึ้นโดยนักออกแบบเวปแทน
* ข้อมูลในที่นี้หมายถึง ตัว token (ยกตัวอย่างเช่น $firstName) ที่ใช้แทนข้อมูลที่จะถูกแทนที่ด้วยข้อมูลจริง(ที่ถูกสร้างขึ้นโดย Servlet) ในระหว่างการแปะข้อมูลเข้ากับ html เพจโดย Template Engine

ส่วนประกอบที่สำคัญสำหรับ Servlet ที่ใช้ใน web application มักจะประกอบด้วย 3 ส่วนใหญ่ ๆ คือ
1. ส่วนข้อมูลกลาง ส่วนนี้เป็นส่วนที่ Servlet ใช้ในการเก็บข้อมูลที่ได้มาจากที่ต่าง ๆ เช่น เดต้าเบสหรือ HttpSession ซึ่งส่วนข้อมูลกลางนี้จะเก็บค่าต่าง ๆ ที่เป็นส่วนประกอบสำคัญในการสร้าง html เพจ อย่างไรก็ตามเรามักจะไม่เห็นส่วนข้อมูลกลางนี้เด่นชัดนักใน web application ทั่ว ๆ ไป เพราะนักพัฒนานิยมนำค่าต่าง ๆ ที่ได้จากเดต้าเบสหรือ HttpSession ไปรวมกับ content ของ html เพจโดยตรงแทนที่จะรวบรวมค่าทั้งหมดที่ต้องการมาเก็บไว้ในส่วนข้อมูลกลางแล้วทำการสร้าง html เพจในภายหลัง
2. ส่วนประมวลผล ส่วนนี้เป็นส่วนที่รับ request จาก client เพื่อนำมาอ่านค่า parameter ต่าง ๆ แล้วทำการประมวลผล โดยอาจมีการติดต่อสื่อสารกับส่วนข้อมูลกลางเพื่อทำการเปลี่ยนแปลงข้อมูลต่าง ๆ ด้วย
3. ส่วนที่ทำหน้าที่ในการสร้างเพจ ส่วนนี้เป็นส่วนที่ทำการสร้าง html เพจออกมาเพื่อส่งกลับไปยัง client โดยข้อมูลต่าง ๆ ที่อยู่ในเพจมักจะเป็นข้อมูลที่ถูกนำมาจากส่วนข้อมูลกลาง

Simple Design
สำหรับส่วนประกอบของ Servlet ทั้ง 3 ส่วนที่กล่าวมาข้างต้น ส่วนที่่น่าสนใจที่สุดเห็นจะเป็นส่วนที่สามซึ่งเป็นส่วนที่ทำหน้าที่ในการสร้างเพจ โดยสำหรับ Servlet ที่ใช้คอนเซ็ปของ Template Engine แล้วส่วนนี้ก็คือส่วนที่ทำหน้าที่เป็น Template Engine นั่นเอง วิธีการแปะข้อมูลต่าง ๆ จากส่วนข้อมูลกลางลงไปยัง html เพจโดย Template Engine นั้นมีหลายวิธี แต่วิธีการที่ผู้เขียนจะกล่าวถึงในวันนี้เป็นวิธีที่ง่ายที่สุดซึ่งเราเรียกวิธีนี้ว่า การแทนที่ String ด้วย String
วิธีการแทนที่ String ด้วย String นี้มีหลักการง่าย ๆ คือขั้นแรกเราจะต้องทำการออกแบบรูปแบบของ String ขึ้นมาลักษณะหนึ่งโดยเมื่อไรก็ตามที่เราเจอ String รูปแบบนี้เราก็จะเข้าใจว่าค่าที่อยู่ข้างในของ String นี้เป็นเพียงชื่อของตัวแปรซึ่งเราจะต้องทำการหาค่าของตัวแปรนี้จากที่ใดที่หนึ่งมาใส่แทน

หลักการข้างต้นเมื่อถูกนำมาประยุกต์ใช้กับ Servlet รูปแบบของ String ที่เราออกแบบก็จะกลายเป็นตัวแปรต่าง ๆ ที่เราใส่ไว้ใน html template โดยเราจะให้ Template Engine ทำการหาตัวแปรเหล่านี้จาก html template ดังกล่าว ซึ่งหลังจากนั้น Template Engine ก็จะทำการหาค่าที่แมชกับชื่อของตัวแปรเหล่านี้จากที่ต่าง ๆ ใส่เข้าไปใน html template แทน โดยทั่วไปเรามักนิยมให้ Template Engine ทำการหาค่าจริงของตัวแปรต่าง ๆ ที่ถูกพบใน html template จากที่ ๆ เดียวซึ่งก็คือส่วนข้อมูลกลางนั่นเอง ท้ายที่สุดผลที่ออกมาจากการแปะค่าต่าง ๆ จากส่วนข้อมูลกลางเข้าไปยัง html template ก็จะกลายเป็น html เพจที่สมบูรณ์และพร้อมที่จะถูกส่งกลับไปยัง client
สมมุติว่าเราให้ส่วนข้อมูลกลางของเราเป็น Hashtable โดยเราเก็บชื่อและค่าของตัวแปรต่าง ๆ ไว้ข้างในดังตัวอย่างข้างล่าง

key="firstName", value="Paul"
key="lastName", value="Clare"
key="sex", value="male"
key="position", value="Director of R&D"

ถ้าเรานิยามรูปแบบของ String ขึ้นมาโดยใช้ <! เป็น prefix และ > เป็น suffix ดังนั้น <!variable1> ก็จะหมายถึงตัวแปรตัวหนึ่งที่ชื่อ variable1 ซึ่งทาง Template Engine จะต้องมีหน้าที่ในการหาค่าของตัวแปรนี้มาใส่แทนส่วนที่เป็น <!variable1> โดยตัวอย่างของ html template ที่เราใช้ในการนำค่าต่าง ๆ ที่เก็บอยู่ในส่วนข้อมูลกลางข้างต้นก็อาจจะเป็นอย่าง html ข้างล่างนี้

<html>
<head>
<title>Simple String Token</title>
</head>
<body>
My firstname is: <!firstName> <br>
My lastname is: <!lastName> <br>
My sex is: <!sex> <br>
My position is: <!position> <br>
<body>
</html>

ถ้าเราให้ Template Engine ทำการแมปค่าต่าง ๆ ที่อยู่ใน Hashtable ข้างต้นกับตัว html template ตัว html เพจหลังสุดก็จะออกมาเป็น

<html>
<head>
<title>Simple String Token</title>
</head>
<body>
My firstname is: paul <br>
My lastname is: clare <br>
My sex is: male <br>
My position is: Director of R&D <br>
<body>
</html>

คลาสง่าย ๆ ที่เราใช้ในการแปะค่าต่าง ๆ ที่อยู่ใน Hashtable เข้าไปยัง html template ที่เป็น String ก็อาจจะเป็นอย่างคลาสนี้ StringUtil.java
คลาส StringUtil มี method อยู่เพียงสอง method คือ

public static String replace(String source, String token, String newValue);
public static String replace(String source, Hashtable nvPairs, String tokenPrefix, String tokenSuffix);

ตัว method แรกใช้สำหรับเปลี่ยนสตริง token ที่ปะปนอยู่ในสตริง source ให้กลายเป็นสตริง newValue ยกตัวอย่างเช่น
String source = "I'm borg. Resistance is futile!!!  borg! borg! borg!";
String token = "borg";
String newValue = "boy";

ผลที่ได้หลังจากการใส่ค่าทั้งสามลงไปยัง method นี้ก็จะเป็น
"i'm boy. Resistance is futile!!! boy! boy! boy!"

ตัว method ที่สองเป็นส่วนที่เราจะใช้เป็นส่วนประกอบสำหรับสร้าง Template Engine ของเราขึ้นมาโดย method นี้มีหน้าที่ในการนำค่าต่าง ๆ ที่อยู่ใน Hashtable ที่ชื่อ nvPairs มาแทนค่าต่าง ๆ ที่อยู่ในสตริง source โดยค่าต่าง ๆ ที่อยู่ในสตริง source นี้จะต้องมีชื่ออยู่ในคีย์ลิสของ nvPairs โดยจะมี prefix เป็นสตริง tokenPrefix และมี suffix เป็น tokenSuffix ยกตัวอย่างเช่น

String source = "I'm <!species>.  Resistance is <!result>!!! <!species>! <!species>! <!species>!";
Hashtable nvPairs = new Hashtable();
nvPairs.put("species", "borg");
nvPairs.put("result", "futile");
String tokenPrefix = "<!";
String tokenSuffix = ">";

ถ้าเรานำค่าต่าง ๆ เหล่านี้ใส่ไปยัง method ที่สอง ผลก็จะออกมาเป็น
"i'm borg. Resistance is futile!!! borg! borg! borg!"

วิธีการแทนที่ String ด้วย String นี้เป็นวิธีการที่ง่ายที่สุดในการสร้าง Template Engine แต่วิธีการที่นิยมกันมากมักเป็นวิธีการแปลงสตริงออกมาเป็น token หลากหลายชนิดโดยอาศัย Parser ที่ถูกสร้างขึ้นเป็นพิเศษเพื่อรองรับรูปแบบของสตริงที่ถูกนิยามขึ้นมาสำหรับ Template Engine นั้น ๆ โดยเฉพาะซึ่งสำหรับผู้อ่านที่สนใจสามารถศึกษารายอะเอียดเพิ่มเติมได้จาก Resources ที่อยู่ในส่วนท้ายสุดของบทความนี้

TemplateEngineDemo
จากหลักการของ Template Engine ข้างต้น เรามาดูตัวอย่างของ Servlet (TemplateEngineDemo.java) ที่ถูกประยุกต์ใช้สำหรับ Template Engine กันหน่อยดีกว่า

...
public class TemplateEngineDemo extends HttpServlet {
  private String documentPath = null;

  public void init(ServletConfig config) throws ServletException {
    super.init(config);

    documentPath = getInitParameter("documentPath");
    // if no directory provided for loading the template, throwing the exception
    if (documentPath == null || documentPath.trim().length() == 0) {
      throw new UnavailableException (this, "No directory provided for loading the template.");
    }
  }
...

โค๊ดส่วนแรกที่เราจะพูดถึงคือส่วนที่อยู่บนสุดของคลาส TemplateEngineDemo โดยขั้นแรกเราจะเห็นว่าคลาสนี้ได้ทำการ extend คลาส HttpServlet เพื่อให้กลายเป็น Servlet คลาสนี้มี instance variable ที่เป็นสตริงตัวหนึ่งชื่อ documentPath เราใช้สตริงตัวนี้สำหรับเก็บค่าของ directory ที่ตัว TemplateEngineDemo ใช้สำหรับอ้างถึง directory ที่จะใช้โหลด html template ต่าง ๆ
ถัดมาในส่วนของ Servlet Initialization คือฟังก์ชัน init(...) ส่วนนี้มีการอ่านค่าของ documentPath ที่มาจาก parameter ที่เราใส่เข้าไปใน environment ของ Servlet โดยถ้าเราลืมกำหนดค่านี้หรือค่าที่ถูกใส่เข้าไปนี้มีความยาวเท่ากับศูนย์ ตัว Servlet นี้ก็จะหยุดทำงานด้วยการ throw ตัว UnavailableException ออกมา

private String getDocumentPath() {
    return documentPath;
  }
...
private final String loadTemplate(String fileName) throws IOException {
    String templatePath = getDocumentPath();
    File tmplFile = new File(templatePath + fileName);
    BufferedReader in = new BufferedReader(new FileReader(tmplFile));

    String data = null;
    StringBuffer buf = new StringBuffer();
    while ( (data = in.readLine()) != null ) {
      buf.append(data);
    }
    return buf.toString();
  }
...

โค๊ดข้างบนเป็นส่วนที่ TemplateEngineDemo ใช้ในการโหลด html template เข้ามาอยู่ในรูปของสตริงโดยอาศัยฟังก์ชันที่ชื่อ loadTemplate(String fileName) โดย directory ที่ฟังก์ชันนี้ใช้จะถูกนำมาจาก private ฟังก์ชันที่ชื่อ getDocumentPath() ซึ่งจะคืนค่าของ documentPath ที่ถูกเซ็ตค่าแล้วในช่วง Servlet Initialization

...
public void service(HttpServletRequest req, HttpServletResponse res)
                throws IOException, ServletException {
      long userId = -1;
      String userIdStr = req.getParameter("userId");

      if (userIdStr != null) {
        try {
          userId = Long.parseLong(userIdStr);
        } catch (NumberFormatException nfe) {
          // so userId = -1;
        }
      }

      // find a user and load his information into a hashtable
      User user = User.findUserById(userId);
      Hashtable nvPairs = new Hashtable();
      nvPairs.put("firstName", user.getFirstName());
      nvPairs.put("lastName", user.getLastName());
      nvPairs.put("sex", user.getSex());
      nvPairs.put("position", user.getPosition());

      // load a template and map the information of user into it
      String template = loadTemplate("userInfo.html");
      String result = StringUtil.replace(template, nvPairs, "<!", ">");

      res.setContentType("text/html");
      PrintWriter out = res.getWriter();
      out.println(result);
      out.close();
  }
...

ฟังก์ชันท้ายสุดที่เป็นหัวใจของ Servlet นี้ก็คือ service() คลาส TemplateEngineDemo ทำการ override ฟังก์ชันนี้ซึ่งเป็นฟังก์ชันที่อยู่ในคลาส HttpServlet เพื่อทำการรวม request ที่มาจาก HTTP GET และ HTTP POST เข้าด้วยกันซึ่งโดยปกติ request สองอันนี้จะถูก handle โดยฟังก์ชัน doGet() และ doPost() ตามลำดับ
หน้าที่แรกสุดของฟังก์ชัน service() นี้คือการอ่านค่าของ userId ที่มาจาก request โดยจะแปลงค่าจาก String ให้กลายเป็น long ซึ่งถ้าค่าดังกล่าวไม่มีอยู่ใน request ค่าของ userId จะกลายเป็น -1 ซึ่งเป็นค่า default โดยอัตโนมัติ ต่อมาค่านี้จะถูกส่งผ่านเข้าไปยัง static ฟังก์ชัน User.findUserById(long userId) ซึ่งเป็นฟังก์ชันหนึ่งที่อยู่ในคลาส User (User.java) โดยคลาสนี้จะเป็นคลาสหลอก ๆ ที่ทำหน้าที่เป็นเสมือนออฟเจคที่เราใช้ในการติดต่อกับเดต้าเบสหรือ Entity Bean เพื่อหา User ที่เราต้องการ โดยผลที่ได้กลับคืนมาจากฟังก์ชัน User.findUserById(...) ก็คือ User ออฟเจคที่มีค่าของ userId ตรงกับ userId ที่ถูกใส่เข้าไปในฟังก์ชันนั้นนั่นเอง หลักจากที่ได้ User ออฟเจคออกมาแล้ว ตัว Servlet ก็จะทำการใส่ค่าต่าง ๆ ของ User นั้นเข้าไปยังส่วนข้อมูลกลางซึ่งในกรณีของเราก็คือ Hashtable ที่ชื่อ nvPairs

หลังจากที่เราใส่ข้อมูลทั้งหมดเข้าไปยังส่วนข้อมูลกลาง Template Engine พระเอกของเราก็ออกมาทำหน้าที่ในการแปะค่าต่าง ๆ ที่อยู่ในส่วนข้อมูลกลางเข้าไปยัง html template ซึ่งโค๊ดที่ทำหน้าที่ดังกล่าวก็คือ

     ...
      // load a template and map the information of user into it
      String template = loadTemplate("userInfo.html");
      String result = StringUtil.replace(template, nvPairs, "<!", ">");
     ...

จากโค๊ดเราจะเห็นว่าขั้นแรกเราจะทำการโหลด html template ที่ชื่อ userInfo.html ขึ้นมาโดยผ่านทางฟังก์ชันที่ชื่อ loadTemplate(...) เสร็จแล้วทำการแมปค่าต่าง ๆ ที่อยู่ในส่วนข้อมูลกลางหรือ nvPairs นี้เข้าไปยัง html template นี้โดยผ่าน static ฟังก์ชันที่ชื่อ StringUtil.replace(...) โดยใช้ <! เป็น prefix และ > เป็น suffix สำหรับค่าตัวแปรต่าง ๆ ที่อยู่ใน userInfo.html ตามลำดับ

เมื่อ Template Engine ทำงานเสร็จแล้วค่าสตริงท้ายสุดที่ออกมาก็คือ html ที่สมบูรณ์ พร้อมที่จะถูกส่งกลับไปยัง client (web browser) ซึ่ง TemplateEngineDemo ก็จะทำการเซ็ต HTTP header, เรียก PrintWriter เพื่อทำการ print ตัวสตริง result ออกไปดัง code snippet ข้างล่าง

     ...
      res.setContentType("text/html");
      PrintWriter out = res.getWriter();
      out.println(result);
      out.close();
     ...

เราจะเห็นว่าโค๊ดทั้งหมดของ TemplateEngineDemo จะไม่มีส่วนที่เป็น html ปะปนอยู่เลย ดังนั้นในกรณีที่เราต้องการเปลี่ยนหน้าตาของ userInfo.html เราก็เพียงทำการเปลี่ยน html layout ที่อยู่ในไฟล์นั้นโดยยังคงเก็บชื่อของตัวแปรต่าง ๆ ที่อยู่ใน <!XXX> ไว้ก็เป็นอันเสร็จวิธี

ผู้อ่านสามารถทำการรัน TemplateEngineDemo Servlet ได้ด้วยตนเอง โดยดาวโหลดไฟล์ที่เกี่ยวข้องทั้งหมดจาก TemplateEngine.zip
 Note: สำหรับผู้อ่านที่ใช้ Tomcat เป็น Servlet Engine หลังจากที่ทำการติดตั้ง TemplateEngineDemo เข้าไปยัง working directory แล้ว ให้ทำการเพิ่มส่วนนี้เข้าไปยังไฟล์ web.xml ของ working directory นั้น ยกตัวอย่างเช่น

<web-app>
...
 <servlet>
  <servlet-name>TemplateEngineDemo</servlet-name>
  <servlet-class>com.jarticles.TemplateEngineDemo</servlet-class>
  <init-param>
   <param-name>documentPath</param-name>
   <param-value>D:/MyWebApplication/template/</param-value>
  </init-param>
 </servlet>
 ...
</web-app>

โดย documentPath ในที่นี้เราอ้างถึง directory ที่ชื่อ D:/MyWebApplication/template/ ซึ่งบรรจุไฟล์ userInfo.html เอาไว้

ในการรันทดสอบ ให้ใช้ url ที่มี parameter ที่ชื่อ userId ใส่รวมเข้าไปด้วย ยกตัวอย่างเช่น
http://serverName:port/contextPath/TemplateEngineDemo?userId=0
http://serverName:port/contextPath/TemplateEngineDemo?userId=1
http://serverName:port/contextPath/TemplateEngineDemo?userId=2 เป็นต้น

Template Engine Framework
โดยทั่วไป Template Engine ต่าง ๆ มักถูกเขียนขึ้นในลักษณะของ framwork โดยเราสามารถใช้ framework นี้สร้าง web application ต่าง ๆ ขึ้นมาตามแต่จุดประสงค์ของงานที่เราต้องการได้ ผู้เขียนจะขอยกตัวอย่างวิธีการทำ framework ของ Template Engine แบบง่าย ๆ โดยใช้โค๊ดจาก TemplateEngineDemo ที่เราพูดถึงมาแล้วข้างต้นเป็นแบบอย่าง

framework โดยทั่วไปมักจะเป็นสิ่งที่อำนวยความสะดวกสำหรับ web application โดยจะถูกออกแบบมาให้ตัว web application สนใจกับสิ่งที่ตนเองสมควรจะทำแต่เพียงอย่างเดียวซึ่งสำหรับ Template Engine แล้วหน้าที่หลัก ๆ ของ framework นี้ก็จะเป็น
1. โหลด html template เข้ามายังระบบ
2. ทำการอ่านค่าต่าง ๆ จากส่วนข้อมูลกลางมาแปะเข้ากับ html template ที่ถูกโหลดเข้ามาไว้ก่อนแล้ว
3. ทำการส่งผลท้ายสุดกลับไปยัง web browser

สำหรับ web application ที่ถูกสร้างขึ้นบน framwork ข้างต้น สิ่งที่ต้องทำก็มีเพียง
1. อ่าน request ที่มาจาก web browser
2. ประมวลผล request ที่ได้มา
3. รวบรวมผลที่ได้ใส่เข้าไปยังส่วนข้อมูลกลางเพื่อให้ framework ทำการแปะข้อมูลต่าง ๆ เข้าไปยัง html template แล้วส่งผลสุดท้ายกลับไปยัง web browser

ถ้าเราต้องการทำ TemplateEngineDemo ให้เป็นลักษณะ Template Engine Framework เราก็สามารถแยกส่วนออกเป็น 2 Layers ด้วยกันคือส่วนที่เป็น Framework Layer และส่วนที่เป็น Appliation Layer โดยขั้นแรกเราจะพูดถึงส่วนที่เป็น Framework Layer กันก่อน

// TemplateBaseServlet.java (Framework Layer)
package com.jarticles;

import java.io.*;
import java.util.*;

import javax.servlet.*;
import javax.servlet.http.*;

public abstract class TemplateBaseServlet extends HttpServlet {
  protected static final String DEFAULT_TOKEN_PREFIX = "<!";
  protected static final String DEFAULT_TOKEN_SUFFIX = ">";

  public void init(ServletConfig config) throws ServletException {
    super.init(config);
  }

  /**
   * implemented by subclass
   */
  protected abstract String getDocumentPath();

  /**
   * the value of tokenPrefix can be changed if subclass override this method
   */
  protected String getTokenPrefix() {
    return DEFAULT_TOKEN_PREFIX;
  }

  /**
   * the value of tokenSuffix can be changed if subclass override this method
   */
  protected String getTokenSuffix() {
    return DEFAULT_TOKEN_SUFFIX;
  }

  /**
   * use this method if you want to set the content type and header yourself
   */
  protected final String getPageContent(String templateName, Hashtable nvPairs)
                           throws IOException {
    String template = loadTemplate(templateName);
    return StringUtil.replace(template, nvPairs, getTokenPrefix(), getTokenSuffix());
  }

  /**
   * use this method if you want TemplateBaseServlet sending the outputStream to the client for you
   */
  protected final void sendPage(HttpServletResponse res, String templateName, Hashtable nvPairs)
                         throws IOException, ServletException {
    String content = getPageContent(templateName, nvPairs);
    res.setContentType("text/html");
    res.setHeader("pragma", "no-cache");

    PrintWriter out = new PrintWriter(res.getOutputStream());
    out.print(content);
    out.close();
    return;
  }

  private final String loadTemplate(String fileName) throws IOException {
    String templatePath = getDocumentPath();
    File tmplFile = new File(templatePath + fileName);
    BufferedReader in = new BufferedReader(new FileReader(tmplFile));

    String data = null;
    StringBuffer buf = new StringBuffer();
    while ( (data = in.readLine()) != null ) {
      buf.append(data);
    }
    return buf.toString();
  }
}

คลาส TemplateBaseServlet เป็นคลาสที่ถูกออกแบบมาเพื่อใช้สำหรับเป็น Framework Layer ให้กับคลาส Servlet อื่นที่ต้องการพัฒนาระบบโดยใช้หลักการของ Template Engine โดยคลาส Servlet เหล่านั้นจะต้องทำการ subclass คลาส TemplateBaseServlet เพื่อที่จะสามารถเรียกใช้ฟังก์ชันต่าง ๆ ที่ทางคลาสนี้จัดเตรียมไว้ให้ ถ้าเราสังเกตดี ๆ เราจะเห็นว่าคลาส TemplateBaseServlet เป็นคลาส abstract โดยฟังก์ชันที่ทาง subclass จะต้องทำการ implement ก็คือฟังก์ชัน getDocumentPath() หลายคนอาจจะเดาได้ว่าทำไม subclass ถึงต้องทำการ implement ฟังก์ชันนี้ เหตุผลก็เพราะว่า Servlet แต่ละตัวที่มา subclass คลาส TemplateBaseServlet มักจะมี working directory ที่ใช้เก็บ html template ต่าง ๆ เป็นของตัวเอง ดังนั้น subclass เหล่านั้นก็ควรที่จะทำการอ้างถึง directory ที่ตนใช้ด้วยตนเองซึ่งทาง TemplateBaseServlet ก็จะใช้ค่าที่ได้จากฟังก์ชันที่ถูก implement โดย subclass นี้โหลด html template ต่าง ๆ โดยผ่านทาง private ฟังก์ชันที่ชื่อ loadTemplate(String fileName) (สำหรับวิธีการให้ subclass ทำการ implement ฟังก์ชันต่าง ๆ ด้วยตัวเองโดยตัว base class จะเรียกใช้ฟังก์ชันเหล่านั้นโดยไม่สนใจว่าส่วนที่เป็น implementation จะเป็นอย่างไร เรามักจะเรียกวิธีการเช่นนี้ว่า Template Method)

นอกจากฟังก์ชัน getDocumentPath() ที่ทาง subclass สามารถทำการ override ได้แล้ว ทางคลาส TemplateBaseServlet ก็ยังมีอีกสองฟังก์ชันที่อนุญาติให้ทาง subclass ทำการ overrdie ได้อีกคือฟังก์ชัน getTokenPrefix() และ getTokenSuffix()
เราจะเห็นว่าฟังก์ชันสองอันนี้จะคืนค่าที่เราใช้เป็นตัว prefix และ suffix สำหรับสตริงที่เราใส่ไว้ใน html template ซึ่งสตริงเหล่านี้จะถูกหาโดย Template Engine แล้วทำการแมปสตริงเหล่านี้เข้ากับชื่อของเดต้าที่เราใส่ไว้ในส่วนของข้อมูลกลาง โดยค่า default ก็จะเป็น "<!" และ ">" ตามลำดับ อย่างไรก็ตามถ้าทาง subclass ต้องการที่จะเปลี่ยนตัว prefix และ suffix นี้ก็สามารถทำได้โดยการ override ฟังก์ชันสองตัวนี้โดยทำการคืนค่าที่เป็นอย่างอื่นแทน ยกตัวอย่างเช่น

public class TemplateBaseSubclass extends TemplateBaseServlet {
...
public String getDocumentPath() {
  return documentPath;
}

protected String getTokenPrefix() {
    return "<--$";
  }

  protected String getTokenSuffix() {
    return "-->";
  }
...

อย่างที่กล่าวมาข้างต้นว่าส่วนที่เป็น framework จะต้องทำหน้าที่ในการแปะเดต้าที่อยู่ในส่วนข้อมูลกลางเข้ากับ html template ที่ถูกโหลดขึ้นมา ซึ่งสำหรับ TemplateBaseServlet แล้วฟังก์ชันที่ทำหน้าที่ดังกล่าวก็คือฟังก์ชัน getPageContent(...) และ sendPage(...) โดยฟังก์ชันแรกเป็นฟังก์ชันที่ใช้สำหรับงานทั่ว ๆ ไปซึ่งจะทำแต่เพียงการแปะเดต้าที่อยู่ในส่วนข้อมูลกลางเข้ากับ html template แล้วคืนค่าของ html ที่สมบูรณ์แบบออกมา ฟังก์ชันที่สองเป็นฟังก์ชันที่ทำทุกอย่างให้กับ subclass ซึ่งสำหรับ subclass ที่เป็น Servlet แล้ว สิ่งสุดท้ายที่จะต้องทำก็คือการส่ง html กลับไปยัง web browser ดัง code snippet ข้างล่าง

...
protected final void sendPage(HttpServletResponse res, String templateName, Hashtable nvPairs)
                         throws IOException, ServletException {
    String content = getPageContent(templateName, nvPairs);
    res.setContentType("text/html");
    res.setHeader("pragma", "no-cache");

    PrintWriter out = new PrintWriter(res.getOutputStream());
    out.print(content);
    out.close();
    return;
  }
...

เรามาดูวิธีการเขียน Servlet (หรือ Application Layer) ที่สร้างอยู่บน Framework Layer ซึ่งในที่นี้ก็คือการ subclass คลาส TemplateBaseServlet กันบ้าง

// MyTemplateServlet.java (Application Layer)
package com.jarticles;

import java.io.*;
import java.util.*;

import javax.servlet.*;
import javax.servlet.http.*;

public class MyTemplateServlet extends TemplateBaseServlet {
  private String documentPath = null;

  public void init(ServletConfig config) throws ServletException {
    super.init(config);

    documentPath = getInitParameter("documentPath");
    // if no directory provided for loading the template, throwing the exception
    if (documentPath == null || documentPath.trim().length() == 0) {
      throw new UnavailableException (this, "No directory provided for loading the template.");
    }
  }

  /**
   * now, implemented by this class
   */
  public String getDocumentPath() {
    return documentPath;
  }

  public void service(HttpServletRequest req, HttpServletResponse res)
                throws IOException, ServletException {
      long userId = -1;
      String userIdStr = req.getParameter("userId");

      if (userIdStr != null) {
        try {
          userId = Long.parseLong(userIdStr);
        } catch (NumberFormatException nfe) {
          // so, userId = -1;
        }
      }

      // find a user and load his information into a hashtable
      User user = User.findUserById(userId);
      Hashtable nvPairs = new Hashtable();
      nvPairs.put("firstName", user.getFirstName());
      nvPairs.put("lastName", user.getLastName());
      nvPairs.put("sex", user.getSex());
      nvPairs.put("position", user.getPosition());

      // send nvPairs to map with userInfo.html template and print
      // the outputStream to the client
      sendPage(res, nvPairs, "userInfo.html");
  }
}

เราจะเห็นว่าโค๊ดส่วนใหญ่ที่อยู่ในคลาสนี้ก็คือ โค๊ดในส่วนของฟังก์ชัน service(...) จากคลาส TemplateEngineDemo นั่นเอง คลาสนี้จะไม่มีส่วนที่ทำหน้าที่เกี่ยวกับ Presentation (HTML) อยู่เลย โดยหน้าที่หลัก ๆ ของคลาสนี้ก็มีแต่เพียงการอ่าน request ที่มาจาก web browser นำค่าที่ได้ไปหา User ออฟเจคที่อยู่ในเดต้าเบสเสร็จแล้วใส่ข้อมูลที่เกี่ยวข้องของ User เช่น firstName, lastName, ฯลฯ ที่ได้เข้าไปยังส่วนข้อมูลกลางซึ่งในที่นี้ก็คือ Hashtable ที่ชื่อ nvPairs ก่อนที่จะเรียกฟังก์ชัน sendPage(...) ซึ่งเป็นฟังก์ชันที่ inherit มาจาก base class โดยจะทำหน้าที่เป็น Template Engine สำหรับแมปเดต้าที่อยู่ในส่วนข้อมูลกลางเข้ากับ html template ที่ชื่อ userinfo.html แล้วส่ง outputStream กลับไปยัง web browser ในท้ายสุด

ผู้อ่านสามารถทำการรัน MyTemplateServlet Servlet ได้ด้วยตนเอง โดยดาวโหลดไฟล์ที่เกี่ยวข้องทั้งหมดจาก TemplateEngine.zip

Comments
คลาส TemplateBaseServlet เป็นเพียงตัวอย่างที่ถูกยกขึ้นมาใช้เพื่อให้ผู้อ่านเข้าใจถึงหลักการทำงานขั้นพื้นฐานของ Template Engine โดยคลาสนี้สามารถที่จะใช้งานได้จริงกับ web application ที่มีจำนวนเพจไม่มากนักซึ่งข้อจำกัดนี้เกิดขึ้นมาจาก

1. คลาสนี้ใช้หลักการแทนที่สตริงด้วยสตริงสำหรับใส่ค่าต่าง ๆ ที่อยู่ในส่วนข้อมูลกลางเข้าไปยัง html template ซึ่งสำหรับ Server Side Application แล้ววิธีการ implementation แบบนี้ใช้ CPU time ค่อนข้างมาก

2. เมื่อไรก็ตามที่มี request เข้ามา คลาสนี้จะทำการโหลด html template ที่เกี่ยวข้องขึ้นมาเพื่อทำการใส่ค่าที่มาจากส่วนข้อมูลกลางเข้าไปทุกครั้ง การโหลด html template ขึ้นมาทุกครั้งนี้ เราจะต้องเสียเวลาส่วนหนึ่งให้กับ I/O ซึ่งวิธีการที่เหมาะสมกว่าคือการ cache ตัว html template ต่าง ๆ ไว้ใน memory หลักจากมีการโหลด html template ดังกล่าวขึ้นมาเป็นครั้งแรก

อย่างไรก็ตามการโหลด html template ทุกครั้งนี้ก็ยังคงเหมาะสำหรับช่วง Development Phase ซึ่งนักพัฒนายังคงทำการเปลี่ยนแปลงและแก้ไขระบบอยู่โดยจะช่วยทำให้ประหยัดเวลาที่เสียไปกับการ restart ตัว Servlet Engine ทุกครั้งที่มีการเปลี่ยนแปลงหน้าตาของ html template ในกรณีที่เราทำการ cache ตัว html template
Note: เราสามารถทำการ turn on/off การ cache ตัว html template ของ Servlet ได้ในช่วง Runtime โดยการเซ็ตค่า on/off ไว้ใน initial properties ของ Servlet ซึ่งเราจะใช้ turn off โหมดในช่วง Development Phase และใช้ turn on โหมดในช่วงที่เราใช้งานจริง(เช่นการรัน Servlet ที่ Production Site) เท่านั้น

3. ในกรณีที่เราต้องการสร้างตารางที่เกิดจากลิสของข้อมูล เรายังคงต้องใส่ html โค๊ดส่วนหนึ่งเข้าไปยัง java โค๊ด ด้วยเหตุที่ว่าคลาส TemplateBaseServlet ไม่ได้ทำการ implement ตัว syntax อื่น ๆ ที่ใช้สำหรับสร้างลูป ยกตัวอย่างเช่น for-each, while เป็นต้น

Wrapping up
มาถึงจุดนี้หลายท่านอาจจะถามว่าการใช้ Servlet ที่ใช้หลักการของ Template Engine กับการใช้ JSP ในการพัฒนา web application เราควรจะเลือกอย่างไหนถึงจะดีกว่ากัน คำถามนี้ยังเป็นคำถามที่ติดอยู่ในใจของนักพัฒนาหลาย ๆ คนซึ่งคำตอบก็มักจะขึ้นอยู่กับประสบการณ์ส่วนตัวของนักพัฒนาแต่ละคนที่มีกับเทคโนโลยีทั้งสองแบบนี้  สำหรับตัวผู้เขียนเองแล้ว ถ้า web application มีเดต้าเป็นในลักษณะที่ถูกแชร์โดยเพจต่าง ๆ ที่อยู่ในระบบทั้งระบบ Servlet ที่ใช้หลักการของ Template Engine มักเป็นตัวเลือกที่ดีกว่าในการสร้างเพจเพื่อควบคุม flow ต่าง ๆ ที่เกิดขึ้นจากกิจกรรมของผู้ใช้แต่ละคน ในทางกลับกันสำหรับ web application ที่ไม่มีความสัมพันธ์ระหว่างเพจต่อเพจมาก ตลอดจนมีเดต้าที่เป็นลักษณะกระจัดกระจายไม่เกี่ยวข้องกันมากนัก การใช้ JSP ก็มักจะเป็นตัวเลือกที่ดีกว่าเสมอ
นอกจากการใช้ Servlet หรือ JSP แยกกันต่างหากแล้ว ทาง SUN ก็มีข้อแนะนำในการพัฒนา web application โดยการใช้คอนเซ็ปของ MVC (Model-View-Controller) ซึ่งใน J2EE คอนเซ็ปนี้มักถูกเรียกว่า Model 2 ซึ่งสำหรับผู้อ่านที่สนใจสามารถหารายละเอียดเพิ่มเติมได้จาก Understanding JavaServer Pages Model 2 architecture

Resources
ผู้อ่านสามารถดาวโหลด source code ทั้งหมดที่เกี่ยวข้องกับบทความนี้ได้จาก TemplateEngine.zip
สำหรับผู้อ่านที่สนใจเกี่ยวกับคอบเซ็ปของ Template Engine แต่ไม่ชอบ TemplateBaseServlet ของผู้เขียน ; ) สามารถศึกษาเพิ่มเติมและทดลองใช้เทคโนโลยีนี้ได้จาก
- WebMacro : เทคโนโลยีแบบ Template Engine ที่มักถูกพูดถึงเสมอเวลาที่มีการถกเถียงกันเรื่องข้อดีข้อเสียของ JSP เทคโนโลยี
- VelocityTemplate Engine ของ Apache ที่เพิ่งออก v1.0 Released มาเมื่อไม่นานมานี้
- FreeMaker : Open-source Template Engine ที่ถูกออกแบบมาเพื่อใช้กับ Servlet โดยเฉพาะ


Copyright (C) 2000-2001 www.jarticles.com.