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

พอดีช่วงนี้ศึกษา 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 มีไว้เก็บข้อมูล และไม่ควรเก็บอะไรมากกว่านั้นครับ

เคยพูดไปเมื่อก่อนว่า โค๊ดที่ถือว่าเป็นโค๊ดมรดก หรือ 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 เอาไว้แค่นี้ละกัน มีอะไรสงสัยก็ถามได้ครับผม

วันนี้จะพูดเรื่อง build script สักหน่อยครับ โดยปรกติเวลาที่เราเขียนโปรแกรมขึ้นมาสักตัวเนี่ย จากซอร์สโค๊ดไปเป็นไฟล์โปรแกรมจะผ่านขั้นตอนบางอย่าง อย่างถ้าเป็นภาษา C/C++ ก็ต้อง compile แล้วก็ link หรือถ้าเป็นพวก java web application ก็ต้อง compile แล้วเอาผลลัพท์ไปแพ็ครวมกันเป็นแ็คเกจ ขั้นตอนนี้จะเรียกรวม ๆ ว่าการ build ก็คือการเอาไฟล์ต้นฉบับมาสร้างให้เป็นไฟล์แอพพลิเคชั่นที่พร้อมจะถูกเรียกใช้งานนั่นเอง

การ build โปรแกรมนั้นสามารถทำด้วยมือได้ อย่างถ้าคุณเขียนโปรแกรมด้วยภาษา C เนี่ยก็สามารถบิลด์ด้วยคำสั่ง

 gcc test.cpp -o test.exe

หรือถ้าเป็น java ก็จะเป็น

javac Test.java

ซึ่ง พอโปรแกรมเริ่มมีขนาดใหญ่ เริ่มมีจำนวนไฟล์มากขึ้น การที่จะสั่ง build โปรแกรมด้วยมือนั้นคงเป็นเรื่องที่น่าหงุดหงิดมิใช่น้อย โปรแกรม IDE แทบทุกตัวจึงให้ระบบ build โปรแกรมติดมาด้วย อย่างใน Visual Studio ก็แค่กด Ctrl+Shift+B หรือใน Eclipse ก็กด Ctrl+B (ทั้งสองโปรแกรมสามารถสั่ง build ได้จากเมนูครับ แต่จำ shortcut ไว้ก็ดี) ระบบก็จะเริ่ม build โปรแกรมให้คุณ ง่ายไหมล่ะ ?

แต่ระบบ build ของ IDE เองก็มีข้อจำกัด ด้วยความที่ IDE นั้นออกแบบมาเพื่อให้ใช้งานได้จาก GUI ถ้าหากว่าโปรแกรมนั้น ๆ มีขั้นตอนในการ build โปรแกรมที่ซับซ้อนมาก ๆ การเขียน GUI ขึ้นมารองรับทุกกรณีที่เป็นไปได้นั้นเป็นเรื่องยากครับ

ผลิตภัณฑ์ที่ผมทำงานด้วยตัวหนึ่งมีเอกสารอธิบายวิธี build โปรแกรมความยาว 20 หน้ากระดาษ A4 ประกอบอยู่ครับ และขั้นตอนนี้ใช้ IDE ในการ build แล้วนะครับ ซึ่งที่ต้องอธิบายเยอะมากขนาดนั้นเป็นเพราะว่าต้องแก้ไฟล์ configuration หลายไฟล์ และต้องมีการเตรียมไฟล์ resource ซึ่งต้องทำด้วยมือเพราะว่า IDE ไม่รองรับอีกด้วย ที่แย่กว่านั้นคือถ้ามีอะไรที่ทำผิดขั้นตอนไปแม้แต่ขั้นตอนเดียว โปรแกรมนั้นอาจจะไม่ทำงานเลยล่ะครับ

แล้วเราจะทำยังไงให้การ build โปรแกรมนั้นทำได้ง่ายขึ้นดีล่ะ ?

Build File

สำหรับปัญหาการ build โปรแกรม ผมเชื่อว่าหลาย ๆ คนน่าจะคิดถึง build file เป็นอันดับแรก

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

ระบบที่อ่าน build file เพื่อที่จะ build โปรแกรมนั้นก็มีที่นิยมอยู่หลายตัว อย่าง Apache Ant สำหรับ Java หรือ GNU Make สำหรับแพลตฟอร์มบน Unix

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

ที่แย่กว่านั้นคือ build file อ่านยากครับ ... มันจะทำอะไรบ้างเราก็ไม่รู้ บางทีต้องปรับแก้โน่นนั่นนี่เพราะว่าไม่รู้ว่าผลลัพท์หน้าตาจะเป็นยังไง

สำหรับผลิตภัณฑ์ที่ผมพูดถึงข้างบน ผมเคยลองเขียน build file แล้ว มันช่วยลดขั้นตอนการบิลด์จาก 20 หน้า เหลือ ... ประมาณ 18 หน้าครับ พูดง่าย ๆ คือมันไม่ช่วยอะไรเลย ...

Build Script

พูดถึงคำว่า script มันก็เป็นโปรแกรมประเภทหนึ่งที่สามารถรันได้โดยไม่ต้องคอมไพล์ ส่วนเจ้า build script นั้นก็คือโปรแกรมประเภท script ที่เราเขียนขึ้นเพื่อ build โปรแกรมโดยเฉพาะ

ด้วยความที่มันเป็นโปรแกรม (ไม่ใช่แค่การบอก input ให้กับโปรแกรม เหมือนการใช้ build file) เราจึงสามารถที่จะเขียนให้มันทำอะไรก็ได้ตามที่เราต้องการ ซึ่งแน่นอนว่ามันยืดหยุ่นมากกว่าการใช้ build file มาก

ในอดีีต build script นั้นก็ใช้ภาษา script ที่นิยม ๆ กันอย่าง perl, shell script อะไรทำนองนี้นั่นล่ะครับ แต่ปัญหาคือเราต้องเขียนทุกอย่างเองหมดเลยทุกขั้นตอน ซึ่งแน่นอนว่ามันยืดหยุ่น แต่มันก็วุ่นวายด้วย เป็นเรื่องยากที่จะทำให้มันถูกต้อง

สำหรับปัจจุบันมีการพัฒนาภาษา script ที่ใช้สำหรับ build โดยเฉพาะ หรือพัฒนาส่วนของ library ของภาษา script ขึ้นมาเพื่อรองรับการ build ตัวที่ผมใช้แล้วได้ผลดีก็คือ Gradle ส่วนที่ลองค้น ๆ ดูแล้วเจอก็คือ Rake ครับ

Gradle เป็นภาษา Build Script ที่พัฒนาขึ้นจากภาษา Groovy ที่เป็นภาษาที่ทำงานบน Java Virtual Machine (เหมือนกับ XTend ที่ผมพูดถึงบ่อย ๆ ครับ) ซึ่งจริงๆ จะเรียกว่าเป็น Script มันก็ไม่เชิงเพราะว่ามันจะถูกคอมไพล์เป็น Java Class ก่อน แต่ว่าตรงนี้ตัวระบบจะเป็นคนจัดการทั้งหมด แถมคอมไพล์ใหม่ทุกครั้งที่รันโปรแกรม ดังนั้นก็พอถูไถเรียกว่าเป็น Script ได้ล่ะครับ แต่ด้วยความที่มันทำงานบน JVM เราจึงสามารถใช้ Gradle ทำทุกอย่างที่ Java ให้ได้หมดเลย ผมสามารถเขียน build script ให้สร้าง UI เป็น Swing ได้ด้วยนะ (แต่คงไม่ทำ 555)

ผมสร้าง build script ของผลิตภัณฑ์ข้างต้นนี้บนภาษา Gradle (ซึ่งที่จริงก็เป็น Groovy นั่นแหละ) ความสำเร็จของผมก็คือ ผมสามารถย่อคู่มือขนาด 20 หน้าให้เหลือแค่ ... บรรทัดเดียว ดังนี้ครับ

gradle jettyRun -Pclient=xxx

script ของผมก็จะทำการคอมไพล์โปรแกรม เตรียมไฟล์ resource แก้ไฟล์ configuration ต่าง ๆ และโหลดเอาโปรแกรมไปรันบน Jetty ซึ่งเป็น Servlet Container ตัวหนึั่ง (เหมือน Tomcat ขั้นตอนต่อไปที่ผมต้องทำคือ ... เปิด Web Browser เพื่อเข้าโปรแกรม!

ผมสามารถรันโปรแกรมได้ภายในเวลาไม่ถึงสองนาที จากปรกติที่ต้องเสียเวลาเกือบชม. ผมว่านี่คือความสำเร็จของผมนะ! (เนื่องด้วยความที่ผลิตภัณฑ์เป็น closed source ผมจึงแสดงโค๊ดให้ดูไม่ได้ครับ)

build script สมัยใหม่นั้นได้รวมเอาความเรียบง่ายของ build file มารวมเข้ากับความยืดหยุ่นของของภาษา script ซึ่ง build script ที่เรียบง่ายมาก ๆ นี่มองผ่าน ๆ จะเหมือน build file เลยครับ เช่นไฟล์ข้างล่างนี้

apply plugin: 'java'

sourceCompatibility = 1.5
version = '1.0'
jar {
    manifest {
        attributes 'Implementation-Title': 'Gradle Quickstart', 'Implementation-Version': version
    }
}

repositories {
    mavenCentral()
}

dependencies {
    compile group: 'commons-collections', name: 'commons-collections', version: '3.2'
    testCompile group: 'junit', name: 'junit', version: '4.+'
}

ดูไม่เหมือนภาษาเขียนโปรแกรมเลยใช่ไหมครับ ?

หลาย ๆ คนคงรู้จักคำว่า mock-up คำนี้มีความหมายว่า แบบจำลอง หรือวัตถุปลอม ๆ ที่เอาไว้สำหรับแสดงให้ลูกค้าดูว่าเมื่อผลิตภัณฑ์สร้างเสร็จแล้วหน้าตามันจะเป็นอย่างไร คำนี้มีเป็นคำผสมที่มีพื้นฐานจากคำว่า mock ที่แปลว่าการล้อเลียน

แล้วไอ้การล้อเลียนที่ว่านี้มันใช้ทำอะไรในการพัฒนาโปรแกรมได้ ?

คืองี้ครับ หลาย ๆ ครั้งที่เราต้องเขียนโปรแกรมที่ซับซ้อน และต้องอาศัย parameter จากคลาสที่มีความซับซ้อนมาก ๆ ซึ่งการทดสอบโปรแกรมนั้น ๆ ด้วยการสร้างตัวแปรขึ้นมาเป็น parameter ส่งเข้าไปนั้นอาจจะเป็นเรื่องที่ทำได้ยาก

อย่างเช่นสมมติว่าเราพัฒนาโปรแกรมที่เป็น Servlet โดยเรามีคลาสที่สืบทอดมาจาก HTTPServlet และเรากำลังจะเขียนโปรแกรมทดสอบเจ้าเมธอด doGet() ที่มีหน้าตาแบบนี้

class MyServlet extends HTTPServlet {
    @Override
    protected void doGet(HttpServletRequest req,
                 HttpServletResponse resp) {
    /* Implementations */
    }
}

คำถามคือ แล้วเราจะสร้างตัวแปรที่มีประเภทเป็น HttpServletRequest และ HttpServletResponse ได้อย่างไร ? เพราะเจ้า type ทั้งสองตัวนี้เป็น Interface ครับ

คำตอบน่ะเหรอ ... เขียนคลาสใหม่ขึ้นมา implement เจ้าสองคลาสนี้ไปเลยครับ

class MockHttpServletRequest implments HttpServletRequest{
     /* implements every method here*/
}

class MockHttpServletResponse implements HttpServletResponse {
    /* implements every method here*/
}

เขียน implementation ของแต่ละ method ข้างในให้ง่ายที่สุดครับ อันไหน get ก็คืนค่าคงที่ไปเลย อันไหน set ก็สร้างตัวแปรมารับ อะไรทำนองนี้ เช่น

class MockHttpServletRequest implments HttpServletRequest{
    String getContextPath() {
        return "/";
    }
}

ทั้งนี้ถ้าเป็น method ที่ใช้ในการทดสอบด้วย ก็ต้องเขียนให้สอดคล้องกับ test case ด้วยนะครับ

ทีนี้เราก็เขียนโปรแกรม unit test ได้ง่าย ๆ แบบนี้ครับ

class MyHttpServletTest {
    @Test
    void testDoGetRootPath() {
        HttpServletRequest req = new MockHttpServletRequest();
        HttpServletResponse resp = new MockHttpServletResponse();
        MyHttpServlet servlet = new MyHttpServlet();

        servlet.service(req, resp);
    }
}

อันนี้พอดีว่า doGet() มันเป็น private method ครับ ไปเรียกตรง ๆ ไม่ได้ ผมเลี่ยงไปเรียก service() แทน เพราะผมรู้ว่ามันจะไปเรียก doGet() แต่ถ้าอยากทดสอบเจ้า doGet() ก็มีอีกวิธี ก็คือเขียนชุดโปรแกรมทดสอบเป็น class ที่สืบทอดเจ้า MyHttpServletTest อีกต่อไปเลยโลด

class MyHttpServletTest extends MyHttpServlet{
    @Test
    void testDoGetRootPath() {
        HttpServletRequest req = new MockHttpServletRequest();
        HttpServletResponse resp = new MockHttpServletResponse();
        MyHttpServletTest servlet = new MyHttpServlet();

        servlet.doGet(req, resp);
    }
}

แค่นี้เอง

(อีกวิธีคือเอาตัว test ใส่ลงไปในคลาสที่จะ test ไปเลย ซึ่งไม่แนะนำครับ เพราะว่าตัว test จะติดไปในโค๊ดที่จะเอาไปรันจริงด้วย ทำให้มันกินแรมมากขึ้นเปล่า ๆ)

ปัญหาของการใช้ Mock คือ ... ในกรณีที่แย่ที่สุดเราอาจจะต้องสร้างคลาสขึ้นมาสำหรับแต่ละเทสต์เคสเลย ซึ่งมันเยอะมาก และมันก็อาจจะไม่ชัดเจนพอที่เอาไปใส่ใน unit test (ซึ่งควรจะเรียบง่ายที่สุดไม่งั้นมันจะอ่านยาก) ที่แย่กว่านั้นเราต้อง implement เมธอดที่เราไม่ได้ใช้ทดสอบด้วย ทำให้โค๊ดยาวโดยใช้เหตุ

สำหรับจุดนี้เราสามารถใช้ mock framework เข้ามาช่วยได้ครับ ในภาษา Java นั้นมี Mock Framework ที่ได้รับความนิยมอยู่หลายตัว (ลอง Google ดูนะครับ) ผมจะลองใช้ Mockito ดู เจ้าโปรแกรมทดสอบมันก็จะเหลือแค่นี้

import static org.mockito.Mockito.*;

class MyHttpServletTest extends MyHttpServlet{
    @Test
    void testDoGetRootPath() {
        HttpServletRequest req = mock(HttpServletRequest.class);
        when(req.getContextPath()).thenReturn("/");
        HttpServletResponse resp = mock(HttpServletResponse.class);
        MyHttpServletTest servlet = new MyHttpServlet();

        servlet.doGet(req, resp);
    }
}

แค่นี้เอง ผมไม่ต้องสร้างคลาสใหม่ขึ้นมาเพื่อใช้ทดสอบด้วย ยอดไปเลย :)

สุดท้ายนี้ถ้าถามว่าทำไมถึงเรียกว่า mock ผมคิดว่ามันเกิดจากการล้อเลียนเจ้า class จริง ๆ น่ะครับ เราสร้าง class ใหม่ขึ้นมาล้อเลียนตัวเดิมที่ใช้งานได้ กลายเป็นคลาสง่อย ๆ ตัวนึง อะไรทำนองนี้ ที่จริงการสร้าง mock-up ก็คือการสร้างวัตถุชิ้นหนึ่งที่มีหน้าตาเหมือนของจริงแต่ทำอะไรไม่ได้ ใช่ไหมล่ะครับ 😉 เจ้า mock object นี่ก็เหมือนกันแหละ

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