Solid java что это
Принципы SOLID в картинках
Если вы знакомы с объектно-ориентированным программированием, то наверняка слышали и о принципах SOLID. Эти пять правил разработки ПО задают траекторию, по которой нужно следовать, когда пишешь программы, чтобы их проще было масштабировать и поддерживать. Они получили известность благодаря программисту Роберту Мартину.
В Сети множество отличных статей, где рассказывается о принципах SOLID, но иллюстрированных среди них мне практически не попадалось. Из-за этого таким людям со склонностью к визуальному восприятию информации – таким, как я – бывает сложно схватывать суть и не отвлекаться.
Основная цель этой статьи – лучше усвоить принципы SOLID через отрисовку иллюстраций, а также определить назначение каждого принципа. Дело в том, что некоторые из принципов кажутся похожими, но функции выполняют разные. Может получиться так, что одному принципу следуешь, а другой при этом нарушаешь, хотя с виду особой разницы между ними нет.
Чтобы проще читалось, я упоминаю здесь только классы, однако всё сказанное в статье применимо также к функциям, методам и модулям, так что имейте это в виду.
Принципы SOLID
S – Single Responsibility (Принцип единственной ответственности)
Каждый класс должен отвечать только за одну операцию.
Если класс отвечает за несколько операций сразу, вероятность возникновения багов возрастает – внося изменения, касающиеся одной из операций вы, сами того не подозревая, можете затронуть и другие.
Принцип служит для разделения типов поведения, благодаря которому ошибки, вызванные модификациями в одном поведении, не распространялись на прочие, не связанные с ним типы.
O — Open-Closed (Принцип открытости-закрытости)
Классы должны быть открыты для расширения, но закрыты для модификации.
Когда вы меняете текущее поведение класса, эти изменения сказываются на всех системах, работающих с данным классом. Если хотите, чтобы класс выполнял больше операций, то идеальный вариант – не заменять старые на новые, а добавлять новые к уже существующим.
Принцип служит для того, чтобы делать поведение класса более разнообразным, не вмешиваясь в текущие операции, которые он выполняет. Благодаря этому вы избегаете ошибок в тех фрагментах кода, где задействован этот класс.
L — Liskov Substitution (Принцип подстановки Барбары Лисков)
Если П является подтипом Т, то любые объекты типа Т, присутствующие в программе, могут заменяться объектами типа П без негативных последствий для функциональности программы.
В случаях, когда класс-потомок не способен выполнять те же действия, что и класс-родитель, возникает риск появления ошибок.
Если у вас имеется класс и вы создаете на его базе другой класс, исходный класс становится родителем, а новый – его потомком. Класс-потомок должен производить такие же операции, как и класс-родитель. Это называется наследственностью.
Необходимо, чтобы класс-потомок был способен обрабатывать те же запросы, что и родитель, и выдавать тот же результат. Или же результат может отличаться, но при этом относиться к тому же типу. На картинке это показано так: класс-родитель подаёт кофе (в любых видах), значит, для класса-потомка приемлемо подавать капучино (разновидность кофе), но неприемлемо подавать воду.
Если класс-потомок не удовлетворяет этим требованиям, значит, он слишком сильно отличается от родителя и нарушает принцип.
Принцип служит для того, чтобы обеспечить постоянство: класс-родитель и класс-потомок могут использоваться одинаковым образом без нарушения работы программы.
I — Interface Segregation (Принцип разделения интерфейсов)
Не следует ставить клиент в зависимость от методов, которые он не использует.
Когда классу приходится производить действия, не несущие никакой реальной пользы, это выливается в пустую трату ресурса, а в случае, если класс выполнять эти действия не способен, ведёт к возникновению багов.
Класс должен производить только те операции, которые необходимы для осуществления его функций. Все другие действия следует либо удалить совсем, либо переместить, если есть вероятность, что они понадобятся другому классу в будущем.
Принцип служит для того, чтобы раздробить единый набор действий на ряд наборов поменьше – таким образом, каждый класс делает то, что от него действительно требуется, и ничего больше.
D — Dependency Inversion (Принцип инверсии зависимостей)
Модули верхнего уровня не должны зависеть от модулей нижнего уровня. И те, и другие должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Для начала объясню термины, которые здесь применяются, простыми словами.
Модули (или классы) верхнего уровня = классы, которые выполняют операцию при помощи инструмента
Модули (или классы) нижнего уровня = инструменты, которые нужны для выполнения операций
Абстракции – представляют интерфейс, соединяющий два класса
Детали = специфические характеристики работы инструмента
Согласно данному принципу, класс не должен соединяться с инструментом, который применяет для выполнения операции. Вместо этого он должен быть соединён с интерфейсом, который поможет установить связь между инструментом и классом.
Кроме того, принцип гласит, что ни интерфейс, ни класс, не обязаны вникать в специфику работы инструмента. Напротив, это инструмент должен подходить под требования интерфейса.
Этот принцип служит для того, чтобы устранить зависимость классов верхнего уровня от классов нижнего уровня за счёт введения интерфейсов.
Простое объяснение принципов SOLID
Принципы SOLID — это стандарт программирования, который все разработчики должны хорошо понимать, чтобы избегать создания плохой архитектуры. Этот стандарт широко используется в ООП. Если применять его правильно, он делает код более расширяемым, логичным и читабельным. Когда разработчик создаёт приложение, руководствуясь плохой архитектурой, код получается негибким, даже небольшие изменения в нём могут привести к багам. Поэтому нужно следовать принципам SOLID.
На их освоение потребуется какое-то время, но если вы будете писать код в соответствии с этими принципами, то его качество повысится, а вы освоите создание хорошей архитектуры ПО.
Чтобы понять принципы SOLID, нужно чётко понимать, как использовать интерфейсы. Если у вас такого понимания нет, то сначала почитайте документацию.
Я буду объяснять SOLID самым простым способом, так что новичкам легче будет разобраться. Будем рассматривать принципы один за другим.
Принцип единственной ответственности (Single Responsibility Principle)
Существует лишь одна причина, приводящая к изменению класса.
Один класс должен решать только какую-то одну задачу. Он может иметь несколько методов, но они должны использоваться лишь для решения общей задачи. Все методы и свойства должны служить одной цели. Если класс имеет несколько назначений, его нужно разделить на отдельные классы.
Приведённый здесь класс нарушает принцип единственной ответственности. Почему он должен извлекать данные из базы? Это задача для уровня хранения данных, на котором данные сохраняются и извлекаются из хранилища (например, базы данных). Это ответственность не этого класса.
Также данный класс не должен отвечать за формат следующего метода, потому что нам могут понадобиться данные другого формата, например, XML, JSON, HTML и т.д.
Код после рефакторинга будет выглядеть так:
Принцип открытости/закрытости (Open-closed Principle)
Программные сущности должны быть открыты для расширения, но закрыты для модификации.
Программные сущности (классы, модули, функции и прочее) должны быть расширяемыми без изменения своего содержимого. Если строго соблюдать этот принцип, то можно регулировать поведение кода без изменения самого исходника.
Как же можно решить поставленную задачу? Смотрите:
Принцип подстановки Барбары Лисков (Liskov Substitution Principle)
Пусть φ(x) — доказуемое свойство объекта x типа T. Тогда φ(y) должно быть верным для объектов y типа S, где S — подтип T.
Функции, использующие указатели ссылок на базовые классы, должны уметь использовать объекты производных классов, даже не зная об этом.
Попросту говоря: подкласс/производный класс должен быть взаимозаменяем с базовым/родительским классом.
Значит, любая реализация абстракции (интерфейса) должна быть взаимозаменяемой в любом месте, в котором принимается эта абстракция. По сути, когда мы используем в коде интерфейсы, то используем контракт не только по входным данным, принимаемым интерфейсом, но и по выходным данным, возвращаемым разными классами, реализующими этот интерфейс. В обоих случаях данные должны быть одного типа.
В этом коде нарушен обсуждаемый принцип и показан способ исправления:
Принцип разделения интерфейса (Interface Segregation Principle)
Нельзя заставлять клиента реализовать интерфейс, которым он не пользуется.
Это означает, что нужно разбивать интерфейсы на более мелкие, лучше удовлетворяющие конкретным потребностям клиентов.
Как и в случае с принципом единственной ответственности, цель принципа разделения интерфейса заключается в минимизации побочных эффектов и повторов за счёт разделения ПО на независимые части.
Принцип инверсии зависимостей (Dependency Inversion Principle)
Высокоуровневые модули не должны зависеть от низкоуровневых. Оба вида модулей должны зависеть от абстракций.
Абстракции не должны зависеть от подробностей. Подробности должны зависеть от абстракций.
Проще говоря: зависьте от абстракций, а не от чего-то конкретного.
Применяя этот принцип, одни модули можно легко заменять другими, всего лишь меняя модуль зависимости, и тогда никакие перемены в низкоуровневом модуле не повлияют на высокоуровневый.
Есть распространённое заблуждение, что «инверсия зависимостей» является синонимом «внедрения зависимостей». Но это разные вещи.
Класс PasswordReminder должен зависеть от абстракций, а не от чего-то конкретного. Но как это сделать? Рассмотрим пример:
Понятнее о S.O.L.I.D
Сегодня мы поговорим о том, как следовать принципам S.O.L.I.D и получать от этого удовольствие.
Что такое S.O.L.I.D? Погуглите — и получите 5 принципов, которые в 90% случаев описываются очень скупо. Скупость эта потом выливается в непонимание и долгие споры. Я же предлагаю вернуться к одному из признанных источников и хотя бы на время закрыть этот вопрос.
Источником принципов S.O.L.I.D принято считать книгу Роберта Мартина «Чистая архитектура». Если у Вас есть время прочесть книгу, лучше отложите эту статью и почитайте книгу. Если времени у Вас нет, а завтра собес — велком.
Single Responsibility Principle — принцип единственной ответственности.
Open-Closed Principle — принцип открытости/закрытости.
Liskov Substitution Principle — принцип подстановки Барбары Лисков.
Interface Segregation Principle — принцип разделения интерфейсов.
Dependency Inversion Principle — принцип инверсии зависимости.
Разберём каждый из принципов. В примерах я буду использовать Java и местами Kotlin.
Single Responsibility Principle — принцип единственной ответственности
Этот принцип кажется очень лёгким, но, как правило, его неправильно понимают. Казалось бы, всё понятно — каждый модуль, будь то класс, поле или микросервис — должен отвечать за что-то одно. Тут и кроется крамола. Такой принцип тоже есть, но непосредственно Принцип единственной ответственности — совсем о другом.
Сформулировать его можно так:
Модуль должен иметь только одну причину для изменения. Или: модуль должен отвечать только за одну заинтересованную группу.
Вот тут становится непонятно, поэтому мы обратимся к признанному первоисточнику — книге Роберта Мартина, у которого есть отличный пример на этот счёт. Воспользуемся им.
Предположим, что существует какая-то система, в которой ведётся учёт сотрудников. Сотрудники интересны как отделу кадров, так и бухгалтерии. Для их нужд, в числе прочих, в сервисе EmployeeService есть 2 метода:
Методом расчёта зарплаты пользуется бухгалтерия. Методом расчёта сверхурочных часов пользуется отдел кадров.
Логично, что для расчёта зарплаты бухгалтерии может потребоваться учесть сверхурочные часы сотрудника. В таком случае, метод calculateSalary() может вызвать calculateOvertimeHours() и применить его результаты в своих формулах.
Окей, прошло полгода, и в отделе кадров решили поменять алгоритм расчёта сверхурочных. Предположим, раньше один сверхурочный час рассчитывается не по коэффициенту * 2, а стал — по коэффициенту * 2,5. Разработчик получил задание, изменил формулу, проверил, что всё работает, и успокоился. Город засыпает, просыпается бухгалтерия. А у бухгалтерии ничего не поменялось, они считают зарплаты по тем же формулам, вот только в этой формуле теперь будут другие цифры, потому что calculateSalary() ходит в calculateOvertimeHours(), а там теперь по просьбе отдела кадров сверхурочные не * 2, а * 2,5. Упс…
Теперь, если мы вернёмся к описанию принципа, всё станет намного понятнее.
Модуль должен иметь только одну причину для изменения. Эта причина, в нашем случае — потребности только отдела кадров, но не бухгалтерии. Модуль должен отвечать только за одну заинтересованную группу. calculateOvertimeHours() должен обрабатывать только одну группу — отдел кадров, несмотря на то, что он может показаться универсальным. Работа этого метода никаким образом не должна касаться бухгалтерии.
Теперь, я надеюсь, стало понятнее.
Каким может быть решение проблемы выше?
Решений проблемы может быть несколько. Но что Вам точно будет необходимо сделать — это разделить функциональность в разные методы — например, calculateOvertimeHoursForAccounting() и calculateOvertimeHoursForHumanRelations(). Каждый из этих методов будет отвечать за свою заинтересованную группу и принцип единой ответственности не будет нарушен.
Open-Closed Principle — принцип открытости/закрытости
Программные сущности должны быть открыты для расширения и закрыты для изменения.
Возьмём пример из суровой реальности.
У нас есть приложение, и, предположим, довольно неплохое. В нём есть сервис, который пользуется внешним сервисом. Предположим, это сервис котировок ЦБ РФ. В простой реализации архитектура этого сервиса будет выглядеть следующим образом:
Вроде всё неплохо, но проект пора отдавать на интеграционное тестирование, и нам нужно отключить запросы в ЦБ. Как бы мы ни попытались решить эту проблему, нам будет необходимо изменить текущую реализацию FinancialQuotesRestService так, чтобы она не отправляла больше запросы. Но в чистой архитектуре такой подход недопустим, поскольку нарушается Принцип открытости-закрытости — старый код не должен меняться.
Как решить проблему и сделать так, чтобы при добавлении новой реализации не трогать старый код?
Конечно же, через интерфейсы. Стоит нам разделить объявление функциональности и её реализацию, как эта проблема будет решена. Теперь мы можем реализовать интерфейс FinancialQuotesRestService сколько угодно раз; в нашем случае, это будет FinancialQuotesRestServiceImpl и FinancialQuotesRestServiceMock. На тестовом стенде приложение будет запускаться по Mock-профилю, на продакшене оно будет запускаться в обычном режиме.
Далее, если мы захотим добавить ещё одну реализацию, нам не придётся менять существующий код. Например, мы хотим добавить получение котировок не ЦБ, а в некоторых случаях Сбербанка. Нет ничего проще:
Мы добавили новую функциональность, но прежние две затронуты не были. Даже если команда разработчиков полностью обновится и приложение будут разрабатывать совсем другие программисты, им не придётся трогать старый код. Таким образом, достигается главная цель Принципа Открытости-Закрытости — сделать систему легко расширяемой и обезопасить её от влияния изменений.
Liskov Substitution Principle — принцип подстановки Барбары Лисков
Принцип подстановки Барбары Лисков можно сформулировать так:
Для создания взаимозаменяемых частей эти части должны соответствовать контракту, который позволяет заменять эти части друг другом.
Если Вы хорошо разобрались с принципом выше, Вы уже понимаете, о чём речь. В той реализации, которую мы разбирали, интерфейс FinancialQuotesRestService как раз и является таким контрактом, который декларирует функциональность всех своих реализаций. Предположим, интерфейс выглядит так:
В данном контракте прописано, что в используемом интерфейсе используется метод getQuotes, который не принимает аргументы, возвращая при этом список котировок. И это всё, что нужно знать классам, которые будут его использовать.
В нашем случае, контроллер будет выглядеть так:
Как мы видим, контроллер не знает, какая именно реализация будет реализована далее. Получит ли он котировки ЦБ, Сбербанка или просто моковые данные. Мы можем запустить приложение в любом режиме, и от этого работа контроллера не изменится.
В этом и есть смысл Принципа подстановки Барбары Лисков. Любой компонент системы должен работать с компонентами, от которых он зависит, через неизменяемый контракт.
Interface Segregation Principle — принцип разделения интерфейсов
Принцип можно сформулировать так:
Необходимо избегать зависимости от того, что не используется.
Например, мы решили реализовать CRUD-интерфейс, который будет поддерживать операции CRUD. И реализовать его во всех сервисах. Выглядеть интерфейс может так:
Окей, мы реализовали его в сервисе UserService:
Далее, нам потребовалось реализовать класс PersonService, который мы тоже наследуем от CrudService. Но проблема в том, что сущности Person — не удаляемые, и нам не нужна реализация метода delete(). Как быть? Можно сделать так, например:
И это будет ужасно. Мало того, что мы имеем в классе мёртвый код, который выглядит, как живой, так его кто-то ещё может и вызвать, не зная, что удаление не произойдёт.
Что делать в такой ситуации?
Выделить реализацию CRUD без удаления в отдельный интерфейс. Например, так:
Да простит меня читатель за такое именование интерфейса, но для наглядности самое то.
Теперь сущность Person нельзя удалить! PersonService реализует интерфейс, в котором не объявлено ничего лишнего. В этом и есть соблюдение Принципа разделения интерфейсов.
Dependency Inversion Principle — принцип инверсии зависимости
Код, реализующий высокоуровневую политику, не должен зависеть от кода, реализующего низкоуровневые детали. Зависимости должны быть направлены на абстракции, а не на реализации.
Связи должны строиться на основе абстракций. Если вы вызываете какой-то сервис (например, автовайрите его), Вы должны вызывать его интерфейс, а не реализацию.
Не ссылайтесь на изменчивые конкретные классы. Не наследуйте изменчивые конкретные классы. Не переопределяйте конкретные методы. Это методы часто сами зависят от других классов. Переопределяя такие методы, вы фактически наследуете эти зависимости. Вместо этого, реализуйте абатрактный метод в интерфейсе и переопределите его. Всё, что уже имеет реализацию, не должно быть переопределено. Иными словами, не стройте зависимостей от всего, что может измениться.
У этого принципа, впрочем, есть исключения. Можно строить зависимости от класса, если он предельно стабилен. Например, класс String. Вероятность изменения чего-либо в String всё-таки очень мала (несмотря на то, что я сам описывал добавления в функциональность в String в Java 11 в одной из предыдущих статей, хехе). В случае со стабильным классом String, мы можем себе позволить вызывать его напрямую. Как и в случае с другими стабильными классами.
В общем, Принцип инверсии зависимости можно постулировать так:
Абстракции стабильны. Реализации нестабильны. Строить зависимости необходимо на основе стабильных компонентов. Стройте зависимости от абстракций. Не стройте их от реализаций.
Принципы SOLID, о которых должен знать каждый разработчик
Объектно-ориентированное программирование принесло в разработку ПО новые подходы к проектированию приложений. В частности, ООП позволило программистам комбинировать сущности, объединённые некоей общей целью или функционалом, в отдельных классах, рассчитанных на решение самостоятельных задач и независимых от других частей приложения. Однако само по себе применение ООП не означает, что разработчик застрахован от возможности создания непонятного, запутанного кода, который тяжело поддерживать. Роберт Мартин, для того, чтобы помочь всем желающим разрабатывать качественные ООП-приложения, разработал пять принципов объектно-ориентированного программирования и проектирования, говоря о которых, с подачи Майкла Фэзерса, используют акроним SOLID.
Что такое SOLID?
Вот как расшифровывается акроним SOLID:
Сейчас мы рассмотрим эти принципы на схематичных примерах. Обратите внимание на то, что главная цель примеров заключается в том, чтобы помочь читателю понять принципы SOLID, узнать, как их применять и как следовать им, проектируя приложения. Автор материала не стремился к тому, чтобы выйти на работающий код, который можно было бы использовать в реальных проектах.
Принцип единственной ответственности
«Одно поручение. Всего одно.» — Локи говорит Скурджу в фильме «Тор: Рагнарёк».
Каждый класс должен решать лишь одну задачу.
Класс должен быть ответственен лишь за что-то одно. Если класс отвечает за решение нескольких задач, его подсистемы, реализующие решение этих задач, оказываются связанными друг с другом. Изменения в одной такой подсистеме ведут к изменениям в другой.
Обратите внимание на то, что этот принцип применим не только к классам, но и к компонентам программного обеспечения в более широком смысле.
Например, рассмотрим этот код:
Как такая структура класса может привести к проблемам?
Если изменится порядок работы с хранилищем данных, используемым приложением, то придётся вносить изменения во все классы, работающие с хранилищем. Такая архитектура не отличается гибкостью, изменения одних подсистем затрагивают другие, что напоминает эффект домино.
Для того чтобы привести вышеприведённый код в соответствие с принципом единственной ответственности, создадим ещё один класс, единственной задачей которого является работа с хранилищем, в частности — сохранение в нём объектов класса Animal :
Вот что по этому поводу говорит Стив Фентон: «Проектируя классы, мы должны стремиться к тому, чтобы объединять родственные компоненты, то есть такие, изменения в которых происходят по одним и тем же причинам. Нам следует стараться разделять компоненты, изменения в которых вызывают различные причины».
Правильное применение принципа единственной ответственности приводит к высокой степени связности элементов внутри модуля, то есть к тому, что задачи, решаемые внутри него, хорошо соответствуют его главной цели.
Принцип открытости-закрытости
Программные сущности (классы, модули, функции) должны быть открыты для расширения, но не для модификации.
Самая главная проблема такой архитектуры заключается в том, что функция определяет то, какой звук издаёт то или иное животное, анализируя конкретные объекты. Функция AnimalSound не соответствует принципу открытости-закрытости, так как, например, при появлении новых видов животных, нам, для того, чтобы с её помощью можно было бы узнавать звуки, издаваемые ими, придётся её изменить.
Добавим в массив новый элемент:
После этого нам придётся поменять код функции AnimalSound :
Как привести функцию AnimalSound в соответствие с принципом открытости-закрытости? Например — так:
Если теперь добавить в массив объект, описывающий новое животное, функцию AnimalSound менять не придётся. Мы привели её в соответствие с принципом открытости-закрытости.
Рассмотрим ещё один пример.
Представим, что у нас есть магазин. Мы даём клиентам скидку в 20%, используя такой класс:
Теперь решено разделить клиентов на две группы. Любимым ( fav ) клиентам даётся скидка в 20%, а VIP-клиентам ( vip ) — удвоенная скидка, то есть — 40%. Для того, чтобы реализовать эту логику, было решено модифицировать класс следующим образом:
Такой подход нарушает принцип открытости-закрытости. Как видно, здесь, если нам надо дать некоей группе клиентов особую скидку, приходится добавлять в класс новый код.
Если решено дать скидку в 80% «супер-VIP» клиентам, выглядеть это должно так:
Как видите, тут используется расширение возможностей классов, а не их модификация.
Принцип подстановки Барбары Лисков
Необходимо, чтобы подклассы могли бы служить заменой для своих суперклассов.
Цель этого принципа заключаются в том, чтобы классы-наследники могли бы использоваться вместо родительских классов, от которых они образованы, не нарушая работу программы. Если оказывается, что в коде проверяется тип класса, значит принцип подстановки нарушается.
Функция нарушает принцип подстановки (и принцип открытости-закрытости). Этот код должен знать о типах всех обрабатываемых им объектов и, в зависимости от типа, обращаться к соответствующей функции для подсчёта конечностей конкретного животного. Как результат, при создании нового типа животного функцию придётся переписывать:
Для того чтобы эта функция не нарушала принцип подстановки, преобразуем её с использованием требований, сформулированных Стивом Фентоном. Они заключаются в том, что методы, принимающие или возвращающие значения с типом некоего суперкласса ( Animal в нашем случае) должны также принимать и возвращать значения, типами которых являются его подклассы ( Pigeon ).
Вооружившись этими соображениями мы можем переделать функцию AnimalLegCount :
Теперь в классе Animal должен появиться метод LegCount :
А его подклассам нужно реализовать этот метод:
В результате, например, при обращении к методу LegCount для экземпляра класса Lion производится вызов метода, реализованного в этом классе, и возвращается именно то, что можно ожидать от вызова подобного метода.
Принцип разделения интерфейса
Создавайте узкоспециализированные интерфейсы, предназначенные для конкретного клиента. Клиенты не должны зависеть от интерфейсов, которые они не используют.
Этот принцип направлен на устранение недостатков, связанных с реализацией больших интерфейсов.
Рассмотрим интерфейс Shape :
Он описывает методы для рисования кругов ( drawCircle ), квадратов ( drawSquare ) и прямоугольников ( drawRectangle ). В результате классы, реализующие этот интерфейс и представляющие отдельные геометрические фигуры, такие, как круг (Circle), квадрат (Square) и прямоугольник (Rectangle), должны содержать реализацию всех этих методов. Выглядит это так:
Как видно, при таком подходе невозможно создать класс, который реализует метод для вывода круга, но не реализует методы для вывода квадрата, прямоугольника и треугольника. Такие методы можно реализовать так, чтобы при их выводе выбрасывалась бы ошибка, указывающая на то, что подобную операцию выполнить невозможно.
В нашем же случае интерфейс Shape решает задачи, для решения которых необходимо создать отдельные интерфейсы. Следуя этой идее, переработаем код, создав отдельные интерфейсы для решения различных узкоспециализированных задач:
Теперь интерфейс ICircle используется лишь для рисования кругов, равно как и другие специализированные интерфейсы — для рисования других фигур. Интерфейс Shape может применяться в качестве универсального интерфейса.
Принцип инверсии зависимостей
Объектом зависимости должна быть абстракция, а не что-то конкретное.
В процессе разработки программного обеспечения существует момент, когда функционал приложения перестаёт помещаться в рамках одного модуля. Когда это происходит, нам приходится решать проблему зависимостей модулей. В результате, например, может оказаться так, что высокоуровневые компоненты зависят от низкоуровневых компонентов.
Здесь класс Http представляет собой высокоуровневый компонент, а XMLHttpService — низкоуровневый. Такая архитектура нарушает пункт A принципа инверсии зависимостей: «Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций».
Класс Http не должен знать о том, что именно используется для организации сетевого соединения. Поэтому мы создадим интерфейс Connection :
Интерфейс Connection содержит описание метода request и мы передаём классу Http аргумент типа Connection :
Перепишем класс XMLHttpService таким образом, чтобы он реализовывал этот интерфейс:
В результате мы можем создать множество классов, реализующих интерфейс Connection и подходящих для использования в классе Http для организации обмена данными по сети:
Итоги
Здесь мы рассмотрели пять принципов SOLID, которых следует придерживаться каждому ООП-разработчику. Поначалу это может оказаться непросто, но если к этому стремиться, подкрепляя желания практикой, данные принципы становятся естественной частью рабочего процесса, что оказывает огромное положительное воздействие на качество приложений и значительно облегчает их поддержку.
Еще больше полезной информации для программистов вы найдете на нашем сайте.