Промышленное производство
Промышленный Интернет вещей | Промышленные материалы | Техническое обслуживание и ремонт оборудования | Промышленное программирование |
home  MfgRobots >> Промышленное производство >  >> Industrial Internet of Things >> Облачные вычисления

SOLID: освоение объектно-ориентированного дизайна для чистого, поддерживаемого кода

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

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

SOLID: освоение объектно-ориентированного дизайна для чистого, поддерживаемого кода

Что такое принципы SOLID?

SOLID представляет набор принципов для проектирования классов. Роберт К. Мартин (Дядя Боб) ввел большинство принципов дизайна и придумал акроним.
SOLID расшифровывается как:

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

SOLID: 5 принципов объяснены

Лучший способ понять, как работают принципы SOLID — через примеры. Все принципы взаимодополняющие и применяются к индивидуальным сценариям. Порядок применения принципов неважен, и не все принципы применимы в каждой ситуации.

SOLID: освоение объектно-ориентированного дизайна для чистого, поддерживаемого кода

Каждый раздел ниже предоставляет обзор каждого принципа SOLID на языке Python. Общие идеи SOLID применяются к любому объектно-ориентированному языку, такому как PHP, Java или C#. Обобщение правил делает их применимыми к современным подходам программирования, таким как микросервисы.

Принцип единственной ответственности (Single-Responsibility Principle — SRP)

Принцип единственной ответственности (SRP) гласит: «Никогда не должно быть более одной причины для изменения класса».

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

В качестве примера посмотрите на следующий класс:

# Класс с множественными ответственностями
class Animal:
    # Конструктор свойства
    def __init__(self, name):
        self.name = name
    # Представление свойства
    def __repr__(self):
        return f'Animal(name="{self.name}")'
    # Управление базой данных
    def save(animal):
        print(f'Saved {animal} to the database')

if __name__ == '__main__':
    # Инстанцирование свойства
    a = Animal('Cat')
    # Сохранение свойства в базу данных
    Animal.save(a)

При внесении любых изменений в метод save(), изменение происходит в классе Animal. При изменении свойств модификации также происходят в классе Animal.

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

Для реализации принципа единственной ответственности, обратите внимание, что пример класса имеет две отдельные работы:

SOLID: освоение объектно-ориентированного дизайна для чистого, поддерживаемого кода

Поэтому лучший способ решить проблему — отделить метод управления базой данных в новый класс. Например:

# Класс, отвечающий за управление свойством
class Animal:
    def __init__(self, name):
        self.name = name
    def __repr__(self):
        return f'Animal(name="{self.name}")'

# Класс, отвечающий за управление базой данных
class AnimalDB:
    def save(self, animal):
        print(f'Saved {animal} to the database')

if __name__ == '__main__':
    # Инстанцирование свойства
    a = Animal('Cat')
    # Инстанцирование базы данных
    db = AnimalDB()
    # Сохранение свойства в базу данных
    db.save(a)

Изменение класса AnimalDB не влияет на класс Animal с примененным принципом единственной ответственности. Код интуитивен и легко модифицируется.

Принцип открытости/закрытости (Open-Closed Principle — OCP)

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

Добавление функциональностей и сценариев использования в систему не должно требовать модификации существующих сущностей. Формулировка кажется противоречивой — добавление новой функциональности требует изменения существующего кода.

Идея проста для понимания через следующий пример:

class Animal:
    def __init__(self, name):
        self.name = name
    def __repr__(self):
        return f'Animal(name="{self.name}")'

class Storage:
    def save_to_db(self, animal):
        print(f'Saved {animal} to the database')

Класс Storage сохраняет информацию из экземпляра Animal в базу данных. Добавление новых функциональностей, таких как сохранение в CSV файл, требует добавления кода в класс Storage:

class Animal:
    def __init__(self, name):
        self.name = name
    def __repr__(self):
        return f'Animal(name="{self.name}")'

class Storage:
    def save_to_db(self, animal):
        print(f'Saved {animal} to the database')
    def save_to_csv(self, animal):
        print(f'Saved {animal} to the CSV file')

Метод save_to_csv модифицирует существующий класс Storage для добавления функциональности. Этот подход нарушает принцип открытости/закрытости, изменяя существующий элемент при появлении новой функциональности.

Код требует удаления общего назначения класса Storage и создания индивидуальных классов для хранения в специфических файловых форматах.

SOLID: освоение объектно-ориентированного дизайна для чистого, поддерживаемого кода

Следующий код демонстрирует применение принципа открытости/закрытости:

class DB():
    def save(self, animal):
        print(f'Saved {animal} to the database')

class CSV():
    def save(self, animal):
        print(f'Saved {animal} to a CSV file')

Код соответствует принципу открытости/закрытости. Полный код теперь выглядит так:

class Animal:
    def __init__(self, name):
        self.name = name
    def __repr__(self):
        return f'"{self.name}"'

class DB():
    def save(self, animal):
        print(f'Saved {animal} to the database')

class CSV():
    def save(self, animal):
        print(f'Saved {animal} to a CSV file')

if __name__ == '__main__':
    a = Animal('Cat')
    db = DB()
    csv = CSV()
    db.save(a)
    csv.save(a)

Расширение дополнительными функциональностями (такими как сохранение в XML файл) не модифицирует существующие классы.

Принцип подстановки Лисков (Liskov Substitution Principle — LSP)

Принцип подстановки Лисков (LSP) гласит: «Функции, использующие указатели или ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом».

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

Посмотрите на пример записи в файл ниже:

# Родительский класс
class FileHandling():
    def write_db(self):
        return f'Handling DB'
    def write_csv(self):
        return f'Handling CSV'

# Дочерние классы
class WriteDB(FileHandling):
    def write_db(self):
        return f'Writing to a DB'
    def write_csv(self):
        return f"Error: Can't write to CSV, wrong file type."

class WriteCSV(FileHandling):
    def write_csv(self):
        return f'Writing to a CSV file'
    def write_db(self):
        return f"Error: Can't write to DB, wrong file type."

if __name__ == "__main__":
    # Инстанцирование родительского класса и вызовы функций
    db = FileHandling()
    csv = FileHandling()
    print(db.write_db())
    print(db.write_csv())
    # Инстанцирование дочерних классов и вызовы функций
    db = WriteDB()
    csv = WriteCSV()
    print(db.write_db())
    print(db.write_csv())
    print(csv.write_db())
    print(csv.write_csv())

Родительский класс (FileHandling) состоит из двух методов для записи в базу данных и CSV файл. Класс обрабатывает обе функции и возвращает сообщение.

Два дочерних класса (WriteDB и WriteCSV) наследуют свойства от родительского класса (FileHandling). Однако оба потомка выбрасывают ошибку при попытке использовать несоответствующую функцию записи, что нарушает принцип подстановки Лисков, так как переопределяющие функции не соответствуют родительским функциям.

SOLID: освоение объектно-ориентированного дизайна для чистого, поддерживаемого кода

Следующий код решает проблемы:

# Родительский класс
class FileHandling():
    def write(self):
        return f'Handling file'

# Дочерние классы
class WriteDB(FileHandling):
    def write(self):
        return f'Writing to a DB'

class WriteCSV(FileHandling):
    def write(self):
        return f'Writing to a CSV file'

if __name__ == "__main__":
    # Инстанцирование родительского класса и вызовы функций
    db = FileHandling()
    csv = FileHandling()
    print(db.write())
    print(csv.write())
    # Инстанцирование дочерних классов и вызовы функций
    db = WriteDB()
    csv = WriteCSV()
    print(db.write())
    print(csv.write())

Дочерние классы теперь правильно соответствуют родительской функции.

Принцип сегрегации интерфейсов (Interface Segregation Principle — ISP)

Принцип сегрегации интерфейсов (ISP) гласит: «Много клиент-специфичных интерфейсов лучше, чем один общего назначения интерфейс».

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

Следующий пример демонстрирует интерфейс общего назначения:

class Animal():
    def walk(self):
        pass
    def swim(self):
        pass

class Cat(Animal):
    def walk(self):
        print("Struts")
    def fly(self):
        raise Exception("Cats don't swim")

class Duck(Animal):
    def walk(self):
        print("Waddles")
    def swim(self):
        print("Floats")

Дочерние классы наследуются от родительского класса Animal, который содержит методы walk и fly. Хотя обе функции приемлемы для определенных животных, некоторые животные имеют избыточную функциональность.

SOLID: освоение объектно-ориентированного дизайна для чистого, поддерживаемого кода

Для обработки ситуации разбейте интерфейс на более мелкие секции. Например:

class Walk():
    def walk(self):
        pass

class Swim(Walk):
    def swim(self):
        pass

class Cat(Walk):
    def walk(self):
        print("Struts")

class Duck(Swim):
    def walk(self):
        print("Waddles")
    def swim(self):
        print("Floats")

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

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

Принцип инверсии зависимостей гласит: «Зависьте от абстракций, не от конкретностей».

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

Следующий пример демонстрирует зависимость класса без абстракционного слоя:

class LatinConverter:
    def latin(self, name):
        print(f'{name} = "Felis catus"')
        return "Felis catus"

class Converter:
    def start(self):
        converter = LatinConverter()
        converter.latin('Cat')

if __name__ == '__main__':
    converter = Converter()
    converter.start()

Пример имеет два класса:

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

SOLID: освоение объектно-ориентированного дизайна для чистого, поддерживаемого кода

Пример решения выглядит так:

from abc import ABC

class NameConverter(ABC):
    def convert(self, name):
        pass

class LatinConverter(NameConverter):
    def convert(self, name):
        print('Converting using Latin API')
        print(f'{name} = "Felis catus"')
        return "Felis catus"

class Converter:
    def __init__(self, converter: NameConverter):
        self.converter = converter
    def start(self):
        self.converter.convert('Cat')

if __name__ == '__main__':
    latin = LatinConverter()
    converter = Converter(latin)
    converter.start()

Класс Converter теперь зависит от интерфейса NameConverter вместо прямой зависимости от LatinConverter. Будущие обновления позволяют определять конвертацию имен с использованием другого языка и API через интерфейс NameConverter.

Почему нужны принципы SOLID?

Принципы SOLID помогают бороться с проблемами паттернов дизайна. Общая цель принципов SOLID — снизить зависимости кода, и добавление новой фичи или изменение части кода не ломает весь билд.

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

Все еще актуальны ли принципы SOLID?

Хотя принципы SOLID старше 20 лет, они все еще предоставляют хорошую основу для дизайна архитектуры ПО. SOLID дает зумные принципы дизайна, применимые к современным программам и средам, не только к объектно-ориентированному программированию.

Принципы SOLID применяются в ситуациях, где код пишется и модифицируется людьми, организован в модули и содержит внутренние или внешние элементы.

Заключение

Принципы SOLID помогают предоставить хороший фреймворк и руководство для дизайна архитектуры ПО. Примеры из этого руководства показывают, что даже динамически типизированный язык, такой как Python, получает выгоду от применения принципов к дизайну кода.

Далее прочитайте о 9 принципах DevOps, которые помогут вашей команде получить максимум от DevOps.

Облачные вычисления

  1. Ключевое слово static в C#: статические переменные, методы и классы
  2. Вложенные классы в C#: определение, использование и наследование
  3. Шаблоны классов в C++: практическое руководство
  4. Анонимные внутренние классы в Java: определение, синтаксис и практические примеры
  5. Освоение Java ObjectOutputStream: сериализация, методы и практические примеры
  6. Освоение Java Generics: создание переиспользуемого, типобезопасного кода
  7. Мастерство файловых операций в Java с java.io: создание, чтение, запись и удаление
  8. 5 принципов проектирования надёжных взаимосоединений для высокоскоростных данных интенсивных приложений
  9. Наследование в C#: базовые и производные классы, интерфейсы
  10. Полиморфизм в C#: статический и динамический, перегрузка и переопределение