Статьи

Шаблоны проектирования в Java

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

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


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

Несовместимые интерфейсы

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

Отличный дизайн — это не только возможность повторного использования, но и расширяемость.

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

Адаптеры на помощь !!

Достаточно болтовни; давайте приступим к делу, не так ли? Наша устаревшая система программного обеспечения использует следующий интерфейс LegacyVideoController для управления видеосистемой.

01
02
03
04
05
06
07
08
09
10
public interface LegacyVideoController{
 
    /**
     * Begins the playback after startTimeTicks
     * from the beginning of the video
     * @param startTimeTicks time in milliseconds
     */
    public void startPlayback(long startTimeTicks);
    …
}

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

1
2
3
4
5
public void playBackVideo(long timeToStart, LegacyVideoController controller){
    if(controller!=null){
        controller.startPlayback(timeToStart);
    }
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
public interface AdvancedVideoController{
    /**
     * Places the controller head after time
     * from the beginning of the track
     * @param time time defines how much seek is required
     */
    public void seek(Time time);
 
    /**
     * Plays the track
     */
    public void play();
}

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public class AdvancedVideoControllerAdapter implements LegacyVideoController {
 
    private AdvancedVideoController advancedVideoController;
     
    public AdvancedVideoControllerAdapter(AdvancedVideoController advancedVideoController){
        this.advancedVideoController = advancedVideoController;
    }
 
    @Override
    public void startPlayback(long startTimeTicks) {
         
        // Convert long into DateTime
        Time startTime = getTime(startTimeTicks);
         
        // Adapt
        advancedVideoController.seek(startTime);
        advancedVideoController.play();
    }
}

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

Это отношение «имеет-а» позволяет адаптеру делегировать запрос клиента фактическому экземпляру.

Адаптеры также помогают в разъединении клиентского кода и реализации.

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

1
2
3
4
AdvancedVideoController advancedController = controllerFactory.createController();
// adapt
LegacyVideoController controllerAdapter = new AdvancedVideoControllerAdapter(advancedController);
playBackVideo(20, controllerAdapter);

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

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

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

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

Чистая реализация в Java будет выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public class ApplicationCache{
     
    private Map<String, Object> attributeMap;
    // Static instance
    private static ApplicationCache instance;
 
    // Static accessor method
    public static ApplicationCache getInstance(){
        if(instance == null){
            instance == new ApplicationCache();
        }
        return instance;
    }
 
    // private Constructor
    private ApplicationCache(){
        attributeMap = createCache();
    }
}

В нашем примере класс содержит статический член того же типа, что и класс, доступ к которому осуществляется через статический метод. Здесь мы используем Lazy Initialization , откладывая инициализацию кэша до тех пор, пока он действительно не понадобится во время выполнения. Конструктор также делается закрытым, так что новый экземпляр этого класса нельзя создать с помощью оператора new . Чтобы получить кеш, мы вызываем:

1
2
ApplicationCache cache = ApplicationCache.getInstance();
// use cache to improve performance

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

Мы синхронизируем отложенную инициализацию, чтобы убедиться, что код инициализации запускается только один раз. Этот код работает с Java версии 5.0 и выше из-за особенностей, связанных с реализацией synchronized и volatile в Java.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ApplicationCache{
     
    private Map<String, Object> attributeMap;
    // volatile so that JVM out-of-order writes do not happen
    private static volatile ApplicationCache instance;
 
    public static ApplicationCache getInstance(){
        // Checked once
        if(instance == null){
            // Synchronized on Class level lock
            synchronized(ApplicationCache.class){
                // Checked again
                if(instance == null){
                    instance == new ApplicationCache();
                }
            }
        }
        return instance;
    }
 
    private ApplicationCache(){
        attributeMap = createCache();
    }
}

Мы делаем переменную экземпляра энергозависимой, чтобы JVM предотвращала неправильные записи для нее. Мы также делаем двойную нулевую проверку (отсюда и имя), например, при синхронизации инициализации, чтобы любая последовательность из 2 или более потоков не повредила состояние или не привела к созданию более одного экземпляра кэша. Вместо этого мы могли бы синхронизировать весь статический метод доступа, но это было бы излишним, поскольку синхронизация необходима только до полной инициализации объекта; никогда больше при доступе к нему.

Более простым способом было бы покончить с преимуществами отложенной инициализации, что также привело бы к более чистому коду:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public class ApplicationCache{
     
    private Map<String, Object> attributeMap;
    // Initialized while declaration
    private static ApplicationCache instance = new ApplicationCache();
 
    public static ApplicationCache getInstance(){
        return instance;
    }
 
    // private Construcutor
    private ApplicationCache(){
        attributeMap = createCache();
    }
}

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

В зависимости от ваших требований вы также можете защитить от:

  • Код, использующий Reflection API для вызова частного конструктора, с которым можно справиться, вызвав исключение из конструктора, если оно вызывается более одного раза.
  • Аналогично, сериализация и десериализация экземпляра может также привести к двум различным экземплярам нашего кэша, которые могут быть обработаны путем переопределения readResolve() из API-интерфейса сериализации.

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var applicationCache = function() {
 
    // Private stuff
    var instance;
 
    function initCache() {
        return {
            proxyUrl: «/bin/getCache.json»,
            cachePurgeTime: 5000,
            permissions: {
                read: «everyone»,
                write: «admin»
            }
        };
    }
 
    // Public
    return {
        getInstance: function() {
            if (!instance) instance = initCache();
            return instance;
        },
        purgeCache: function() {
            instance = null;
        }
    };
};

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


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

Слово предостережения необходимо: не злоупотребляйте! Не каждая проблема требует использования определенного шаблона проектирования. Вам нужно тщательно проанализировать ситуацию, прежде чем остановиться на шаблоне. Изучение шаблонов проектирования также помогает в понимании других библиотек, таких как jQuery, Spring и т. Д., Которые интенсивно используют многие из таких шаблонов.

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