Пять шаблонов проектирования, которые необходимо знать каждому разработчику

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

Как архитектор, вы проектируете в настоящем, с осознанием прошлого и ради будущего, которое по существу неизвестно.  —  Норман Фостер

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

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

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

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

1. Синглтон

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

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

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

public class SingletonDemo {
   private static SingletonDemo instance = null;   

   private SingletonDemo() {
   }   

   public static SingletonDemo getInstance() {
      if(instance == null) {
         instance = new SingletonDemo();
      }
      return instance;
   }
}

Метод getInstance() в приведенном выше коде гарантирует, что во время выполнения будет создан только один экземпляр класса.

Предпочтительный сценарий

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

Ограничение

Метод getInstance() в этом шаблоне  —  не потокобезопасен и поэтому допускает многократную инициализацию. Можно ограничить это, сделав метод синхронизированным, но так замедлится выполнение.

2. Фабричный метод

Слово “фабрика” относится здесь к тому, что как обычная фабрика производит продукцию, так и фабрика программного обеспечения производит объекты. Но делает это по-другому, а именно вызывая фабричный метод вместо вызова конструктора конкретного класса.

Обычно создание объекта происходит следующим образом:

DemoClass demoClassObject = new DemoClass();

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

Проблема решается с помощью фабрики шаблонов, как показано ниже:

public interface Notification{
    String getType();
}
public class Call implements Notification{
    public String getType(){
        return "call"
    }
}
public class Message implements Notification{
    public String getType(){
        return "message"
    }
}
public class NotificationFactory {
    private static Map<String, Notification> instances;    static {
        instances = new HashMap<>();        instances.put("call", new Call());
        instances.put("message", new Message());
    }
public static <T extends Notification> T getNotification(String   type)
    {
        return (T) instances.get(type); 
    }
}
Notification notif = NotificationFactory.getNotification("call");

Если требуется конкретное уведомление, нужно только указать тип, и оно будет возвращено.

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

Предпочтительный сценарий

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

Ограничение

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

3. Шаблон строителя

Как следует из названия, шаблон “Строитель” применяют для построения объектов. Иногда объекты бывают сложными, состоят из нескольких подобъектов или требуют сложного процесса конструирования.

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

Приведенный ниже пример поможет разобраться лучше:

public class Product {
    private String id;
    private String name;
    private String description;
    private Double value;    
    private Product(Builder builder) {
        setId(builder.id);
        setName(builder.name);
        setDescription(builder.description);
        setValue(builder.value);
    }
    public static Builder newProduct() {
        return new Builder();
    }
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    public Double getValue() {
        return value;
    }
    public void setValue(Double value) {
        this.value = value;
    }
    public static final class Builder {
        private String id;
        private String name;
        private String description;
        private Double value;
        private Builder() {}
        public Builder id(String id) {
            this.id = id;
            return this;
        }
        public Builder name(String name) {
            this.name = name;
            return this;
        }
        public Builder description(String description) {
            this.description = description;
            return this;
        }
        public Builder value(Double value) {
            this.value = value;
            return this;
        }
        public Product build() {
            return new Product(this);
        }
    }
}
Product product = Product.newProduct()
                         .id(1l)
                         .description("TV 46'")
                         .value(2000.00)
                         .name("TV 46'")
                         .build();

Здесь мы создали объект для класса Product с помощью шаблона “Строитель”.

Предпочтительный сценарий

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

Ограничение

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

4. Шаблон адаптера

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

Как применяется адаптер:

  1. Клиент делает запрос к адаптеру, вызывая на нем метод с использованием целевого интерфейса.
  2. Адаптер преобразует этот запрос с помощью интерфейса адаптера.
  3. Клиент получает результаты вызова и не знает о присутствии адаптера.
interface Bird 
{ 
   // птицы реализуют интерфейс Птица, который позволяет 
   // им летать и издавать звуки
   public void fly(); 
   public void makeSound(); 
}
class Sparrow implements Bird 
{ 
   // конкретная реализация птицы
   public void fly() 
   { 
     System.out.println("Flying"); 
   }
   public void makeSound() 
   { 
     System.out.println("Chirp Chirp"); 
   } 
}
interface ToyDuck 
{ 
   // целевой интерфейс
   // игрушечные утки не летают, они просто издают 
   // квакающие звуки
   public void squeak(); 
}
class PlasticToyDuck implements ToyDuck 
{ 
   public void squeak() 
   { 
     System.out.println("Squeak"); 
   } 
}
class BirdAdapter implements ToyDuck 
{ 
   // Вам нужно реализовать интерфейс
   // который хочет клиент. 
   Bird bird; 
   public BirdAdapter(Bird bird) 
   { 
     // нам нужна отсылка к объекту 
     // который мы адаптируем
     this.bird = bird; 
   }
   public void squeak() 
   { 
     // переводим метод соответственно
     bird.makeSound(); 
   } 
}
class Main 
{ 
 public static void main(String args[]) 
 { 
  Sparrow sparrow = new Sparrow(); 
  ToyDuck toyDuck = new PlasticToyDuck();  // Оборачиваем птицу Bird в birdAdapter, так что она
  // ведет себя как игрушечная утка
  ToyDuck birdAdapter = new BirdAdapter(sparrow);
  System.out.println("Sparrow..."); 
  sparrow.fly(); 
  sparrow.makeSound();
  System.out.println("ToyDuck..."); 
  toyDuck.squeak();  // toy duck behaving like a bird 
  System.out.println("BirdAdapter..."); 
  birdAdapter.squeak(); 
 } 
}

В приведенном выше коде предположим, что есть птица, которая может издавать звуки  —  makeSound(), и у нас есть пластиковая игрушечная утка toyDuck, которая может квакать  —  squeak(). Теперь предположим, что наш клиент меняет требование и хочет, чтобы toyDuck сделал makeSound()?

Простое решение  —  поменять класс реализации на новый класс адаптера и сказать клиенту передать экземпляр птицы (которая хочет squeak()) этому классу.

Предпочтительный сценарий

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

Ограничение

Адаптер не сочетается с подклассами Adaptee или Target.

5. Шаблон состояния

Этот шаблон помогает представить несколько состояний объекта. Предположим, у нас есть объект для класса “Радио”. Радио может находиться в двух состояниях: включенном или выключенном. Эти состояния и представлены шаблоном состояний.

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

Давайте разберемся в этом на примере с радио:

public class Radio {
    private boolean on;
    private RadioState state;

    public Radio(RadioState state){
        this.state = state;
    }

    public void execute(){
        state.execute(this);
    }

    public void setState(RadioState state){
        this.state = state;
    }

    public void setOn(boolean on){
        this.on = on;
    }

    public boolean isOn(){
        return on;
    }

    public boolean isOff(){
        return !on;
    }
}
public interface RadioState {
    void execute(Radio radio);
}
public class OnRadioState implements RadioState {
    public void execute(Radio radio){
        //выбрасывает исключение, если радио уже включено
        radio.setOn(true);
    }
}
public class OffRadioState implements RadioState {
    public void execute(Radio radio){
        //выбрасывает исключение, если радио уже выключено
        radio.setOn(false);
    }
}
Radio radio = new Radio(new OffRadioState()); //initial status
radio.setState(new OnRadioState());
radio.execute(); //radio on
radio.setState(new OffRadioState());
radio.execute(); //radio off

Приведенный пример демонстрирует различные состояния радио с шаблоном состояний.

Предпочтительный сценарий

Случай, когда нужно представить несколько состояний объекта, способного к внутренним изменения. Если обходиться без шаблона состояния, код становится негибким и слишком полагается на структуру if-else.

Ограничение

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

Заключение

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

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

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Shubham Pathania: “5 Design Patterns Every Software Developer Should Know”

Предыдущая статьяОптимизация структур в Golang для эффективного распределения памяти
Следующая статьяРасширение Jupyter для VS Code