DEV Community

sherlyzi
sherlyzi

Posted on

SOLID Principle คือ?

Image description

หลักการ 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)
Enter fullscreen mode Exit fullscreen mode

ปัญหาก็คือ ในคลาสนี้มีการทำหลายหน้าที่เกินไป และหากมีการเปลี่ยนแปลงก็ต้องดูทั้งหมด

เช่น หากเปลี่ยน 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)
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

การเขียนแบบนี้ หากเรามีสัตว์ชนิดอื่นๆ เพิ่มมาในสวนสัตว์ก็ต้องแก้โค้ดส่วนนี้ไปตลอด ซึ่ง 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")
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

เมธอด 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.)
Enter fullscreen mode Exit fullscreen mode

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!"
Enter fullscreen mode Exit fullscreen mode

หากรวม 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()
Enter fullscreen mode Exit fullscreen mode

Image description

จากรูปจะเห็นว่า 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)