About Wutipong Wongsakuldej

Programmer, interested in frontend applications, music and multimedia.

วันนี้จะมาเล่าเรื่องของคลาสชุด "วันและเวลา" บน 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 ไม่ใช้โค๊ดเรา แล้วก็ไปเขียนฟังก์ชันใหม่แทน กลายเป็นว่าเกิดโค๊ดที่ซ้ำซ้อนโดยไม่จำเป็นครับ

อันนี้เหมือนจะเป็นตอนต่อจาก entry ที่เขียนไว้เมื่อเกือบ ๆ 7 ปีที่แล้วครับ

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

แต่ก่อนหน้านั้นขออธิบายเรื่องการเล่นไฟล์เสียงแบบ streaming ก่อน

การเล่นเสียงแบบ Streaming

โดยทั่วไปเวลาเราจะเล่นไฟล์เสียง เราจะโหลดข้อมูลทั้งไฟล์ขึ้นไปในบัฟเฟอร์ ในรูปแบบที่ตัว hardware หรือ api สามารถเล่นได้ (มักจะเป็น PCM - Pulse-Code Modulatation) ซึ่งปัญหาคือถ้าไฟล์เสียงเรายาวมาก ๆ มันจะใช้พื้นที่ในหน่วยความจำค่อนมาก โดยทั่วไปก็ราว ๆ 60MB ต่อนาที (คิดที่ PCM 44.1KHz 16bit Stereo ครับ) การโหลดข้อมูลใหญ่ ๆ เข้าในเมโมรีทีเดียวทั้งหมดอาจจะเยอะเกินไป อาจจะทำให้โปรแกรมไม่ทำงานได้

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

พักตรงนี้ก่อน โค๊ดตัวอย่างนี้ใช้ไฟล์จาก Persona 4 OST แต่ผมคงไม่สามารถแปะเพลงนี้ได้ ถ้าสนใจอยากลองฟัง ก็ลองดูคลิปนี้ละกันครับ (คนละเวอร์ชันกันครับ)

ปัญหาเรื่องการเล่นไฟล์เพลงให้ลูปใน SDL_mixer

SDL_Mixer จริง ๆ มีคำสั่งที่ใช้เล่นไฟล์เพลงอยู่ครับ และเราสามารถสั่งให้มันลูปได้ด้วย โค๊ดก็ประมาณนี้

auto music = Mix_LoadMUS("./songs/2.04. Heartbeat, Heartbreak.ogg");
Mix_PlayMusic(music, -1);

ซึ่งสิ่งที่มันทำมันก็เหมือนกับที่ผมเล่าไปข้างบนนั่นแหละ เราเลยไม่ต้องมากังวลเรื่องการเติมบัฟเฟอร์ การสลับบัฟเฟอร์ หรืออะไรทำนองนั้น

ปัญหาคือ มันเล่นตั้งแต่ต้นจนจบไฟล์ แล้วเริ่มที่จุดเริ่มต้นใหม่ครับ เราไม่สามารถกำหนดได้ว่าจะลูปจากตรงไหน และลูปไปตรงไหน

อ้อ SDL_mixer มีคำสั่งให้กรอเพลง (ใช้คำนี้ได้ไหม :-)) กลับไปยังตำแหน่งที่เราต้องการได้ แต่ดันไม่มีคำสั่งที่เช็คว่าเล่นไปถึงตำแหน่งไหนแล้ว ... เราก็เลยไม่สามารถใช้วิธีการเช็คตำแหน่งแล้วย้ายตำแหน่งได้จาก SDL_mixer ตรง ๆ ตรงนี้มีคนด่าเยอะเหมือนกันครับ แต่ว่าก็ไม่มีใครแก้ (ไม่รู้ทำไม ไม่ได้เช็ค)

ทางแก้ สำหรับคนที่ยังอยากให้ SDL_mixer

บอกก่อนว่าถ้าไม่อยากใช้ SDL_mixer ก็ทำได้ครับ แต่นั่นคือต้องเขียน mixer เอง เหนื่อยน่าดู อีกวิธีคือการเปลี่ยนไปใช้ API อื่นอย่าง OpenAL เลย (ผมเคยเล่าให้ฟังเมื่อ 7 ปีที่แล้วล่ะมั้ง ลืมละ)

โชคดีอย่าง SDL_mixer มีกลไกที่อนุญาตให้เราเติมบัฟเฟอร์ได้เอง ซึ่งเราจะไปถอดรหัสด้วยวิธีไหนมาจากอะไรก็ได้ แล้วแต่เราเลย ตรงนี้จะเป็นการใช้คำสั่ง Mix_HookMusic() แทนนั่นเอง ปัญหาคือใช้แล้วใช้คำสั่ง Play/Stop ไม่ได้นะครับ เรียกปุ๊บมันเล่นเลย เราต้องเขียนกลไกสั่งให้มันหยุดเอง แต่คำสั่ง Pause/Resume ยังใช้ได้นะ อันนี้ก็ต้องลองกันเอาเองดูนะครับ

ข้างล่างเป็นโค๊ดตัวอย่างครับ

OggVorbis_File vf;
ov_fopen("./songs/2.04. Heartbeat, Heartbreak.ogg", &vf);

Mix_HookMusic([](void *udata, Uint8 *stream, int len) {
  OggVorbis_File *pvf = (OggVorbis_File *) udata;
  const long int start_loop = 447151L;
  const long int end_loop = 3410688L;
  int bitstream;
  int read_total = 0;
  while (true) {
    if (read_total >= len) break;
    auto pos = ov_pcm_tell(pvf);

    if (pos > end_loop)
      ov_pcm_seek(pvf, start_loop);

    int read = ov_read(pvf, (char *) stream + read_total,
                       len - read_total, 0, 2, 1, &bitstream);
    read_total += read;
  }

}, &vf);

จะเห็นว่าผมมี lambda ตัวหนึ่งในพารามิเตอร์ของ Mix_HookMusic() ฟังก์ชันนี้เป็น callback โดยทุกครั้งที่ตัว SDL_Mixer ต้องการข้อมูล มันจะเรียกฟังก์ชันนี้โดยให้บัฟเฟอร์เรามา เรามีหน้าที่ต้องเติมให้เต็มบัฟเฟอร์ครับ (อันนี้ต่างกับบาง API ที่เราสามารถเติมให้ไม่เต็มได้ แค่บอกว่าเราเติมไปเท่าไหร่) ส่วนเรื่องที่ว่ามันจะเอาบัฟเฟอร์ไปทำอะไรเป็นหน้าที่ของ SDL_Mixer เราไม่ต้องยุ่งเลย

ผมใช้ตำแหน่งแบบ pcm ซึ่งจะนับจำนวน sample จากจุดเริ่มต้นจนถึงตำแหน่งที่เรากำหนดไว้ ซึ่งจะแม่นยำกว่าการใช้ตำแหน่งแบบเวลา (เช่น วินาที) แต่การจะได้ตำแหน่งนี้มานั้นคงต้องคุยกับ sound design ดี ๆ ล่ะครับ เราสามารถหาตำแหน่งของลูปได้จากโปรแกรมแก้ไขไฟล์เสียง แล้วก็โปรแกรมพวก DAW ครับ (ไฟล์นี้ผมใช้ Sound Forge Audio Studio 10 ที่แถมมากับแลปท็อปผมครับ ... น่าเสียดายที่ Sony ขาย Vaio ไปแล้วจริง ๆ )

2016-01-12 05_13_34-Greenshot

วิธีนี้จะใช้วิธีการเช็คตำแหน่งที่เราอยู่ในปัจจุบันก่อนที่จะสั่ง decode โดยถ้าเราเล่นเกินตำแหน่งลูปไปแล้ว ก็ให้กรอกลับไปยังตำแหน่งเริ่มต้นใหม่ วิธีนี้ยังค่อนข้างหยาบอยู่ เพราะว่าเราเล่น "เกิน" กว่าตำแหน่งลูปก่อนจะวนกลับไป (ตรงนี้จำเป็นเพราะว่าเราถอดรหัสไฟล์ออกมาเป็นข้อมูลก้อนใหญ่ ๆ ก้อนหนึ่งครับ ไม่ใช่ว่าเราโหลดราย sample เลย ดังนั้นมันจะโหลดมาเกินกว่าที่เราตั้งไว้) เป็นสาเหตุว่าทำไมในหลาย ๆ lib ถึงไม่การันตีว่ามันจะลูปที่ตำแหน่งที่เราตั้งไว้ด้วยครับ เพราะมันถอดมาเกินนี่ล่ะ แต่เท่าที่ผมลองฟังดูผมไม่รู้สึกว่ามันมีรอยต่อครับ อาจจะเพราะว่าเลือกจุด loop point ได้ดีด้วย (เป็นจุดที่ amplitude ค่อนข้างต่ำครับ)

อ้อ ถ้าเกิดว่ามันลูปมาไม่เนียน เราสามารถปรับตำแหน่ง loop in/out ได้ครับ โค๊ดตรงนี้ทำงานค่อนข้างเหมือนเดิมในแต่ละครั้ง ถ้าเรามั่วค่าได้ดีรอยต่อมันจะหายไปเอง

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

สุดท้ายคือ วิธีนี้ใช้ custom callback ในการโหลดข้อมูลเข้าบัฟเฟอร์ การที่จะรองรับ file format ต่าง ๆ นั้นเราต้องทำเองหมดเลย (ต่างกับการเล่นไฟล์ด้วยกลไกของ SDL_mixer เอง) ถ้าเราต้องการรองรับหลาย ๆ ฟอร์แมทนี่อาจจะเหนื่อยหน่อยนะครับ :-) ส่วนตัวผมแนะนำว่าให้ใช้แค่ฟอร์แมทเดียวกับทั้งโปรเจคดีกว่า จัดการง่ายกว่าครับ

สุดท้ายทิ้งท้ายด้วยไฟล์โค๊ดตัวอย่าง ลองเข้าไปดูเล่น ๆ นะครับ

ก่อนจะเข้าเรื่อง ผมว่าผมเคยบ่นเรื่องของวิธีการ 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 ตัวนี้มาไทยคงได้คุยกันบ้าง

ก่อนเริ่มขอนอกเรื่องนิด ไอ้เจ้าช่อง PG-Talk ที่ผมไม่ได้ทำมาตั้งครึ่งปีเนี่ย ผมคิดว่าน่าจะได้กลับมาทำในเร็ว ๆ นี้ครับ เพิ่งเคลียร์อะไรหลาย ๆ อย่างไปก็เลยว่าจะกลับมาทำต่อ

เรื่องที่จะเล่าวันนี้เป็นเรื่องนึงที่ผมคิดระหว่างการขับรถกลับบ้าน (อันตรายครับไม่ควรทำ 55) ก่อนจะเริ่มไปถึงจุดที่ผมว่าผมขอเล่าให้ฟังนิด คือได้ยินว่าในหลาย ๆ องค์กรมักจะวาง ecosystem ของระบบไอที ทั้งที่เป็นแบบให้บริการภายในบริษัทเอง หรือเป็นที่ให้บริการลูกค้า ตาม "ผลิตภัณฑ์" ที่แต่ละคนทำงานอยู่ ซึ่งไอ้เจ้า "ผลิตภัณฑ์" ที่ว่านี้นั้นสร้างจากเทคโนโลยีที่แตกต่างกัน เช่น ตอนแรกสร้างแอพเป็นคอนโซล เขียนด้วยภาษาดาต้าเบส (PL/SQL ?) แล้วต่อมาก็สร้างระบบ UI เป็น Windows ด้วยภาษา C++ หลังจากนั้ันก็พัฒนา web frontend ด้วย Python หรืออะไรก็ว่าไป ซึ่งฟังดูผ่าน ๆ มันก็เหมาะสมดีนะ เอาคนที่ทำงานคล้ายๆ กันมาอยู่ด้วยกัน มีอะไรก็แทนกันได้

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

กลับมาต่อ ผมบอกว่าดูเหมือนมันจะเหมาะสมใช่ไหมครับ ? ปัญหาคือหลาย ๆ ครั้งที่ผลิตภัณฑ์มีส่วนที่ทับซ้อนกัน และการ implement ระบบเพื่อขึ้นมารองรับตัว functionality นั้น ๆ ก็ต่างคนต่างทำ ทำให้มันไม่ไปในทิศทางเดียวกัน ... และถ้าผลิตภัณฑ์หนึ่งมีปัญหา ก็จะแก้ไขกันที่ผลิตภัณฑ์นั้น ๆ ตัวเดียว ไม่มีใครยืนยันว่าผลิตภัณฑ์อื่น ๆ ที่ให้บริการแบบเดียวกันจะมีปัญหาด้วยหรือไม่ ...

แล้วก็ ปัญหาที่สำคัญที่สุดคือ ทำไมมันถึงมีส่วนที่ทับซ้อนกัน ?? การสร้างอะไรซ้ำซ้อนกันหลาย ๆ ที่นั้นเป็นข้อบ่งชี้ที่ดีอย่างหนึ่งว่า ระบบของเรากำลังจะมีปัญหาครับ ปัญหาที่ผมเล่ามาข้างบนนี่ล่ะ

ระหว่างที่ขับรถผมก็คิดว่า แล้วทำไมเราถึงไม่แบ่งตาม "functionality" ของตัว ecosystem แทนล่ะ ? เช่นสมมติเราทำระบบธนาคาร ก็มีส่วนงานหนึ่งดูแลแต่เรื่องข้อมูลส่วนที่ไม่ใช่การเงิน (เช่นพวกที่อยู่) อีกส่วนงานหนึ่งดูแลเรื่องการประมวลผล transaction (transaction processing) อีกส่วนงานหนึ่งดูแลเรื่องระบบสร้าง transaction ไป และอื่น ๆ ... คือมันก็เหมือนกับการแบ่งงานให้คนทำล่ะครับ ทำตามหน้าที่ของตัวงาน ไม่ใช่แบ่งตาม เอ่อ ... "สถาบันการศึกษา" หรืออะไรคล้าย ๆ กันนั่นล่ะครับ (ถ้ามองว่าคนงานคือตัวแผนกงานที่ทำ function ต่าง ๆ ผมว่าตัวภาษาที่ใช้สร้างตัวชิ้นส่วนที่ทำงานตรงนั้นก็คล้าย ๆ กับสถาบันการศึกษาที่สร้างคนงานมานั่นล่ะมั้ง ? 555)

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

คราวนี้ก็จะมีคนสงสัยว่า อ้าว ถ้าต่างคนต่างดูแล function ของตัวเองแล้วมันจะเชื่อมโยงได้อย่างไร ถ้าเป็นในอดีตก็ยากครับ อาจจะมีการปิดโปรแกรมโน้นเปิดโปรแกรมนั้น เพราะว่าแอพบนเดสก์ท็อปจะมีความเชื่อมโยงกันน้อย แต่เราอยู่ในยุคที่เว็บครองเมืองแล้วใช่ไหมครับ ? แต่ละทีมก็สร้างผลิตภัณฑ์แบบเว็บ ใช้วิธีที่แตกต่างกันหรือเหมือนกันก็ได้ (เหมือนกันดีกว่าครับ) ของใครของมันไปเลย แต่สามารถเชื่อมโยงถึงกันได้ แชร์ session id กันได้ สามารถสลับไปมาระหว่างผลิตภัณฑ์โดยที่ผู้ใช้ไม่รู้ตัว (แบบเดียวกับ Google -> Google Drive -> GMail -> Google Docs อะไรทำนองนี้ล่ะครับ) คราวนี้ต่อให้เป็นซอฟต์แวร์ที่แยกขาดออกจากกันก็ยังสามารถทำงานร่วมกันได้ ผู้ใช้ไม่รู้ตัว อ้อ วิธีนี้มีข้อดีอีกอย่างคือสามารถขายแยกส่วนได้ครับ เพราะมันไม่อิงซึ่งกันและกันนั่นเอง

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

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

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

2

ผมเริ่มรู้สึกรำคาญความอืดของ NAS ที่ใช้ก็เลยคิดว่าจะเปลี่ยน คือผมใช้ ZyXEL NSA210 มาก่อน (ก่อนหน้ามีอีกตัวแต่ขายทิ้งไปแล้ว) ก่อนสลับไปใช้ Raspberry Pi แล้วสลับกลับมาใช้ตัวนี้ ผมทำ Peak Data Transfer ได้ที่ราว ๆ 7MB/s ครับ ซึ่งถ้าก็อปปี้ไฟล์ใหญ่ ๆ เนี่ยมันต้องรอเป็นชั่วโมงเลยครับ

หลังจากที่หาข้อมูลอยู่สักพักผมก็ตกลงปลงใจกับเจ้า NSA325 ซึ่งก็เป็นรุ่นที่ใหม่กว่าของ ZyXEL ผมเลือกเจ้านี่ด้วยความคุ้มเงินครับ เพราะว่าถ้าจะเล่นตัวหรู ๆ อย่าง QNAP หรือ Synology เนี่ยราคามันจะแพงกว่ามาก (รุ่นที่สเปคเท่ากันราคาแพงกว่า 50%) และอีกสาเหตุนึงคือผมสามารถที่จะติดตั้ง Linux เข้าไปได้อย่างไม่ยากเย็นจนเกินไป (ใช้แค่ Thumb drive ตัวเดียวเอง)

ภาพจาก ZyXEL

ภาพจาก ZyXEL นะครับ

Spec

เจ้านี่เนี่ยสเปคมันจริง ๆ ไม่เลวเลย คือเป็น NAS สองช่องที่รอบรับ SATA I/II ความจุสูงสุด 12TB ใช้ CPU จาก Marvell (ตระกูล Kirkwood เป็น ARMv5 ครับ เก่าหน่อย) ความเร็ว 1.6GHz คู่กับแรม 512MB ถ้าเทียบกับเจ้า NSA210 แล้วสเปคสูงขึ้นมหาศาลเลยครับ (NSA210 ใช้ซีพียูความเร็วแค่ 200MHz) ซึ่งด้วยสเปคขนาดนี้ความสามารถมันก็ค่อนข้างดีทีเดียว

Spec ของ NSA325v2

อ้อ ตัวมันเองมีอินเตอร์เฟซ Gigabit Ethernet 1Gbps ครับ (ความเร็วตามสเปคนะ) และมี USB 3.0 1 ช่อง กับ USB 2.0 อีกสองช่อง ซึ่งเราสามารถเอา Thumbdrive ไปเสียบเพื่อก็อปปี้ไฟล์เข้าไปในเครื่องได้เช่นกัน

Set Up

สำหรับการติดตั้งนั้น เนื่องจากผมเลือกที่จะติดตั้งตัว Arch Linux ARM หรือ ALARM แทนที่จะใช้ Firmware เดิม ๆ ดังนั้นมันจะวุ่นวายนิดนึง โดยวิธีการนั้นสามารถเข้าไปดูที่เว็บของทางโครงการได้เลยครับ ซึ่งสิ่งที่ต้องใช้เพิ่มเติมนอกจากตัว NAS และ HDD ก็คือ Thumbdrive สักตัว ขนาดไม่ต้องใหญ่มากก็ได้ครับ (ผมใช้ Sandisk Ultra 16GB เพราะว่าที่บ้านมี)

ผมใช้ HDD เป็น Western Digital Red ขนาด 2TB หนึ่งตัว ไม่ได้ต่อ RAID นะครับ ทั้งนี้การใช้ Arch Linux นั้นเราไม่จำเป็นว่าจะต้องต่อ RAID ในกรณีที่ใส่ HDD ครบสองช่อง (ไม่เหมือนตัว Stock Firmware) ดังนั้นเราจึงสา่มารถใช้พื้นที่ของ HDD สองตัวรวมกันได้โดยที่ไม่เสี่ยงกับปัญหาข้อมูลเสียหายเพราะ HDD ตัวใดตัวนึงพัง

หลังจากที่ติดตั้งเจ้า ALARM ไปแล้วเจ้า NSA นี้จะมีแค่ส่วนของระบบปฎิบัติการณ์เท่านั้น เราจะต้องคอนฟิกทุกอย่างเองทั้งหมด ซึ่งก็ทำเหมือนบน Arch Linux ปรกติทุกอย่างครับ การติดตั้ง Software จะใช้คำสั่ง pacman (package manager) ในการติดตั้งหรือถอดถอนโปรแกรม เช่น ถ้าจะติดตั้ง Samba เราก็พิมพ์ว่า

pacman -S samba

เป็นต้น

ทั้งนี้ตัวระบบปฎิบัติจะไม่มีส่วนของ Web GUI เลยนะครับ ทุกอย่างทำผ่าน Command Line หมด อาจจะเหนื่อยนิดนึง จริง ๆ มีทางเลือกอีกทางก็คือใช้ Debian แล้วรันเจ้า OpenMediaVault ซึ่งมี Web Interface แต่คราวนี้ผมเลือกจะใช้ ALARM เพราะว่า OMV เนี่ยจะไม่ให้ใช้ไดร์ฟระบบในการเก็บข้อมูลครับ (นั่นคือผมจะเสียที่ไปฟรี ๆ 2TB)

สำหรับโปรแกรมที่ผมติดตั้งไปนั้นหลัก ๆ ก็ตามนี้ครับ

  1. Samba (SMB/CIFS Filesharing สำหรับแชร์ไฟล์กับวินโดวส์)
  2. VSFTPD (FTP server)
  3. Lighttpd (HTTP server)
  4. GIT
  5. MiniDLNA (DLNA/UPNP server เอาไว้แชร์หนัง เพลง รูปถ่ายให้เครื่องอื่น ๆ ดู)
  6. Transmission (Bittorrent Client)

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

Test

ผมใช้ PC ที่มี Gigabit Ethernet (ใช้ชิพของ Realtek) กับ Router ของ Buffalo ที่เป็น Gigabit Ethernet 4 พอร์ต นะครับ ผมลองก็อปปี้ไฟล์ขนาด 42GB เข้าไปผ่าน Samba ก็สามารถทำความเร็วได้ที่ราว ๆ 35MB/s (ความเร็วสูงสุดประมาณ 40MB/s)

ทดลองโอนไฟล์

ถ้าดูกราฟจะเห็นว่าแทบจะไม่แกว่ง และอันที่จริงนอกเหนือจากที่ผมโอนไฟล์แล้ว ระหว่างการทดสอบผมก็กำลังดูหนัง Bluray ที่เป็นไฟล์ iso บน NSA นี่อยู่ครับ ผมเองก็ไม่แน่ใจว่าจริง ๆ แล้วมันติดที่ตัว Router เองหรือเปล่า เดี๋ยวคงต้องลองเปลี่ยนตัว Hub/Switch ที่ใช้ในการทดสอบดู นอกจากนี้แล้วก็มี HTTP Server กับ Bittorrent ที่กำลัง Seed อยู่ด้วยครับ (5 ไฟล์มั้ง ?)

อ้อ อีกอย่างคือระหว่างการโอนไฟล์ 2 session นี่ ระบบใช้ซีพียูแค่ราว ๆ 10% เองนะ ผมเดาเล่น ๆ ว่ามันน่าจะเร็วกว่านี้ได้อีกครับ ความเร็วระดับก็ถือว่าใช้ได้แล้วล่ะครับ เพราะปรกติไม่ค่อยโอนไฟล์ใหญ่ระดับ 40GB บ่อย ๆ อยู่แล้ว (ที่บ่อยกว่าคือระดับ 5GB)

Conclusion

สำหรับคนที่กำลังมองหา NAS ที่สามารถปรับแต่งได้ละเอียดสุด ๆ และสามารถใช้ Command Line ได้ ผมว่า NSA325v2 นี้ก็เป็นทางเลือกที่น่าสนใจครับ (ถึงแม้ว่าการลง Linux เองอาจจะทำให้หมดประกันก็เถอะ) ที่ค่าตัวประมาณสี่พันกว่าบาทปลาย ๆ กับความสามารถระดับนี้นั้นผมว่าคุ้มค่าทีเดียว

ปล. HDD ต้องซื้อเพิ่มนะครับ ไม่แถมนะ

ในการพัฒนาโปรแกรมเนี่ยเรามักจะมีการตั้ง 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 เนี่ยมันไม่ใช่คอมไพล์เลอร์ มันไม่จับโค๊ดที่ผิดนะครับ อย่างโค๊ดข้างบนเองเอาจริง ๆ ก็คอมไพล์ไม่ผ่านนะขอบอก

พอดีช่วงนี้ศึกษา Web Application มากขึ้นกว่าแต่ก่อน ก็เลยได้เห็นเทรนด์นึงที่ในระยะหลังเกิดขึ้นอยู่ประมาณนึง ก็คือการเอา Database ไปเก็บ Configuration เอาซะเลย ด้วยความที่ว่าคนที่เขียน Enterprise Application ย่อมคุ้นเคยกับ SQL อยู่แล้ว (แต่สารภาพตรง ๆ ผมเกลียด SQL แบบสุด ๆ ไปเลย) แล้วก็มีข้อดีของ Database หลาย ๆ อย่าง เช่นเรื่องของ Consistency และอื่น ๆ

เอาจริง ๆ ผมค่อนข้างคัดค้านแนวความคิดนี้นะครับ ผมคิดว่าการเอา Database ไปเก็บ Configuration เป็นแนวความคิดที่แย่เอามาก ๆ แต่พอมาคิดหาเหตุผลก็ ... อืม ... ตอนแรกก็ช็อตไปเหมือนกันว่าจะตอบว่าอะไรดี 555

ผมคิดว่าประเด็นการเก็บ Configuration ใน Database มีปัญหาอยู่หลายอย่างนะ

  1. ความชัดเจน คือ ผู้ที่จะเข้าไปแก้ไข Configuration จะต้องเป็นคนที่มีความรู้ในตัว Implementation ของตัวที่อ่าน Configuration พอสมควร เพราะว่าไอ้การหาว่า คอนฟิกอะไร เก็บค่าอะไร ค่าเดิมคืออะไร หรืออะไรทำนองนี้จะต้องกระทำผ่าน Query (ซึ่งอาจจะเป็นได้ทั้ง SQL Query หรือ MapReduce Query หรืออะไรก็ว่าไป) ในขณะที่ถ้าเป็น Flat File เนี่ยเราสามารถประยุกต์เอาพวก markup file เช่น xml, json และอื่น ๆ มาใช้ได้ ทำให้มีความชัดเจนมากกว่า (แต่ทั้งนี้ก็ต้องมีความเข้าใจในระดับนึงเช่นกันแต่อาจจะต่ำกว่า)

  2. การดูแลรักษา เท่าที่ทราบ DBMS แทบจะทุกเจ้าบนโลกนี้ทำงานกับ Binary File และไม่ใช่แค่ไฟล์เดียวด้วย (บางทีก็เพียบเลย) ดังนั้นลืมเรื่องการเก็บ DB เอาไว้ใน Version Control ไปได้เลย เพราะว่า Version Control เนี่ยจะทำงานได้ดีกับไฟล์ที่เป็น Text File ซึ่งการใช้ Binary File นั้นทำให้เสียฟังก์ชันหลักอันหนึ่งของ Version Control นั่นคือการ Merge Version

    เช่น สมมติว่า ผมต้องการจะแก้คอนฟิกทั้งหมดที่มีการเปลี่ยนแปลงในช่วงเดือนมีนาคมทั้งหมด (ซึ่งจริง ๆ ก็มีอยู่แค่ 2-3 อัน) การใช้ Flat File กับ VCS นั้นสิ่งที่ต้องทำคือ เปิด Version History เลือกเวอร์ชันที่ต้องการเอาออก แล้วสั่ง remove change มันจะทำการเอาการเปลี่ยนแปลงในช่วงที่เราเลือกออกไปทั้งหมด (แล้วก็ Commit)

    ในขณะที่ถ้าเป็น DB สิ่งที่คุณต้องทำคือเขียน Query เข้าไปลบ/แก้ไข ซึ่งก็คือการ "ทำใหม่" นั่นแหละ เพราะว่าต้องทำการทบทวนความเปลี่ยนแปลงทั้งหมดในช่วงเวลานั้น แล้วก็เขียน update query เข้าไปแก้ นี่ไม่ใช่การเอาออกแล้วครับ นี่มันคือการสร้างของใหม่เข้าไปทับของเก่าต่างหาก

    อ้อ จะว่าไป ชื่อเล่นของ VCS คือ SCM (Software Configuration Manager) ครับ ดังนั้นมันจึงเก็บ Configuration Item ทั้งหมด (ซึ่งปรกติจะรวมเฉพาะโค๊ดและคอนฟิกไฟล์) น่ะครับ

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

    ทั้งนี้เราสามารถที่จะดูแล config ในรูปของ text file เดี่ยว ๆ เพื่อใช้ใน DB ได้เหมือนกันครับ ก็คือการสร้าง Master Create Query ขนาดยักษ์ที่ทำงานโดยการ Drop Table ทิ้งแล้วสร้างข้อมูลใหม่ตั้งแต่ 0 แต่ผมว่าสุดท้ายแล้วมันก็เยอะเกินไปอยู่ดี และโอกาสพลาดสูงกว่าด้วย เพราะว่าในไฟล์จะมีทั้งส่วนของคำสั่งและส่วนของข้อมูล ในขณะที่ถ้าเป็น flat file ธรรมดามันมีแต่ข้อมูลอย่างเดียวครับ

  3. ทรัพยากร คือ DBMS เนี่ยจริง ๆ แล้วมันเป็นซอฟต์ที่ค่อนข้างกินแรมน่ะครับ (ผมไม่ได้พูดถึงพวก Oracle นะ) ในขณะที่ flat file เนี่ยใช้พื้นที่เท่ากับขนาดของไฟล์เท่านั้นแหละ โดยส่วนตัวผมว่าการใช้ DB เนี่ยเป็นการขี่ช้างจับตั๊กแตนครับ เพราะตัวมันเองออกแบบมาเพื่อเก็บข้อมูลจำนวนมาก ๆ แต่เราเอามาเก็บคอนฟิกไฟล์ซึ่ง...อาจจะมีแค่ 1 record เท่านั้น ระบบที่ออกแบบมาสำหรับข้อมูลขนาดใหญ่มักจะไม่เหมาะกับงานเล็ก ๆ น่ะครับ

ทั้งนี้ถ้าเราใช้แต่ flat file เพียว ๆ เลยเราจะเจอปัญหาด้านการจัดการแทน เพราะถ้าเรามี server ที่ต้อง deploy เยอะ ๆ เนี่ยการไล่ตามอัพเดต config ในแต่ละเครื่องมันก็เป็นงานใหญ่อยู่นะ ผมคิดว่าเราสามารถที่จะสร้าง micro service สำหรับงานด้าน configuration โดยเฉพาะได้ โดยอาจจะเป็น REST Web Service สักตัวนึงที่มีหน้าที่คืนค่า Configuration ต่าง ๆ แทนที่จะให้ตัว Web App ไปโหลดมาจากตัวไฟล์โดยตรง (ก็อาจจะมี Network Performance Penalty อยู่นะแต่คิดว่าไม่ต่างกับ DB หรอกครับ)

อีกอย่างหนึ่งที่สามารถทำได้คือการใช้ VCS Trigger ก็คือเราสามารถสร้าง Trigger ให้มีการสั่งให้ Service ต่าง ๆ ทำการอัพเดต Configuration หลังจากการ Check-in ตัวไฟล์คอนฟิก ซึ่งก็ไม่ได้เป็นเรื่องยากอะไรเลยและโอกาสพลาดต่ำกว่าด้วย (อะไรที่มีมนุษย์มาเกี่ยวข้องเนี่ยพลาดได้หมดละครับ) Configuration จะถูกอ่านจากตัว VCS โดยตรง ทำให้ปัญหาพวก Consistency ระหว่าง Service นั้นหมดไปนั่นเอง

เอาจริง ๆ เลยผมว่า ... แนวคิดนี้เกิดจากพวกบ้า Database ที่เอะอะอะไรก็ใช้ Database (คนกลุ่มนี้มีจริงครับและเยอะด้วย ... เยอะในหลายความหมายนะ) ถ้ามองกันตามจริงผมว่า DB มีไว้เก็บข้อมูล และไม่ควรเก็บอะไรมากกว่านั้นครับ