Все, что вам нужно знать о твердых принципах в Java



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

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

История принципов SOLID в Java

Роберт С. Мартин привел пять принципов объектно-ориентированного дизайна, и для этого используется аббревиатура «S.O.L.I.D». Когда вы используете все принципы S.O.L.I.D в сочетании, вам становится проще разрабатывать программное обеспечение, которым можно легко управлять. Другие особенности использования S.O.L.I.D:





  • Избегает запаха кода.
  • Быстро рефрактор кода.
  • Может заниматься адаптивной или гибкой разработкой программного обеспечения.

Когда вы используете принцип S.O.L.I.D в коде, вы начинаете писать эффективный и действенный код.



В чем смысл S.O.L.I.D?

Solid представляет пять принципов Java, а именно:

  • S : Принцип единственной ответственности
  • ИЛИ : Принцип открыт-закрыт
  • L : Принцип подстановки Лисков
  • я : Принцип разделения интерфейса
  • D : Принцип инверсии зависимостей

В этом блоге мы подробно обсудим все пять принципов SOLID Java.



Принцип единой ответственности в Java

Что там написано?

Роберт С. Мартин описывает это как один класс, который должен нести только одну и только ответственность.

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

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

  • Подключен к базе данных

  • Прочитать данные из таблиц базы данных

  • Наконец, запишите его в файл.

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

Например, класс «Автомобиль» может запускаться или останавливаться, но задача его стирки принадлежит классу «CarWash». В другом примере класс Book имеет свойства для хранения собственного имени и текста. Но задача печати книги должна принадлежать классу Book Printer. Класс Book Printer может печатать на консоли или другом носителе, но такие зависимости удалены из класса Book.

Почему требуется этот принцип?

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

Пример для пояснения этого принципа:

Предположим, вас просят реализовать службу UserSetting, в которой пользователь может изменять настройки, но перед этим пользователь должен пройти аутентификацию. Один из способов реализовать это:

public class UserSettingService {public void changeEmail (User user) {if (checkAccess (user)) {// Предоставление опции для изменения}} public boolean checkAccess (User user) {// Проверяем, действителен ли пользователь. }}

Все выглядит хорошо, пока вы не захотите повторно использовать код checkAccess в другом месте ИЛИ вы не захотите внести изменения в способ выполнения checkAccess. Во всех двух случаях вам придется изменить один и тот же класс, и в первом случае вам также придется использовать UserSettingService для проверки доступа.
Один из способов исправить это - разложить UserSettingService на UserSettingService и SecurityService. И переместите код checkAccess в SecurityService.

public class UserSettingService {public void changeEmail (User user) {if (SecurityService.checkAccess (user)) {// Предоставить возможность изменения}}} public class SecurityService {public static boolean checkAccess (User user) {// проверить доступ. }}

Принцип открытости и закрытости в Java

Роберт С. Мартин описывает это как программные компоненты, которые должны быть открыты для расширения, но закрыты для модификации.

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

  • Наследование от класса

  • Перезапись требуемого поведения из класса

  • Расширение определенных моделей поведения класса

Отличный пример принципа открытости-закрытости можно понять с помощью браузеров. Вы помните, как устанавливали расширения в свой браузер Chrome?

Основная функция браузера Chrome - просмотр разных сайтов. Хотите проверить грамматику, когда пишете электронное письмо с помощью браузера Chrome? Если да, вы можете просто использовать расширение Grammarly, оно обеспечивает проверку грамматики содержимого.

как отсортировать массив в c ++

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

Зачем нужен этот принцип?

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

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

public class Rectangle {public double length public double width}

Затем мы создаем класс для вычисления площади этого прямоугольника.который имеет метод calculateRectangleAreaкоторый принимает прямоугольникв качестве входного параметра и вычисляет его площадь.

открытый класс AreaCalculator {public double calculateRectangleArea (Rectangle rectangle) {return rectangle.length * rectangle.width}}

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

общественный класс Circle {public double radius}

Затем модифицируем Areacalculatorкласс для добавления круговых вычислений с помощью нового метода calculateCircleaArea ()

открытый класс AreaCalculator {public double calculateRectangleArea (Rectangle rectangle) {return rectangle.length * rectangle.width} public double calculateCircleArea (Circle circle) {return (22/7) * circle.radius * circle.radius}}

Однако обратите внимание, что в описанном выше способе разработки нашего решения были недостатки.

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

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

Модификация вышеуказанной конструкции для соответствия принципу открытия / закрытия:

Давайте теперь посмотрим на более элегантный дизайн, который устраняет недостатки в вышеупомянутом дизайне, придерживаясь принципа открытого / закрытого. В первую очередь сделаем дизайн расширяемым. Для этого нам нужно сначала определить базовый тип Shape и заставить Circle и Rectangle реализовать интерфейс Shape.

открытый интерфейс Shape {public double calculateArea ()} открытый класс Rectangle реализует Shape {double length double width public double calculateArea () {return length * width}} открытый класс Circle реализует Shape {общедоступный двойной радиус public double calculateArea () {return (22 / 7) * радиус * радиус}}

Есть базовый интерфейс Shape. Все фигуры теперь реализуют базовый интерфейс Shape. В интерфейсе формы есть абстрактный метод calculateArea (). И круг, и прямоугольник предоставляют собственную переопределенную реализацию метода calculateArea () с использованием собственных атрибутов.
Мы внесли некоторую степень расширяемости, поскольку формы теперь являются экземпляром интерфейсов Shape. Это позволяет нам использовать Shape вместо отдельных классов.
Последний пункт выше упомянул потребитель этих форм. В нашем случае потребителем будет класс AreaCalculator, который теперь будет выглядеть так.

открытый класс AreaCalculator {public double calculateShapeArea (Shape shape) {return shape.calculateArea ()}}

Этот AreaCalculatorclass теперь полностью устраняет наши недостатки дизайна, упомянутые выше, и дает чистое решение, которое придерживается принципа открытого-закрытого. Давайте перейдем к другим принципам SOLID в Java.

Принцип подстановки Лискова в Java

Роберт С. Мартин описывает это как производные типы должны быть полностью заменяемыми для своих базовых типов.

Принцип подстановки Лискова предполагает, что q (x) является свойством, доказуемым для объектов x, принадлежащих типу T. Теперь, согласно этому принципу, q (y) теперь должно быть доказуемо для объектов y, принадлежащих типу S, и S на самом деле является подтипом T. Вы сейчас запутались и не знаете, что на самом деле означает принцип подстановки Лискова? Определение этого может быть немного сложным, но на самом деле это довольно просто. Единственное, что каждый подкласс или производный класс должен заменять их родительский или базовый класс.

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

Зачем нужен этот принцип?

Это позволяет избежать неправильного использования наследования. Это помогает нам соответствовать отношениям «есть-а». Мы также можем сказать, что подклассы должны выполнять контракт, определенный базовым классом. В этом смысле это связано сДизайн по контрактуэто было впервые описано Бертраном Мейером. Например, соблазнительно сказать, что круг - это тип эллипса, но у кругов нет двух фокусов или больших / малых осей.

LSP обычно объясняется на примере квадрата и прямоугольника. если мы предположим связь ISA между Square и Rectangle. Таким образом, мы называем «Квадрат - это прямоугольник». Код ниже представляет отношения.

public class Rectangle {private int length private intwidth public int getLength () {return length} public void setLength (int length) {this.length = length} public int getBreadth () {return width} public void setBreadth (int width) { this.breadth = ширина} public int getArea () {вернуть this.length * this.breadth}}

Ниже приведен код для Square. Обратите внимание, что Square расширяет Rectangle.

разница между final finally и finalize
открытый класс Square расширяет Rectangle {public void setBreadth (int width) {super.setBreadth (width) super.setLength (width)} public void setLength (int length) {super.setLength (length) super.setBreadth (length)}}

В этом случае мы пытаемся установить связь ISA между Square и Rectangle, так что вызов «Square is a Rectangle» в приведенном ниже коде начнет вести себя неожиданно, если будет передан экземпляр Square. Ошибка утверждения будет выдана в случае проверки «Площадь» и проверки «Ширина», хотя программа будет завершена, поскольку ошибка утверждения будет выдана из-за сбоя проверки области.

открытый класс LSPDemo {public void calculateArea (Rectangle r) {r.setBreadth (2) r.setLength (3) assert r.getArea () == 6: printError ('area', r) assert r.getLength () == 3: printError ('length', r) assert r.getBreadth () == 2: printError ('width', r)} private String printError (String errorIdentifer, Rectangle r) {return 'Unnexpected value of' + errorIdentifer + ' например, '+ r.getClass (). getName ()} public static void main (String [] args) {LSPDemo lsp = new LSPDemo () // Передан экземпляр Rectangle lsp.calculateArea (new Rectangle ()) // Экземпляр Square передается lsp.calculateArea (new Square ())}}

Класс демонстрирует принцип замещения Лискова (LSP). Согласно принципу, функции, использующие ссылки на базовые классы, должны иметь возможность использовать объекты производного класса, не зная об этом.

Таким образом, в примере, показанном ниже, функция calculateArea, который использует ссылку «Rectangle», должна иметь возможность использовать объекты производного класса, такие как Square, и выполнять требование, поставленное определением Rectangle. Следует отметить, что согласно определению Rectangle, следующее всегда должно выполняться, учитывая данные ниже:

  1. Длина всегда должна быть равна длине, переданной в качестве входных данных в метод setLength.
  2. Ширина всегда должна быть равна ширине, передаваемой в качестве входных данных в метод setBreadth.
  3. Площадь всегда должна быть равна произведению длины и ширины.

В случае, если мы пытаемся установить связь ISA между Square и Rectangle, так что мы называем «Square is a Rectangle», приведенный выше код начнет вести себя неожиданно, если будет передан экземпляр Square. В случае проверки области и проверки будет выдана ошибка утверждения. для широты, хотя программа будет завершена, поскольку ошибка утверждения будет выдана из-за сбоя проверки области.

Классу Square не нужны такие методы, как setBreadth или setLength. Классу LSPDemo необходимо знать подробности производных классов Rectangle (например, Square) для правильного кодирования во избежание возникновения ошибки. Изменение в существующем коде прежде всего нарушает принцип «открыт-закрыт».

Принцип разделения интерфейса

Роберт С. Мартин описывает это как то, что клиентов не следует заставлять реализовывать ненужные методы, которые они не будут использовать.

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

Например, единый интерфейс для записи и чтения журналов полезен для базы данных, но не для консоли. Для консольного регистратора нет смысла читать журналы. Переходим к статье SOLID Principles in Java.

Зачем нужен этот принцип?

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

Теперь давайте создадим интерфейс Java для ресторана и назовем его RestaurantInterface.java.

открытый интерфейс RestaurantInterface {public void acceptOnlineOrder () public void takeTelephoneOrder () public void payOnline () public void walkInCustomerOrder () public void payInPerson ()}

В RestaurantInterface определены 5 методов, которые предназначены для приема онлайн-заказа, приема телефонного заказа, приема заказов от обычного клиента, приема онлайн-платежей и приема платежей лично.

Давайте начнем с реализации RestaurantInterface для онлайн-клиентов как OnlineClientImpl.java.

public class OnlineClientImpl реализует RestaurantInterface {public void acceptOnlineOrder () {// логику для размещения онлайн-заказа} public void takeTelephoneOrder () {// Неприменимо для онлайн-заказа throw new UnsupportedOperationException ()} public void payOnline () {// логику для оплаты online} public void walkInCustomerOrder () {// Не применимо для онлайн-заказа throw new UnsupportedOperationException ()} public void payInPerson () {// Не применимо для онлайн-заказа throw new UnsupportedOperationException ()}}
  • Поскольку приведенный выше код (OnlineClientImpl.java) предназначен для онлайн-заказов, вызовите исключение UnsupportedOperationException.

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

  • Классы реализации для Telephonic client и Walk-in client будут иметь неподдерживаемые методы.

  • Поскольку 5 методов являются частью RestaurantInterface, классы реализации должны реализовать все 5 из них.

  • Методы, которые каждый из классов реализации выдает исключение UnsupportedOperationException. Как видите, реализация всех методов неэффективна.

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

  • RestaurantInterface.java нарушает принцип единой ответственности, потому что логика платежей, а также логика размещения заказов сгруппированы в едином интерфейсе.

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

  1. Разделите функции оплаты и размещения заказов на два отдельных простых интерфейса, PaymentInterface.java и OrderInterface.java.

  2. Каждый из клиентов использует по одной реализации каждого из PaymentInterface и OrderInterface. Например - OnlineClient.java использует OnlinePaymentImpl и OnlineOrderImpl и так далее.

  3. Принцип единой ответственности теперь прикреплен как интерфейс оплаты (PaymentInterface.java) и интерфейс заказа (OrderInterface).

  4. Изменение одного из интерфейсов заказа или оплаты не влияет на другой. Теперь они независимы: не нужно будет создавать какую-либо фиктивную реализацию или генерировать исключение UnsupportedOperationException, поскольку каждый интерфейс имеет только методы, которые он всегда будет использовать.

После применения ISP

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

Роберт С. Мартин описывает это как зависящее от абстракций, а не от конкреций. Согласно ему, высокоуровневый модуль никогда не должен полагаться на какой-либо низкоуровневый модуль. Например

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

Даже если вы дали карту Visa, он не выпустит автомат Visa для считывания вашей карты. Тип кредитной или дебетовой карты, которую вы используете для оплаты, даже не имеет значения, они просто проведут ее. Итак, в этом примере вы можете видеть, что и вы, и клерк зависят от абстракции кредитной карты, и вас не беспокоят ее особенности. Вот что такое принцип инверсии зависимостей.

Зачем нужен этот принцип?

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

публичный класс Студент {частный адрес публичный Студент () {адрес = новый адрес ()}}

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

На этом мы подошли к концу этих принципов SOLID в Java.

Проверьте от Edureka, надежной компании по онлайн-обучению с сетью из более чем 250 000 довольных учащихся по всему миру. Курс обучения и сертификации по Java J2EE и SOA от Edureka предназначен для студентов и профессионалов, которые хотят стать Java-разработчиками. Курс разработан, чтобы дать вам хорошее начало в программировании на Java и обучить вас как основным, так и продвинутым концепциям Java, а также различным средам Java, таким как Hibernate и Spring.

Есть вопрос к нам? Пожалуйста, укажите это в разделе комментариев этого блога «Принципы SOLID в Java», и мы свяжемся с вами в ближайшее время.