SOLID หลักการพื้นฐานที่โปรแกรมเมอร์ควรรู้ (ตอนที่ 3 – LSP)
มาถึงหลักการข้อที่ 3 ของ SOLID Principles กันแล้วนะครับ และหลักการนี้มีชื่อว่า Liskov Substitution Principle (LSP) ซึ่งถูกตั้งตามชื่อของผู้ที่คิดหลักการนี้ขึ้นมาเป็นคนแรก Barbara Liskov [1] และในภายหลังก็ถูกนำมารวมอยู่ใน SOLID Principles ด้วย มาดูกันครับว่าหลักการนี้พูดถึงอะไรบ้าง
สารบัญสำหรับตอนอื่นๆ
- ตอนที่ 1 – Single Responsibility Principle (SRP)
- ตอนที่ 2 – Open Closed Principle (OCP)
- ตอนที่ 3 – Liskov Substitution Principle (LSP)
- ตอนที่ 4 – Interface Segregation Principle (ISP)
- ตอนที่ 5 – Dependency Inversion Principle (DIP)
Liskov Substitution Principle (LSP)
ในบทความของ Barbara Liskov ที่ถูกตีพิมพ์เมื่อปี 1987 หลักการนี้ได้กล่าวไว้ว่า
Let q(x) be a property provable about objects x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
ซึ่งในภายหลัง Robert C. Martin หรือ Uncle Bob ของพวกเราก็ให้คำนิยามที่ฟังดูง่ายขึ้นสำหรับหลักการนี้ใน SOLID Principles ว่า
Subtypes must be substitutable for their base types.
และนั่นก็หมายความว่า
ถ้าเรามีคลาส T1 ซึ่ง extend คลาส B แล้ว ที่ใดก็ตามที่มีคลาส B ถูกใช้งานอยู่จะต้องสามารถสับเปลี่ยนไปใช้คลาส T1 ได้โดยที่ต้องยังคงสามารถใช้งานได้เหมือนเดิม (ไม่มี Error)
มาถึงจุดนี้แล้วเจอกันแต่คำนิยามและทฤษฎีทั้งหลายก็ยังอาจจะงงๆ กันอยู่ เรามาลองดูตัวอย่างกันซะหน่อยดีกว่าครับ
เราเริ่มต้นกันด้วยคลาส OrderProcessor ซึ่งมีหน้าในที่การจัดการ การสั่งซื้อสินค้าที่เข้ามา และขั้นตอนหนึ่งที่ต้องทำก็คือเก็บข้อมูล รายการการสั่งซื้อสินค้า (Order Log) โดยเราจะสมมุติว่าในช่วงแรกเราจะบันทึกรายการทั้งหมดลงในไฟล์ CSV และใช้ Excel เปิดดูรายการทั้งหมด ก็จะได้คลาสประมาณนี้
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 |
class OrderProcessor{ protected $orderRepo; public function __construct(OrderRepositoryInterface $orderRepo) { $this->orderRepo = $orderRepo; } public function process(Order $order, $userId){ // ดำเนินการต่างๆ . . . // บันทึกรายการๆ สั่งซื้อสินค้า $this->orderRepo->logOrder($userId, $order, $order->price); } } interface OrderRepositoryInterface { public function logOrder($userId, Order $order, $amountPaid); } class CsvOrderRepository implements OrderRepositoryInterface { public function logOrder($userId, Order $order, $amountPaid) { // บันทึกข้อมูลลงไฟล์ในรูปแบบ CSV } } |
เรียกใช้งานโดย
1 2 3 4 5 6 7 8 |
$order = new Order(); $order->id = 1; $order->price = 200; $csvRepo = new CsvOrderRepository(); $processor = new OrderProcessor($csvRepo); $processor->process($order, 5); |
3 เดือนผ่านไปปรากฏว่าธุรกิจเราเจริญเติบโตขึ้น และต้องการที่จะเปลี่ยนการเก็บข้อมูลลง Database แทน ทำให้เราต้องเพิ่ม Class ใหม่ชื่อว่า DbOrderRepository
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class DbOrderRepository implements OrderRepositoryInterface{ protected $connection; public function connect($username, $password){ // สร้าง connection $this->connection = new DatabaseConnection($username, $password); } public function logOrder($userId, Order $order, $amountPaid) { $this->connection->run( 'INSERT INTO orders VALUE (?, ?, ?)', [ $userId, $order->id, $amountPaid ] ); } } |
และเนื่องจากคลาส DbOrderRepository ต้องใช้ Method ชื่อ connect ในการสร้างการเชื่อมต่อ (Connection) ทำให้เราต้องแก้คลาส OrderProcessor เป็น
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class OrderProcessor{ protected $orderRepo; public function __construct(OrderRepositoryInterface $orderRepo) { $this->orderRepo = $orderRepo; } public function process(Order $order, $userId){ // ดำเนินการต่างๆ . . . $this->orderRepo->connect('db_user01', 'password'); // บันทึกรายการๆ สั่งซื้อสินค้า $this->orderRepo->logOrder($userId, $order, $order->price); } } |
หลังจากนั้นเมื่อเราสลับ DbOrderRepository เข้าไปแทน CsvOrderRepository ตัว App ก็ยังคงสามารถใช้งานได้ปกติ
แต่เดี๋ยวก่อน…
แล้ว…ถ้าเรามีความจำเป็นต้องเปลี่ยนการบันทึกสินค้ากลับไปใช้ CsvOrderRepository ล่ะ… นั่นก็หมายความว่าตัวโปรแกรมก็จะเกิด Error ขึ้นทันทีเนื่องจากว่า CsvOrderRepository นั้นไม่มี Method ที่ชื่อ connect นั่นเอง
จากสถานการณ์ด้านบนเมื่อเราต้องการสลับ CsvOrderRepository และ DbOrderRepository ที่ Implement OrderRepositoryInterface เหมือนกันกันแต่ไม่สามารถทำได้โดยทันทีหรือต้องแก้โค้ดเพื่อให้โปรแกรมใช้งานได้นั่นก็คือการฝ่าฝืนหลักการ LSP นั่นเอง
เวอร์ชันหลัง Refactor
จากหลักการ LSP ที่เราต้องสามารถแทนที่หรือสลับ Class ที่เป็น Type เดียวกันซึ่งกันและกันได้ จะเห็นว่าปัญหาของเราก็จะอยู่ที่ Method connect และแน่นอนเราควรจะหาทางกำจัดการสร้าง Connection ของ Database ออกไปจาก OrderProcessor นั่นก็คือการย้ายการสร้าง Connection ออกไปไว้ในตัว DbOrderRepository นั่นเอง ซึ่งก็จะได้เป็น
// DbOrderRepository (v.1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class DbOrderRepository implements OrderRepositoryInterface{ protected $connection; public function connect($username, $password){ // สร้าง connection $this->connection = new DatabaseConnection($username, $password); } public function logOrder($userId, Order $order, $amountPaid) { $this->connect('db_user01', 'password'); $this->connection->run( 'INSERT INTO orders VALUE (?, ?, ?)', [ $userId, $order->id, $amountPaid ] ); } } |
จากโค้ดด้านบนจะทำให้เราสามารถลบ
1 |
$this->connect('db_user01', 'password'); |
ออกไปได้
และก็นั่นจะทำให้ OrderProcessor นั้นสามารถทำงานได้ไม่ว่าเราจะส่ง CsvOrderRepository หรือ DbOrderRepository เข้ามา ซึ่งนั่นก็จะเป็นไปตามหลัก LSP
เพิ่มเติม
อย่างไรก็แล้วแต่…จากโค้ดด้านบนยังคงมีบางส่วนที่ฝ่าฝืนหลัก SRP ซึ่งบอกว่าคลาสนั้นควรจะมีหน้าที่เดียวเท่านั้น แต่เราจะเห็นได้ว่าถึงจุดนี้ DbOrderRepository ของเราไม่ได้มีหน้าที่เพียงแค่การรับและเขียนข้อมูลกับฐานข้อมูลเท่านั้น แต่ยังมีหน้าที่ในการสร้างเชื่อมต่อกับฐานข้อมูลอีกด้วย ดังนั้นเราควรที่จะแยกหน้าที่ในการสร้างการเชื่อมต่อกับฐานข้อมูลไปไว้ที่ DatabaseConnector ได้ดังนี้
// DbOrderRepository (v.2)
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 |
class DbOrderRepository implements OrderRepositoryInterface{ protected $connector; public function __construct(DatabaseConnector $connector) { $this->connector = $connector; } public function connect($username, $password){ // สร้าง connection return $this->connector->bootConnection(); } public function logOrder($userId, Order $order, $amountPaid) { $connection = $this->connect(); $connection->run( 'INSERT INTO orders VALUE (?, ?, ?)', [ $userId, $order->id, $amountPaid ] ); } } |
บทสรุป
ในความเห็นส่วนตัวของผมหลักการข้อนี้จริงๆ แล้วเหมือนจะเป็นข้อควรระวังและเตือนใจซะมากกว่า ว่าการ Extend หรือ Implement คลาสใดๆ นั้นเราต้องคิดเสมอว่าถ้าหากเรา Override method ใดๆ แล้วการทำงานของ Method นั้นก็ยังต้องเป็นไปในทิศทางเดิม และเมื่อเราสลับ Class ใหม่ไปใช้แทนที่ๆ Class ที่มีอยู่เดิม การทำงานของโปรแกรมยังคงต้องเป็นเหมือนเดิมโดยที่ไม่ต้องเพิ่มโค้ดหรือ Logic ใดๆ เข้าไป
Pingback: SOLID หลักการพื้นฐานที่โปรแกรมเมอร์ควรรู้ (ตอนที่ 2 - OCP) - IKQ.ME()
Pingback: SOLID หลักการพื้นฐานที่โปรแกรมเมอร์ควรรู้ (ตอนที่ 1 - SRP)()
Pingback: SOLID หลักการพื้นฐานที่โปรแกรมเมอร์ควรรู้ (ตอนที่ 4 - ISP) - IKQ.ME()
Pingback: 5b4143efd6f5d2a20854899.com follow the link()
Pingback: d2a20854899.com()
Pingback: topfuckgals.mobi provided link()
Pingback: topfucksearch.mobi more detailed on this page r4bKW()
Pingback: freesexsite.mobi see more()
Pingback: new siriustube971 abdu23na6373 abdu23na23()