หลักการ SOLID เป็นหลักการที่ใช้ในการออกแบบซอฟต์แวร์ ความสามารถที่เด่นชัดที่สุดคือสามารถลด coupling ลงได้
Coupling คือการที่คลาสหลายๆ คลาสพึ่งพากัน (dependent) หรือมีความเกี่ยวข้องกันมากเกินไป ทำให้ซอฟต์แวร์ยากต่อการเปลี่ยนแปลง เพราะจะเปลี่ยนอย่างหนึ่งก็ต้องไปหาว่าสิ่งที่เราแก้ไปถูกใช้ที่ไหนอีกบ้าง แล้วก็แก้ต่อไปเรื่อยๆ เป็นทอดๆ
สิ่งสำคัญที่ต้องคำนึงถึงเวลาพัฒนาซอฟต์แวร์ขึ้นมาให้บำรุงรักษาง่ายคือเราต้องมี Coupling น้อยๆ และ Cohesion เยอะๆ
Cohesion คือความสอดคล้องกันภายในคลาส ถ้าเราแก้เรื่องเดียวกัน ก็ควรจะหาได้จากคลาสหรือโมดุลเดียวกัน
SOLID เป็นตัวย่อของหลักการทั้งหมด 5 หลักการที่ใช้จัดการซอฟต์แวร์ให้มี Coupling น้อยๆ และ Cohesion เยอะๆ ได้ดีเยี่ยม ให้ซอฟต์แวร์ของเราอ่านง่ายและบำรุงรักษาง่าย
แต่ละตัวอักษรมาจากแต่ละหลักการดังนี้
S: Single Responsibility Principle
O: Open-Closed Principle
L: Liskov Substitution Principle
I: Interface Segregation Principle
D: Dependency Inversion Principle
1) Single Responsibility Principle (SRP)
โดยทั่วไปมักอธิบายหลักการนี้ไว้ว่า “แต่ละโมดุลควรมีเพียงเหตุผลเดียวที่จะเปลี่ยนแปลง” ว่าง่ายๆ คือให้มันทำงานแค่งานเดียวพอ อย่าเอาอย่างอื่นมาปน
ในขณะที่ซอฟต์แวร์มักจะเปลี่ยนแปลงเพราะผู้ใช้งานและผู้มีส่วนได้ส่วนเสียเสมอ ซึ่งมักจะเรียกแต่ละกลุ่มของผู้ใช้งานหรือผู้มีส่วนได้ส่วนเสียว่า actor ดังนั้นจากหนังสือ Clean Architecture ของ Robert C. Martin ยังสามารถกล่าวอีกได้ว่า “แต่ละโมดูลควรรับผิดชอบแค่หนึ่ง actor”
ตัวอย่างเช่น มีคลาส Book ทำสองหน้าที่คือจัดการข้อมูลหนังสือและจัดการ file storage
class Book:
def __init__(self, title, author, content):
self.title = title
self.author = author
self.content = content
def get_book_info(self):
return f"Title: {self.title}, Author: {self.author}"
def save_to_file(self, filename):
with open(filename, 'w') as file:
file.write(self.content)
ปัญหาก็คือ ในคลาสนี้มีการทำหลายหน้าที่เกินไป และหากมีการเปลี่ยนแปลงก็ต้องดูทั้งหมด
เช่น หากเปลี่ยน file storage ก็ต้องเปลี่ยนคลาส Book ด้วย ซึ่งละเมิด SRP
นอกจากนี้ การพัฒนาโดยไม่คำนึงถึง SRP ก็จะได้ซอฟต์แวร์ที่ทำความเข้าใจยากและไม่มี modularity
Modularity คือการที่ซอฟต์แวร์ถูกแบ่งเป็นส่วนย่อยๆ เพื่อลดความซับซ้อนลง และทำงานกับแต่ละโมดูลได้โดยไม่ส่งผลกระทบต่อส่วนอื่น สำคัญมากในการทำงานแบบทีม
คลาส Book สามารถแก้ไขได้เป็น
# Book class handles only the book's data
class Book:
def __init__(self, title, author, content):
self.title = title
self.author = author
self.content = content
def get_book_info(self):
return f"Title: {self.title}, Author: {self.author}"
# BookPersistence class handles saving the book to a file
class BookPersistence:
def __init__(self, book: Book):
self.book = book
def save_to_file(self, filename):
with open(filename, 'w') as file:
file.write(self.book.content)
2) Open/Closed Principle (OCP)
หลักการนี้กล่าวไว้ว่า “คลาสควรเปิดให้ขยายความสามารถ (Open for extension) แต่ปิดรับการเปลี่ยนแปลง (Closed for modification)” หมายความว่าเราควรเพิ่มฟังก์ชันใหม่ ๆ ได้โดยไม่ต้องแตะโค้ดเดิม
ตัวอย่างเช่น คลาส Feeder ควรจะมีวิธีแยกว่าสัตว์แต่ละชนิดต้องให้อาหารอย่างไร
class Feeder:
def feed_animal(self, animal: Animal):
if animal.species == "dog":
print(f"Feeding {animal.name} the {animal.species} with meat")
elif animal.species == "cat":
print(f"Feeding {animal.name} the {animal.species} with fish")
else:
print(f"Feeding {animal.name} the {animal.species} with generic food")
การเขียนแบบนี้ หากเรามีสัตว์ชนิดอื่นๆ เพิ่มมาในสวนสัตว์ก็ต้องแก้โค้ดส่วนนี้ไปตลอด ซึ่ง OCP สามารถนำมาจัดการได้ โดยแทนที่จะแก้คลาส Feeder ซ้ำๆ เราจะ extend คลาสนี้แทน โดยจะมีคลาส DogFeeder และ CatFeeder
class Feeder:
def __init__(self, name):
self.name = name
def feed_animal(self, animal: Animal):
print(f"Feeding {animal.name} the {animal.species} with generic food")
class DogFeeder(Feeder):
def feed_animal(self, animal: Dog):
print(f"Feeding {animal.name} the dog with meat")
class CatFeeder(Feeder):
def feed_animal(self, animal: Cat):
print(f"Feeding {animal.name} the cat with fish")
3) Liskov Substitution Principle (LSP)
หลักการนี้กล่าวว่า “ซับคลาสหรือคลาสลูกต้องสามารถแทนที่ซูเปอร์คลาสหรือคลาสแม่ได้โดยไม่ทำให้การทำงานของโปรแกรมผิดพลาด” คือซับคลาสต้องสามารถทำงานแทนที่ซูเปอร์คลาสได้อย่างสมบูรณ์
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
raise NotImplementedError("Subclass must implement abstract method")
class Dog(Animal):
def speak(self):
return f"{self.name} says Woof!"
class Cat(Animal):
def speak(self):
return f"{self.name} says Meow!"
# Usage
def animal_sound(animal: Animal):
return animal.speak()
dog = Dog("Buddy")
cat = Cat("Whiskers")
print(animal_sound(dog)) # Output: Buddy says Woof!
print(animal_sound(cat)) # Output: Whiskers says Meow!
เมธอด animal_sound(animal: Animal) นี้แสดงให้เห็นว่าซับคลาสของคลาส Animal สามารถถูกใช้งานได้เมื่อใดก็ตามที่ต้องใช้งานออบเจกต์ของคลาส Animal
หากซับคลาสไม่สามารถแทนที่ซูเปอร์คลาสได้โดยสมบูรณ์ เมื่อใช้งานออบเจกต์ของซับคลาสในที่ที่ต้องการออบเจกต์ของซูเปอร์คลาส อาจเกิดพฤติกรรมที่ไม่พึงประสงค์หรือ error
เช่น
class Bird:
def __init__(self, name):
self.name = name
def fly(self):
print("Bird is flying.)
class Sparrow(Bird):
def fly(self):
print("Sparrow is flying.)
class Ostrich(Bird):
def fly(self):
print("Ostrich is flying.)
Ostrich บินไม่ได้ ดังนั้นการที่ Ostrich สืบทอด Bird ถือเป็นการฝ่าฝืน LSP
4) Interface Segregation Principle (ISP)
หลักการนี้กล่าวว่า “คลาสไม่ควรต้อง implement interface ที่ไม่เกี่ยวข้องกับมัน” คือ ควรแยกอินเตอร์เฟสให้เป็นส่วน ๆ ที่จำเป็นต่อการใช้งานจริง ไม่ควรมีอินเตอร์เฟสที่ใหญ่เกินไปและมีฟังก์ชันการทำงานมากเกินจำเป็น
from abc import ABC, abstractmethod
# Flyer interface
class Flyer(ABC):
@abstractmethod
def fly(self):
pass
# Swimmer interface
class Swimmer(ABC):
@abstractmethod
def swim(self):
pass
class Duck(Flyer, Swimmer):
def fly(self):
return "Duck is flying!"
def swim(self):
return "Duck is swimming!"
class Bird(Flyer):
def fly(self):
return "Bird is flying!"
หากรวม Swimmer และ Flyer เข้าด้วยกัน คลาส Bird ที่ต้อง implement แค่ fly() เมธอดเดียวจะถูกบังคับให้ implement swim() ด้วย ซึ่ง Bird ทำไม่ได้
5) Dependency Inversion Principle (DIP)
หลักการนี้กล่าวว่า “โมดูลระดับสูงไม่ควรพึ่งพาโมดูลระดับต่ำ แต่ทั้งสองควรพึ่งพา abstraction (อินเตอร์เฟสหรือ abstract class)”
ในตัวซอฟต์แวร์ของเรานั้นมี High-level components และ Low-level components
High-level components ควรจะนำมาใช้ซ้ำๆ ได้ และไม่ได้รับผลกระทบจาก Low-level components ว่าง่ายๆ ก็คือ เรา decouple source code ของเราได้แล้ว High-level components ของเราเป็นอิสระจาก Low-level components แล้ว
ตัวอย่างเช่น มีคลาส Overseer เป็น High-level component ถ้าออกแบบกันตามปกติ Overseer จะมีความสัมพันธ์โดยตรงกับคลาส Zookeeper
การทำแบบนี้จะทำให้ High-level component พึ่งพา Low-level component ถ้าจะเปลี่ยนแปลง Zookeeper ทีนึง Overseer ก็จะได้รับผลกระทบไปด้วย การทำแบบนี้ก็เลยจะทำให้ซอฟต์แวร์ของเรามี coupling เยอะมาก
แต่ถ้าเราสร้าง Employee interface ขึ้นมา ให้ Zookeeper implement interface นี้แทน และ Overseer เรียก Employee แทน
ตอนนี้ Overseer ไม่จำเป็นต้องพึ่งพาคลาส Zookeeper แล้ว
from abc import ABC, abstractmethod
class Employee(ABC):
@abstractmethod
def work(self):
pass
class Zookeeper(Employee):
def work(self):
print("Zookeeper is working.")
class Overseer:
def __init__(self, employee: Employee):
self.employee = employee
def oversee_work(self):
print("Overseer is delegating tasks.")
self.employee.work()
จากรูปจะเห็นว่า Overseer เรียกฟังก์ชัน work() ในคลาส Zookeeper ผ่าน Employee interface
ตอนรันไทม์จริงๆ interface จะถือว่าไม่มีตัวตน ดังนั้น Overseer เรียกฟังก์ชัน work() ในคลาส Zookeeper ได้แล้ว แม้จะไม่ใช่วิธีตรงๆ แต่วิธีนี้จะช่วยลด coupling ของซอฟต์แวร์เรา
References
Dependency Inversion
Solid Principle
Clean Architecture by Robert C. Martin
Coupling and Cohesion
Top comments (0)