ในยุคสมัยนี้ทุกคนคงเหมือนกันว่ามันไม่มีใครแล้วที่จะเขียนโค้ดเริ่มจากศูนย์เลย (Make it from scratch.) จะหันซ้ายหันขวา ไม่ว่าจะเป็น developer ในภาษาใดๆ ก็มักจะเขียนอิง framework อย่างน้อยตัวหนึ่ง หรือบางทีอาจถึงขั้นปวารณาระตัวเองเป็นสาวกกันเลยทีเดียว ผมเองก็ด้วยคนหนึ่งเช่นกัน ซึ่งสำหรับผมคือ Laravel นั้นเอง
แล้วปัญหาที่มันเกิดขึ้นตามมาคือ คุณกลายเป็นทาสของ framework นั้นๆ ไปซะแล้ว ทำไม? คุณลองคิดดูว่ามันให้อะไรกับคุณบ้าง การใช้งานที่ง่ายขึ้น / helpers ที่มีมาพร้อมใช้ / ระบบ security เพื่อป้องกัน logic code / คุณสามารถใช้งานได้เกือบจะทันทีโดยไม่ต้องลงแรงอะไร และมันก็ทำงานได้ดีเลยทีเดียว!
แล้วการที่ framework ที่ความสะดวกสบายขนาดนี้มันเป็นเรื่องไม่ดีเหรอ?
จริงๆ ไม่ใช่ว่ามันจะไม่ดีนะครับ คุณควรใช้มันซะด้วยซ้ำ เพราะมันถูกทดสอบมาอย่างดี ชุดคำสั่งมันก็มีประสิทธิภาพและเพิ่มความเร็วในการทำงานด้วย เพียงแต่ว่า… มันทำให้คุณขี้เกียจขึ้นมา แล้วไอ้ความ ขี้เกียจ นี้แหละที่ทำให้โค้ดที่คุณเขียนเองมันทำงานเจาะจงกับ framework นั้นๆ มากเกินไป
เราลองมาดูตัวอย่างโค้ดสักอันกัน (แน่นอนว่ามันเป็น PHP และผมอิง Laravel 4 ให้เป็นตัวอย่าง)
class NewsController extends Controller { public function getRecentNews() { return View::make('news.recent', array( 'news' => News::orderBy('id', 'desc')->paginate(10) )); } }
ลองมาคิดว่าถ้าลูกค้าคุณต้องการแสดงข่าวบนหน้าเว็บหลายๆ หน้า คุณก็สามารถคัดลอกคำสั่งไปวางที่ controller ต่างๆ ได้เลย มันเร็วและง่าย แต่แล้วลูกค้าคุณเกิดอยากเปลี่ยนการลำดับของการแสดงข่าวขึ้นมา นั้นแหละ ปัญหาที่เกิดขึ้น เป็นหนี้ทางเทคนิคที่คุณต้องไปชดใช้ด้วยการ search-replace เองทั้งหมด ปัญหาไม่ได้เกิดจากการที่ลูกค้าไม่ได้บอกตั้งแต่แรก แต่มันเกิดจากไอ้ “ทางลัด” ง่ายๆ ที่คุณทำมันไว้
แล้วจะแก้ไขกันยังไงดี?
เราลองมาดูคำสั่งใหม่ที่ผมเขียนเพื่อใช้งานแทนข้างบนบนกันดู
class NewsController extends AppController { protected $viewFactory; protected $newsRepository; public function __construct($viewFactory, $newsRepository) { $this->viewFactory = $viewFactory; $this->newsRepository = $newsRepository; } public function getRecentNews() { return $this->viewFactory->make('news.recent', array( 'news' => $this->newsRepository->getRecent() )); } } class AppController extends Controller { // ... }
คุณจะเห็นว่ามันให้ความสามารถเหมือนกับโค้ดก่อนหน้านี้ และยังมีสิ่งอื่นๆ เพิ่มขึ้นมา ได้แก่
- มันไมไ่ด้จำเป็นต้องพึ่งพา framework ใดๆ อีกแล้ว
- มันสามารถทำ Dependency Injection ได้ ข้อดีเสริมคือ คำสั่งของเรามันทดสอบได้ (testable) และมันยังทำแบบจำลองได้ (mockable) ซึ่งมัน win เอามากๆ
- พฤติกรรมในคำสั่งระดับ method นั้นสามารถแก้ไขได้ง่ายๆ แล้ว ลืมเรื่องไป search-replace หลายๆ จุดได้เลย
เราลองมาวิเคราะห์คำสั่งเพิ่มเติมกันดูแบบทีล่ะขั้นกัน
1. ที่ระดับ controller เราใช้ AppController
แทน Controller
ที่เป็นคำสั่งหลักจาก Laravel 4 ซึ่งเพิ่มระดับชั้น (Layer) เพิ่มขึ้นมาอีก ทำคำสั่งของเราไม่ถูกผูกกับ framework ใดๆ จึงทำให้โค้ดของคุณสามารถเคลื่อนย้ายง่ายๆ ด้วยการเข้าไปแก้ไขใน AppController
แทนที่จะต้องมาแก้ทุก controller ในระบบทั้งหมด รวมถึงคำสั่งพิเศษที่คุณต้องใช้บ่อยๆ คุณยังนำมันไปเขียนไว้ใน AppController
ได้อีกด้วย
2. ในส่วน NewsRepository
กับ ViewFactory
นั้นเป็นการทำ class ของ model กับ framework ไปยัดใส่ในกลุ่มที่เรียกว่า repository กับ factory ซึ่งเรื่องนี้ช่วยได้การแก้ไขพฤติกรรมของคำสั่งเฉพาะ เช่น $newRepository->getRecent()
นั้นมันง่ายขึ้น คุณแค่เข้าไปแก้ใน getRecent()
เท่านั้น รวมถึงโค้ดนี้ยังสามารถทดสอบได้ และยังปั้น mock class instance เขาไปได้ด้วย แต่เรื่องนี้เอาไว้เป็นบทความคราวหน้าแทนล่ะกัน (ไม่สัญญาว่าจะเขียนนะครับ XD )
เพิ่มอีกนิดให้มันเข้าท่าด้วย type hinting
ในตอนนี้คุณสามารถคำ class instance ใดๆ เข้าไปยังโค้ดของคุณได้แล้ว แต่เรามาเพิ่ม type hint เพื่อจำกัด class ที่จะเข้าไปได้กันดีกว่า
ยกตัวอย่าง
public function __construct($viewFactory, $newsRepository)
คุณสามารถเขียนเป็นแบบนี้ได้ (ซึ่งในตัวอย่างผมไมไ่ด้เขียน grouping namespace ให้ถูกหลัก เรื่องนี้ไว้คราวอื่นนะครับ)
public function __construct(\ViewFactory $viewFactory, \NewsRepository $newsRepository)
ซึ่งจากอันนี้แปลว่าตัวแปล $viewFactory
นั้นจำเป็นต้องเป็น class ชื่อว่า ViewFactory
เท่านั้น ซึ่งแบบนี้เราจะมั่นใจได้แน่นอนว่า class instance ที่เขามานั้นมีคุณสมบัติหรือความสามารถที่เราต้องการจริงๆ
แต่แล้วเราลองมาดูในรูปแบบอื่นกัน
protected function fetchLog(\VCS\Driver\Svn $log)
ยกตัวอย่างว่าคุณได้เขียนคำสั่งสำหรับดึง log จาก S์VN เอาไว้ แต่แล้วในปัจจุบันคุณอยากเปลี่ยนมาใช้ GIT แน่นอนว่าคำสั่งของคุณก็ต้องรองรับ GIT ด้วยเช่นกัน แต่แล้วปัญหาก็เกิดขึ้นว่า fetchLog()
นั้นรองรับแต่ instance ของ SVN เท่านั้น พอคุณแก้ให้มันรองรับ GIT มันก็จะไม่รองรับ SVN อีก (ดีงาม….)
คำตอบที่ดีที่สุดสำหรับเรื่องนี้คือ interface type hinting เราลองมาดูคำสั่งเหล่านี้กัน
\\file: log.php protected function fetchLog(\VCS\Driver\DriverInterface $log) \\file: VCS/Driver/Svn.php namespace VCS\Driver; class Svn implements DriverInterface {} \\file: VCS/Driver/Git.php namespace VCS\Driver; class Git implements DriverInterface {}
จากคำสั่งนี้จะทำให้เห็นว่า fetchLog()
นั้นมี type hint เป็นชื่อ interface ว่า DriverInterface
ซึ่งการเขียน interface type hinting แบบนี้มันจะไม่เป็นการบังคับว่าคุณต้องส่ง class อะไรเข้ามา แต่มันจะบอกว่าส่งอะไรมาก็ได้ตราบใดที่ class instance นั้นๆ ได้ทำการ implement interface ที่ถูกต้องเข้ามา
เกร็ดสำหรับมือโปร: คุณสามารถ mock class ด้วยการ implement interface นั้นๆ ซึ่งมันจะทำให้คำสั่งนั้นสามารถทดสอบได้ง่าย!
บทสรุป
ด้วยการเขียนคำสั่งในครั้งถัดไปของคุณโดยการคิดไว้เสมอว่าคำสั่งของคุณนั้นต้องสามารถเคลื่อนย้ายได้ และไม่ผูกติดกับ framework ใดๆ (ทำตัวให้โค้ดเชื่อว่า framework ไม่มีอยู่จริง) โค้ดของคุณจะพิเศษ ปรับเปลี่ยนได้ และทดสอบได้ง่าย มันจะมีประโยชน์มากเมื่อทำงานในระยะยาวครับ
Thanks this article as original content. Don’t Hard-Code Yourself. Make Your Code Framework-Agnostic
เขียนตามแบบ psr เลยครับ
ไม่ผูกติดของจริง อนาคตสบายแน่