При разработке программы некоторые программисты склонны просто писать код по ходу дела, не задумываясь о том, как они написали программу. Для этого есть разные причины, такие как ограниченное время (это, скорее всего, основная причина), лень, сложность уже сделанной программы или забывчивость, список можно продолжить. В краткосрочной перспективе этот подход кажется наиболее приятным способом написания кода, но в долгосрочной перспективе понимание кода может быть довольно утомительным.

Как решить эту проблему?

Проблема в том, как мы будем структурировать нашу программу. Структурирование является наиболее важным и сложным, поскольку мы не можем сказать, как лучше всего структурировать наш код. Это связано с тем, что мы придумывали решения и идеи постепенно, поэтому недостающих строительных блоков еще не было, когда мы впервые написали это. Итак, нам нужно что-то, что может обрабатывать динамику и изменения, которые происходят, когда мы пишем программу.

Проще всего было бы следовать определенным утверждениям, которые могут действовать как система убеждений для рассуждений. Хорошие новости для нас: некоторые люди (Робер С. Мартин) уже думали об этих предложениях, и одно из них — SOLID Principle.

ТВЕРДЫЙ?

SOLID — хорошая аббревиатура, потому что ее легко запомнить и согласовать с нашей целью — прочным структурным кодом. Давайте рассмотрим пошагово, начиная с S, принципа единой ответственности.

Единая ответственность

из определения, мы бы имели.

У класса должна быть одна и только одна причина для изменения

Прежде всего, нам нужно понять часть изменения, что означает изменение? изменения здесь относятся к функциональности, поведению или реализации класса. Вторая часть — это причина, это причина, из-за которой класс изменился. поэтому нам нужна только причина того же типа, которая вызвала изменение класса. Если у нас их было больше одного, то мы нарушаем принцип единой ответственности.

Давайте посмотрим на пример.

class User:
  def __init__(self, name, email,database):
    self.name = name
    self.email = email
    self.database = database
  
  def send_email(self, message):
    # code to send email
    email.connect()
    email.send(message)
  
  def save_to_database(self):
    # code to save user data to database
    self.database.connect()
    self.database.save(name)

На первый взгляд может показаться нормальным, чтобы этот пользовательский класс имел методы send_email и save_to_database. Но что, если в классе электронной почты есть изменения? тогда пользовательскому классу также необходимо изменить метод send_email, что является одной причиной. Далее, класс базы данных также изменится, пользователю также необходимо изменить метод save_to_database, что является двумя причинами. Представьте, если бы мы сделали это не только с пользователем, но и с другими классами, это стало бы сильно связанным, и небольшие изменения вызывают изменение всего кода.

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

class User:
  def __init__(self, name, email):
    self.name = name
    self.email = email

class EmailManager:
  def __init__(self):
    # code to initialize email manager
    pass
  def send_email(self, message, recipient):
    # code to send email
    pass

class DatabaseManager:
  def __init__(self):
    # code to initialize database manager
    pass
  def save_to_database(self, data):
    # code to save data to database
    pass

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

Открыто закрыто

Объекты или сущности должны быть открыты для расширения, но закрыты для модификации.

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

Вот пример кода, нарушающего принцип Open Closed.

class Shape:
    def __init__(self, x, y):
        self.x = x
        self.y = y

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

class Square(Shape):
    def __init__(self, x, y, side_length):
        super().__init__(x, y)
        self.side_length = side_length

    def calculate_area(shapes):
        total_area = 0
        for shape in shapes:
          if isinstance(shape, Circle):
              total_area += 3.14 * shape.radius ** 2
          elif isinstance(shape, Square):
              total_area += shape.side_length ** 2
        return total_area

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

class Shape:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def area(self):
            pass

class Circle(Shape):
    def __init__(self, x, y, radius):
        super().__init__(x, y)
        self.radius = radius
    def area(self):
        return 3.14 * self.radius ** 2

class Square(Shape):
    def __init__(self, x, y, side_length):
        super().__init__(x, y)
        self.side_length = side_length
    def area(self):
        return self.side_length ** 2
    def calculate_area(shapes):
        total_area = 0
        for shape in shapes:
            total_area += shape.area()
        return total_area

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

Этот подход уменьшает количество изменений, необходимых при добавлении новых функций и скрытых ошибок, которые могут проявиться (представьте, что мы забыли добавить оператор if else в calculate_area).

Замена Лискова

Пусть q(x) — доказуемое свойство объектов x типа T. Тогда q(y) должно быть доказуемо для объектов y типа S, где S — подтип T.

Прежде всего, что такое собственность? Свойство в основном является утверждением, которое может быть выражено как истинное или ложное, в зависимости от значения x, или может быть вызвано как предикат. В контексте программирования q(x) будет методом. Что такое х? х является объектом. тип T будет классом.

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

Например, утверждение «каждое нечетное число является простым» верно для 3, 5, 7, но не для 9. Следовательно, это утверждение недоказуемо.

По сути, это утверждение говорит о том, что метод, применимый к объекту x, также должен применяться ко всем объектам, являющимся подтипом T.

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

class Square(Rectangle):
    def __init__(self, side_length):
        super().__init__(side_length, side_length)
    def set_width(self, width):
        self.width = width
        self.height = width
    def set_height(self, height):
        self.width = height
        self.height = height

Проблема заключается в свойстве get_area ( методе ). Не каждая формула площади прямоугольника представляет собой ширину * высоту, поэтому это свойство недоказуемо. Поскольку это так, это не применимо к квадрату, даже несмотря на то, что квадрат является подтипом прямоугольника, поскольку площадь квадрата равна либо ширине * ширине, либо высоте * высоте.

Решением будет создание формы интерфейса, которая имеет свойство площади. затем Rectangle и Square реализуют свойство площади.

Разделение интерфейса

Клиента никогда не следует заставлять реализовывать интерфейс, который он не использует, или клиентов нельзя заставлять зависеть от методов, которые они не используют.

На что ссылается клиент? в этом контексте клиент означает класс или интерфейс, который реализует интерфейс. Поскольку это довольно просто, мы сразу перейдем к примеру кода.

class Animal:
    def speak(self):
        pass
    def fly(self):
            pass

class Dog(Animal):
    def speak(self):
        return "Woof!"

class Bird(Animal):
    def speak(self):
        return "Chirp!"
    def fly(self):
        return "I'm flying!"

class Fish(Animal):
    def speak(self):
        return "Blub!"
d = Dog()
print(d.speak()) # Output: "Woof!"
b = Bird()
print(b.fly()) # Output: "I'm flying!"
f = Fish()
print(f.fly()) # Error: 'Fish' object has no attribute 'fly'

Этот код нарушает разделение интерфейса. Причина в том, что класс dog вынужден реализовывать интерфейс, который ему не нужен.

Один из способов исправить это — разделить метод fly на другой класс (например, FlyingAnimal).

Инверсия зависимости

Сущности должны зависеть от абстракций, а не от конкретики.

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

Эти концепции очень похожи на математику, где вы определяете x как переменную, которая хранит значение как число. Вместо того, чтобы думать о 1,2,3,… вы думаете о его свойствах, то есть вы можете складывать, вычитать, делить и так далее.

Таким образом, намного проще изменить реализацию, так как мы заботимся только о ее свойствах. Чтобы было проще, давайте рассмотрим пример.

import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

class EmailSender:
    def send_email(self, to_address, subject, body):
        msg = MIMEMultipart()
        msg['To'] = to_address
        msg['Subject'] = subject
        msg.attach(MIMEText(body, 'plain'))
        smtp_server = smtplib.SMTP('smtp.gmail.com', 587)
        smtp_server.starttls()
        smtp_server.login('username', 'password')
        smtp_server.sendmail('username', to_address, msg.as_string())
        smtp_server.quit()

class UserService:
    def __init__(self):
        self.email_sender = EmailSender()
    def create_user(self, username, email, password):
        # Create the user...
        # Send a welcome email
        subject = "Welcome to MySite"
        body = f"Dear {username}, welcome to MySite!"
        self.email_sender.send_email(email, subject, body)

Вы можете видеть, что UserService напрямую использует конкретную реализацию EmailSender, если мы хотим изменить, используя другую электронную почту, это может быть довольно сложно изменить. Чтобы справиться с этим, мы сначала должны создать интерфейс EmailSender и позволить UserService использовать этот интерфейс.

Заключение…

Принципы SOLID можно использовать в качестве руководства, когда мы пишем наш код. Нам не нужно думать, как правильно структурировать код, нам просто нужно убедиться, что наш код не нарушает принципов. Если мы будем следовать этим принципам, структура будет возникать, и иногда эта структура настолько распространена, что имеет шаблон. Это называется шаблон проектирования. Шаблон проектирования должен быть нашей следующей темой для изучения после понимания принципов, чтобы мы могли понять обоснование этих шаблонов.

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

  • Единая ответственность, нужно ли моему коду более одной причины для изменения?
  • Open Closed, требует ли мой код новых изменений, когда я добавляю новую реализацию?
  • Замена Лискова, верны ли свойства, которые я определил, для всех подтипов?
  • Разделение интерфейса, должен ли мой код реализовывать бесполезный метод?
  • Инверсия зависимостей, мой код напрямую зависит от конкретных классов?

Я надеюсь, что вы найдете эту статью полезной.

Спасибо за чтение 🙏