SOLID ไม่ได้หมายถึงของแข็งแต่อย่างไร เป็นเพียงคำพ้อง เพื่อให้ง่ายต่อการจำ โดย คำ ๆ นี้ย่อมาจากหลักการ 5 ข้อ
SOLID ย่อมาจาก
> S — Single-responsibility Principle
> O — Open-closed Principle
> L — Liskov Substitution Principle
> I — Interface Segregation Principle
> D — Dependency Inversion Principle
ในภาษาโปรแกรมเชิงวัตถุ (Object-oriented programming) มี SOLID ที่เป็นแนวคิดยอดนิยม 5 ข้อ ที่มีจุดมุ่งหมายเพื่อที่ทำให้การดีไซน์ของซอฟต์แวร์มีความเข้าใจมากขึ้น ยึดหยุ่น และดูแลรักษาได้ โดยผู้ที่ได้นำเรื่องนี้มาเผยแพร่และมีชื่อเสียงคือ Robert C Martin
ก่อนที่จะไปเข้าใจ SOLID Principles มาเข้าใจปัญหาของการเขียนโปรแกรมในปัจจุบันก่อน โดยมีหลัก ๆ อยู่ 2 เรื่อง คือ
1. High Coupling
ลักษณะของซอฟต์แวร์ที่ผูกมัดกันแน่น ปัญหาคือหากตัวใดตัวหนึ่งพัง โปรแกรมอาจจะพังเป็นแบบโดมิโน และยากต่อการพัฒนา เพราะ 1 การแก้ไขส่งผลกับระบบในส่วนอื่น
2. Low Cohesion
ลักษณะซอฟต์แวร์ที่การทำงานเรื่องเดียวกัน ถูกกระจายไปทั่ว ทำให้ยากต่อการบำรุงรักษา ทำเรื่องเดียวแต่ต้องไปตามแก้หลาย ๆ module
ตามที่กล่าวไปข้างต้นแสดงให้เห็นถึงลักษณะของซอฟต์แวร์ที่ยากต่อการบำรุงรักษา Low Cohension และ High coupling และในอีกมุมนึงก็แสดงให้เห็นถึงแนวทางการออกแบบซอฟต์แวร์ที่ดี High Cohension และ Low coupling ตามรูปที่แสดงด้านล่าง
โดยบทบาทที่ SOLID เข้ามาเพื่อแนะแนวทางให้นักพัฒนาสามารถออกแบบซอฟต์แวร์ที่ดี ที่สามารถบำรุงรักษา ง่ายต่อการอ่าน มีความยืดหยุ่นต่อการเพิ่มขยาย เรามาทำความรู้จักแต่ละตัวไปพร้อมกันดีกว่า
Single-responsibility Principle (SRP)
คลาสนึงควรมีแค่เหตุผลเดียวที่จะแก้มัน หมายความว่า 1 คลาสควรทำแค่งาน ๆ เดียว อย่าเอาเรื่องอื่นมาปน
ตัวอย่าง สมมติเรามีรูปทรงที่หลากหลาย แล้วต้องการผลรวมพื้นที่ของรูปทรงทั้งหมด
อยากแรกคือการสร้าง class สำหรับรูปทรง ที่มีการส่งข้อมูลเฉพาะของรูปทรงนั้น ๆ ผ่าน constructor ถัดไปเราจะไปสร้าง AreaCalculator ที่จะถูกเขียนลอจิกที่เกี่ยวข้องกับการหาผลรวมของพื้นที่
การใช้งาน AreaCalculator เราจะสร้าง array ของรูปทรง แล้วให้แสดงผลรวมพื้นที่ผ่านทาง output
ปัญหาคือฟังก์ชัน output ของ AreaCalculator จัดการผลลัพธ์ที่จะออกมา แล้วจะทำยังไงถ้าผู้ใช้อยากได้ผลลัพธ์ออกมาเป็น json หรืออะไรอย่างอื่น?
ดังนั้นลอจิกที่ถูกจัดการด้วย AreaCalculator ขัดกับ SRP สุดท้าย class ควรจะเหลือเพียงการคำนวณผลรวมของรูปทรงก็พอ ไม่จำเป็นต้องสนใจว่าผู้ใช้ต้องการข้อมูลเป็น json หรือ HTML
ดังนั้นเพื่อแก้ปัญหาการจัดการการแสดง เราจะสร้าง class SumCalculatorOutputter เพื่อจัดการเรื่องรูปแบบการแสดงผลนั้นไป
SumCalculatorOutputter จะทำงานลักษณะนี้
ตอนนี้ ลอจิกอะไรก็ตามที่เกี่ยวกับการแสดงผล จะถูกจัดการผ่าน SumCalculatorOutputter
Open-closed Principle
Object ควรจะเปิดให้ขยายความสามารถต่อ ๆ ไปได้ แต่ไม่ควรไปแก้ไขโค้ดเดิม
ลองมาดูตัวอย่างของ AreaCalculator ที่มีฟังก์ชัน sum กัน
ถ้าเราต้องการให้ฟังก์ชัน sum สามารถหาผลรวมพื้นที่ของรูปทรงอื่น ๆ ได้ เราจะต้องมาเพิ่ม if/else หรือเปล่า? ถ้าใช่มันหมายถึงเรากำลังทำสิ่งที่ตรงกันข้ามกับ Open-closed Principle อยู่
แนวการในการจัดการ เพื่อให้โปรแกรมคำนวณผลรวมมีความยึดหยุ่นต่อรูปทรงใด ๆ คือการลบลอจิกในการคำนวณพื้นที่ของแต่ละรูปทรงออกจากฟังก์ชัน sum แล้วใส่ไปที่แต่ละรูปทรงแทน
เราจะทำแบบเดียวกันนี้กับ class Square เมื่อฟังก์ชัน area ถูกเพิ่มแล้ว ตอนนี้เพื่อให้เราสามารถคำนวณผลรวมพื้นที่ของรูปทรงได้ทุกประเภทอย่างง่ายดาย ที่ต้องทำก็แค่
ตอนนี้เราสามารถสร้างอีกรูปทรงแล้วส่งไปให้คำนวณโดยที่ไม่ต้องไปแก้โค้ดเดิมเลย 🙂 ยังไงก็ตามยังมีอีกปัญหาตามมา เราจะรู้ได้ยังไงว่า object ที่ถูกส่งให้ AreaCalculator เป็น shape จริง ๆ หรือ shape ที่มีฟังก์ชัน area?
การเขียน interface มีความจำเป็นมากในการที่เราจะใช้ SOLID มาดูตัวอย่างการทำ interface สำหรับทุก ๆ รูปทรงกัน
ใน AreaCalculator ฟังก์ชัน sum เราสามารถทำให้ class สามารถเช็คได้ว่ารูปทรงจะต้องเป็น ตัวแปร IShape เท่านั้น ด้วยการปรับ parameter ของ AreaCalculator ให้เป็น Ishape[]
ก็ทำให้เรามั่นใจว่ารูปทรงที่ส่งเข้ามามีฟังก์ชัน area
Liskov Substitution Principle
คลาสลูก (subclass) ทุกตัว สามารถเป็นตัวแทนให้คลาสแม่ (parent class) ได้ “แบบไม่แหกกฎแม่”
สมมติว่าวันนึงเราอยากได้ตัวคำนวณผลรวมของปริมาตร ซึ่งเราจะสืบทอดมาจาก AreaCalculator
ใน SumCalculatorOutputter
สิ่งที่เกิดขึ้นคือฟังก์ชัน sum ใน VolumeCalculator จะพังอยู่เนื่องจากไป break กฎของคลาสแม่นั่นคือ sum ของแม่ต้อง return กลับไปเป็น number แต่ sum ของ VolumeCalculator กลับเป็น array
แก้ไขด้วยการส่งข้อมูลที่ถูกต้องกลับไป
โดยที่ summedData คือข้อมูลประเภท number
Interface Segregation Principle
คลาสลูกจะต้องไม่ถูกบังคับให้ implement ฟังก์ชันที่ไม่ได้ใช้
แยก interface ออกเป็นหลาย ๆ ตัวเพื่อให้เหมาะสมกับหลาย ๆ งานแทนที่จะมีเพียง interface เดียวที่ครอบคลุม ปัญหาคือตัวคลาสลูกจะต้องมา implement method ที่ไม่จำเป็น ทำให้นักพัฒนาคนอื่นมาดูก็เกิดความไม่เข้าใจว่าทำไม override แล้วไม่เขียนอะไรใน method
ต่อจากตัวอย่างก่อนหน้า เรามีรูปทรง 3 มิติเกิดขึ้นมา ทำให้เราควรแยกการคำนวณปริมาตรมาไว้ที่รูปทรงด้วย เราจึงเพิ่มฟังก์ชัน volume() ไปที่ interface ที่ชื่อว่า IShape
ทุก ๆ รูปทรง จะต้อง implement ฟังก์ชัน volume แต่อย่างที่เรารู้ square เป็นรูปทรง 2 มิติและไม่จำเป็นต้องมี ฟังก์ชัน volume ดังนั้น interface กำลังบังคับให้ class Square ต้อง implement ฟังก์ชันที่ไม่จำเป็นต้องใช้
ISP บอกว่า แทนที่รวม interface เป็นก้อนเดียว ก็แยกมันออกมาเป็น ISolidShape ที่มี volume และรูปทรง 3 มิติ เช่น ลูกบาศก์ (cube) สามารถ implement interface นี้
Dependency Inversion Principle
เราควรขึ้นอยู่กับ abstraction ไม่ใช่ concreation — สิ่งที่เป็น high level ไม่ควรขึ้นอยู่กับสิ่งที่เป็น low level module
หลักการข้อนี้เป้าหมายคือเพื่อลดการผูกมัดกับการทำงานบางอย่าง (decoupling) อ่านแล้วงงละซิ ไปดูตัวอย่างกัน
จากโค้ดตัวอย่างจะเห็นว่า class รับข้อมูลเป็น MySQLConnection ตรงนี้เรามองว่าเป็น low level module และ PasswordReminder เป็น high level module
ลองนึกภาพดูว่าหากต้องเปลี่ยน Database เป็นเจ้าอื่นละ สิ่งที่จะเกิดขึ้นคือต้องมาแก้โค้ด ไม่ก็สร้าง duplicate code ส่งผลให้ break หลักการข้อ Open-closed Princicple ไปด้วย
แนวทางจัดการคือลองนำ interface มาใช้
ที่นี้ลองปรับ class ให้ติดต่อกับ interface ที่เราสร้างขึ้นใหม่แทน
ตอนนี้เราก็จะเห็นแล้วว่าทั้ง high level module (PasswordReminder) และ low level module (MySQLConnection) ขึ้นอยู่กับ abstraction ไปแล้ว (IDBConnection)
ครบหมดแล้วกับ SOLID Principles ทั้ง 5 ตัว ถือว่าเป็นแนวทางที่ควรค่าแก่การไปปรับใช้กับงานที่ทำอยู่ หากผิดพลาดอะไรขออภัยนะตรงนี้ หรือสงสัยอะไรพิมพ์ถามไว้ได้เลย จะตอบเท่าที่ตอบได้ครับ 😆 ขอบคุณครับ
ขอบคุณข้อมูลจาก
Special Thanks
ขอขอบคุณ “สำนักงานส่งเสริมเศรษฐกิจดิจิทัล (depa)” และคณาจารย์ “คณะเทคโนโลยีสารสนเทศ มจธ. (SIT)” ที่ให้การสนับสนุน “ทุนเพชรพระจอมเกล้าเพื่อพัฒนาเทคโนโลยีและนวัตกรรมดิจิทัล (KMUTT-depa)” ซึ่งเป็นทุนที่มอบความรู้ ทักษะและโอกาสดีๆ กับผมอย่างมาก~