5 Шаблонов проектирования на Java, которые решают основные проблемы!
В предыдущем посте я показал вам, как выглядит мир ООП на Java. ООП - это довольно удобная парадигма для определения требований, а также для облегчения их понимания.
Различные концепции ООП, такие как наследование и полиморфизм, имеют ряд вариантов использования в популярных приложениях.
Однако в некоторых ситуациях ООП само по себе недостаточно. Чтобы реализовать ту или иную функцию, вам может потребоваться использовать концепции ООП таким образом, чтобы сделать ваш код лучше и понятнее.
В этом посте вы узнаете о 5 шаблонах проектирования, которые могут улучшить вашу жизнь программиста Java. Перед этим давайте разберемся с двумя вещами.
Зачем нужны шаблоны проектирования в ЯП Java?
Давайте возьмём класс Boat, который имеет различные подклассы, представляющие типы лодок. Объект Boat выполняет два действия: sway и roll.
Методы sway() и roll() наследуются каждым подклассом, поскольку они одинаковы для каждого типа лодки. Метод present() является абстрактным, поскольку каждая лодка имеет определённый способ представления себя.
Две лодки, FishBoat и DinghyBoat, расширяют класс Boat. Пока всё работает нормально. Вы также можете добавлять новые лодки по своему усмотрению.
Теперь появился запрос на создание новой функции, которая будет отвечать за погружение лодки. Благодаря этой функции лодка превращается в подводную лодку и ныряет под воду. Вопрос в том, где вы определяете метод dive()?
Есть два типа возможных решений:
Наследование
Вы можете определить метод dive() в классе Boat, который затем унаследует SubBoat. Это обеспечивает возможность повторного использования кода и его ремонтопригодность.
В чём заключается проблема этого решения? Что ж, вы добавили метод dive() в суперкласс, что означает, что теперь все подклассы наследуют эту функцию.
Но мы не хотим видеть, как рыбацкая лодка превращается в подводную лодку и ныряет. Дело в том, что не все классы должны наследовать вашу новую функцию.
Создание интерфейса
Решение путём наследования не соответствовало нашей цели, поэтому вы могли бы попробовать создать интерфейс с возможностью погружения, который определяет метод dive(). Только те лодки, которые должны иметь функцию погружения, будут реализовывать интерфейс и переопределять метод dive().
Это решает проблему предыдущего способа. Метод dive() унаследуют только необходимые классы. Но это создаёт совершенно новый набор проблем.
Поскольку вы используете интерфейс, у вас нет фактической реализации метода. Итак, вам нужно будет реализовать метод внутри подкласса.
На первый взгляд это не кажется плохим вариантом, но не обманывайте себя. Что, если у вас есть ещё 50 классов, которым нужна новая функция? Вам придётся применить один и тот же метод 50 раз.
Это кошмар, так как данный способ добавляет слишком много работы и полностью разрушает ремонтопригодность кода.
Из приведённого выше примера вы поняли, что использование принципов ООП обычным способом не помогает эффективно решить проблему. Нам нужен новый способ решения этой и многих подобных проблем. Вот тут-то и вступают в игру шаблоны проектирования.
Что такое шаблоны проектирования в ЯП Java?
Когда традиционные подходы не работают, вы знаете, что пришло время разрабатывать новые решения. В этом и заключается суть разработки программного обеспечения - изменить ваши текущие, неэффективные способы, чтобы придумать более эффективные подходы.
Давайте посмотрим на проблемы, с которыми мы столкнулись. В наследовании вы добавили новую функциональность, но в процессе были изменены существующие. Лодки, которые не должны были погружаться, внезапно обнаруживают, что наследуют метод dive().
Использование интерфейса, казалось, решило проблему, но это привело к появлению новых. Для каждого класса, нуждающегося в новой функции, вы должны были реализовать метод dive(). Если в функции есть хотя бы небольшое изменение, вам нужно будет внести изменения во все классы, реализующие метод.
Согласно принципам проектирования, когда у вас есть какой-то код, который является новым, поведение должно быть инкапсулировано и сохраняться отдельно от существующего кода, чтобы это не привело к неожиданным последствиям.
Этот принцип служит основой для многих шаблонов проектирования, некоторые из которых вы увидите в этой статье. Вы также посмотрите на шаблон проектирования, используемый для решения вышеуказанной проблемы.
1. Одиночка (шаблон проектирования)
В этом шаблоне вы можете создать только один экземпляр класса. Даже если вы создадите несколько ссылочных переменных, все они будут указывать на один и тот же объект.
Довольно прямолинейно, не так ли? Но как вы можете сделать это возможным? Как вы гарантируете, что будет создан только один объект? Вы поймёте это на примере.
Давайте создадим класс Probe с его переменными экземпляра и методами:
Наш класс Probe не любит иметь несколько объектов. Итак, мы делаем его приватным.
Теперь этот конструктор может быть вызван только изнутри класса. Создайте статический метод getInstance().
Этот метод принимает ссылочную переменную класса Probe и проверяет, был ли объект уже создан. Если нет, то создаётся новый и возвращается клиенту.
Поскольку getInstance() является статическим методом, он может быть вызван только на уровне класса. Каждый раз, когда вызывается этот метод, он возвращает один и тот же объект. Таким образом, шаблон способен блокировать создание нескольких объектов.
Шаблон Одиночка используется для объектов, где требуется только один экземпляр. Например, объектам для настройки реестра и ведения журнала требуется только один экземпляр, иначе они могут вызвать непреднамеренные побочные эффекты.
2. Наблюдатель (шаблон проектирования)
Допустим, вы подписаны на страницу в социальной сети. Всякий раз, когда на этой странице добавляется новая запись, вы хотели бы получать уведомления об этом.
Итак, в том случае, если один объект (страница) выполняет действие (добавляет пост), другой объект (подписчик) получает уведомление. Этот сценарий может быть реализован с помощью шаблона наблюдателя.
Давайте создадим страницу класса и последователь интерфейса. На странице могут быть разные типы подписчиков: обычный пользователь, рекрутер и официальное лицо. У нас будет класс для каждого типа подписчика, и все классы будут реализовывать интерфейс подписчика.
Здесь класс страницы - это тема, а подписчики - классы наблюдателей. Если тема меняет своё состояние (страница добавляет новую запись), все наблюдатели, то есть подписчики, получают уведомление.
Страница класса будет содержать следующие методы:
- registerFollower() : Этот метод регистрирует новых подписчиков.
- notifyFollowers() : Этот метод уведомляет всех подписчиков о том, что на странице появилась новая запись.
- getLatestPost() и addNewPost(): получатель и установщики для последней записи на странице.
С другой стороны, интерфейс последователя имеет только один метод update(), который будет переопределен типами последователей, реализующими этот интерфейс, также называемыми конкретными наблюдателями.
Метод update() вызывается, когда субъекту необходимо уведомить наблюдателя об изменении состояния, т.е. о новой записи.
Давайте реализуем класс страницы.
В этом классе у нас есть список всех подписчиков. Когда новый подписчик хочет перейти на страницу, он вызывает метод registerFollower(). latestPost содержит новую запись, добавленную страницей.
Когда добавляется новая запись, вызывается метод notifyFollowers(), где он перебирает каждого подписчика и уведомляет их, вызывая метод update().
Теперь давайте внедрим наш первый вид подписчика - User.
Когда создаётся новый пользовательский объект, он выбирает страницу, на которую хочет перейти, и регистрируется для неё. Когда страница добавляет новую запись, пользователь получает уведомление с помощью метода update().
Давайте создадим ещё два класса, которые будут следить за страницей:
Давайте протестируем наш шаблон. Сначала создайте страницу и добавьте новый пост.
Никто ещё не перешёл на страницу, так что никто не будет уведомлен.
Теперь пользователь будет уведомлён и получит следующее сообщение:
Далее рекрутер и должностное лицо также последуют за постом
Все трое из них будут уведомлены об этой активности:
3. Стратегия (шаблон проектирования)
Теперь давайте вернёмся к проблеме с лодкой. Мы хотели добавить функцию погружения только на некоторые объекты. Два метода, наследование и переопределение методов, не смогли реализовать нашу цель.
Если вы помните принцип проектирования, нам нужно отделить изменяющийся код от того, что уже существует. Единственная изменяющаяся часть - это поведение dive(), поэтому мы создаём интерфейс, доступный для погружения, и создаём еще два класса, которые его реализуют.
Теперь в вашем классе Boat создайте ссылочную переменную для интерфейса и метод performDive(), который вызывает dive().
Классы FishBoat и DinghyBoat не должны иметь поведения при погружении, поэтому они унаследуют класс NoDiveBehaviour. Давайте посмотрим, как это реализовать:
Когда ссылочная переменная diveable создаётся для объекта NoDiveBehaviour, класс FishBoat наследует метод dive() от него.
Для нового класса SubBoat может быть унаследовано новое поведение.
Теперь давайте протестируем функциональность:
Когда вызывается функция performDive(), она вызывает метод погружения класса NoDiveBehaviour.
Теперь наша новая лодка превращается в подводную и производит погружение.
4. Декоратор (шаблон проектирования)
Иногда вам хочется внести некоторые изменения в функциональность кода. Делая это, вы должны убедиться, что не произойдёт ничего непредвиденного.
Мы возьмём пример класса Car, который представляет из себя два подкласса, Ford и Audi. У него есть метод build(). Этот метод является абстрактным, поскольку каждый автомобиль имеет своё собственное выполнение.
Всё работает нормально. Тем не менее, клиенты хотят внести несколько изменений, таких как добавление ярких фар, добавление спойлера или добавление закиси азота. Как вы будете вносить эти дополнения?
Один из способов - создать различные подклассы для автомобилей с этими модификациями, например, Audi со спойлером, Ford с закисью азота и так далее. Вы можете увидеть, как быстро это становится большой проблемой., так как нет никаких ограничений на количество возможных комбинаций.
Есть лучший и более гибкий способ сделать это. Вы можете определить отдельные классы для каждого объекта и обернуть свой автомобиль вокруг них. Что это значит? Вы скоро это поймёте.
Создайте класс CarModificastion, который расширяет Car.
Создавая объект Car внутри CarModifications, вы оборачиваете Car. Класс mod является абстрактным классом и он расширен ещё тремя классами: ColorLight, Spoiler и Nitrous.
Он реализует метод build(), сначала собирая автомобиль, а затем добавляя к нему спойлер. Два других класса имеют аналогичную реализацию.
Теперь давайте протестируем этот шаблон. Мы создадим Audi и добавим к ней спойлер.
После создания объекта Car, вы используете тот же экземпляр для создания нового объекта Car с добавленным к нему спойлером. Вызовите метод build() для выполнения шаблона, дающего следующий результат:
Если вы также хотите автомобиль с закисью азота, создайте подобный объект Car.
Вывод:
5. Фабричный метод (шаблон проектирования)
Этот шаблон предлагает другой способ создания экземпляров объектов, который обеспечивает большую гибкость в соответствии с изменяющимися требованиями. Когда вы расширяете абстрактный класс и реализуете его абстрактные методы, вы создаёте конкретный класс.
С появлением новых требований количество конкретных классов в вашем коде может увеличиться. Это затрудняет изменение вашего кода.
Думаю, мы обязательно рассмотрим данный шаблон проектирования в другой статье.
Заключение
Программирование некоторых сценариев может стать проблематичным при использовании обычных методов. Важно продолжать пробовать разные методы и быть открытым для изменения вашего подхода к проблеме.
В этом посте я начал с причины, по которой нам нужны шаблоны проектирования, и принципов, которым они следуют. Затем я объяснил четыре шаблона проектирования с примерами и блоками кода. Надеюсь, это помогло вам лучше понять их.
Статья была взята из этого источника: