JavaRush /Java блог /Random /Кофе-брейк #267. Основные шаблоны проектирования в Java, ...

Кофе-брейк #267. Основные шаблоны проектирования в Java, которые вам нужно знать

Статья из группы Random
Источник: Medium Данное руководство объясняет принципы работы и варианты применения наиболее популярных шаблонов проектирования: Singleton, Strategy, Factory, Abstract Factory и Builder. Кофе-брейк #267. Основные шаблоны проектирования в Java, которые вам нужно знать - 1

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

Шаблоны (паттерны) проектирования — это многократно используемые решения при разработке программного обеспечения. Они представляют собой передовой опыт решения определенных типов проблем в виде последовательного, эффективного и удобного в сопровождении способа разработки надежных программных систем. Согласно известной книге “Шаблоны проектирования: элементы объектно-ориентированного программного обеспечения многократного использования” авторства Эриха Гамма, Ричарда Хелма, Ральфа Джонсона и Джона Влиссидеса, существует 23 шаблона проектирования, которые разделены на три основные категории:

Шаблоны творческого проектирования:

Эта категория шаблонов позволяют нам создавать объекты, скрывая при этом детали реализации создания экземпляров. Они абстрагируют процесс создания экземпляров, делая систему более гибкой и расширяемой. Вот некоторые распространенные шаблоны творческого проектирования: Singleton, Factory, Abstract Factory, Builder и Prototype.

Структурные шаблоны проектирования:

Они занимаются композицией классов и объектов для формирования более крупных структур, позволяющих им эффективно работать вместе. В группу шаблонов структурного проектирования входят: Adapter, Bridge, Composite, Decorator, Facade и Proxy.

Шаблоны поведенческого проектирования:

Эта категория шаблонов фокусируется на коммуникации между объектами и на том, как они взаимодействуют друг с другом для достижения общей цели. К распространенным шаблонам поведенческого проектирования относятся: Command, Iterator, Observer, Strategy. Кофе-брейк #267. Основные шаблоны проектирования в Java, которые вам нужно знать - 2

Преимущества шаблонов проектирования

  1. Возможность повторного использования: они предоставляют решения общих проблем, которые можно повторно использовать в разных проектах.
  2. Гибкость: они делают наш код гибким и адаптированным к изменениям. Они позволяют нам изменять наш код с минимальным влиянием на существующий код.
  3. Сотрудничество: Они хорошо известны среди разработчиков. Мы можем легко понять и внести свой вклад в кодовую базу.
  4. Решение проблем: они помогают нам решать повторяющиеся проблемы проектирования, используя лучшие решения.
  5. Соответствие принципу открытости-закрытости: открыты для расширения и закрыты для модификации.

Шаблон Singleton

Мы используем шаблон Singleton, когда нам нужно иметь только один экземпляр нашего класса и предоставить глобальную точку доступа к нему. Существует две формы шаблона проектирования Singleton:
  1. Раннее создание экземпляра: мы создаем экземпляр во время загрузки.
  2. Ленивое создание экземпляра: мы создаем экземпляр, когда нам это необходимо.
Чтобы создать класс Singleton, нам нужно иметь:
  • статический (static) член класса
  • частный (private) конструктор (предотвращает создание экземпляра одноэлементного класса вне класса)
  • статический фабричный метод (предоставляет глобальную точку для доступа к одноэлементному объекту и возвращает экземпляр клиенту)
Вот пример реализации быстрой загрузки шаблона Singleton:

public class NotificationService {

    // Также может быть инициализирован в статическом блоке
    private static final NotificationService instance = new NotificationService();

    private NotificationService() {
    }

    public static NotificationService getInstance() {
        return instance;
    }

    public void sendNotification(String message) {
        // Логика отправки уведомления
        System.out.println("Notification sent " + message);
    }
}
При активной загрузке экземпляр NotificationService создается и инициализируется во время загрузки. Метод getInstance() просто возвращает предварительно инициализированный экземпляр. Пример реализации ленивой (lazy) загрузки шаблона Singleton:

public class NotificationService {

    private static NotificationService instance;

    private NotificationService() {
    }

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

    public void sendNotification(String message) {
        // Логика отправки уведомления
        System.out.printf("Notification sent: %s", message);
    }
}
Во многих случаях, особенно когда создание объекта является ресурсоемким и дорогостоящим, мы должны предпочесть отложенную загрузку и предотвратить создание объекта во время загрузки. То есть, создавать объект тогда, когда он действительно необходим. Так мы можем ускорить запуск и снизить использование памяти. Теперь нам нужно сделать приведенный выше пример потокобезопасным, используя реентерабельную блокировку (reentrant lock) или синхронизированный блок.

public class NotificationService {
    // Решение проблемы видимости в многопоточности с помощью ключевого слова volatile 
    private static volatile NotificationService instance;

    private NotificationService() {
    }

    public static NotificationService getInstance() {
        if (instance == null) {
            synchronized (NotificationService.class) {
                if (instance == null) {
                    instance = new NotificationService();
                }
            }
        }
        return instance;
    }

    public void sendNotification(String message) {
        System.out.printf("Notification sent: %s", message);
    }
}
Мы можем вызвать статический фабричный метод нашего класса, чтобы получить его экземпляр:

public  class  SingletonClient { 

    public  static  void  main (String[] args) { 
        // Получение одноэлементного экземпляра службы уведомлений 
        NotificationService  NotificationService  = NotificationService.getInstance(); 

        // Отправка уведомлений
         NotificationService.sendNotification( "Hello world!" ); 
    } 
}
В среде разработки Spring синглтон является областью действия по умолчанию для bean-компонентов Spring. Когда вы определяете bean-компонент в Spring без указания области действия, он по умолчанию считается одноэлементным. Мы можем создать bean-компонент, явно определив его в классе конфигурации с помощью @Bean или аннотировав ваш класс с помощью @Component.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public NotificationService notificationService() {
        return new NotificationService();
    }
}

Шаблон Strategy

Шаблон проектирования Strategy (Стратегия) — это один из шаблонов поведенческого проектирования. Он используется, когда у нас есть несколько алгоритмов для конкретной задачи, и позволяет клиенту выбирать желаемую реализацию во время выполнения, инкапсулируя каждый из них и делая эти реализации взаимозаменяемыми. Ключевые компоненты шаблона проектирования Strategy:
  1. Контекст: это класс, который имеет ссылку на интерфейс стратегии и может переключаться между различными конкретными стратегиями.
  2. Интерфейс стратегии: интерфейс, определяющий общие методы, которые должны реализовать все конкретные стратегии.
  3. Конкретные стратегии: это различные реализации интерфейса стратегии. Каждая конкретная стратегия предусматривает свой алгоритм или поведение.

// Интерфейс Strategy
public interface PaymentStrategy {
  void pay(BigDecimal amount);
}

// Конкретные стратегии
@Component
public class CreditCardStrategy implements PaymentStrategy {
  
  public void pay(BigDecimal amount) {
    System.out.printf("Amount %s paid using CreditCardStrategy", amount);
  }
}

@Component
public class PayPalStrategy implements PaymentStrategy {

  public void pay(BigDecimal amount) {
    System.out.printf("Amount %s paid using PayPalStrategy", amount);
  }
}
У нас будет два типа способов оплаты: кредитная карта и PayPal.

public enum PayMethod {
  PAYPAL("payPal"), 
  CREDIT_CARD("creditCard");

  private final String value;

  PayMethod(String value) {
    this.value = value;
  }

  public String getValue() {
    return value;
  }
}
Теперь нам нужно создать контекст PaymentContext и установить стратегии в EnumMap. Мы можем использовать ApplicationContext в Spring для динамического извлечения bean-компонентов, что повышает гибкость, уменьшает модификацию кода и отделяет конфигурацию от логики.

@Component
public class PaymentContext {

  private static final String BEAN_SUFFIX = "Strategy";

  @Autowired
  private ApplicationContext context;

  private EnumMap strategies;

  public void processPayment(PayMethod payMethod, BigDecimal amount) {
    PaymentStrategy paymentStrategy = strategies.get(payMethod);
    if (paymentStrategy == null) {
      throw new IllegalArgumentException("No strategy found for PayMethod: " + payMethod);
    } 
     
     paymentStrategy.pay(amount);
  }

  @PostConstruct
  public void init() {
    this.strategies = new EnumMap<>(PayMethod.class);
    Arrays.stream(PayMethod.values())
      .forEach(payMethod -> strategies.put(payMethod, (PaymentStrategy) context.getBean(payMethod.getValue() + BEAN_SUFFIX)));}
  }
}
Также мы можем вызвать метод processPayment, чтобы получить конкретный класс для каждого метода оплаты:

public class StrategyClient {

  public static void main(String[] args) {
    ApplicationContext springContext = new AnnotationConfigApplicationContext("com.mina.strategy");
    PaymentContext paymentContext = springContext.getBean(PaymentContext.class);
    
    paymentContext.processPayment(PayMethod.CREDIT_CARD, new BigDecimal(10.50));
    paymentContext.processPayment(PayMethod.PAYPAL, new BigDecimal(60.20));
  }
}
С помощью этого шаблона мы можем легко добавлять или удалять стратегии оплаты без изменения контекста, придерживаясь принципа открытости-закрытости принципов SOLID.

Шаблон Factory

Фабричный шаблон (Factory) — это шаблон проектирования, который позволяет создавать группу связанных объектов на основе критерия, не раскрывая их конкретные классы. Он абстрагирует процесс создания экземпляра. К примеру, нам нужно создать интерфейс для создания объектов и конкретные классы, реализующие этот интерфейс, для создания экземпляров определенных типов объектов. Вот как мы создадим различные способы оплаты:

// Интерфейс Payment
public interface Payment {
    void processPayment();
}

// Реализация интерфейса Payment
public class CreditCardPayment implements Payment {

    @Override
    public void processPayment() {
        System.out.println("Processing payment using Credit Card...");
    }
}

public class PayPalPayment implements Payment {

    @Override
    public void processPayment() {
        System.out.println("Processing payment using PayPal...");
    }
}


public class PaymentFactory {

    public static Payment createPayment(PayMethod payMethod) {
      if (payMethod == PayMethod.CREDIT_CARD) {
        return new CreditCardPayment();
      } else if (payMethod == PayMethod.PayPal) {
        return new PayPalPayment();
      }
    }
  }

public class FactoryClient {

    public static void main(String[] args) {
        // Использование factory для создания способа оплаты с помощью Credit Card и PayPal
        Payment creditCardPayment = PaymentFactory.createPayment(PayMethod.CREDIT_CARD);
        creditCardPayment.processPayment();

        Payment payPalPayment = PaymentFactory.createPayment(PayMethod.PayPal);;
        payPalPayment.processPayment();
    }
}
В этом примере Payment — это интерфейс для разных способов оплаты, а CreditCardPayment и PayPalPayment — реализации. Класс PaymentFactory имеет статический метод, который использует PatMethod для создания экземпляра и возврата соответствующей реализации Payment. Клиентский код использует Factory для создания конкретных экземпляров платежей. Это позволяет легко расширять возможности добавления новых способов оплаты без изменения существующего клиентского кода.

Шаблон Abstract Factory

Шаблон “Абстрактная фабрика” (Abstract Factory) — это шаблон проектирования, позволяющий создавать объекты разных категорий. Он является расширением шаблона Factory, и мы можем считать его как двухуровневый или иерархический фабричный шаблон. Сначала нам нужно создать экземпляр Factory для разных категорий, а затем использовать его для получения конкретных объектов этой категории. Основная идея заключается в том, что клиентский код взаимодействует с абстрактными интерфейсами (абстрактной фабрикой и абстрактными объектами), а конкретные объекты создаются конкретными фабриками. Вот пример абстрактного шаблона Factory для создания компонентов пользовательского интерфейса (таких как кнопки (buttons), TextField и так далее) со светлой или темной темой.

// Abstract Product: Button
interface Button {
    void display();
}

// Concrete Product: LightButton
class LightButton implements Button {
    public void display() {
        System.out.println("Displaying Light Button");
    }
}

// Concrete Product: DarkButton
class DarkButton implements Button {
    public void display() {
        System.out.println("Displaying Dark Button");
    }
}

// Abstract Product: TextField
interface TextField {
    void display();
}

// Concrete Product: LightTextField
class LightTextField implements TextField {
    public void display() {
        System.out.println("Displaying Light Text Field");
    }
}

// Concrete Product: DarkTextField
class DarkTextField implements TextField {
    public void display() {
        System.out.println("Displaying Dark Text Field");
    }
}
Теперь нам нужно создать интерфейс абстрактной фабрики с набором методов для возврата абстрактного продукта. Затем мы создаем конкретные фабрики, которые реализуют абстрактный фабричный интерфейс для создания конкретных продуктов.

// Abstract Factory: UIFactory
interface UIFactory {
    Button createButton();
    TextField createTextField();
}

// Concrete Factory: LightUIFactory
class LightUIFactory implements UIFactory {
    public Button createButton() {
        return new LightButton();
    }

    public TextField createTextField() {
        return new LightTextField();
    }
}

// Concrete Factory: DarkUIFactory
class DarkUIFactory implements UIFactory {
    public Button createButton() {
        return new DarkButton();
    }

    public TextField createTextField() {
        return new DarkTextField();
    }
}
Вот как наш клиент может использовать абстрактный шаблон фабрики.

public class AbstractFactoryClient {

    public static void main(String[] args) {
        // Выбираем светлую тему
        UIFactory lightFactory = new LightUIFactory();
        Button lightButton = lightFactory.createButton();
        TextField lightTextField = lightFactory.createTextField();

        lightButton.display(); // Вывод: отображение светлой кнопки
        lightTextField.display(); // Вывод: отображение светлого текстового поля

        // Выбор темной темы
        UIFactory darkFactory = new DarkUIFactory();
        Button darkButton = darkFactory.createButton();
        TextField darkTextField = darkFactory.createTextField();

        darkButton.display(); // Вывод: отображение темной кнопки
        darkTextField.display(); // Вывод: отображение темного текстового поля
    }
}

Шаблон Builder (Строитель)

Builder — это шаблон, который позволяет нам создавать сложные объекты. Основная его идея состоит в том, чтобы отделить построение сложного объекта от его представления, позволяя одному и тому же процессу конструирования создавать разные представления. Предположим, нам нужен шаблон Builder для пользователя. Два ключевых компонента — это классы UserBuilder и User.
  • Класс UserBuilder инкапсулирует создание экземпляра объекта User, предоставляя методы для установки свойств пользователя.
  • Класс User представляет объект, экземпляр которого создается, и содержит фактические свойства.

public class User {

  private final String ssn; //требуется  
  private final String firstName; //требуется
  private final String lastName; //требуется
  private final String phone; //необязательно
  private final String address; //необязательно

  private User(UserBuilder builder) {
    this.ssn = builder.ssn;
    this.firstName = builder.firstName;
    this.lastName = builder.lastName;
    this.phone = builder.phone;
    this.address = builder.address;
  }
  public String getSsn() {
    return ssn;
  }

  public String getFirstName() {
    return firstName;
  }

  public String getLastName() {
    return lastName;
  }

  public String getPhone() {
    return phone;
  }

  public String getAddress() {
    return address;
  }

  @Override
  public String toString() {
    return "User{" +
      "ssn='" + ssn + '\'' +
      ", firstName='" + lastName + '\'' +
      ", lastName='" + lastName + '\'' +
      ", phone='" + phone + '\'' +
      ", address='" + address + '\'' +
      '}';
  }

  public static class UserBuilder {

    private final String ssn;
    private final String firstName;
    private final String lastName;
    private String phone;
    private String address;

    public UserBuilder(String ssn, String firstName, String lastName) {
      this.ssn = ssn;
      this.firstName = firstName;
      this.lastName = lastName;
    }

    public UserBuilder phone(String phone) {
      this.phone = phone;
      return this;
    }

    public UserBuilder address(String address) {
      this.address = address;
      return this;
    }

    public User build() {
      return new User(this);
    }
  }
}
Вот как мы можем использовать шаблон Builder:

public class UserBuilderClient {

  public static void main(String[] args) {
    User user = new User.UserBuilder("553388", "Mina", "Rashidi")
      .address("Stockholm")
      .build();
    System.out.println(user);
  }
}

Преимущества шаблона Builder:

  • Нет необходимости перегружать конструктор.
  • Нет необходимости в конструкторе с длинным списком параметров.
  • Гибкость в создании экземпляров объектов: мы можем создавать объекты с разными наборами свойств. Это особенно полезно для сложных объектов со многими свойствами.
  • Неизменяемость: может помочь в создании неизменяемых объектов. Обеспечивая невозможность изменения объекта после его создания, чтобы предотвратить случайные изменения.
  • Улучшенная читаемость и удобство обслуживания. Он делает код более читабельным и удобным в сопровождении, отделяя конструкцию объекта от его представления.
Я надеюсь, что это руководство помогло вам лучше понять шаблоны проектирования и способы их эффективного использования.
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Yuliya Уровень 11
14 декабря 2023
Статья 🔥