วันนี้จะมาเล่าเรื่องของคลาสชุด "วันและเวลา" บน Java ให้ฟังนิดหน่อยครับ

วันเก่า ๆ ...

ตั้งแต่ Java 1.0 เรามีคลาสที่ดูแลเรื่องของ "วันและเวลา" อยู่หนึ่งคลาส ชื่อว่า ... Date ซึ่ง จากมุมมองของนักพัฒนาหลาย ๆ คน คลาสนี้เป็นคลาสที่เรียกได้ว่าออกแบบมาได้แย่ที่สุดของ Java เลยทีเดียว ด้วยเหตุผลที่ว่า มันผูกวันที่และเวลาเอาไว้ในคลาสเดียว

หลังจากนั้นไม่นาน ใน Java 1.1 ทาง Sun ก็มีการเพิ่มคลาสที่เกี่ยวข้องกับเวลาเข้ามาอีกหนึ่งคลาส มีชื่อว่า Calendar ด้วยเหตุผลด้านการจัดการ Timezone การรองรับ Internationalization และตัว year parameter ใน method ต่าง ๆ ของ Date (ที่จะถูก + 1900 เข้าไปอัตโนมัติ) แต่ปัญหาที่สำคัญที่สุดอย่างการเป็น สองคลาสในคลาสเดียว ก็ยังคงอยู่

สองคนในร่างเดียว ?

ทั้ง Date และ Caleandar นั้นเป็นคลาสที่จริง ๆ แล้วเป็น wrapper ครอบเวลาของเครื่องคอมพิวเตอร์ที่มันทำงานอยู่ โดยตัวข้อมูลข้างในนั้นจริง ๆ เป็นตัวเลขที่นับจำนวนมิลลิวินาทีนับตั้งแต่เที่ยงคืนของวันที่ 1 มกราคม 1970 ตัว method ที่เอาไว้หาว่า ตอนนี้วันที่เท่าไหร่ เดือนอะไร ปีอะไร กี่โมง กี่นาที กี่วินาที นั้นจะคำนวนว่าไอ้เจ้าค่าที่เก็บไว้นั้นมันผ่านมาจากเวลาข้างต้นมาเท่าไหร่ แล้วคืนค่ากลับออกมา

ธรรมชาติของคลาสกลุ่มนี้ คือ มันจะเก็บทั้งวันที่ และเวลา ลงไปในคลาสเดียวนั่นเอง

แต่ในมุมมองของผู้ใช้คลาส พอเราเห็นชื่อคลาส เราก็จะนึกว่า เอ๊ะนี่มันคือ "วันที่" นะ (เพราะปฎิทินมันไม่มีเวลาบอก) ดังนั้นเราก็จะคาดหวังว่า ถ้าเรารันโปรแกรมข้างล่าง ในวันที่ 29 มกราคม 2016 แล้วมันควรจะคืนค่า true บอกว่าเป็นวันนี้แหละ

public bool IsToday2016Jan29() {
  Calendar c = new GregorianCalendar(2016, Calendar.JANUARY, 29);
  Calendar today = Calendar.getInstance();
  return today.equals(c);
}

method นี้จะมีโอกาส return true ได้แค่ 1 วินาทีเท่านั้น (ไม่ใช่ทั้งวัน) และเอาเข้าจริง ๆ ต่อให้เราเช็คแต่ละฟิล์ดว่าเป็นวันเดียวกันหรือไม่ ก็มีโอกาสจะไม่ตรงกันได้อีก ขึ้นอยู่กับ timezone ของตัวแปรทั้งสองตัว ...

เดือน ...

อันนี้เป็นบั๊กที่คนเขียน Java แทบทุกคนโดนมาแล้วกับตัว ... ลองดูโค๊ดข้างล่างนี้นะครับ ...

// create a calendar with date = January 30, 2015
Calendar c = new GregorianCalendar(2015, 1, 30);

มีใครบอกได้ไหมว่าทำไมมันถึงพังตรงนี้ ? คำตอบคือ ถ้าเราไปดูใน Java Doc เราจะพบว่า เดือนมกราคมมีค่าเป็น 0 และไอ้ค่าเดือน 1 นั่นน่ะครับมันคือ February ... ซึ่งไม่มีวันที่ 30

นี่ยังโชคดี แค่รันแล้วเจอ exception ทุกรอบ ถ้าเกิดว่าเป็นวันอื่น ๆ เราก็จะไปรู้ตัวอีกทีตอนผลลัพท์ผิด นั่นน่ากลัวกว่าเยอะครับ

โค๊ดที่ถูกต้องสำหรับโจทย์ด้านบน คือ

Calendar c = new GregorianCalendar(2015, Calendar.January, 30);

สำหรับเราคนไทย เราใช้เดือนเป็นชื่อ ไม่ว่าทั้งภาษาไทยและภาษาอังกฤษ คนฝรั่งส่วนใหญ่ก็ยังพอจะโชคดี เพราะใช้ชื่อเดือนเหมือนกัน แต่ความซวยมันไปตกที่คนญี่ปุ่นครับ เพราะว่าในภาษาญี่ปุ่น มันไม่มีชื่อเดือนครับ พวกเขาจะเรียก เดือนหนึ่ง (一月 ishigatsu) เดือนสอง (二月nigatsu) เดือนสาม (三月 sangatsu) อะไรแบบนี้ ถ้าจำไม่ผิด ภาษาเกาหลีและจีนก็เป็นแบบนี้เช่นกัน

อีกอย่างคือนี่คือการอ้างอิงปฎิทิน Gregorian ที่มี 12 เดือน แต่ ไม่ใช่ทุกปฎิทินจะมี 12 เดือน (ปฎิทินแบบฮิบบรูวจะมี 13 เดือนในบางปี ส่วนแบบเขมรนั้นบางปีจะมีเดือน 8 สองเดือน!) ดังนั้นนี่เป็นอีกจุดที่เป็นปัญหาในการออกแบบคลาสชุดนี้

ดังนั้น เพื่อให้ระบบเดือนครอบคลุมหลายๆ ภาษา และหลายๆ ปฎิทิน การเรียกเดือนนั้นจะเรียกเป็นตัวเลข นับตั้งแต่ 1 จนถึง 12 หรือ 13 แล้วแต่ปฎิทินที่ใช้ครับ ทั้งนี้การเรียกเดือนเป็นตัวเลขนั้นก็สอดคล้องกับมาตรฐาน ISO8601 ด้วยครับ แต่มาตรฐานนี้เขาใช้ Gregorian เป็นฐานน่ะนะ

interface สุดพิศดาร

คลาส Date มีลักษณะพิเศษคือมันเป็น Immutable Class ก็คือ เมื่อสร้างเสร็จแล้วใครจะไปเปลี่ยนอะไรมันไม่ได้ เมื่อสร้างเสร็จแล้ว ถ้าเกิดจำเป็นจะต้องเปลี่ยนวันที่ใหม่ ทางเดียวที่ทำได้คือ ... สร้าง object ใหม่ครับ

สำหรับ Calendar นั้นดีกว่าตรงที่มันยังพอจะเป็น Mutable Class ที่เรายังพอเปลี่ยนค่าข้างในได้บ้าง แต่ด้วยความที่มันเป็นปฎิทิน มันถูกออกแบบมาให้รองรับกับปฎิทินหลาย ๆ แบบ ดังนั้นแทนที่มันจะมี getDate(), getYear() หรืออะไรแบบนี้ พี่ท่านดันมาพร้อมกับ method เดียว นั่นคือ get(int field) เป็นอะไรที่น่าหงุดหงิดมาก เพราะเวลาเราเขียนเราก็จะคิดว่าเฮ้ยมันน่าจะมีเมธอดนี้นะ ปัญหาอีกอีกอย่างคือถ้าใส่ตัว input ผิดโปรแกรมก็พัง ... แถม input ดันเป็น int ธรรมดาอีก (ต้องเข้าใจว่ายุคนั้นเรายังไม่มี enum นะครับ)

คือพูดกันตรง ๆ เลยว่า Calendar เนี่ยเป็นอะไรที่น่ารำคาญมากเวลาใช้ ...

ปีใหม่ใหม่

สำหรับท่านที่อ่านถึงตรงนี้แล้ว ขออนุญาตพักเบรค ขอเสนอเพลงที่ว่า "ปีใหม่ใหม่" จากคุณโรส สิรินทิพย์นะครับ

ชีวิตยังมีความหวังในปีใหม่ใหม่ ...

ที่ผมจะบอกคือใน Java 8 มีการเพิ่ม Date Time API ตัวใหม่ในแพคเกจที่ชื่อว่า java.time ซึ่งเป็นผลจาก JSR-310 ซึ่ง กว่าจะได้ใช้กันก็ต้องรอกันเป็นสิบปี (ตัว JSR ตัวนี้เริ่มต้นตั้งแต่ปี 2007 ครับ น่าจะประมาณยุค Java 6)

คลาสชุดนี้เป็นการแยกเอาไอ้เจ้าคลาสครอบจักรวาลข้างบนมาแตกออกให้กลายเป็นคลาสเล็กคลาสน้อยยิบย่อย ซึ่งเราสามารถเลือกมันขึ้นมาใช้ได้ตามความเหมาะสมครับ โดยคลาสหลัก ๆ ก็จะมีดังนี้

  • LocalDate เป็นคลาสที่เก็บวันที่โดยไม่มีเวลามาเกี่ยวข้อง คือเก็บแค่ วัน เดือน ปี แค่นั้น ไม่มีเวลา ไม่มีไทม์โซน และอื่น ๆ
  • LocalTime เป็นคลาสที่มีแต่เวลา ไม่มีวันที่ มีแค่ ชั่วโมง นาที วินาที แค่นั้น ไม่มีไทม์โซนด้วยครับ
  • LocalDateTime ก็ตามชื่อ คือมีแค่ วัน เดือน ปี และ ชั่วโมง นาที วินาที ไม่มีไทม์โซน
  • ZonedDateTime อันนี้จะมีไทม์โซนเข้ามาเกี่ยวแล้วครับ
  • OffsetDateTime คล้าย ๆ ข้างบน แต่จะเป็นการระบุ offset แทนที่จะระบุ timezone คือ มีหลาย timezone ที่มี offset เท่ากันครับ ถ้าเกิดว่าตัวชื่อ timezone ไม่สำคัญ เราก็สามารถใช้ OffsetDateTime ได้

นอกจากนี้ java.time ยังมีคลาสที่ใช้ในการคำนวนหาระยะเวลานับจากจุดหนึ่งไปยังอีกจุดหนึ่งของแกนเวลา (เท่ห์ไหม ?) อีกด้วย

  • Period เป็นระยะเวลานับเป็นวัน สามารถคำนวนหาเป็นเดือน หรือปีได้ด้วย
  • Duration เป็นระยะเวลานับเป็นวินาที สามารถเอาไปคำนวนเป็นระยะอื่น ๆ ได้เช่นกัน

การคำนวนเวลาทำได้ง่ายขึ้นมากครับ เพราะว่าเราไม่จำเป็นต้องเอาไอ้คลาสสองคลาสมาทำเป็นทุกสิ่งทุกอย่างแล้ว โค๊ดก็เขียนง่ายและอ่านง่ายขึ้น อย่างไอ้ฟังก์ชันที่หาว่าวันที่ที่ใส่เข้ามาเป็นวันนี้หรือเปล่า เราสามารถยุบเหลือ date.equals(LocalDate.now()) ได้เลย

ส่วนเรื่องเดือนก็แก้แล้วเหมือนกัน คลาสชุดนี้ใช้เดือน 1-12 ครับ อย่าง January 30, 2015 ก็เขียนว่า

LocalDate date = LocalDate.of(2015, 1, 30) 

ไม่มีการหลงละว่า เดือน 1 คือเดือนอะไรกันแน่

ความเหนือชั้นอีกอย่งของ java.time คือคลาสในชุดนี้มีชื่อที่สือความหมายว่าตัวมันคืออะไร ในขณะที่ java.util.Date และ java.util.Calendar นั้นมีชื่อที่ไม่ได้สื่อถึงการออกแบบและวิธีใช้งานอย่างชัดเจน ทำให้คนใช้กันแบบผิด ๆ

สำหรับคนที่ 'ปีใหม่ใหม่' ยังมาไม่ถึง

สำหรับคนที่ยังคงติดอยู่กับวังวนของ Java 7 นะครับ อันที่จริงก่อน JSR-310 จะถูกสร้างขึ้น ก็มี library ตัวหนึ่งที่ชื่อว่า Joda-Time ที่ออกแบบมาเพื่อสู้รบกับความห่วยของ Date และ Calendar (อันที่จริงจะโทษสองคลาสนี้ก็ไม่ถูกนัก แค่ว่าชื่อมันไม่สื่อถึงสิ่งที่มันเป็นเท่านั้นเอง) Joda-Time ได้รับความนิยมสูงมาก ใน mvn central repository นั้นมีบางเวอร์ชันที่ถูกนำไปใช้งานมากกว่า 450 artifact (ก็ประมาณว่าซอฟต์แวร์ + เวอร์ชันน่ะครับ

Joda-Time มีลักษณะใกล้เคียงกับ Java 8 Date/Time API มากทีเดียว ชื่อคลาสก็ใกล้เคียงกัน คอนเซพท์ก็เหมือนกัน ดังนั้นเรียนรู้ไปไม่เสียหายครับ

ทั้งนี้ผู้พัฒนา Joda-Time แนะนำว่า ถ้าเป็นไปได้ควร migrate โค๊ดไปใช้ Java 8 Date/Time ไปเลยดีกว่าถ้าทำได้ (เข้าใจว่าคงไม่อยากดูแลแล้ว เพราะมีตัวที่ทำงานได้ใกล้เคียงกันมาในตัว standard library เรียบร้อยแล้ว)

ก็ลองศึกษากันดูนะครับ

อันนี้เป็นวิธีที่ผมใช้ นอกเหนือจากพวก style guide ที่ให้มาตามภาษาต่าง ๆ เอามาแชร์ให้ฟังกันเผื่อว่าน่าสนใจนะครับ

ชื่อ Method ควรสื่อถึงการกระทำ (action)

นึกภาพก่อนนะครับว่า เวลาเขียนโค๊ด method คือการสั่งให้วัตถุ (object) ทำอะไรสักอย่าง ดังนั้น ชื่อ method ควรจะสื่อถึงการกระทำอย่างชัดเจน และไม่ยาวเยิ่นเย้อ

โดยทั่ว ๆ ไปเราก็จะเขียนชื่อ method เป็น verb + object ตามไวยากรณ์ภาษาอังกฤษ อย่างเช่น

  • println()
  • getCount()
  • calculate()

ตัว verb อาจจะเป็นเอกพจน์ หรือพหูพจน์ก็ได้ แต่ตาม style guide ส่วนใหญ่มักจะระบุให้เขียนเป็นเอกพจน์ ทั้ง ๆ ที่มันมักใช้กับตัวแปรที่เป็นเอกพจน์ก็ตาม (เข้าใจว่าเป็นการประหยัดหน่วยความจำครับ ได้ 1byte :)) อันนี้เราก็เขียนตามไกด์เลย ไม่ต้องกังวลอะไร

get กับ set ใช้กับ getter และ setter ตามลำดับ

อันนี้ไม่ได้เกี่ยวกับการ์ตูนหุ่นยนต์ หรือวอลเล่ย์บอลแต่อย่างใด getter กับ setter เป็น method ที่ทำหน้าที่เป็นตัวดึงข้อมูลออกจากวัตถุ กับ กำหนดข้อมูลเข้าไปในวัตถุ (ตามลำดับ) อย่างเช่น ...

class Student {
  public Student(String name) { setName(name); }
  private String name;
  public String getName(){ return name; }
  public String setName(String name) { this.name = name;}
}

ตามความหมายแล้ว getter เป็นการดึงข้อมูลออกมาจากวัตถุ ก็คือสิ่งที่ตัววัตถุนั้นเป็นเจ้าของอยู่ แต่บางครั้งเราจะเจอ method แบบนี้

class StudentUtil {
  public static Student getNewStudent(String name) { 
    return new Student(name); 
  }
}

ซึ่งตัว class มีความสัมพันธ์กับตัวผลลัพท์น้อยมาก ตัวผลลัพท์เองไม่ได้มีใครเป็นเจ้าของ (สังเกตว่าเป็นวัตถุใหม่) ดังนั้นชื่อ getNewStudent() อาจจะดูไม่สื่อความหมายสักเท่าไหร่ method นี้จะดีกว่าถ้าใช้ชื่อแบบ createNewStudent() เป็นต้น

เขียนชื่อ method ตาม design pattern

design pattern เป็นรูปแบบการออกแบบคลาสที่เป็นทำกันบ่อย ๆ เป็นรูปแบบซ้ำ ๆ ซึ่งมีโปรแกรมเมอร์ 4 ท่านสังเกตและนำเอารูปแบบที่ซ้ำ ๆ กันมารวบรวมออกมาเป็นหนังสือที่ชื่อว่า Design Patterns โดยกำกับชื่อเอาไว้ว่ารูปแบบไหนมีชื่อเป็นอย่างไร

ตัวอย่างข้างบนนั้น ตัว method createNewStudent() เป็นแพทเทิร์นที่ชื่อว่า Factory Method ซึ่งเป็น method ที่ทำหน้าที่สร้างวัตถุใหม่แล้วคืนกลับไปให้ผู้เรียก method ในแพทเทิร์นนี้เรามักจะใช้ชื่อที่ขึ้นต้นว่า "create" ซึ่งมันก็สื่อความหมายในตัวของมันได้ดี

ผมจะยกอีกสักตัวอย่าง คือ Builder ซึ่งเป็นคลาสที่ทำหน้าที่สร้างวัตถุใหม่เช่นกัน แต่ว่าวิธีการสร้างจะซับซ้อนกว่า อย่างเช่น class Uri.Builder ใน Android เนี่ยจะใช้ในการสร้าง uri ซึ่งเจ้าคลาสนี้จะมี method จำนวนหนึ่งในการตั้งค่าพารามิเตอร์ของวัตถุที่มันกำลังสร้างอยู่ และมี method ชื่อ build() ในการเอา parameter ทั้งหมดที่มันรับมาก่อนหน้านี้ไปสร้างเป็นวัตถุใหม่ (ต่างกับ create() ที่มักจะใช้ parameter ของ method ในการสร้างวัตถุ)

ผมยกตัวอย่างการใช้คลาสนี้จาก StackOverflow นะครับ แต่จะดัดแปลงให้ดูเข้าใจง่ายก่อน (วิธีใช้ที่เหมาะสมจริง ๆ อยู่ในลิงค์ ดูเองนะครับ)

Uri.Builder builder = new Uri.Builder();
builder.scheme("https");
builder.authority("www.myawesomesite.com");
builder.appendPath("turtles");
builder.appendPath("types");
builder.appendQueryParameter("type", "1");
builder.appendQueryParameter("sort", "relevance");
builder.fragment("section-name");
Uri uri = builder.build();

โค๊ดชุดนี้สร้าง uri ว่า https://www.myawesomesite.com/turtles/types?type=1&sort=relevance#section-name ครับ อ้อ ... URL เป็น URI ประเภทนึงครับ

method ที่ใช้รับ event (event handler) นำหน้าว่า on

อันนี้แหกกฎข้อบนนิดหน่อย ตรงที่ onXXX ไม่ได้เป็นรูปของ action ครับ (on เป็น ...preposition เป็นคำนำหน้าจังหวะเวลา ประมาณว่า on sunday, on monday หรืออะไรก็ว่าไป ;-)) ตรงนี้เป็นข้อยกเว้น เป็นรูปแบบที่ใช้กันโดยทั่วไปครับ ไปแหกกฎเขาเดี๋ยวชาวบ้านจะอ่านโค๊ดเราไม่รู้เรื่องน่ะ

ถ้าเกิดว่าดีไซน์ของคลาสมีการกระทำบางอย่างที่ บางส่วนถูกกำหนดตายตัว และบางส่วนสามารถแก้ไขได้ในคลาสย่อย ให้แยกเป็นสอง method โดย method ที่แก้ไขได้ให้ขึ้นต้นด้วยคำว่า do

สมมติว่า เรามีคลาสที่เก็บข้อมูลแล้วมีการตรวจสอบข้อมูล (validation) แล้วเราดันจำเป็นต้องเคลียร์เจ้า errorList ทุกครั้งที่มีการ validate เราก็สามารถเขียนแบบ

class abstract Data {
  private List<Error> errorList;
  public final void validate() {
    errorList.clear();
    doValidate(errorList);
  }
  public abstract void doValidate(List<Error> errorList);
}

class abstract IntegerData {
  private int value;
  private static final int MAX_VALUE = 100000;
  public void doValidate(List<Error> errorList) {
    if(value > MAX_VALUE ) 
      errorList.add(new MoreThanMaximumWarning(value, MAX_VALUE));
    else if(value < 0) {
      errorList.add(new LessThanZeroError(value);
      return;
    }
  }
}

เจ้า doValidate เนี่ยเป็น method ที่ทำงานในส่วนที่ถูกเปลี่ยนแปลงได้จากคลาสย่อย ที่เราใส่คำว่า do ข้างหน้าเพื่อเป็นการบอกว่าจริง ๆ แล้วมันจะถูกเรียกจาก method ไหน (ซึ่งตามหลักแล้วก็ไม่ควรมีมากกว่า 1 method ที่เรียกครับ) เหมือนกับเป็นตัวย้ำนั่นแหละ

เล่าเรื่อง Java บ้าง นาน ๆ ที (หลัง ๆ ไม่ค่อยได้จับครับ ไม่มีโอกาสเท่าไหร่) วันนี้พอดีไปเปิดโค๊ดคนอื่นดู เป็นคลาสที่ออกแบบไว้เป็น utility class แบบว่าย่ออะไรที่ใช้งานบ่อย ๆ ให้เป็นคลาสเดียวกัน ดูผ่าน ๆ ก็ไม่มีปัญหา แต่ว่าผมก็เจออะไรเกี่ยวกับการจัดการ Exception อยู่

ผมว่าหลาย ๆ คนคงเคยเขียนโค๊ดจับ Exception แบบ

public static Socket connect(String host) {
  Socket s = null;
  try {
    s = new Socket(host, 80);
  } catch (Exception e) {
  }
  return s;
}

แล้วไปลุ้นว่า user จะต้องรู้ว่า เฮ้ย ถ้ามันมีปัญหาอะไรสักอย่างแล้ว เมธอดนี้จะคืนค่าเป็น null นะเออ

นอกจากจะแบบนี้แล้ว บางคนอาจจะใจดีขึ้นมาหน่อยนึง ... เขียนแบบนี้

public static Socket connect(String host) {
  Socket s = null;
  try {
    s = new Socket(host, 80);
  } catch (Exception e) {
    e.printStackTrace();
  }
  return s;
}

เพื่อที่จะให้มันพิมพ์ออกคอนโซลว่ามีปัญหานะเอ้อ

ส่วนตัวผมว่าเขียนแบบนี้ดีกว่าครับ

public static Socket connect(String host) 
    throws UnknownHostException, IOException {
  Socket s = new Socket(host, 80);
  return s;
}

พูดง่าย ๆ คือ ให้ user ไปจัดการกับ error เอาเอง แทนที่จะจัดการให้เขา แบบว่า user ก็โตเป็นผู้ใหญ่แล้ว เอ๊ย คือ มันเป็นการให้ทางเลือกกับ user ว่า ถ้าเกิดมีปัญหากับเมธอดนี้แล้ว จะทำยังไงต่อไป โดยบอกเขาด้วยว่ามันเกิดอะไรขึ้น user อาจจะเลือกที่จะ ignore ไป หรือ จะล็อกบน console หรือ แสดงผลออกมา หรืออะไรก็แล้วแต่ การไปจัดการให้เขาตั้งแต่แรกเป็นการลดทางเลือกของเขา ให้เหลือแค่ว่า ทำงานปรกติ กับมีปัญหา (แต่ไม่รู้ว่าเกิดอะไรขึ้น) ซึ่งจะเป็นการบีบให้ user ไม่ใช้โค๊ดเรา แล้วก็ไปเขียนฟังก์ชันใหม่แทน กลายเป็นว่าเกิดโค๊ดที่ซ้ำซ้อนโดยไม่จำเป็นครับ

ก่อนจะเข้าเรื่อง ผมว่าผมเคยบ่นเรื่องของวิธีการ build และ run โปรดักท์ตัวนึงของบริษัทผมใช่ไหมครับ ? ข่าวดีคือมันกำลังจะถูกแทนที่ด้วยตัวใหม่กว่า ...

ข่าวร้ายคือ ... คู่มือการ build และ run เจ้าตัวใหม่เนี่ย หนา 75 หน้า ...

ครับ 75 หน้าครับ งานนี้ไม่ใช่เสร็จภายในสองชั่วโมงครับ บางคนทำสองอาทิตย์ยังไม่เสร็จเลยครับ ...

Build และ Deployment Automation

จากประสพการณ์ส่วนตัว (ตามข้างบนเลยครับ) ผมพบว่าหลาย ๆ คนมักจะมองข้ามความสำคัญของ Build Automation และ Deployment Automation ทุกคนจะคิดแค่ว่า ก็มีคู่มือให้แล้วก็เอาไปใช้สิ บางคนไม่รู้แม้กระทั่งว่ามันมีเจ้านี่อยู่บนโลกของเราด้วยซ้ำครับ

ว่าแต่มันคืออะไรล่ะ ?

Build Automation คือระบบการบิลด์ซอฟต์แวร์อัตโนมัติ (ตามชื่อเลย) คือเป็นเหมือนเครื่องมือที่ช่วยใช้ในการบิลด์ ทำให้เราไม่จำเป็นต้องจำคำสั่งซับซ้อน หรือต้องทำตามขั้นตอนที่ระบุเอาไว้ในคู่มือ แค่สั่งให้มันทำงานตามสคริปท์หรือไฟล์ที่เราสร้างขึ้น มันก็จะทำการทำตามขั้นตอนที่กำหนดเอาไว้ จากโค๊ดให้ออกมาเป็นซอฟต์แวร์นั่นเอง

หรืออย่างน้อย ๆ ก็ช่วยให้กระบวนการบิลด์มันทำงานง่ายขึ้นเท่านั้นเอง

ตัวอย่างคลาสสิคที่สุดน่าจะมาจากฝั่ง Linux ที่เราสามารถที่จะสร้างตัวซอฟต์แวร์ดได้หลังจากที่แตกไฟล์โค๊ดออกมาแล้ว ด้วยคำสั่งข้างล่าง ...

./configure
make

สำหรับ Deployment Automation ก็คล้าย ๆ กัน เพียงแต่เป็นขั้นตอนการติดตั้งละ ถ้าผมยกตัวอย่าง Linux ข้างบน หลังจากที่บิลด์เสร็จเรียบร้อย เราสามารถใช้คำสั่ง

make install

เพื่อที่จะติดตั้งตัวซอฟต์แวร์เข้าไปในระบบครับ พอทำเสร็จแล้วก็สามารถใช้งานได้เลย

ส่วนลึก ๆ แล้วมันทำอะไรบ้างก็ขึ้นอยู่กับสคริปท์ที่เราเขียนไว้เท่านั้นเอง

ความสำคัญ

ทีนี้แล้วทำไมผมถึงมองว่ามันสำคัญ คืออย่างงี้ครับ สมมติว่าผมมีโปรเจคที่เขียนด้วยภาษา C++ ซึ่งการที่จะรันมันได้เนี่ยผมก็ต้องคอมไพล์มันก่อน เวลาคอมไพล์ก็ต้องใช้คำสั่งอย่าง ...

g++ -o ZipPicView MainFrame.cpp ImageViewerPanel.cpp FileEntry.cpp ZipEntry.cpp -lzip -lwxwidgets -lpng -lz -ltiff -llzma -ljpeg 

ยาว และ ยาก ... คือมีโอกาสที่ผมจะพิมพ์ผิดสูงมาก ว่าไหมครับ นี่คือทั้งโปรเจคมีอยู่สิบไฟล์ ถ้าไปเจอโปรเจคที่มีไฟล์เป็นหมื่นก็พิมพ์กันมือหงิกครับ วิธีที่ง่ายที่สุดในตอนนี้ก็คือเขียน shell script หรือ batch file เพื่อที่จะได้ไม่ต้องพิมพ์ยาว ๆ บ่อย ๆ แต่ก็จะยังมีปัญหาเรื่องของการดูแลรักษา ถ้าเกิดว่าใช้บรรทัดเดียวแบบข้างบน พอมีไฟล์เยอะ ๆ เข้าก็มึน มันจะเละไปหมดครับ เราก็ต้องค่อย ๆ เพิ่มตัวแปรเข้ามา ตรงไหนเป็นชื่อผลลัพท์ ตรงไหนเป็นแฟลก ตรงไหนเป็นชื่อไฟล์ เยอะแยะตาแป๊ะไก่

และเมื่อโปรเจคซับซ้อนถึงจุดนึงเราจะเจอว่ามันต้องทำหลาย ๆ ขั้นตอน อย่างอันนี้ผมต้องสร้างตัว resource file (.rc) เข้าไปเพื่อให้แสดงผลบนวินโดวส์ได้สวยงามยิ่งขึ้น ในขณะที่บน Linux นั้นจะต้องไม่ใช้ไฟล์นี้ เราก็ต้องสร้างสคริปท์ที่ซับซ็อนขึ้นไปอีก

แล้วไหนจะยังมีเรื่องของการดูแล library ที่เราใช้ มีอยู่ในระบบที่บิลด์ไหม ใช้เวอร์ชันไหน ลิงค์กับไฟล์ไหนบ้าง ฯลฯ การเขียนสคริปท์ให้ครอบคลุมจึงเป็นเรื่องที่ค่อนข้างยากทีเดียว

ส่วนตัวผมพบว่าแม้กระทั่งการใช้ IDE ช่วย บางโปรเจคเราจะเจอขั้นตอนบางอย่างที่ IDE ช่วยไม่ได้ อย่างการก็อปปี้ resource file ให้ไปอยู่ในสถานที่ที่ถูกต้อง หรือการสร้างไฟล์คอนฟิก อะไรแบบนี้

พอมันความลำบากซับซ้อนมากมาย เราก็จะเจอปัญหาหลายอย่าง อย่างเราจะเจอคนที่ ...

  1. setup ครั้งเดียว ใช้มันไปเรื่อย ๆ ไม่เคยดูว่ามันความเปลี่ยนแปลงอะไรหรือเปล่า
  2. บางคนถึงขั้นไม่เคยอัพเดตโค๊ดเลย เพราะกลัวเซ็ตอัพพัง
  3. บางคนไม่ยุ่งกับโปรดักท์ตัวนี้อีกเลย เพราะเซ็ตยาก ทำยังไงก็รันไม่ได้ สุดท้ายก็ได้แต่โบ้ยให้คนอื่นทำ
  4. คนที่เซ็ตบ่อย ๆ ก็จะเจอปัญหา ใช้ได้บ้างไม่ได้บ้าง ทำซ้ำ ๆ มันต้องมีผิดสักครั้งสองครั้งบ้างแหละ

ที่สำคัญที่สุดคือ มันเสียเวลาครับ ถ้าโค๊ดตัวนี้ใช้เวลา setup นานสองชั่วโมงต่อครั้ง ปี ๆ นึงเราอาจจะเสียเวลาไปหลายสิบแมนเดย์เลยนะเอ้อ ...

ส่วนตัวผมแนะนำว่า สำหรับโปรเจคใหม่ ให้เริ่มต้นจาก Build Automation และ Deployment Automation ก่อน ตั้งเป้าไว้ก่อนเลยว่าจะบิลด์อย่างไร จะดีพลอยอย่างไร แล้วค่อย ๆ เขียนโค๊ดให้อยู่ในกรอบที่ระบบจะรองรับได้ อย่าสร้างโค๊ดก่อนแล้วมาเขียน automation script ตามหลัง เสียเวลาครับ และมันจะทำให้เราไม่อยากเขียนอีกต่างหาก (ก็มันใช้ได้แล้วจะทำไปทำไม)

การตั้งเป้าหมายไว้ก่อนตั้งแต่แรกจะช่วยให้เรามีทิศทางเวลาที่เราจำเป็นจะต้องเพิ่มกลไกอะไรบ้างอย่างเข้าไปในโปรเจค สมมติว่า ซอฟต์แวร์เราต้องรองรับ client หลาย ๆ เจ้า โดยทุกเจ้ามี skin ของตัวเอง เราก็จะรู้ละว่าควรจะออกแบบระบบการโหลด resource อย่างไรให้อยู่ในกรอบที่กำหนดไว้แต่แรกว่าถึงตอน deploy แล้วจะอ่านไฟล์พวกนี้อย่างไร จะต้องทำอะไรบ้าง อะไรแบบนี้

ผมขอสรุปไว้เลยละกันนะว่า ระบบ build และ deployment automation นั้นมีส่วนสำคัญในการเพิ่มประสิทธิภาพในการพัฒนาโปรเจค โดยเป็นการทำให้ทีมงานโฟกัสอยู่กับตัวงานจริง ๆ อย่างการเขียนโค๊ด การทำรีซอรส์ การทดสอบ ไม่ใช่เรื่องเสียเวลาอย่างการบิลด์และการดีพลอย

ถ้าคุณกำลังจะเริ่มโปรเจคใหม่ ลองเริ่มจากการหา build automation ที่เหมาะสมกับโปรเจคมาลองก่อน

ถ้าคุณมีโปรเจคแต่ยังไม่ได้ใช้ build automation ก็ลองหาทางเอามาใช้สักระบบนึงนะครับ

ระบบที่ผมใช้ (และแนะนำ)

โปรเจค ZipPicViewWx ของผม ซึ่งเขียนด้วยภาษา C++นั้นใช้ CMake ครับ ตัวนี้ไม่ได้เป็นตัวบิลด์โปรแกรม แต่เป็นตัวกลางสำหรับสร้างไฟล์ที่ใช้ในการบิลด์อีกทีหนึ่ง ตัวมันรองรับทั้ง Makefiles และ Visual Studio solution มีความสามารถสูงและยืดหยุ่น สามารถเขียนตัวสคริปท์ที่ซับซ้อนได้สบาย ๆ ของผมจะใช้กับ MSYS2 นะครับ (สำหรับผม Visual Studio มันวุ่นวายชีวิตไปหน่อยครับ เอาง่าย ๆ แบบนี้ดีกว่า)

2015-11-13 03_44_41-~_ZipPicViewWx-build64

หลังจากนั้นก็สั่ง make ไป ... เราก็จะได้ตัวแอพลิเคชันสมใจอยากละครับ (warning ข้างล่างเป็นบั๊กของ wxWidget ครับ ปรกติผมมองว่า warning เป็น error ประเภทนึงอยู่แล้ว ไม่ปล่อยให้เหลือแบบนี้ แต่ว่าอันนี้ช่วยไม่ได้จริง ๆ)

2015-11-13 03_50_16-~_ZipPicViewWx-build64

ความเท่ห์ของมันอีกอย่างคือมันมาพร้อมกับซอฟต์แวร์อีกตัวที่ช่วยเรื่อง deployment ด้วย เรียกว่า CPack คือมันจะเป็นตัวสร้างตัวติดตั้งครับ แค่เรียกใช้โปรแกรมนี้มันก็จะสร้างตัวติดตั้งให้เลย

2015-11-13 03_52_33-~_ZipPicViewWx-build64

ผมใช้ Zip file บ้าน ๆ เพราะไม่ค่อยอยากให้ติดตั้งลงไปในระบบครับ

2015-11-13 03_53_31-C__msys64_home_Wutipong_ZipPicViewWx-build64_ZipPicView-DEV.0.0-win64.zip_ZipPic

รันแล้วเป็นแบบนี้ครับ :-)

2015-11-13 04_00_33-ZipPicView

ระบบที่ผมใช้ (และแนะนำ) #2

ข้างบนเป็นโปรเจคที่ผมเขียนด้วย C++ ก็เลยใช้ CMake ครับ ส่วนตัวผมใช้อีกตัวหนึ่งสำหรับ Java นั่นคือ Gradle

Gradle เป็นระบบ Build Automation ที่ค่อนข้างสมบูรณ์ในตัวมันเอง เราสามารถที่จะสั่งให้มันคอมไพล์โค๊ด สร้าง jar file (หรือ war file ในกรณีของ Web App) แล้ว Deploy เข้าไปในระบบได้เลยในคำสั่งเดียว (เท่ห์มาก !) ตัวมันเองยืดหยุ่นมาก เพราะว่าตัวสคริปท์ที่ใช้เป็นภาษา Groovy ที่ทำงานบน JVM จึงสามารถใช้คำสั่งทุกอย่างที่ Java 2SE มีให้คุณใช้เลยทีเดียว (ทำได้ทุกอย่างเท่าที่จะนึกออก) แค่เขียน Task ไปเรียกใช้งานเท่านั้นเอง

อันนี้ผมไม่มีตัวอย่างเพราะว่ามันอยู่อีกเครื่อง แต่ถ้าให้เล่าก็คือ ไอ้โปรดักท์ข้างบน (ที่บอกว่าโดนโละแล้วน่ะครับ) ผมใช้ task เดิม ๆ ที่ gradle ให้มา อย่าง เช่น

gradle JettyRun 

ซึ่งจะรัน Web Application ที่เราดึงโค๊ดลงใน Jetty ซึ่งเป็น Servlet Container ได้เลยโดยไม่ต้องติดตั้ง Tomcat

อีกตัวอย่างหนึ่งคือ task ที่ผมเขียนเองชื่อว่า buildResource โดยหลัก ๆ เป็นการเตรียมพวก resource file (ไฟล์รูปภาพ ไฟล์ xml และอื่น ๆ ) ก็แค่เรียก

gradle buildResource

จุดแข็งอีกอย่างของ gradle คือ task dependency ก็คือว่า ผมสามารถตั้งได้ว่า task B จะใช้ output จาก task A ดังนั้นต้องรัน A ก่อน อะไรแบบนี้ อย่างกรณีของ JettyRun นั้นผมเซ็ตให้มัน depend บน buildResource พอผมสั่งปุ๊บมันจะ buildResource ก่อน อะไรแบบนี้ครับ

อีกจุดหนึ่งที่น่าสนใจก็คือ dependency management กล่าวคือเราสามารถระบุในตัว build script ได้ว่าโปรแกรมเรามี dependency อะไรบ้าง ซึ่งเราก็ระบุแค่ direct dependency อย่างเดียวก็พอ พวก transitive ไม่ต้อง gradle จะไปดึงเอา library ที่ใช้จาก repository อย่าง maven central มาให้เอง พร้อมทั้ง transitive dependency ทั้งหมดที่จำเป็นต้องใช้ สมมติอย่างของผมใช้ slf4j ก็ระบุไปว่าใช้ตัวนี้นะ gradle จะไปดึง slf4j พร้อม dependency อื่น ๆ อย่าง log4j มาให้ด้วยนั่นเอง อะไรแบบนี้

อ้อ แล้วก็ ถ้าเกิดเรามี Tomcat หรือ Wildfire เราสามารถรัน task เพื่อที่จะ deploy ขึ้นไปได้เลยเหมือนกัน

เรียกได้ว่าสะดวกสุด ๆ ทำได้แทบทุกอย่างจากปลายนิ้วเลยทีเดียว

ที่สำคัญคือ task dependency นั้นทำให้เราไม่จำเป็นจะต้องรันที่ละ task ตามลำดับ ระบบจะเป็นคนจัดการตรงนี้เอง สะดวกสุด ๆ ครับ

แต่ gradle เองก็มีข้อจำกัดเหมือนกันครับ คือถ้าเรามี architecture ของระบบที่ซับซ้อนมาก ๆ อย่าง web app เราต้องใช้ database server หลาย ๆ ตัว (อย่าเพิ่งขำครับผมเพิ่งเจอกับตัว) และใช้แชร์กันไม่ได้ เพราะดันเก็บคอนฟิก เนี่ย gradle เองก็อาจจะไม่ตอบโจทย์ซะทีเดียว

เราอาจจะต้องใช้ application container อย่าง Docker ช่วย ผมเองกำลังศึกษาอยู่ครับ ไว้จะมาเล่าให้ฟังวันหลังนะ

นับเป็นโอกาสที่ดีที่ที่ทำงานผมจะมีโปรดักท์ใหม่เสียที แต่ไอ้ที่ไม่ดีคือมันเซ็ตอัพได้โคตรยาก 555 ผมล่ะอยากบินไปทะเลาะกับ architect ชะมัด เพราะว่ากะอีแค่จะให้มันรันโปรดักท์ตัวนี้ได้ต้องใช้คู่มือขนาด 50 หน้า ... เซ็ตกันตาเหลือกเลยครับ

เอาเถอะ ...

เท่าที่ลองวิเคราะห์ดู ตัวโปรดักท์ตัวนี้ตัวมันเองมีสองส่วน ส่วนหนึ่งคือชุดของ HTML Single Page Application เขียนด้วย Angular.JS กับเฟรมเวิร์คอีกหลายตัว ตรงนี้เซ็ตไม่ยากมีแค่ web server ตัวเดียวเอาไฟล์ไปวางไว้ก็ทำงานได้ละ

จุดที่ยากคือส่วนของ Middle Tier ที่เป็น Web Application แบ่งเป็นส่วนที่เก็บไอ้ส่วนแรกนั่นแหละ กับส่วนที่เป็นเรื่องของ service ต่าง ๆ ตรงนี้มันพึ่งพากับบริการสองส่วน ก็คือ JBoss EAP กับ MySQL (อันหลังเนี่ยอยากด่าสุด ๆ) ซึ่งบริการสองตัวนี้ก็ต้อง config ไม่งั้นมันก็ทำงานไม่ได้

มาถึงตรงนี้เรามีสี่ส่วนที่ต้องดูแล โชคดีว่าไอ้ Single Page นั่นเป็นแค่ static html ธรรมดาเลยไม่ต้องสนใจ ไอ้ที่เหลือเนี่ยสิงานช้าง ...

ตัวโปรดักท์เก่าเป็น J2EE Web App ธรรมดา ดังนั้นที่ต้องทำคือทำให้มันคอมไพล์ผ่าน ตัว application server ไม่จำเป็นต้องมีการคอนฟิกอะไรมากนัก ผมเคยเสนอ build automation ที่ชื่อว่า Gradle พร้อมเขียนสคริปท์ไปให้เรียบร้อย (ใช้วันหยุดไปตั้งหลายวัน เซ็งเป็ด) แต่ตัวใหม่เนี่ยแค่ Build Automation ไม่พอแล้ว เพราะว่าตัว application server ก็ต้องมีการ setup วุ่นวายมากมายตาแป๊ะไก่ นี่ยังไม่ได้พูดถึง MySQL เลยนะ

วันนี้เลยจะจดว่า แค่ Gradle มันไม่พอแล้ว เพราะว่าตัว JBoss eap ไม่มีวิธีที่จะ deploy module เข้าไปผ่านทางคำสั่ง ต้องเอาไฟล์ไปวางเอง ซึ่งตรงนี้เขียนเป็นสคริปท์ก็ลำบากเพราะว่าแต่ละเครื่องก็วางกันคนละที่ ส่วน MySQL นั้นยิ่งแล้วใหญ่ เพราะว่าถ้าใช้วิธีมี DB Server ในเครื่องแล้วโหลดเข้าโหลดออก (อย่าลืมว่าผมต้องทำงาน 3 release พร้อม ๆ กันนะครับ) แล้วมันอลวนสุด ๆ

ทางออกที่นึกออกตอนนี้คือ ใช้ Docker ซะเลย Docker เป็น Application Container มันจะมีซอฟต์แวร์ทุกอย่างที่จำเป็น เราสามารถสร้าง image ที่คอนฟิกทุกอย่างเอาไว้เรียบร้อยแล้ว แค่โหลดลงมาแล้วเรียกใช้ตรง ๆ ไม่ต้องคอนฟิกอะไรอีก ผมว่าวิธีนี้เวิร์คสุดในการคอนฟิกตัว service สองตัวที่จำเป็นต้องใช้

ส่วน Application Code นั้นเราก็สร้าง Gradle script ขึ้นมาใช้แบบเดิม น่าจะดีกว่า ...

ข้อเสียเหรอ ? มันต้องลงโปรแกรมเพิ่ม และ Docker ใช้งานได้เฉพาะบน Linux ถ้าเป็นแพลตฟอร์มอื่นต้องลง Virtual Machine ซึ่งก็มีเครื่องมือจัดการอยู่แล้วไม่น่ามีปัญหาอะไร

ปัญหาใหญ่ที่สุดคือทำอย่างไรให้คนอื่นฟังความเห็นเรานี่ล่ะ โปรดักท์เก่าเคยเสนอไปแล้วก็โดนตีกลับมาอย่างไม่มีความเห็น คราวนี้ manager product ตัวนี้มาไทยคงได้คุยกันบ้าง

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

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

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

ด้วยปัญหาหลาย ๆ อย่างของการจัดรูปแบบด้วยมือก็เลยมีคนทำโปรแกรมที่คอยจัดฟอร์แมทของไฟล์ให้ โปรแกรมประเภทนี้บางคนก็จะเรียกว่า code format หรือบางคนก็จะเรียกว่า code beautifier ซึ่งใน IDE แพง ๆ ส่วนใหญ่ก็จะมีฟีเจอร์นี้อยู่ในตัวครับ

วันนี้จะแนะนำโปรแกรมตัวนึง ชื่อว่า clang-format ซึ่งเจ้านี่เนี่ยเป็นทูลที่ติดมากับชุดคอมไพล์เลอร์ clang ถ้าใครสนใจอยากลองก็หามาติดตั้งได้ครับ เป็นคอมไพล์เลอร์ฟรี แต่บนวินโดวส์อาจจะติดตั้งลำบากนิดนึง เอาไว้วันหลังจะมาแนะนำวิธีง่าย ๆ ให้ฟัง

สมมติว่าผมมีโค๊ดนี้นะครับ

        #include <iostream>
    using namespace std;

    struct Point{ int x, int y};
enum class Direction {Left, Right, Forward, Backward};

void PrintDirection(const Direction &dir){
  switch(dir){
    case Direction.Left: cout<<"Turn Left"<<endl; break;
    case Direction.Right: cout<<"Turn Right"<<endl; break;
    case Direction.Forward: cout<<"Move Forward"<<endl; break;
    case Direction.Barkward: cout<<"Move Backward"<<endl; break;
    default: throw;
  }
}

int main(int, char **) 
{
    PrintDirection(Direction.Left);

    Point p{50,100};

  for(int i = 0; i< 100; i++) {p.x ++; p.y--;}

  return 0;
}

ดูเละ ๆ แบบนี้แหละ 555 ผมสามารถใช้คำสั่ง clang test.pp เพื่อให้มันฟอร์แมทโค๊ดสวย ๆ โดยเมื่อเราสั่งมันจะพิมพ์ออกมาบนคอนโซลครับ

#include <iostream>
using namespace std;

struct Point {
  int x, int y
};
enum class Direction { Left, Right, Forward, Backward };

void PrintDirection(const Direction &dir) {
  switch (dir) {
  case Direction.Left:
    cout << "Turn Left" << endl;
    break;
  case Direction.Right:
    cout << "Turn Right" << endl;
    break;
  case Direction.Forward:
    cout << "Move Forward" << endl;
    break;
  case Direction.Barkward:
    cout << "Move Backward" << endl;
    break;
  default:
    throw;
  }
}

int main(int, char **) {
  PrintDirection(Direction.Left);

  Point p{50, 100};

  for (int i = 0; i < 100; i++) {
    p.x++;
    p.y--;
  }

  return 0;
}

ดูดีขึ้นไหมครับ ทั้งนี้เราสามารถเพิ่มพารามิเตอร์ -i เข้าไปเพื่อให้มันบันทึกลงไปในไฟล์เลยได้ด้วยครับ

ทีนี้ฟอร์แมทของโค๊ดที่ได้เนี่ยมันจะเป็นตามมาตรฐานของโครงการ llvm แต่เราสามารถเลือกที่จะใช้มาตรฐานอื่น ๆ ได้ (ตัวโปรแกรมมีมา 5 รูปแบบครับ) และเราสามารถสร้างรูปแบบของเราเองได้ด้วยอีกเช่นกัน ตัวอย่างของล่างนี้เป็นฟอร์แมทของ WebKit นะครับ

#include <iostream>
using namespace std;

struct Point {
    int x, int y
};
enum class Direction { Left,
    Right,
    Forward,
    Backward };

void PrintDirection(const Direction& dir)
{
    switch (dir) {
    case Direction.Left:
        cout << "Turn Left" << endl;
        break;
    case Direction.Right:
        cout << "Turn Right" << endl;
        break;
    case Direction.Forward:
        cout << "Move Forward" << endl;
        break;
    case Direction.Barkward:
        cout << "Move Backward" << endl;
        break;
    default:
        throw;
    }
}

int main(int, char**)
{
    PrintDirection(Direction.Left);

    Point p{ 50, 100 };

    for (int i = 0; i < 100; i++) {
        p.x++;
        p.y--;
    }

    return 0;
}

ผมเคยดูวิดีโองาน CppCon (ถ้าจำไม่ผิดนะครับ) ซึ่งเซสชั่นนึงมีผู้จัดการของโครงการ LLVM เป็นผู้บรรยาย เขาเล่าให้ฟังว่าตัวโค๊ดของ LLVM เนี่ยจะถูกฟอร์แมทด้วยทูลตัวนี้ก่อนที่จะถูกคอมมิทเข้าไป (ผมเดาว่าเป็น Trigger ครับ) ดังนั้นทุกไฟล์จะมีลักษณะเดียวกันหมด มีความคงเส้นคงวา และถึงอาจจะไม่ได้สวยงามเหมือนโค๊ดที่ทำด้วยมือ แต่ว่าก็ดูแลรักษาง่ายกว่าและมีอัตราการผิดพลาดน้อยกว่าครับ

ส่วนตัวผมก็สนับสนุนให้มีการตั้ง commit trigger ให้ฟอร์แมทโค๊ดก่อนที่จะบันทึกทุกครั้งเหมือนกัน แต่ส่วนตัวยังไม่ได้ทำครับ ตอนนี้ใช้วิธีใช้ Text Editor (Atom) ไปเรียก clang-format ก่อนบันทึกไฟล์แทน ก็พอทดแทนกันได้ในระดับหนึ่งครับ

ทั้งนี้เรื่องหนึ่งที่ต้องบอกคือเจ้า clang-format เนี่ยมันไม่ใช่คอมไพล์เลอร์ มันไม่จับโค๊ดที่ผิดนะครับ อย่างโค๊ดข้างบนเองเอาจริง ๆ ก็คอมไพล์ไม่ผ่านนะขอบอก

เคยพูดไปเมื่อก่อนว่า โค๊ดที่ถือว่าเป็นโค๊ดมรดก หรือ Legacy Code นั้น ว่ากันว่าเป็นโค๊ดที่ไม่พึงประสงค์ เหมือนเป็นภาระที่สืบทอดกันมาเป็นรุ่น ๆ ในองค์กร ซึ่งเกิดจากการที่โค๊ดนั้นมันไม่มี Unit Test

ทีนี้ ก็ในเมื่อเรารู้อยู่แล้วว่าโค๊ดมันไม่มี Unit Test เลยมีแต่คนรังเกียจ แล้วเราจะเพิ่ม Unit Test เข้าไปในโค๊ดเดิมเพื่อให้มันไม่เป็น Legacy Code เลยไม่ได้เหรอ คำตอบก็คือทำได้ครับ และก็ควรทำด้วย เพียงแต่ว่าการเพิ่ม Unit Test เข้าไปในโค๊ดเดิมนั้นมันมีค่าใช้จ่ายสูง ใช้เวลามาก และต้องมีการแก้ไขโค๊ดเดิมบางส่วน (หรืออาจจะเป็นส่วนมาก)

ที่ว่าค่าใช้จ่ายสูง ใช้เวลามากนั้น น่าจะพอนึกภาพออก ยิ่งโค๊ดมีขนาดใหญ่มากก็ต้องเขียน Unit Test มากก็ไม่แปลกถูกไหมครับ

แต่ว่าโค๊ดเดิมต้องแก้ด้วยหรือ ?

คำตอบคือ แล้วแต่ว่าโค๊ดเดิมครับ แต่โดยทั่ว ๆ ไปมันก็มักจะมีโค๊ดที่เทสต์ได้ยากถ้าไม่มีการแก้ไขเลย

สมมติว่าผมมีคลาส Java ชื่อว่า Logger ละกัน มันไม่ได้ทำอะไรมากมาย แค่ว่าเขียน log ลงไปในไฟล์เท่านั้นเอง ... แบบนี้ (เขียนไม่เต็มนะครับ ขี้เกียจ 55)

class Logger {
  private FileWriter writer;
  public Logger(File file) {
    writer = new FileWriter(file);
  }
  public void log(String message) {
    Date date = new Date();
    writer.write(String.format("%D : %s"), date, message);
  }
}

ถ้าเราจะเขียนเนี่ยมันก็ค่อนข้างเป็นงานช้าง เพราะว่าสิ่งที่มันทำคือมันเขียนลงไฟล์ตรง ๆ เลย ... ตัว Unit Test ต้องไปอ่านไฟล์มันกลับขึ้นมาแล้วดูว่ามันเขียนถูกหรือเปล่า ... แล้วพอยิ่งพ่วงวันที่เข้าไปอีกก็เริ่มยากละ เพราะว่าเวลามันจะเปลี่ยนไปเรื่อย ๆ ตามวันที่เทสต์นั่นเอง

เราก็ต้อง refactor code โค๊ดให้มันมีการ "ยึดติด" น้อยลง แบบนี้ครับ

class Logger {
  private Writer writer;
  public Logger(Writer writer) {
    this.writer = writer;
  }
  public Logger(File file) {
    this.writer = new FileWriter(file);
  }

  public void log(Date date, String message) {
    writer.write(String.format("%D : %s"), date, message);
  }

  public void log(String message) {
    log(new Date(), message);
  }
}

ผมใช้วิธีสองอย่างในการทำให้ "การยึดติด" ลดลง คือการใช้ polymorphism - ใช้คลาส Writer แทนที่จะเป็น FileWriter และการใช้ parameter passing จากข้างนอกแทนที่จะเป็นการสร้างใหม่จากภายในคลาส (และมีอีก method นึงทำหน้าที่เป็นเหมือนกับการกำหนด default parameter) วิธีนี้ทำให้ผมไม่จำเป็นจะต้องอ่านไฟล์หลังจากที่เขียนลงไปแล้ว เพราะว่าผมสามารถใช้ StringWriter แทน FileWriter ได้นั่นเอง นอกจากนี้วิธีนี้ทำให้โค๊ดที่ใช้งานคลาสนี้สามารถใช้งานได้เหมือนเดิม เพราะว่า interface เดิมยังอยู่ครบเลยครับ

ส่วนตัว Unit Test นั้นก็สามารถเขียนแบบนี้ :-

class LoggerTest {
  @Test
  void test(){
    Stringwriter writer = new StringWriter();
    Logger logger = new Loger(writer);
    Date date = new GregorianCalendar(2000, 3, 15).getTime();

    logger.log(date, "test message");
    writer.close();
    assertEquals(writer.toString(), "03/15/2000 : test message");
  }
}

ยาวกว่าที่คิดแฮะ 555 setup ยาวครับ แต่จริง ๆ ก็ไม่ได้ซับซ้อนอะไร เทียบกับการไปอ่านไฟล์กลับขึ้นมาแล้วลำบากกว่าเยอะ แถมต้องมา parse ว่าวันที่ที่ log ไปมันวันที่เท่าไหร่อีก ... ดูลำบากชีวิตว่ามั้ยครับ

ตรงนี้แหละครับที่เป็นการวัดว่า ความสามารถในการถูกทดสอบ หรือ Testability นั้นมีมากน้อยแค่ไหน

ทีนี้คงมีคำถามเพิ่มเข้ามาว่า method ที่เราเทสต์กับ method ที่เราใช้จริงนั้นมันเป็นคนละ method กัน ซึ่งก็จริงครับ แต่ตราบใดที่ method ที่ใช้งานจริงนั้นมีความเรียบง่ายพอ ผมว่าก็สามารถปล่อยมันไว้แบบนั้นได้โดยที่ไม่มีปัญหาอะไร โดยเฉพาะอย่างยิ่งถ้าเป็น method ที่แค่สร้างค่า default เข้ามาแบบนี้ก็ไม่ต้องต้องไปสนใจมันมากก็ได้ครับ

จุดที่ต้องระวังอีกอย่างคือเปิดให้เข้าถึง method นั้นจะทำให้ user เข้ามาใช้ method นั้น ๆ โดยที่เราไม่ได้ตั้งใจให้เขาใช้ เพราะว่าต้องเปิดให้เข้าถึงจากภายนอก เราก็ต้องทำการจำกัดการเข้าถึงโดยไม่อนุญาตให้คลาสอื่นนอกจากคลาสที่เป็น unit test เข้าถึงได้นั่นเอง ซึ่งก็เป็นต้องดูเป็นรายภาษาไป อย่างของ C++ ก็อาจจะใช้ friend class ช่วย ส่วน Java ก็อาจจะเป็นการใช้ default access modifier ที่ไม่อนุญาตให้คลาสอื่น ๆ นอกจากคลาสใน package เดียวกันเข้าถึงได้ครับ

หรือในกรณีที่แย่ที่สุดก็ comment ไปเลยว่า user ไม่ควรใช้ method นี้ตรง ๆ นะ (ถ้าใช้แล้วเกิดอะไรขึ้นไม่รับผิดชอบ) ก็ยังได้ครับผม

ปล. โค๊ดที่เขียนมานี่เขียนสดหมดเลย ไม่ได้เทสต์แม้แต่บรรทัดเดียว Exception ก็ไม่จับ ... ดังนั้นเอาไปใช้จริงไม่ได้นะครับ เอาไว้ดูเป็นไอเดียละกัน

จั่วหัวภาษาไทยให้งงเล่นอีกแล้ว วันนี้จะพูดถึงเรื่องของ Business Logic นะครับ

ความหมายของคำว่า Business Logic ก็คือ บรรดาลอจิกทั้งหลายที่เกี่ยวข้องกับการทำธุรกรรมต่าง ๆ ในระบบของเรา โดยคำนี้เรามักจะใช้กับโปรแกรมที่สร้างขึ้นมาสำหรับงานด้านธุรกิจต่าง ๆ เช่น พวกระบบจัดเก็บข้อมูลของธนาคาร ที่ใช้กันตามแบงค์นั่นล่ะครับ Business Logic นั้นก็จะรวมตั้งแต่ การตรวจสอบข้อมูล (Validation) การประมวลข้อมูลเพื่อประกอบการตัดสินใจ การสร้างค่าปริยาย (Default) และอื่น ๆ ที่เกี่ยวข้อง ทั้งนี้จะไม่เกี่ยวกับการแสดงผลข้อมูล และการจัดเก็บข้อมูลครับ

ถ้าเป็นใน Model-View-Controller (MVC) ส่วนของ Business Logic จะเป็นส่วนของ Controller ครับ

ในส่วนของ Business Logic เท่าที่ผมพอจะรู้นะ ในปัจจุบันจะมีส่วนประกอบหลัก ๆ ก็คือ Business Object กับ Business Process โดย Business Object ก็คือวัตถุที่เกี่ยวข้องกับการทำธุรกรรมต่าง ๆ เช่นวัตถุที่แทนข้อมูลบัญชีลูกค้า วัตถุที่เก็บข้อมูลการซื้อขาย เป็นต้น ส่วน Business Process นั้นหมายถึงการทำธุรกรรมต่าง ๆ เช่นการซื้อขาย การเรียกดูข้อมูลย้อนหลัง การเรียกดูและเปลี่ยนแปลงข้อมูลที่อยู่ลูกค้า เป็นต้น

คงมีคนสงสัย อ้าวไหนบอกว่ามันเป็นส่วนของ Controller ไง ไหง BO ถึงเป็นวัตถุเรียกเก็บข้อมูลขึ้นมาล่ะ คืองี้ครับ เจ้า Business Object เนี่ยมันจะมีส่วนของการตรวจสอบข้อมูล การประมวลผลข้อมูลประกอบการตัดสินใจ การสร้างค่าปริยาย อะไรทำนองนี้เอาไว้ด้วย เช่น สมมติว่าผมมี BO ชื่อว่า Entity

class Entity extends BusinessObject {
  final String accountNumber;
  String name;
  String lastname;
  int age;
  Date dateOfBirth;
  //..some other related fields
}

เวลาที่ผมเปลี่ยนค่าใน dateOfBirth จะต้องมีการเปลี่ยนค่าของ age ด้วย ถูกไหมครับ ?

void setDateOfBirth(Date date) {
  dateOfBirth = date;
  age = calculateAge(dateOfBirth);
}

ถ้าเรายกกระบวนการตรงนี้ไปไว้ใน Business Process เนี่ยจะเกิดการซ้ำซ้อน (redundant) เกิดขึ้นได้

ทั้งนี้เราอาจจะใช้การคำนวนอายุใหม่ทุกครั้งที่มีการเข้าถึงแทน อันนี้ก็แล้วแต่ความเหมาะสมครับ

จะว่าไปผมควรจะเขียนตัวอย่างของ Business Process ให้ดูสักหน่อยเนอะ ? สมมติว่าผมมี BP สำหรับการเรียกดูและปรับเปลี่ยนที่อยู่ของผู้ใช้นะครับ

class AddressMaintenanceProcess extends BusinessProcess {
  private final Entity entity;
  private ArrayList<Address> addressList;
  private int index = 0;
  AddressMaintenanceProcess(String entityId) throws DataNotFoundException {
    entity = Entity.searchForId(entityId);
    addressList = copyList(entity.getAddressList());
  }

  public String name { return entity.getName();}
  public String lastName { return entity.getLastName();}
  public String[] getAddressText { return addressList[currentIndex].getAddressText();}
  public String[] getValidPostalCodes { return addressList[currentIndex].getValidPostalCodes(); }

  public void setIndex(int index) { this.index = index;}
  public void getIndex() { return index }

  public void commitChanges() { 
    entity.setAddressList(addressList);
    entity.commitChanges(); 
  }
}

อะไรทำนองนี้

ในความเป็นจริง โค๊ดส่วนของ Business Logic นั้นจะถูกเขียนเพื่อให้ไม่เกิดการยึดติด (Code Cohesion) กับโค๊ดส่วนอื่น ๆ (เช่นส่วนของ UI) มากนัก เพราะเราอาจจะมีการยกเอา BL ไปใช้กับแอพลิเคชั่นหลาย ๆ ตัวที่ใช้ส่วนประกอบอื่น ๆ ที่แตกต่างกัน ซึ่งตรงนี้สามารถที่จะทำเป็นลักษณะของ Web Service ไปก็ได้ (ใช้โปรโตคอล HTTP ในการคุยกันระหว่างส่วนประกอบต่างๆ) หรือจะยังทำเป็นโค๊ดโปรแกรมธรรมดา แต่มีการออกแบบคลาสให้มีชุดฟังก์ชั่นมาตรฐานในแต่ละคลาสเพื่อให้สามารถแยกส่วนไปใช้กับโปรแกรมอื่นได้ง่ายก็ได้ครับ (อันนี้เป็นเรื่องของการออกแบบซึ่งก็ต้องศึกษาข้อดีข้อเสียกันเองนะ)

ตัวที่ผมทำงานด้วยประจำจะเป็นแบบการสร้างฟังก์ชั่นมาตรฐานที่ใช้การรับส่งค่าผ่าน key-value น่ะครับครับ (ผมเรียก key ว่า fieldId นะ) คือก็ประมาณว่าแต่ละ process และ business object จะมีมีฟังก์ชั่น getField กับ setField เพื่อที่จะใช้ในการรับส่งค่าระหว่างกัน อะไรทำนองนี้ครับ

ก็ขอจบเรื่องของ Business Logic เอาไว้แค่นี้ละกัน มีอะไรสงสัยก็ถามได้ครับผม

ไปตอบคำถามในฟอรัมหนึ่ง เรื่องของ Unit Test เขาถามว่ามันคืออะไร ผมก็อธิบายไปประมาณนึง เห็นว่าน่าสนใจก็เลยเอามาแชร์ให้ฟังพร้อมเพิ่มเนื้อหา

Unit test คือการทดสอบตัว Code Unit โดยทั่วไปอาจจะหมายถึง ฟังก์ชั่น หรือเมธอด การทดสอบนั้นมีเพื่อเอาไว้ยืนยันว่าแต่ละยูนิทนั้นทำงานได้ถูกต้อง

การทำ Unit Test โดยทั่วไปจะทำมือก็ได้ เช่นการเรียกฟังก์ชั่นด้วยอินพุตที่แตกต่างกัน แล้วพิมพ์ผลลัพท์ในคอนโซลดูว่าผลลัพท์ที่ได้นั้นเป็นไปอย่างที่เราคาดหวังไว้หรือไม่ แต่วิธีนี้ทั้งเหนื่อยและช้า เขาก็เลยแนะนำให้เขียนเป็นโปรแกรมสำหรับทดสอบโดยเฉพาะ ซึ่งโปรแกรมนี้ก็เรียกว่า Unit Test เช่นกัน

สมมติ เช่น ผมมีฟังก์ชั่นที่คืนค่าสัมบูรณ์ของ input ใด ๆ

int absolute(const int& a);

ผมสามารถเขียนโปรแกรมเพื่อทดสอบฟังก์ชั่นนี้ได้ดังนี้

void test_absolute() {
  assert(absolute(10) == 10);
  assert(absolute(-5) == 5);
  assert(absolute(0) == 0);
}

ถ้าโปรแกรมเราทำงานไม่ถูกต้องก็จะพัง ถูกไหมครับ พอเรารู้ว่ามันพังเราก็จะรู้ว่า อ๋อ ฟังก์ชั่นนี้ทำงานผิดนะ เราก็ต้องแก้โปรแกรม ซึ่งเราจะแก้แล้วรันใหม่กี่ครั้งก็ได้ตามแต่ที่ต้องการ

ในปัจจุบันการทำ Unit Test จะอาศัย Unit Testing Framework ในการสร้างโปรแกรมขึ้นมาเป็นชุดการทดสอบ (Unit Test Suite) โดยตัวที่ได้รับความนิยมก็คือ jUnit ของภาษา Java ซึ่งก็แตกยอดออกมากลายเป็นตระกูล xUnit ในหลาย ๆ ภาษาครับ

หน้าตาของ Unit Test ที่เขียนด้วย jUnit ก็ไม่ต่างอะไรกับตัวโปรแกรมทดสอบของผมข้างบนสักเท่าไหร่ เช่น ผมอาจจะมีเมธอดสำหรับตรวจสอบที่อยู่อีเมล์

Utility.validateEmailAddress(String address)

ก็สามารถเขียนตัว Unit Test ได้แบบนี้

class ValidateEmailAddressTest {
    @Test
    void testValidateEmailAddress() {
        Utility utility = new Utility();
        assertTrue("mr_tawan@hotmail.com is valid", 
                   utility.validateEmailAddress("mr_tawan@hotmail.com"));
        assertFalse("mr_tawan is invalid",
                   utility.validateEmailAddress("mr_tawan"));
    }
}

เมื่อตัว unit test นี้ถูกเรียกใช้งาน ถ้าหากว่ามีเทสต์เคสไหนที่ไม่ผ่าน ตัวฟังก์ชั่น assert* จะโยน exception ออกมาเพื่อให้รู้ว่ามันมีปัญหาครับ

การใช้ framework มีข้อดีที่ว่ามี output ที่แน่นอน ทำให้สามารถใช้เครื่องมือในการอ่าน output ออกมาเพื่อแสดงผลเป็นรายงานได้ด้วย (เช่น IDE แทบทุกเจ้ารองรับ jUnit หมด)

ข้อดีของ framework อีกอย่างคือการรองรับการทดสอบที่หลากหลายกว่า เช่นเราอาจจะกำหนดช่วงของผลลัพท์ที่เราคาดหวังเอาไว้และให้รายงานเฉพาะเวลาที่ผลลัพท์ที่ได้อยู่นอกเหนือช่วงดังกล่าว ทำให้โปรแกรมสามารถเขียนเทสต์ได้ง่ายกว่า (ถ้าใช้ฟังก์ชัน assert() เฉย ๆ แบบกรณีข้างบนนั้นโปรแกรมเมอร์จะต้องเป็นคนกำหนดวิธีการเช็คเอง)

ในหลาย ๆ โปรเจคจะมีกำหนดเอาไว้ว่า ทุก ๆ วันหลังจากที่ทุกคนกลับบ้าน จะมี server ตัวนึงที่ดึงโค๊ดออกมาจาก repository เพื่อที่จะรัน Unit Test Suit ที่เก็บเอาไว้กับโค๊ดนั้น ๆ และจะมีรายงานออกมาว่าโปรแกรมมีปัญหาหรือไม่ ในอีกหลาย ๆ โปรเจคนั้เนตัว Unit Test จะทำงานทันทีหลังเช็คอินอีกต่างหาก นี่คือสิ่งที่การทำ Unit Test ด้วยมือไม่สามารถทำได้แน่นอน