Принципы SOLID — это набор из пяти принципов проектирования, которые могут помочь разработчикам создавать программное обеспечение, которое легче поддерживать, расширять и тестировать. Эти принципы были впервые представлены Робертом С. Мартином, также известным как «дядя Боб», в его книге «Гибкая разработка программного обеспечения, принципы, шаблоны и практика». ».

В этом руководстве мы объясним каждый из принципов SOLID и предоставим реальные примеры кода, чтобы продемонстрировать, как их можно применять.

Принцип единой ответственности (SRP)

Принцип единственной ответственности гласит, что у класса должна быть только одна причина для изменения.

Это означает, что у каждого класса должна быть одна обязанность или одна работа, и он должен выполнять ее хорошо.

Следуя этому принципу, мы можем гарантировать, что наши классы легче поддерживать и что изменения в одном классе не повлияют на другие части нашей кодовой базы.

Перед использованием SRP:

class Employee:
    def calculate_pay(self):
        pass

    def save_employee(self):
        pass

    def print_employee_report(self):
        pass

После применения СРП:

class Employee:
    def calculate_pay(self):
        pass


class EmployeeSaver:
    def save_employee(self):
        pass


class EmployeeReportPrinter:
    def print_employee_report(self):
        pass

В примере «До использования SRP» класс Employee выполняет несколько функций: расчет заработной платы, сохранение данных о сотрудниках и печать отчетов о сотрудниках.

Это нарушает SRP, поскольку затрудняет поддержку и повторное использование класса.

В примере «После применения SRP» мы разделили каждую ответственность на отдельный класс, что делает код более удобным для сопровождения и повторного использования.

Принцип открытия/закрытия (OCP)

Принцип открытости/закрытости (OCP) гласит, что объекты программного обеспечения (классы, модули, функции и т. д.) должны быть открыты для расширения, но закрыты для модификации.

Другими словами, вы должны иметь возможность добавлять новые функции в программную систему без изменения существующего кода.

Следуя этому принципу, мы можем сделать наш код более гибким и простым в обслуживании.

Перед использованием ОКП:

class Shape:
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * self.radius ** 2

После применения ОКП:

class Shape:
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * self.radius ** 2

class Triangle(Shape):
    def __init__(self, base, height):
        self.base = base
        self.height = height

    def area(self):
        return 0.5 * self.base * self.height

В примере перед использованием OCP, если мы хотим добавить новую фигуру, мы должны изменить класс Shape, добавив новый метод для вычисления площади этой фигуры. Это нарушает ОКП.

В примере «После применения OCP» мы расширили класс Shape, добавив новый класс для новой формы, Triangle. Это делает код более расширяемым и простым в обслуживании.

Принцип замещения Лискова (LSP)

Принцип подстановки Лисков гласит, что подкласс должен иметь возможность использоваться вместо своего родительского класса, не влияя на корректность программы.

Это означает, что подклассы не должны изменять поведение своих родительских классов неожиданным образом.

Следуя этому принципу, мы можем сделать наш код более гибким и простым в обслуживании.

Перед использованием ЛСП:

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def get_width(self):
        return self.width

    def set_width(self, width):
        self.width = width

    def get_height(self):
        return self.height

    def set_height(self, height):
        self.height = height

    def area(self):
        return self.width * self.height

class Square(Rectangle):
    def __init__(self, side):
        self.width = side
        self.height = side

    def set_width(self, width):
        self.width = width
        self.height = width

    def set_height(self, height):
        self.height = height
        self.width = height

После применения ЛСП:

class Shape:
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Square(Shape):
    def __init__(self, side):
        self.side = side

    def area(self):
        return self.side ** 2

В примере «До использования LSP» класс Square является подклассом класса Rectangle, но он не следует LSP, поскольку переопределяет методы set_width и set_height, которые ведут себя иначе, чем в классе Rectangle. Это может вызвать проблемы при использовании класса Square вместо класса Rectangle.

В примере «После применения LSP» мы исправили это, создав новый класс для формы Square, который не наследуется от класса Rectangle. Это гарантирует, что LSP не будет нарушен.

Принцип разделения интерфейсов (ISP)

Интернет-провайдер заявляет, что клиенты не должны зависеть от методов, которые они не используют. Это означает, что интерфейсы должны соответствовать потребностям каждого клиента.

Следуя этому принципу, мы можем сделать наш код более гибким и простым в обслуживании.

Перед использованием провайдера:

class Vehicle:
    def drive(self):
        pass

    def fly(self):
        pass

class Car(Vehicle):
    def drive(self):
        pass

class Airplane(Vehicle):
    def fly(self):
        pass

После обращения к провайдеру:

class Drivable:
    def drive(self):
        pass

class Flyable:
    def fly(self):
        pass

class Car(Drivable):
    def drive(self):
        pass

class Airplane(Flyable):
    def fly(self):
        pass

В примере «До использования ISP» класс Vehicle имеет два метода, drive и fly, которые не используются всеми его подклассами. Это нарушает ISP, поскольку заставляет клиентов класса Vehicle зависеть от методов, которые они не используют.

В примере После применения ISP мы разделили методы на два отдельных интерфейса, Drivable и Flyable, и сделали подклассы зависимыми только от тех интерфейсов, которые им нужны.

Принцип инверсии зависимостей (DIP)

DIP утверждает, что модули высокого уровня не должны зависеть от модулей низкого уровня. Вместо этого оба должны зависеть от абстракций.

Это означает, что зависимости должны быть инвертированы, а абстракции должны использоваться для разделения высокоуровневых и низкоуровневых модулей.

Следуя этому принципу, мы можем сделать наш код более гибким и простым в обслуживании.

Давайте рассмотрим гипотетический пример, где у нас есть система обработки платежей. Эта система принимает платежи с помощью кредитной карты, банковского перевода и услуг мобильных платежей. Каждый способ оплаты имеет собственную реализацию, и система обработки платежей отвечает за организацию платежного потока.

Перед применением ДИП:

class CreditCardPayment:
    def process(self, amount):
        # process the payment via credit card

class BankTransferPayment:
    def process(self, amount):
        # process the payment via bank transfer

class MobilePayment:
    def process(self, amount):
        # process the payment via mobile payment service

class PaymentProcessor:
    def process_payment(self, payment_method, amount):
        if payment_method == "credit_card":
            payment = CreditCardPayment()
        elif payment_method == "bank_transfer":
            payment = BankTransferPayment()
        elif payment_method == "mobile_payment":
            payment = MobilePayment()

        payment.process(amount)

В приведенной выше реализации класс PaymentProcessor зависит от конкретных реализаций способов оплаты (CreditCardPayment, BankTransferPayment и MobilePayment). Это нарушает DIP, так как высокоуровневый модуль PaymentProcessor зависит от низкоуровневых модулей способа оплаты.

Чтобы исправить это, нам нужно инвертировать зависимость, чтобы высокоуровневый модуль зависел от абстракции (интерфейса или абстрактного класса), а не от конкретной реализации методов оплаты.

После применения ДИП:

class PaymentMethod(ABC):
    @abstractmethod
    def process(self, amount):
        pass

class CreditCardPayment(PaymentMethod):
    def process(self, amount):
        # process the payment via credit card

class BankTransferPayment(PaymentMethod):
    def process(self, amount):
        # process the payment via bank transfer

class MobilePayment(PaymentMethod):
    def process(self, amount):
        # process the payment via mobile payment service

class PaymentProcessor:
    def process_payment(self, payment_method: PaymentMethod, amount: float):
        payment_method.process(amount)

В приведенной выше реализации мы инвертировали зависимость, введя абстракцию PaymentMethod. Высокоуровневый модуль PaymentProcessor теперь зависит от абстракции PaymentMethod, а не от конкретных реализаций способов оплаты.

Классы CreditCardPayment, BankTransferPayment и MobilePayment теперь наследуются от класса PaymentMethod и предоставляют собственную реализацию метода process.

Придерживаясь DIP, мы можем легко заменить реализацию методов оплаты, не затрагивая класс PaymentProcessor. Это делает код более модульным, его легче тестировать и поддерживать.

Заключение

В этом руководстве мы представили принципы SOLID и предоставили реальные примеры кода для иллюстрации каждого принципа.

Следуя этим принципам, мы можем создавать более гибкий, удобный и масштабируемый код. Хотя для реализации принципов SOLID может потребоваться некоторое время и усилия, они в конечном итоге приводят к созданию более надежной кодовой базы.

Дополнительные материалы на PlainEnglish.io.

Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .

Заинтересованы в масштабировании запуска вашего программного обеспечения? Ознакомьтесь с разделом Схема.