В интернете можно найти много статей описывающий принципы SOLID. Но большинство из них сложны для понимая, а некоторые даже не содержать примеров кода, а те в которых есть примеры используют свойственные выбранному языку абстракции, такие как интерфейсы или абстрактные классы. Поэтому я постарался создать статью описывающие эти принципы простым языком и содержащие примеры не использующие особенности какого либо языка. Это статья создана на основе вот этой очень хорошей презентации и дополнена описанием и примерами.
И так в этой статье я расскажу про пять архитектурных принципов программирования, которые помогут сделать ваш код гибким, понятным и легко поддерживаемым. Все о чем будет рассказано далее находится в книги Роберт К. Мартин Гибкая разработка программ. Принципы, примеры, практика (Agile Software Development Principles, Patterns and Practices )
Solid — переводится с английского как твердый, крепкий прочный, но с другой стороны SOLID это акроним, где каждая буква этого слова это отдельный принцип:
S — Single Responsible Principle (SRP)
O — Open Closed Principle (OCP)
L — Liskov Substitution Principle (LSP)
I — Interface Segregation Principle (ISP)
D — Dependency Inversion Principle (DIP)
Каждый принцип сам по себе является целой идеологией и все они не зависят друг от друга. Но вместе они позволяют делать код более понимаемым, лучше описаным и поддерживаемым. Говоря о эти принципах, вначале нужно сказать, а почему вообще эти принципы так важны и для чего они вообще нужны и какой смысл, в том что бы их изучать и что такое плохой код.
Самая большая проблема в написание программ заключается в том что всегда происходят изменения. В реальной жизни почти никогда не бывает что бы вы один раз написали код и больше к нему не возвращались. Как правило всегда нужно вносить какие либо изменения, для добавление нового функционала, исправления старых ошибок и т.п. Поэтому нужно писать код так что бы эти изменения не создавали новых проблем, а процесс внесения изменений был естественной часть технологического процесса и вам приходилось бы тратить больше времени не на поддержку старого кода, а на написания нового.
В книги Роберта Мартина описано 7 признаков плохого кода:
И так начнем описание принципов.
Каноническое определение принципа:
«Существует лишь одна причина, приводящая к изменению класса»
Том Де-Марко (1979) и Мейлер Пейдж-Джонсан (1988)
То есть этот принцип говорит, что каждый класс должен иметь лишь одну ответственность, то есть он должен отвечать только за одну вещь.
Хотя концепция, лежащая в основе SRP, достаточно проста для понимания, но реализация этого принципа на практике требует определенных навыков, поскольку ответственность класса не всегда сразу очевидна.
Мартин рассматривает ответственность как причину изменения и приходит к выводу, что у класса и модуля должна быть единственная причина для изменения. Объединение двух сущностей, которые меняются по разным причинам в разное время, — является плохим дизайном. Чем больше обязанностей у класса, тем больше запросов на изменение он получит, и тем сложнее будет реализовать эти изменения. Цель принципа SRP — бороться со сложностью, которая возникает в процессе разработки логики приложения.
Рассмотрим пример. Пусть у нас есть один класс Product, в котором определены все необходимые методы для работы с сущностью продукта, с базой и методы отображения продукта.
class Product { function get(name) {} function set(name, value) {} function save() {} function update() {} function delete() {} function show() {} function print() {} }
В данном случае мы явно нарушаем принцип так как наш класс содержит множество ответственностей. Перепишем его, так что бы выделить методы в отдельные классы.
class ProductEntity { function get(name) {} function set(name, value) {} } class ProductRepository { function save() {} function update() {} function delete() {} } class ProductView { function show() {} function print() {} }
Как мы можем видеть код стал намного более понятным.
Рассмотрим еще один пример, класс User:
// сохранение данных user = new User() user.firstname = 'James' user.lastname = 'Bond' user.save() // получение данных из базы user2 = new User() user2.getByFirstName('John') // получение данных их связанной таблицы role = user2.role
При такой архитектуре все будет хорошо работать если проект будет небольшим. Но с ростом проекта увеличением количества требуемых методов, будет расти и сложность архитектуры, все очень быстро может стать запутанным, поэтому лучше это переделать. Разделить на сущность пользователя класс UserEntity и класс для работы с базой данных UserRepository
// сохранение данных user = new UserEntity() user.firstname = 'James' user.lastname = 'Bond' userRepository = new UserRepository() userRepository.save(user) // получение данных userRepository2 = new UserRepository() userRepository2.getByFirstName('John') // получение данных их связанной таблицы role = userRepository2.role
Еще один пример. Пусть у нас есть класс, который отвечает как за генерацию отчета и за его отправку определенному пользователю
class FinancialReportMailer function FinancialReportMailer(transactions, account) this.transactions = transactions this.account = account this.report = '' function generate_report() // generate report this.report = make_report function send_report() // send report this.report mailer = FinancialReportMailer(transactions, account) mailer.generate_report() mailer.send_report()
Класс FinancialReportMailer, показанный выше, выполняет две задачи: генерация отчета (метод «generate_report!») И отправка отчета (метод «send_report»). Класс выглядит довольно простым. Однако расширение этого класса в будущем может быть проблематичным, поскольку нам, вероятно, придется изменить логику класса. Принцип SRP говорит нам, что класс должен реализовывать одну единственную задачу, и поэтому в соответствии с этим принципом мы должны разделить класс FinancialReportMailer на два класса.
Давайте посмотрим, как выглядит этот код после того, как мы реорганизовали его для соответствия требованиям SRP:
class FinancialReportMailer function deliver(report, account) // send report class FinancialReportGenerator function generate() // generate report report = FinancialReportGenerator().generate() FinancialReportMailer.deliver(report, account)
После рефакторинга у нас есть два класса, каждый из которых выполняет определенную задачу. Класс FinancialReportMailer отправляет электронные письма, содержащие тексты, созданные классом FinancialReportGenerator. Если бы мы хотели расширить класс, отвечающий за генерацию отчетов в будущем, мы могли бы просто внести необходимые изменения, не касаясь класса FinancialReportMailer.
Допустим нам нужно написать класс Modem, о котором мы точно знаем что в будущем мы не будем вносить в него изменения.
class Modem { function dial(){} function hangup() {} function send(request) {} function receive() {} }
Если мы будем придерживаться принципа SRP, то нам нужно будет разделить этот класс на два класса DataChannel для организации соединения и Connection для передачи данных.
class DataChannel { function dial(){} function hangup() {} } class Connection { function send(request) {} function receive() {} }
Но так как мы заранее знаем, что мы никогда не будем менять класс Modem, то в данном случае все таки выгоднее оставить все как есть, иначе в будущем наш код будет иметь избыточную сложность
Суть принципа
На каждый объект должна быть возложена одна единственная обязанность
Каноническое определение принципа:
Программные объекты (классы, модули, функции и т.п.) должны быть открыты для расширения, но в то же время закрыты для модификации.
Бертран Майер (1988)
Как что-то может быть открытым и закрытым одновременно?!
Класс следует OCP, если он удовлетворяет этим двум критериям:
Открыт для расширения
То есть класс должен быть создан таким образом, что бы поведение класса можно было бы дополнить в любой момент. По мере изменения требований мы должны иметь возможность дополнить класс новым функционалом.
Закрыт для модификации
При добавление нового функционала нельзя вносить изменения в исходный код такого класса. То есть весь новый код не должен затрагивать старый.
Цель принципа
Разработчики всегда должны писать новый код, а не переписывает старый. Тестирование кода должно происходит единожды. И разработчики не должны тратить время на частый рефакторинг
Рассмотрим простой пример. Пусть у нас есть класс Order и метод getTotalPrice, которые вначале получает все позиции корзины потом получает возможные скидки, а потом рассчитывает итоговую сумму.
class Order { function getTotalPrice() { // получение всех позиций корзины // получение накопительности скидки текущего клиента // расчет итоговой цены } }
В начале перепишем его, так что бы он удовлетворял принципу единой обязанности SIP
class BaseOrderAlgorithm { function getTotalPrice() { products = this.getProducts() discount = this.getDiscount() return this.calculate(products, discount) } function getProducts() {} function getDiscount() {} function calculate(products, discount) {} }
Далее перепишем его так что бы он удовлетворил принципу OCP. У нас будет два возможных решения. Первой очень простое, второе чуть интереснее.
Первое решение для соответствия принципу OCP. Мы можем просто унаследовать дочерний класс MainOrderAlgorithm от BaseOrderAlgorithm и переопределить внутренние методы расчета getProducts и getDiscount. Таким образом мы сохраним старый код и добавим новый.
class MainOrderAlgorithm extends(BaseOrderAlgorithm) { function getProducts() {} function getDiscount() {} }
Второе решение для соответствия принципу OCP.
class Order { function Order(IProduct product, IDiscount discount) { this.product = product this.discount = discount } function getTotalPrice() { products = this.product.getProducts() discount = this.discount.getDiscount() return this.calculate(products, discount) } function calculate(products, discount) {} }
Через конструктор класса передаем отдельные классы product и discount И таким образом мы написали один раз класс Order и можем написать множество классов для обработки.
Применяйте принцип в местах подтвержденных частым изменениям.
Так же наличие условных операторов (if, case) при ветвление бизнес логики может сигнализировать о потенциальных точках изменения логики.
Пусть у нас есть некая фабрика которая на основе неких параметров создает новый класс
switch (name) { case 'Shape': return new Shape() case 'Rectangle': return new Rectangle() }
Тут проблема в том что когда вам надо добавить новую фигуру вам каждый раз надо будет вставлять/редактировать текущий код то есть каждый раз переписывать этот класс.
Полностью избавиться от подобных классов/методов почти невозможно поэтому:
Максимальное использование абстракций может показаться идеальным способом приведения любых классов в соответствие с принципом OCP. Но тут есть определенные проблемы о которых заранее лучше знать:
Только большой опыт разработки позволит делать точные предположение, о том как следует делать код не изменяемым.
В приведенном ниже коде класс Logger форматирует и отправляет журналы логов. Но принцип OCP не соблюдается, поскольку нам придется изменять регистратор каждый раз, когда нам нужно добавить дополнительных типы отправлений или типы форматов:
class Logger function Logger(format, delivery) this.log(this.format(format), this.delivery(delivery)) function log(string) this.deliver(this.format(string)) function format(string): case: string == raw: // краткий формат string == with_date: // формат с датами string == with_date_and_details: // детализированный формат с датами else raise NotImplementedError function deliver(text): case: text == by_email: // отправка по почте text == by_sms: // отправка по смс text == to_stdout: // вывод в консоль else raise NotImplementedError logger = Logger('raw', 'by_sms') logger.log('Emergency error! Please fix me!')
После того, как мы проведем рефакторинг этого кода, он может быть расширен. В приведенном ниже примере мы создали отдельные классы для отправителей и для форматирования и сделали возможным подключение новых отправителей и средств форматирования без необходимости изменения базового кода:
class Logger function Logger(formatter, sender) this.formatter = formatter this.sender = sender function log(string) this.sender.deliver(this.formatter.format(string)) class LogSms function deliver(text): // отправка по СМС class LogMailer function deliver(text): // отправка по почте class LogWriter function deliver(log) // вывод в консоль class DateFormatter function format(string) // формат с датой class DateDetailsFormatter function format(string) // детализированный формат class RawFormatter def format(string) // краткая форма формата logger = Logger.new(RawFormatter(), LogSms()) logger.log('Emergency error! Please fix me!')
Суть принципа
Изменения в программе должны происходить при написание нового кода, а не модификации старого.
Далее в следующей статье я продолжу описание принципов.
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…