Паттерни проектування, які має знати кожен розробник
Розбираємо основні патерни проектування, які допомагають створювати гнучкий, зрозумілий код, що підтримується, без зайвого ускладнення архітектури.
Що таке патерни проєктування?
Патерни проєктування — це перевірені способи розв'язання типових архітектурних завдань. Вони не є готовими бібліотеками або універсальними фрагментами коду. Патерн описує загальну структуру рішення: які об'єкти створити, як розподілити відповідальність і яким чином компоненти повинні взаємодіяти.
Патерни допомагають не вигадувати архітектуру з нуля щоразу, коли необхідно створювати складні об'єкти, замінювати алгоритми, підключати сторонні сервіси або повідомляти компоненти про зміни.
При цьому патерни не слід використовувати заради ускладнення проєкту. Їхнє завдання — робити код зрозумілішим, гнучкішим і зручнішим для підтримки.
Навіщо розробнику знати патерни?
Патерни зменшують зв'язність компонентів, допомагають розділяти відповідальність і спрощують розширення програми. Вони також формують спільну професійну мову всередині команди.
Наприклад, фраза «тут використовується стратегія» швидко пояснює, що кілька алгоритмів винесені в окремі класи й можуть замінюватися без зміни основної логіки.
Патерни особливо корисні, коли:
- один клас виконує надто багато завдань;
- необхідно замінювати окремі компоненти;
- створення об'єктів стало складним;
- зміна однієї частини програми зачіпає безліч інших;
- код важко тестувати або розширювати.
Основні групи патернів
Породжувальні патерни керують створенням об'єктів. До них належать Factory Method, Builder і Singleton.
Структурні патерни допомагають об'єднувати класи й об'єкти. Серед них можна виділити Adapter, Decorator і Facade.
Поведінкові патерни визначають взаємодію між об'єктами. До цієї групи належать Strategy, Observer, Command і State.
Factory Method: створення об'єктів
Factory Method застосовується, коли програма повинна створювати об'єкти різних класів, але основний код не повинен залежати від конкретної реалізації.
Наприклад, застосунок може надсилати сповіщення через електронну пошту або SMS. Замість створення об'єктів безпосередньо в бізнес-логіці це завдання можна передати фабриці.
interface Notification {
void send(String message);
}
class EmailNotification implements Notification {
public void send(String message) {
System.out.println("Email: " + message);
}
}
class SmsNotification implements Notification {
public void send(String message) {
System.out.println("SMS: " + message);
}
}
class NotificationFactory {
public static Notification create(String type) {
if (type.equalsIgnoreCase("email")) {
return new EmailNotification();
}
if (type.equalsIgnoreCase("sms")) {
return new SmsNotification();
}
throw new IllegalArgumentException("Unknown type");
}
}Інша частина програми працює з інтерфейсом Notification і не залежить від конкретного класу. Це спрощує додавання нових способів надсилання повідомлень.
Builder: створення складних об'єктів
Builder корисний, коли об'єкт містить багато обов'язкових і необов'язкових параметрів. Замість довгого конструктора об'єкт створюється покроково.
User user = new User.Builder("Alex")
.email("[email protected]")
.age(28)
.phone("+123456789")
.build();Такий код легше читати, оскільки видно призначення кожного значення. Усередині методу build() також можна перевіряти коректність даних.
Builder часто використовується для створення конфігурацій, HTTP-запитів, моделей і об'єктів із великою кількістю налаштувань.
Singleton: один екземпляр класу
Singleton гарантує, що в застосунку існує лише один екземпляр певного класу. Зазвичай він використовується для конфігурації, журналювання або спільного кешу.
public class AppConfig {
private static AppConfig instance;
private AppConfig() {
}
public static AppConfig getInstance() {
if (instance == null) {
instance = new AppConfig();
}
return instance;
}
}Закритий конструктор забороняє створювати об'єкт ззовні, а метод getInstance() повертає наявний екземпляр або створює його під час першого звернення.
Використовувати Singleton слід обережно. Він створює глобальний стан, ускладнює тестування й потребує додаткової синхронізації в багатопотокових застосунках.
Adapter: сумісність інтерфейсів
Adapter дозволяє використовувати сторонній клас із невідповідним інтерфейсом. Адаптер приймає виклики у форматі застосунку та перетворює їх у формат зовнішнього компонента.
interface PaymentService {
void pay(double amount);
}
class ExternalPaymentApi {
public void makePayment(double value) {
System.out.println("Paid: " + value);
}
}
class PaymentAdapter implements PaymentService {
private final ExternalPaymentApi api;
public PaymentAdapter(ExternalPaymentApi api) {
this.api = api;
}
public void pay(double amount) {
api.makePayment(amount);
}
}Тепер програма працює зі звичним інтерфейсом PaymentService, а особливості зовнішнього API залишаються всередині адаптера.
Патерн часто застосовується під час підключення платіжних систем, сторонніх бібліотек, старого коду та зовнішніх сервісів.
Decorator: розширення поведінки
Decorator дозволяє додавати об'єкту нову поведінку без зміни початкового класу та створення великої кількості нащадків.
interface MessageSender {
void send(String message);
}
class BasicSender implements MessageSender {
public void send(String message) {
System.out.println(message);
}
}
class LoggingDecorator implements MessageSender {
private final MessageSender sender;
public LoggingDecorator(MessageSender sender) {
this.sender = sender;
}
public void send(String message) {
System.out.println("Log: sending message");
sender.send(message);
}
}Декоратори можна комбінувати. Наприклад, до відправника повідомлень можна послідовно додати журналювання, шифрування та перевірку даних.
Facade: простий інтерфейс складної системи
Facade надає єдиний і зрозумілий інтерфейс для роботи з кількома внутрішніми компонентами.
Наприклад, оформлення замовлення може включати резервування товару, оплату, створення доставки та надсилання повідомлення. Клієнтському коду не обов'язково самостійно керувати кожним етапом.
class OrderFacade {
private final InventoryService inventory;
private final PaymentService payment;
private final DeliveryService delivery;
public void createOrder(Product product) {
inventory.reserve(product);
payment.pay(product.getPrice());
delivery.createDelivery(product);
}
}Фасад зменшує кількість залежностей і приховує складну послідовність внутрішніх операцій.
Strategy: заміна алгоритмів
Strategy виносить взаємозамінні алгоритми в окремі класи. Основний об'єкт працює через спільний інтерфейс і може змінювати стратегію під час виконання програми.
interface DeliveryStrategy {
double calculate(double orderPrice);
}
class StandardDelivery implements DeliveryStrategy {
public double calculate(double orderPrice) {
return 10;
}
}
class ExpressDelivery implements DeliveryStrategy {
public double calculate(double orderPrice) {
return 25;
}
}
class Order {
private DeliveryStrategy strategy;
public void setStrategy(DeliveryStrategy strategy) {
this.strategy = strategy;
}
public double getDeliveryPrice(double orderPrice) {
return strategy.calculate(orderPrice);
}
}Кожен алгоритм зберігається окремо, легко тестується й замінюється без зміни класу замовлення.
Strategy використовується для розрахунку знижок, доставки, вибору способу оплати, сортування, валідації та обробки даних.
Observer: система підписок
Observer застосовується, коли зміна одного об'єкта повинна автоматично повідомляти кілька інших об'єктів.
interface Subscriber {
void update(String news);
}
class NewsPublisher {
private final List subscribers = new ArrayList<>();
public void subscribe(Subscriber subscriber) {
subscribers.add(subscriber);
}
public void publish(String news) {
for (Subscriber subscriber : subscribers) {
subscriber.update(news);
}
}
} Видавець не знає деталей роботи підписників. Він лише викликає спільний метод повідомлення.
Observer зустрічається в графічних інтерфейсах, системах подій, сповіщеннях, реактивному програмуванні та архітектурі MVC.
Command: дія як об'єкт
Command перетворює дію на самостійний об'єкт. Такі команди можна зберігати, поміщати в чергу, повторювати або скасовувати.
interface Command {
void execute();
void undo();
}
class TurnOnLightCommand implements Command {
private final Light light;
public TurnOnLightCommand(Light light) {
this.light = light;
}
public void execute() {
light.turnOn();
}
public void undo() {
light.turnOff();
}
}Патерн використовується в текстових редакторах, користувацьких інтерфейсах, чергах завдань і системах зі скасуванням дій.
State: поведінка залежно від стану
State корисний, коли поведінка об'єкта залежить від його поточного стану. Замість великої кількості умов кожен стан представляється окремим класом.
Наприклад, замовлення може бути створене, оплачене, відправлене або скасоване. У кожному стані доступні різні дії. Відправлене замовлення не можна оплатити повторно, а скасоване не можна передати в доставку.
State розділяє ці правила й допомагає уникнути великої кількості вкладених конструкцій if і switch.
Repository та Dependency Injection
Repository відокремлює бізнес-логіку від способу зберігання даних. Основний код працює через інтерфейс і не залежить безпосередньо від SQL-запитів або конкретної бази даних.
interface UserRepository {
User findById(long id);
void save(User user);
void delete(long id);
}Dependency Injection передбачає, що об'єкт не створює залежності самостійно, а отримує їх ззовні.
class UserService {
private final UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository;
}
}Такий підхід зменшує зв'язність компонентів і спрощує тестування, оскільки реальну залежність можна замінити тестовою реалізацією.
Які патерни вивчати насамперед?
Новачку не обов'язково одразу запам'ятовувати всі наявні патерни. Для початку достатньо вивчити кілька найпоширеніших рішень.
- Factory Method і Builder допомагають створювати об'єкти.
- Adapter і Facade спрощують інтеграцію компонентів.
- Decorator додає нову поведінку.
- Strategy дозволяє замінювати алгоритми.
- Observer використовується для подій і підписок.
- Command представляє дії у вигляді об'єктів.
- Repository та Dependency Injection зменшують залежність бізнес-логіки від інфраструктури.
Помилки під час використання патернів
Патерн без реальної проблеми. Не варто створювати додаткові інтерфейси, фабрики й абстрактні класи, якщо вони не спрощують проєкт.
Занадто багато абстракцій. Кожен додатковий шар ускладнює читання та підтримку коду. Абстракція корисна лише тоді, коли приховує складність або дозволяє замінити реалізацію.
Копіювання навчальних прикладів. У реальному проєкті необхідно враховувати обробку помилок, багатопотоковість, тестування та вимоги використовуваного фреймворку.
Ігнорування особливостей мови. Один і той самий патерн по-різному реалізується в Java, Python, JavaScript, C#, Go або Rust. Іноді замість кількох класів достатньо функції або замикання.
Висновок
Патерни проєктування допомагають створювати об'єкти, розділяти відповідальність, замінювати алгоритми й організовувати взаємодію між компонентами.
Кожному розробнику корисно розуміти Factory Method, Builder, Adapter, Decorator, Facade, Strategy, Observer і Command. У сучасних проєктах також часто використовуються Repository та Dependency Injection.
Головне — не кількість вивчених патернів, а вміння обирати відповідне рішення. Хороший патерн зменшує складність проєкту. Якщо після його застосування код став заплутанішим без очевидної користі, отже архітектуру варто спростити.
Більше цікавих новин
Почему Python – лучший выбор для машинного обучения
Выставка в Лас-Вегасе: какие роботы были представлены
10 отличных API для ваших проектов
5 советов для начинающих программистов