SOLID หลักการพื้นฐานที่โปรแกรมเมอร์ควรรู้ (ตอนที่ 2 – OCP)
หลังจากเราได้ทราบถึงหลักการแรกของ SOLID Principles กันไปแล้วนั่นก็คือ Single Responsibility (SRP) ซึ่งพูดถึงการกำหนดขอบเขตการทำงานและหน้าที่ของ Class ให้มีเพียงอย่างเดียว สำหรับบทความนี้เราจะมาดูหลักการที่ชื่อว่า Open Closed Principle (OCP) กันครับ
สารบัญสำหรับตอนอื่นๆ
- ตอนที่ 1 – Single Responsibility Principle (SRP)
- ตอนที่ 2 – Open Closed Principle (OCP)
- ตอนที่ 3 – Listkov Substitution Principle (LSP)
- ตอนที่ 4 – Interface Segregation Principle (ISP)
- ตอนที่ 5 – Dependency Inversion Principle (DIP)
Open Closed Principle (OCP)
หลักการ SOLID ข้อนี้ได้กล่าวไว้ว่า
code is open for extension but closed for modification
ซึ่งก็หมายความว่า
โค้ดนั้นต้องเปิดรับต่อส่วนขยาย แต่ปิดสำหรับการแก้ไข
หรือพูดง่ายๆ ก็คือ เมื่อเราเขียน Class หนึ่ง Class เสร็จแล้ว การเพิ่มความความสามารถใหม่ให้ Class นั้นต้องทำได้โดยที่ไม่ต้องมีการแก้ไข Class นั้นๆ เลย
มาลองดูโค้ดตัวอย่างกันดีกว่าครับเพื่อความเข้าใจที่ง่ายขึ้น โดยเราจะใช้ Class จากบทความที่แล้วเรื่อง SRP เป็นโค้ดเริ่มต้น ซึ่ง Class มีหน้าที่ในการจัดการ การสั่งซื้อสินค้า (Order) ตามด้านล่าง
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class OrderProcessor { protected $biller; protected $orderRepo; public function __construct(PaypalBiller $biller, OrderRepository $orderRepo) { $this->biller = $biller; $this->orderRepo = $orderRepo; } public function process(Order $order, $userId) { // ดึงค่าเปอร์เซนต์ส่วนลด $percentage = $this->orderRepo->getCategoryDiscountPercentage($order); // คำนวณราคาหลังจากหักส่วนลด (สมมุติว่าค่าที่ได้รับมาคือ 10% ดังนั้นคือ 0.1) $order->price = $order->price * (1 - $percentage); // เรียกเก็บเงิน $this->biller->bill($userId, $order->price); // บันทึกการสั่งซื้อ $this->orderRepo->logOrder($userId, $order, $order->price); } } |
สำหรับบทความนี้เราจะเพิ่ม Requirement ใหม่เข้าไปคือ การลดราคาสินค้าทุกชนิดอีก 15% เป็นเวลา 1 วันสำหรับวันวาเลนไทน์ ซึ่งนั่นก็เป็น Requirement ที่ไม่ได้ซับซ้อนอะไร สิ่งที่ต้องทำก็คือการเช็ควันและทำการลดราคาตามโค้ดด้านล่าง (บรรทัดที่ 21-27)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class OrderProcessor { protected $biller; protected $orderRepo; public function __construct(PaypalBiller $biller, OrderRepository $orderRepo) { $this->biller = $biller; $this->orderRepo = $orderRepo; } public function process(Order $order, $userId) { // ดึงค่าเปอร์เซนต์ส่วนลด $percentage = $this->orderRepo->getCategoryDiscountPercentage($order); // คำนวณราคาหลังจากหักส่วนลด (สมมุติว่าค่าที่ได้รับมาคือ 10% ดังนั้นคือ 0.1) $order->price = $order->price * (1 - $percentage); $now = Carbon::now(); // เช็คว่าวันนี้เป็นวาเลนไทน์หรือไม่ if($now->month === 2 && $now->day === 14){ // คำนวณราคา ลด 15% $order->price = $order->price * (1 - 0.15); } // เรียกเก็บเงิน $this->biller->bill($userId, $order->price); // บันทึกการสั่งซื้อ $this->orderRepo->logOrder($userId, $order, $order->price); } } |
เราอาจจะเห็นว่าโค้ดยังคงอ่านง่ายและไม่มีอะไรผิด แต่ถ้าในอนาคตเราต้องการที่จะมีส่วนลดเพิ่มเติมหรืออาจจะมี Business Logic ที่เพิ่มขึ้นจากการเติบโตของธุรกิจ เราจำเป็นที่จะต้องเพิ่ม Code เข้าไปเรื่อยๆ ใน Method นี้ สุดท้ายแล้วก็จะทำให้ Method นี้ก็มีความซับซ้อนเพิ่มขึ้นเรื่อยๆ ซึ่งทำให้ยากต่อการแก้ไข มากไปกว่านั้น Class นี้ยังฝ่าฝืนหลัก SRP อีกด้วยเนื่องจาก OrderProcessor ไม่ควรมีหน้าที่ในการคำนวณส่วนลด ดังนั้นตามหลัก SRP และ OCP ของ SOLID Principles แล้วเราควรหลีกเลี่ยงการแก้ไขโค้ดให้มากที่สุด เพราะการแก้ไขโค้ดนั้นถือว่าเป็นการเพิ่มความเสี่ยงให้ระบบเราเกิดข้อผิดพลาด
เริ่ม Refactor…
จากหลัก OCP ที่แล้วสิ่งที่เราจะทำคือการแยกหน้าที่ในการคำนวณส่วนลดออกไปจาก OrderProcessor และทำให้ OrderProcessor นั้นยืดหยุ่นต่อการเพิ่มหรือลดวิธีการคำนวณส่วนลด โดยเราจะสร้าง Class ขึ้นใหม่อีก 2 Class คือ CategoryDiscounter และ ValentineDiscounter
// CategoryDiscounter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class CategoryDiscounter{ private $orderRepo; public function __construct(OrderRepository $orderRepo) { $this->orderRepo = $orderRepo; } public function discount(Order $order){ // ดึงค่าเปอร์เซนต์ส่วนลด $percentage = $this->orderRepo->getCategoryDiscountPercentage($order); // คำนวณราคาหลังจากหักส่วนลด (สมมุติว่าค่าที่ได้รับมาคือ 10% ดังนั้นคือ 0.1) $order->price = $order->price * (1 - $percentage); return $order; } } |
// ValentineDiscounter
1 2 3 4 5 6 7 8 9 10 11 12 |
class ValentineDiscounter{ public function discount(Order $order){ $now = Carbon::now(); // เช็คว่าวันนี้เป็นวาเลนไทน์หรือไม่ if ($now->month == 2 && $now->day == 14) { // คำนวณราคา ลด 15% $order->price = $order->price * (1 - 0.15); } } } |
หลังจากนั้นก็แก้ไขให้ OrderProcessor รับ array ของ Class ที่ทำหน้าที่ในการคำนวณส่วนลดผ่าน Constructor
1 2 3 4 5 6 7 8 |
protected $discounters; public function __construct(PaypalBiller $biller, OrderRepository $orderRepo, array $discounters = []) { $this->biller = $biller; $this->orderRepo = $orderRepo; $this->discounters = $discounters; } |
เพิ่มเติม : สำหรับทั้งสอง Class ด้านบนจริงๆ แล้วเราควรเขียน Interface เพิ่ม โดยอาจจะตั้งชื่อว่า DiscounterInterface โดยมี public function discount(Order $order); แล้วให้ทั้งสอง Class นั้น Implements แต่สำหรับบทความนี้ขอข้ามเรื่อง Interface ไปง่ายต่อความเข้าใจครับ
และปรับแก้ไข Method process()
1 2 3 4 5 6 7 8 9 10 11 12 |
public function process(Order $order, $userId) { foreach($this->discounters as $discounter){ $discounter->discount($order); } // เรียกเก็บเงิน $this->biller->bill($userId, $order->price); // บันทึกการสั่งซื้อ $this->orderRepo->logOrder($userId, $order, $order->price); } |
มาลองเรียก Class ที่เราสร้างขึ้นมาดูกันครับ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// สร้างสินค้า $order = new Order(); $order->id = 56; $order->price = 25000; $order->category = 'electronic'; // สร้าง Class ที่ต้องใช้ (Dependencies) $paypalBiller = new PaypalBiller(); $orderRepository = new OrderRepository(); // สร้าง array ของ Class สำหรับคำนวณส่วนลด $discounters = [ new CategoryDiscounter($orderRepository), new ValentineDiscounter(), ]; $processor = new OrderProcessor( $paypalBiller, $orderRepository, $discounters ); // ดำเนินการสั่งซื้อสินค้า $processor->process($order, 5); |
เพิ่มเติม : โดยส่วนมากแล้ว Framework สมัยใหม่ต่างๆ จะใช้หลักการที่ชื่อว่า Dependency Injection ซึ่งจะทำให้เราไม่จำเป็นต้องทำการสร้าง Class และส่งเข้าไปเหมือนตัวอย่างด้านบน เช่น สำหรับ Laravel Framework การ Register ตัว Dependency ต่างๆ จะทำผ่าน Service Provider
หลังจากที่ได้ทำการ Refactor กันไปเป็นที่เรียบร้อยแล้วจะเห็นได้ว่าหากเรามี Logic สำหรับการลดราคาสิ่นค้าเพิ่มขึ้นในภายหลัง สิ่งที่เราต้องทำคือ
- สร้าง Class สำหรับการลดราคาใหม่ เช่น SongkranDiscounter
- เพิ่ม Class เข้าไปใน array
discounters
12345$discounters = [new CategoryDiscounter($orderRepository),new ValentineDiscounter(),new SongkranDiscounter(),];
ในทางกลับกันถ้าเราอยากยกเลิกส่วนลดไหนสิ่งที่เราต้องทำก็คือแค่ลบ Class นั้นๆ ออกจาก array เท่านั้นเอง
เพียงเท่านี้ Class ของเราตอนนี้ก็จะเป็นไปตามหลัก OCP ที่บอกว่า Class ของเรานั้นควรจะเปิดต่อการต่อเติม (นั่นก็คือการเพิ่ม Class การคำนวณส่วนลดได้ตามที่เราต้องการ) และปิดต่อการแก้ไข (ซึ่งการเพิ่มหรือลด Class สำหรับการคำนวณส่วนลดนั้นสามารถทำได้โดยที่ไม่ต้องแก้ไข OrderProcessor เลย)
มุมมอง TDD
สิ่งที่น่าสนใจก็คือเมื่อเราทำ Class ของเราให้ตามหลัก OCP แล้ว Class ของเราก็จะเป็นไปตามหลัก SRP ไปด้วยโดยอัตโนมัติ คือ CategoryDiscounter มีหน้าที่เดียวคือการคำนวณส่วนลดตาม Category ที่เรากำหนดไว้ หรือ ValentineDiscounter ก็มีหน้าที่เพียงลดราคาสินค้าสำหรับวันวาเลนไทน์ ซึ่ง! เมื่อหากเราต้องการเขียน Unit Test เราสามารถ Focus เฉพาะหน้าที่ของ Class นั้นๆ ได้อย่างง่าย เช่น แทนที่เราต้องเขียน Test Scenario ในการเช็คว่าราคาสินค้าถูกลดแล้วหรือยังใน OrderProcessorTest เราสามารถไปแยกเขียนตามเฉพาะ CategoryDiscounterTest และ ValentineDiscounterTest ได้เลย
บทสรุป
ในความคิดเห็นส่วนตัวหลัก SRP และ OCP นี้เป็นหลักการพื้นฐานที่เราควรจะทำตามตลอดไม่ว่าระบบที่เราพัฒนาจะใหญ่หรือจะเล็ก เนื่องจากสองหลักการนี้เป็นหลักการที่ทำตามได้ไม่ยากและให้ผลลัพธ์ค่อนข้างชัดเจนทั้งในเรื่องของความง่ายในการในการอ่านโค้ด (Code Readability) และการเขียน Test
สำหรับผู้ที่รู้สึกว่าชื่อหลักการดูเหล่านี้ของ SOLID Principles ดูค่อนข้างน่ากลัวหรือคิดว่าการทำตามนั้นดูยุ่งยากก็ไม่ถือว่าเป็นเรื่องแปลกครับ ลองค่อยๆ ใจเย็นๆ ค่อยๆ อ่านดูครับ การที่จะเข้าใจหลักการเหล่านี้ไม่ได้จำเป็นที่เราต้องเข้าใจตั้งแต่ครั้งแรกที่อ่าน แต่ก็ต้องอาศัยการคิดหรือค้นคว้าเพิ่มเติมและการที่เราได้เจอสถานการณ์จริงๆ ด้วย
สำหรับใครที่มีข้อสงสัย คำแนะนำ หรือว่าจะแชร์เรื่องอื่นๆ สามารถทิ้งข้อความไว้ด้านล่างได้เลยนะครับ ลองมาคุยและแลกเปลี่ยนความคิดเห็นเพื่อที่จะทำให้ตัวเราและสังคมโปรแกรมเมอร์ของเราพัฒนาไปพร้อมๆ กันครับ
Pingback: SOLID หลักการพื้นฐานที่โปรแกรมเมอร์ควรรู้ (ตอนที่ 1 - SRP)()
Pingback: SOLID หลักการพื้นฐานที่โปรแกรมเมอร์ควรรู้ (ตอนที่ 3 - LSP) - IKQ.ME()
Pingback: SOLID หลักการพื้นฐานที่โปรแกรมเมอร์ควรรู้ (ตอนที่ 4 - ISP) - IKQ.ME()
Pingback: SOLID หลักการพื้นฐานที่โปรแกรมเมอร์ควรรู้ (ตอนที่ 5 - DIP) - IKQ.ME()