SOLID:принципы объектно-ориентированного проектирования
SOLID — это мнемоническая аббревиатура для проектирования классов в объектно-ориентированном программировании. Принципы устанавливают практики, которые помогают выработать хорошие привычки программирования и поддерживать код.
Принимая во внимание обслуживание и расширяемость кода в долгосрочной перспективе, принципы SOLID обогащают среду разработки кода Agile. Учет и оптимизация зависимостей кода помогают сделать жизненный цикл разработки программного обеспечения более простым и организованным.
Что такое принципы SOLID?
SOLID представляет собой набор принципов проектирования классов. Роберт С. Мартин (дядя Боб) представил большинство принципов дизайна и придумал аббревиатуру.
SOLID означает:
- Принцип единой ответственности
- Принцип открытого-закрытого
- Принцип замены Лисков
- Принцип разделения интерфейса
- Принцип инверсии зависимостей
Принципы SOLID представляют собой набор передовых методов разработки программного обеспечения. Каждая идея представляет собой структуру проектирования, которая ведет к улучшению навыков программирования, улучшению дизайна кода и уменьшению количества ошибок.
SOLID:объяснение 5 принципов
Лучший способ понять, как работают принципы SOLID, — это рассмотреть примеры. Все принципы дополняют друг друга и применяются к отдельным случаям использования. Порядок применения принципов не имеет значения, и не все принципы применимы в любой ситуации.
В каждом разделе ниже представлен обзор каждого принципа SOLID в языке программирования Python. Общие идеи SOLID применимы к любому объектно-ориентированному языку, такому как PHP, Java или C#. Обобщение правил делает их применимыми к современным подходам к программированию, таким как микросервисы.
Принцип единой ответственности (SRP)
Принцип единой ответственности (SRP) гласит:"У класса никогда не должно быть более одной причины для изменения".
При изменении класса мы должны изменить только одну функциональность, что означает, что каждый объект должен иметь только одно задание.
В качестве примера рассмотрим следующий класс:
# A class with multiple responsibilities
class Animal:
# Property constructor
def __init__(self, name):
self.name = name
# Property representation
def __repr__(self):
return f'Animal(name="{self.name}")'
# Database management
def save(animal):
print(f'Saved {animal} to the database')
if __name__ == '__main__':
# Property instantiation
a = Animal('Cat')
# Saving property to a database
Animal.save(a)
При внесении любых изменений в save()
метод, изменение происходит в Animal
учебный класс. При внесении изменений в свойства изменения также происходят в Animal
класс.
У класса есть две причины для изменения, и он нарушает принцип единой ответственности. Несмотря на то, что код работает так, как ожидалось, несоблюдение принципов разработки затрудняет управление кодом в долгосрочной перспективе.
Обратите внимание, что для реализации принципа единой ответственности в примере класса есть две разные задачи:
- Управление свойствами (конструктор и
get_name()
). - Управление базой данных
(save()
).
Поэтому лучший способ решить эту проблему — выделить метод управления базой данных в новый класс. Например:
# A class responsible for property management
class Animal:
def __init__(self, name):
self.name = name
def __repr__(self):
return f'Animal(name="{self.name}")'
# A class responsible for database management
class AnimalDB:
def save(self, animal):
print(f'Saved {animal} to the database')
if __name__ == '__main__':
# Property instantiation
a = Animal('Cat')
# Database instantiation
db = AnimalDB()
# Saving property to a database
db.save(a)
Изменение AnimalDB
класс не влияет на Animal
класса с применением принципа единой ответственности. Код интуитивно понятен и легко модифицируется.
Принцип открытия-закрытия (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):
printf(f’Saved {animal} to the CSV file’)
save_to_csv
метод изменяет существующий Storage
класс для добавления функциональности. Этот подход нарушает принцип открытого-закрытого, изменяя существующий элемент при появлении новой функциональности.
Код требует удаления общего назначения Storage
класс и создание отдельных классов для хранения в определенных форматах файлов.
Следующий код демонстрирует применение принципа открытого-закрытого:
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-файл) не изменяет существующие классы.
Принцип замещения Лискова (LSP)
Принцип подстановки Лискова (LSP) гласит:«Функции, использующие указатели или ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом».
Принцип гласит, что родительский класс может заменить дочерний класс без каких-либо заметных изменений в функциональности.
Посмотрите пример записи файла ниже:
# Parent class
class FileHandling():
def write_db(self):
return f'Handling DB'
def write_csv(self):
return f'Handling CSV'
# Child classes
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__":
# Parent class instantiation and function calls
db = FileHandling()
csv = FileHandling()
print(db.write_db())
print(db.write_csv())
# Children classes instantiations and function calls
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
). Однако оба дочерних элемента выдают ошибку при попытке использовать недопустимую функцию записи, что нарушает принцип подстановки Лискова, поскольку переопределяющие функции не соответствуют родительским функциям.
Следующий код решает проблемы:
# Parent class
class FileHandling():
def write(self):
return f'Handling file'
# Child classes
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__":
# Parent class instantiation and function calls
db = FileHandling()
csv = FileHandling()
print(db.write())
print(csv.write())
# Children classes instantiations and function calls
db = WriteDB()
csv = WriteCSV()
print(db.write())
print(csv.write())
Дочерние классы правильно соответствуют родительской функции.
Принцип разделения интерфейсов (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
методы. Хотя обе функции приемлемы для определенных животных, некоторые животные имеют избыточные функции.
Чтобы справиться с ситуацией, разделите интерфейс на более мелкие разделы. Например:
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
, предоставляя дополнительные функциональные возможности соответствующим дочерним классам. Пример удовлетворяет принципу разделения интерфейса.
Добавление другого животного, например рыбы, требует дальнейшей атомизации интерфейса, поскольку рыба не может ходить.
Принцип инверсии зависимостей (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()
В примере есть два класса:
LatinConverter
использует воображаемый API для получения латинского названия животного (жестко запрограммированное «Felis catus
"для простоты).Converter
это высокоуровневый модуль, использующий экземплярLatinConverter
и его функция для преобразования предоставленного имени.Converter
сильно зависит отLatinConverter
класс, который зависит от API. Такой подход нарушает принцип.
Принцип инверсии зависимостей требует добавления уровня интерфейса абстракции между двумя классами.
Пример решения выглядит следующим образом:
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 уже более 20 лет, они по-прежнему обеспечивают хорошую основу для проектирования архитектуры программного обеспечения. SOLID предлагает надежные принципы проектирования, применимые к современным программам и средам, а не только к объектно-ориентированному программированию.
Принципы SOLID применяются в ситуациях, когда код пишется и модифицируется людьми, организуется в модули и содержит внутренние или внешние элементы.
Заключение
Принципы SOLID помогают обеспечить хорошую основу и руководство по проектированию архитектуры программного обеспечения. Примеры из этого руководства показывают, что даже такой язык с динамической типизацией, как Python, выигрывает от применения этих принципов в разработке кода.
Далее прочитайте о 9 принципах DevOps, которые помогут вашей команде максимально эффективно использовать DevOps.
Облачные вычисления