5 Шаблонов проектирования на Java, которые решают основные проблемы!

Концепции ООП, такие как наследование и полиморфизм, имеют много вариантов применения в работе над кодом.

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

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

Зачем нужны шаблоны проектирования в ЯП Java?

5 Шаблонов проектирования на Java, которые решают основные проблемы!

Давайте возьмём класс Boat, который имеет различные подклассы, представляющие типы лодок. Объект Boat выполняет два действия: sway и roll.

abstract class Boat {
    void sway() { ... }
    void roll() { ... }
    abstract void present();
}

Методы sway() и roll() наследуются каждым подклассом, поскольку они одинаковы для каждого типа класса Boat. Метод present() является абстрактным, поскольку каждая лодка(Boat) имеет определённый способ представления себя.

class FishBoat extends Boat {
    void present() { ... }
}
void DinghyBoat extends Boat {
    void present() { ... }
}

Две лодки, FishBoat и DinghyBoat, расширяют класс Boat. Пока всё работает нормально. Вы также можете добавлять новые лодки по своему усмотрению.

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

Есть два типа возможных решений:

Наследование

Вы можете определить метод dive() в классе Boat, который затем унаследует SubBoat. Это обеспечивает возможность повторного использования кода.

В чём заключается проблема этого решения? Что ж, вы добавили метод dive() в суперкласс, что означает, что теперь все подклассы наследуют эту функцию.

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

Создание интерфейса

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

Это решает проблему предыдущего способа. Метод dive() унаследуют только необходимые классы. Но это создаёт совершенно новый набор проблем.

Поскольку вы используете интерфейс, у вас нет фактической реализации метода. Итак, вам нужно будет реализовать метод внутри подкласса.

На первый взгляд это не кажется плохим вариантом, но не обманывайте себя. Что, если у вас есть ещё 50 классов, которым нужна новая функция? Вам придётся применить один и тот же метод 50 раз.

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

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

Что такое шаблоны проектирования в ЯП Java?

5 Шаблонов проектирования на Java, которые решают основные проблемы!

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

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

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

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

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

1. Одиночка (шаблон проектирования)

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

Довольно прямолинейно, не так ли? Но как вы можете сделать это возможным? Как вы гарантируете, что будет создан только один объект? Вы поймёте это на примере.

Давайте создадим класс Probe с его переменными экземпляра и методами:

class Probe {
    // Instance variables

    // Important methods
}

Наш класс Probe не любит иметь несколько объектов. Итак, мы делаем его приватным.

private Probe() { 
    // Initialize variables here
}

Теперь этот конструктор может быть вызван только изнутри класса. Создайте статический метод getInstance().

Этот метод принимает ссылочную переменную класса Probe и проверяет, был ли объект уже создан. Если нет, то создаётся новый и возвращается клиенту.

private static Probe getInstance(Probe probe) {
    if(probe == null)
      probe = new Probe();

    return probe;
}

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

Шаблон Одиночка используется для объектов, где требуется только один экземпляр. Например, объектам для настройки реестра и ведения журнала требуется только один экземпляр, иначе они могут вызвать непреднамеренные побочные эффекты.

2. Наблюдатель (шаблон проектирования)

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

Итак, в том случае, если один объект (страница) выполняет действие (добавляет пост), другой объект (подписчик) получает уведомление. Этот сценарий может быть реализован с помощью шаблона наблюдателя.

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

Здесь класс страницы – это тема, а подписчики – классы наблюдателей. Если тема меняет своё состояние (страница добавляет новую запись), все наблюдатели, то есть подписчики, получают уведомление.

Страница класса будет содержать следующие методы:

  • registerFollower() : Этот метод регистрирует новых подписчиков.
  • notifyFollowers() : Этот метод уведомляет всех подписчиков о том, что на странице появилась новая запись.
  • getLatestPost() и addNewPost(): получатель и установщики для последней записи на странице.

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

interface Follower {
    public void update();
}

Метод update() вызывается, когда субъекту необходимо уведомить наблюдателя об изменении состояния, т.е. о новой записи.

Давайте реализуем класс страницы.

class Page {
    private ArrayList<Follower> followers;
    String latestPost;
    
    public Page() {
        followers = new ArrayList();
    }
    
    public void registerFollower(Follower f) {
        followers.add(f);
    }
    
    public void notifyFollowers() {
        for(int i = 0; i < followers.size(); i++) {
            Follower follower = followers.get(i);
            follower.update();
        }
    }
    
    public String getLatestPost() {
        return latestPost;
    }
    
    public void addNewPost(String post) {
        this.latestPost = post;
        notifyFollowers();
    }
    
}

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

Когда добавляется новая запись, вызывается метод notifyFollowers(), где он перебирает каждого подписчика и уведомляет их, вызывая метод update().

Теперь давайте внедрим наш первый вид подписчика – User.

class User implements Follower {
    Page page;
    public User(Page page) {
        this.page = page;
        page.registerFollower(this);
    }
    public void update() {
        System.out.println("Latest post seen by a normal user: " + page.getLatestPost());
    }
}

Когда создаётся новый пользовательский объект, он выбирает страницу, на которую хочет перейти, и регистрируется для неё. Когда страница добавляет новую запись, пользователь получает уведомление с помощью метода update().

Давайте создадим ещё два класса, которые будут следить за страницей:

class Recruiter implements Follower {
    String company;
    // Rest is the same as User class
}
class Official implements Follower {
    String designation;    
    // Rest is the same as User class
}

Давайте протестируем наш шаблон. Сначала создайте страницу и добавьте новый пост.

Page page = new Page();
page.addNewPost("I am feeling lucky!");

Никто ещё не перешёл на страницу, так что никто не будет уведомлен.

User user = new User(page);
page.addNewPost("It's a beautiful day!");

Теперь пользователь будет уведомлён и получит следующее сообщение:

Latest post seen by a normal user: It's a beautiful day!

Далее рекрутер и должностное лицо также последуют за постом:

Recruiter recruiter = new Recruiter(page);
Official official = new Official(page);
page.addNewPost("Ready to go for a run!!");

Все трое из них будут уведомлены об этой активности:

Latest post seen by a normal user: Ready to go for a run!!
Latest post seen by a recruiter: Ready to go for a run!!
Latest post seen by an official: Ready to go for a run!!

3. Стратегия (шаблон проектирования)

Теперь давайте вернёмся к проблеме с лодкой. Мы хотели добавить функцию погружения только на некоторые объекты. Два метода, наследование и переопределение методов, не смогли реализовать нашу цель.

Если вы помните принцип проектирования, нам нужно отделить изменяющийся код от того, что уже существует. Единственная изменяющаяся часть – это поведение dive(), поэтому мы создаём интерфейс, доступный для погружения, и создаём еще два класса, которые его реализуют.

interface Diveable {
    public void dive();
}
class DiveBehaviour implements Diveable {
    public void dive() {
      // Implementation here
    }
}
class NoDiveBehaviour implements Diveable {
    public void dive() {
       // Implementation here
    }
}

Теперь в вашем классе Boat создайте ссылочную переменную для интерфейса и метод performDive(), который вызывает dive().

abstract class Boat {
    Diveable diveable;
    void sway() { ... }
    void roll() { ... }
    
    abstract void present();
    
    public void performDive() {
        diveable.dive();
    }
}

Классы FishBoat и DinghyBoat не должны иметь поведения при погружении, поэтому они унаследуют класс NoDiveBehaviour. Давайте посмотрим, как это реализовать:

class FishBoat extends Boat {
    ...

    public FishBoat() {
        diveable = new NoDiveBehaviour();
    }
    ...
}

Когда ссылочная переменная diveable создаётся для объекта NoDiveBehaviour, класс FishBoat наследует метод dive() от него.

Для нового класса SubBoat может быть унаследовано новое поведение.

class SubBoat extends Boat {
    ...  
  
    public FishBoat() {
        diveable = new DiveBehaviour();
    }
    ...
}

Теперь давайте протестируем функциональность:

Boat fishBoat = new FishBoat();
fishBoat.performDive();

Когда вызывается функция performDive(), она вызывает метод погружения класса NoDiveBehaviour.

Boat subBoat = new SubBoat();
subBoat.performDive();

Теперь наша новая лодка превращается в подводную и производит погружение.

4. Декоратор (шаблон проектирования)

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

Мы возьмём пример класса Car, который представляет из себя два подкласса, Ford и Audi. У него есть метод build(). Этот метод является абстрактным, поскольку каждый автомобиль имеет своё собственное выполнение.

abstract class Car {
    abstract void build();
}
class Ford extends Car {
    public void build() {
        System.out.println("Ford built");
    }
}

class Audi extends Car {
    public void build() {
        System.out.println("Audi built");
    }
}

Всё работает нормально. Тем не менее, клиенты хотят внести несколько изменений, таких как добавление ярких фар, добавление спойлера или добавление закиси азота. Как вы будете вносить эти дополнения?

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

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

Создайте класс CarModificastion, который расширяет Car.

abstract class CarModifications extends Car {
    Car car;

    public CarModifications(Car car) {
        this.car = car;
    }
}

Создавая объект Car внутри CarModifications, вы оборачиваете Car. Класс mod является абстрактным классом и он расширен ещё тремя классами: ColorLight, Spoiler и Nitrous.

class Spoiler extends CarModifications {
    public Spoiler(Car car) {
        super(car);
    }
    
    public void build() {
        car.build();
        addSpoiler();
    }
    
    void addSpoiler() {
        System.out.println("Spoiler built");
    }
}

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

Теперь давайте протестируем этот шаблон. Мы создадим Audi и добавим к ней спойлер.

Car audi = new Audi();
Car audiWithSpoiler = new Spoiler(audi);
audiWithSpoiler.build();

После создания объекта Car, вы используете тот же экземпляр для создания нового объекта Car с добавленным к нему спойлером. Вызовите метод build() для выполнения шаблона, дающего следующий результат:

Audi built
Spoiler built

Если вы также хотите автомобиль с закисью азота, создайте подобный объект Car.

Car audiWithMods = new Nitrous(audiWithSpoiler);
audiWithMods.build();

Вывод:

Audi built
Spoiler built
Nitrous built

5. Фабричный метод (шаблон проектирования)

Этот шаблон предлагает другой способ создания экземпляров объектов, который обеспечивает большую гибкость в соответствии с изменяющимися требованиями. Когда вы расширяете абстрактный класс и реализуете его абстрактные методы, вы создаёте конкретный класс.

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

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

Заключение

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

В этом посте я начал с причины, по которой нам нужны шаблоны проектирования, и принципов, которым они следуют. Затем я объяснил четыре шаблона проектирования с примерами и блоками кода. Надеюсь, это помогло вам лучше понять их.

+1
0
+1
3
+1
0
+1
0
+1
1

Ответить

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