Что такое solid java

Что такое solid java. Смотреть фото Что такое solid java. Смотреть картинку Что такое solid java. Картинка про Что такое solid java. Фото Что такое solid java

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

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

S — Single Responsibility Principle — принцип единственной ответственности.

O — Open-Closed Principle — принцип открытости/закрытости.

L — Liskov Substitution Principle — принцип подстановки Барбары Лисков.

I — Interface Segregation Principle — принцип разделения интерфейсов.

D — Dependency Inversion Principle — принцип инверсии зависимости.

Посмотрим на конкретных примерах, как работает каждый принцип.

Принцип единственной ответственности (SPA)

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

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

Взглянем на пример:

Так что же не так с кодом? Как вы заметили, у нашей службы уведомлений есть две обязанности: она отправляет электронные письма и посылает текстовые сообщения на телефон. Со временем код вырастет и превратится в невообразимую путаницу, так что изменение может сломать какую-то часть системы, что заставит нас тратить время на обслуживание, а не на разработку.

Чтобы избежать подобных проблем, нужно разделить обязанности. У службы уведомлений их несколько. Например, она отправляет электронные письма и текстовые сообщения на телефон. Чтобы применить SRP, нужно иметь отдельные классы и дать каждому из них свою собственную ответственность.

Проведем небольшой рефакторинг и расставим все по местам.

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

Принцип открытости/закрытости (OCP)

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

Всегда опасно изменять существующий код. Он уже протестирован и развернут на производстве. Но, к счастью, есть множество способов избежать подобных проблем. Один из них заключается в использовании принципа открытости/закрытости.

Предположим, мы работаем над инструментом “холст”, в котором пользователь может рисовать многоугольники.

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

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

Принцип подстановки Барбары Лисков (LSP)

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

Другой пример. Представьте, что вы работаете над открытым API, и у вас есть абстрактный класс и производные классы, а некоторые из производных классов не соответствуют абстрактному классу. Когда разработчик начнет использовать ваш API, его собьет с толку то, что некоторые производные классы работают некорректно.

Взглянем на пример с Bird:

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

Теперь классы Penguin (Пингвин) and Sparrow (Воробей) соответствуют абстрактным классам.

Принцип разделения интерфейсов (ISP)

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

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

Взглянем на пример:

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

Посмотрим на пример:

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

1. Модули высокого уровня не должны зависеть от модулей низкого уровня. И те, и другие должны зависеть от абстракций.

2. “Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций”.

Рассмотрим сценарий, в котором есть класс, на который ссылаются сотни классов. Если однажды вы решите изменить ссылку, то придется пройти все сто классов и поменять их один за другим. Таким образом, вы потратите много времени на изменение кода. Кроме того, вы должны протестировать каждую часть системы, чтобы убедиться, что ничего не сломали. Это трудоемкая задача, которой нам, разработчикам, всегда следует избегать.

Взглянем на этот пример:

Как видите, наше приложение поддерживает две базы данных, SQL и Mongo, а также у нас есть UserRepository:

Репозиторий имеет прямую ссылку на базу данных. Это плохо по нескольким причинам: во-первых, репозиторий ориентирован на того, кто выполняет операцию, а не на производимые операции; во-вторых, если мы хотим изменить Mongo с помощью SQL, то должны пройти через все репозитории, в которых есть ссылка на Mongo, и изменить их один за другим. Наконец, мы вносим сложность в репозитории. Так что, по сути, UserRepository тесно связан с Mongo.

Чтобы применить принцип DIP, мы должны изменить UserRepository:

Теперь UserRepository зависит от абстракции высокого уровня. Он ориентирован только на операцию записи, а не на того, кто ее выполняет.

Источник

SOLID принципы. Рефакторинг

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

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

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

Содержание:

Рефакторинг

В литературе достаточно подробно описаны методы рефакторинга [2, 3, 4], зачастую ими являются весьма нехитрые приемы [2], например:

Процессы написания нового кода и рефакторинга должны быть разделены — эту мысль замечательно выразил Кент Бек:

Рефакторинг не добавляет новые возможности, но добавление новых возможностей не должно изменять структуру кода.

Рефакторинг должен проводиться постоянно, даже во время изучения кода. Однако он выделяется в качестве отдельного этапа TDD (Test Driven Development, разработки через тестирование) [5].

Даже у опытных программистов нередко бывает предчувствие, что в программе что-то не так, но не удается определить что именно. Фаулер замечательным образом систематизировал методы рефакторинга, а также отметил признаки плохого кода — так называемые «запахи» [2], однако более фундаментальными для объектно-ориентированного подхода являются принципы SOLID.

Принципы чистого кода (SOLID)

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

LSP — Liskov Substitution Principle

Принцип подстановки изначально сформулирован Барбарой Лисков и регламентирует правильное использование механизма наследования. Выделяются некоторый базовый тип, его подтип (класс-наследник). Согласно принципу LSP, программы должны быть написаны таким образом, чтобы в любом месте вместо базового типа мог быть подставлен подтип. Это означает, что классы наследники должны реализовывать интерфейс согласованно с интерфейсом базового класса.

В качестве примера рассмотрим классы геометрических фигур — точка, окружность, сфера.

Мы могли бы реализовать три класса, независимо друг от друга, но тогда каждый из них содержал бы данные с координатами и соответствующий набор функций — т.е. в нашей программе появился бы повторяющийся код. Согласно Фаулеру «дублирование кода свидетельствует об упущенной возможности для абстракции» [2], т.е. среди трех классов нам надо найти наиболее общий и применить механизм наследования, сделать это можно различными способами:

Что такое solid java. Смотреть фото Что такое solid java. Смотреть картинку Что такое solid java. Картинка про Что такое solid java. Фото Что такое solid javaLiskov Substitution Principle example

Программист, привыкший думать в терминах структур, мог бы заметить, что окружность расширяет интерфейс и данные точки за счет добавления радиуса. Аналогично, сфера расширяет окружность добавлением третьей пространственной координаты. Мы используем открытое наследование, а значит фактически утверждаем, что окружность является разновидностью точки, а сфера — разновидностью окружности. Возникнут, например, следующие проблемы:

Следовательно, такая иерархия наследования приведет к проблемам, предсказать которые позволяет принцип LSP: если мы не можем утверждать, что сфера является точкой или окружностью — то не должны применять открытое наследование.

Другой программист мог бы заметить, что точка — «является» окружностью без радиуса, а окружность — сферой без третьей координаты. Однако, в этом случае у точки появится открытый интерфейс окружности, позволяющий получить радиус, что нельзя считать корректным. Скрыть интерфейс базового класса можно за счет использования закрытого наследования, однако оно задает отношение «реализуется посредством», поэтому принцип LSP на него не распространяется. Любой вид наследования является очень сильной связью между классами и создает множество проблем. К счастью, отношение «реализуется посредством», может быть определено посредством композиции, что всегда является более гибким и предпочтительным решением.

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

Таким образом, принцип подстановки Лисков требует использования открытого наследования лишь для реализации отношения «является разновидностью». На примере показано, что мы не можем установить факт нарушения принципа LSP до тех пор, пока не узнаем как именно будут использоваться наши классы.

DIP — Dependency Inversion Principle

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

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

И композиция, и агрегация выражают отношение «часть-целое». При использовании композиции зависимость между классами является более сильной, т.к. мы не можем изменить ее во время выполнения программы, но при использовании агрегации для этого достаточно изменить ссылку.

Допустим, мы разработали класс TextReceiver, который принимает по какому-либо каналу связи текст и расшифровывает его. При этом TextReceiver реализуется посредством класса TextDecription, ответственного за расшифровку текста, в связи с этим мы могли бы использовать композицию (верхняя часть рисунка):

Что такое solid java. Смотреть фото Что такое solid java. Смотреть картинку Что такое solid java. Картинка про Что такое solid java. Фото Что такое solid javaStrategy pattern example

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

Само по себе использование агрегации вместо композиции решило бы не все проблемы, т.к. новый класс должен был бы наследовать TextDecription, но наследование нарушало бы принцип подстановки Лисков, ведь алгоритм шифрования DES не является разновидностью алгоритма XOR. Чтобы оба принципа были соблюдены — необходимо создавать зависимость от абстрактного класса.

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

Подобная замена композиции агрегацией лежит в основе шаблона проектирования стратегия (Strategy) [8, 11], однако принцип DIP является более общим. Согласно формулировке принципа инверсии зависимостей от Роберта Мартина:

Таким образом, Мартин говорит о любых зависимостях, а не только об отношении «часть-целое», т.е. он не сводит принцип к замене композиции агрегацией. В качестве примера возьмем игру, где персонаж перемещается по лабиринту с артефактами:

Что такое solid java. Смотреть фото Что такое solid java. Смотреть картинку Что такое solid java. Картинка про Что такое solid java. Фото Что такое solid javaDependency Inversion Principle example

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

В нижней части слайда показано другое решение — теперь клиентский код передает состояние персонажа артефакту для обработки. Зависимости между артефактами и состоянием персонажа не нарушают принцип инверсии зависимостей если мы уверены, что не будем создавать разновидности состояний (наследовать класс State). Объекты таких классов, как State называются объектами данных, зависимости от них также не страшны, как и от классов стандартной библиотеки.

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

OCP — Open/Closed principle

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

Открытым называется то, что можно менять. Закрытое изменять нельзя. Существуют различные подходы к понимаю того, каким образом класс должен закрываться — вплоть до требования распространять закрытые модули в скомпилированном виде, но обычно «закрытие» является чисто административным решением.

В настоящее время под принципом OCP обычно обычно понимают требование закрытия интерфейса классов, но сохранения реализации в открытом виде. Закрытый интерфейс вашего класса позволит другим программистам приступить к его использованию до того, как вы закончите реализацию. Открытая реализация позволит вам и другим программистам исправлять ошибки в коде. Интерфейс класса при этом обычно описывают при помощи абстрактных базовых классов, их использование позволяет полностью заменить старую реализацию на новую при необходимости.

Очень важным требованием принципа открытости/закрытости является сохранение устойчивости интерфейса к расширению системы. Второй пример к принципу DIP иллюстрирует эту проблему: до тех пор, пока класс Person содержал отдельный метод для каждого типа артефакта — его интерфейс нельзя было назвать устойчивым к изменениям. Для решения проблемы мы могли бы создать абстракцию — базовый класс для всех артефактов и добавить в класс Person метод, принимающий эту абстракцию. Новый метод должен выполнять какие-либо конкретные действия в зависимости от типа, при добавлении нового вида артефакта — изменения коснутся лишь реализации этого метода, значит интерфейс можно считать стабильным.

SRP — Single Responsibility Principle

Роберт Мартин выделяет правило одной операции, заключающееся в том, что каждая функция должна решать всегда лишь одну задачу [3]. Принцип единой обязанности — это тоже самое правило, распространенное на класс/модуль.

Фаулер описывает принцип SRP через понятие зоны ответственности [2], под которой он имеет ввиду некоторый контекст в котором работает класс или отдельная функция. Внесения изменений в зону ответственности может повлечь необходимость изменения нашего класса. Класс каким либо образом изменяет свою зону ответственности — это является его обязанностью. Если принцип SRP не нарушается, то каждый класс имеет единственную обязанность, а значит — единственную зону ответственности и единственную причину для изменения. В связи с этим, Фаулер считает необходимость частого внесения изменений в класс «запахом» нарушения принципа единой обязанности.

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

ISP — Interface Segregation Principle

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

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

Что такое solid java. Смотреть фото Что такое solid java. Смотреть картинку Что такое solid java. Картинка про Что такое solid java. Фото Что такое solid javaInterface Segregation Principle example

В верхней части диаграммы приведен класс автомобиля. Клиенты могут смотреть на него по-разному, например инспектор ГИБДД видит лишь номер двигателя и скорость, а жена водителя — бардачок и магнитолу. Мы не можем разделить класс на несколько частей, т.к. водителю нужно отслеживать текущую скорость (как инспектору) и хранить в бардачке документы (как это делает жена). Тем не менее, класс может выступать в разных ролях.

Жене совершенно не обязательно знать о номере двигателя и назначении ручника, сотрудник ГИБДД не должен рыться в нашем бардачке. Для решения всех этих проблем достаточно создать несколько специализированных классов интерфейса (как показано в нижней части рисунка), которые и следует передавать соответствующим клиентам. Исходный класс при этом останется неизменным.

Заключение и литература по теме

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

В двух фирмах из четырех, где я работал, менеджеры были именно такими — где-то удавалось убедить их обманом в необходимости рефакторинга под предлогом внедрения новых фич, но не везде. Один проект был начат с чистого листа через 4 года разработки. Если в ваши менеджеры ведут себя таким образом, а сроки сдачи всегда «вчера» — скорее всего вам не пригодятся знания по рефакторингу, SOLID, да и любая другая информация для развития профессиональных качеств программиста.

Источник

Что такое solid java. Смотреть фото Что такое solid java. Смотреть картинку Что такое solid java. Картинка про Что такое solid java. Фото Что такое solid java

Если вы не знаете, что такое SOLID, то можете даже не идти на собеседование, да, да, я серьезно ;). Поэтому давайте разбираться:

Запомнить названия по первости будет сложно, да к этому и не нужно стремиться. Главное понимать содержимое и те идеи, которые предлагаются в каждом из них. Я покажу принципы SOLID на примере языка Java, однако смысл применим к любому языку программирования.

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

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

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

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

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

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

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

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

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

Одним словом, изменять код базового класса строго настрого запрещено!

Принцип подстановки Барбары Лисков, самый не понятный принцип из-за названия :). Но все достаточно просто и немного похоже на предыдущий принцип. Принцип гласит, что поведение методов в дочернем классе должно следовать принципам базового класса, а не изменять их. То есть, дочерний класс переопределяя методы или переменные, не должен менять заложенную логику базового класса.

Например:

Выглядит немного странно, правда? Класс Square(Квадрат) наследуется от Rectangle(прямоугольник), а так как у квадрата все стороны равны, в классе Square мы просто задаем ширину и высоту, одну и ту же. В итоге, мы испортили изначальную идею класса Rectangle, в который заложена логика, что стороны могут отличаться. Получается, не меняя базовый класс, мы умудрились нарушить данный принцип.

В этом примере Square должен быть отдельным классом и ни в коем случае не наследоваться от Rectangle. То есть, поведение методов не должно изменяться. Если написано, что метод возвращает ширину, значит он и должен возвращать ширину, а не что-то другое.

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

Например:

Итак, есть интерфейс, который требует реализовать два метода: короткое нажатие и длинное нажатие. Но что, если нам необходимо только короткое нажатие? В этом случае у нас будет, что-то такое:

Что-бы этого избежать необходимо сделать два разных интерфейса, которые можно применять по отдельности.

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

Модули верхних уровней не должны зависеть от модулей нижних уровней. Если по простому, то нужно делать код так, что-бы этот код имел как можно меньше зависимостей и не было круговых зависимостей, например модуль A зависит от модуля B, а модуль B зависит от модуля A.

Например, у вас есть следующие модули в приложении: presentation(модуль для показа чего либо на экране) и data(модуль для хранения и получения данных). Соответственно presentation будет зависеть от модуля data, так как ему необходимо получать данные для отображения их на экране, но вот модулю data совсем не нужно ничего знать о presentation, ему абсолютно все равно, как эти данные будут отображаться, модуль data просто предоставляет данные.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *