Припиніть хардкодити: Використовуйте Factory Method для рефакторингу вашої системи сповіщень (приклад на Java)

💡 Усі статті, обговорення, новини про Java — в одному місці. Приєднуйтесь до Java спільноти!

Чи доводилося вам коли-небудь тонути в блоках if-else просто для того, щоб відправити просте повідомлення? Це ознака того, що настав час впровадити патерн проектування Factory Method.

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

Проблема: Безладний, негнучкий код

Припустимо, ви хочете відправляти сповіщення. Спочатку це лише email, тому код виглядає нормально. Але потім ви додаєте SMS... а завтра, можливо, Slack, Push, WhatsApp або навіть API для голубиної пошти.

Старий код:

public class NotificationService {
    public void sendNotification(String type, String message) {
        if (type.equals("EMAIL")) {
            System.out.println("Sending EMAIL: " + message);
        } else if (type.equals("SMS")) {
            System.out.println("Sending SMS: " + message);
        } else {
            throw new IllegalArgumentException("Unknown notification type: " + type);
        }
    }

    public static void main(String[] args) {
        NotificationService service = new NotificationService();
        service.sendNotification("EMAIL", "Welcome to the system!");
        service.sendNotification("SMS", "Your code is 1234.");
    }
} 

Що тут не так?

  • Не масштабується: Кожен новий тип сповіщення = новий блок if.
  • Порушує принцип відкритості/закритості: Ви повинні модифікувати існуючий метод для додавання нової поведінки.
  • Важко тестувати: Логіка тісно пов’язана і захардкожена.

Рішення: Factory Method

Давайте відрефакторимо цей код, використовуючи патерн Factory Method. Ідея полягає в тому, щоб відокремити створення об’єктів від їх використання.

1. Визначте спільний інтерфейс:

public interface Notification { 
  void notifyUser(String message); 
} 

2. Створити конкретні реалізації:

public class EmailNotification implements Notification {
  @Override
  public void notifyUser(String message) {
    System.out.println("Sending EMAIL: " + message);
  }
}

public class SMSNotification implements Notification {
  @Override
  public void notifyUser(String message) {
    System.out.println("Sending SMS: " + message);
  }
} 

3. Визначити фабрику:

public abstract class NotificationFactory {
    public abstract Notification createNotification();
} 

4. Реалізувати конкретні фабрики:

public class EmailNotificationFactory extends NotificationFactory {
  @Override
  public Notification createNotification() {
    return new EmailNotification();
  }
}

public class SMSNotificationFactory extends NotificationFactory {
  @Override
  public Notification createNotification() {
    return new SMSNotification();
  }
} 

5. Відрефакторити клієнтський код:

public class NotificationService {
  public void send(NotificationFactory factory, String message) {
    Notification notification = factory.createNotification();
    notification.notifyUser(message);
  }

  public static void main(String[] args) {
    NotificationService service = new NotificationService();
    service.send(new EmailNotificationFactory(), "Welcome to the system!");
    service.send(new SMSNotificationFactory(), "Your code is 1234.");
  }
} 

Переваги такого підходу

  • Принцип відкритості/закритості: Щоб додати Slack або Push — просто створіть новий клас і фабрику. Не потрібно модифікувати існуючу логіку.
  • Розділення обов’язків: Створення об’єктів відокремлено від їх використання.
  • Легше тестування: Ви можете створювати моки або заглушки для фабрик або сповіщень.

Бонус: Динамічний вибір фабрики

Якщо ви все ще хочете якусь динамічну поведінку (наприклад, на основі рядкового вводу):

public class NotificationFactoryProvider {
  public static NotificationFactory getFactory(String type) {
    return switch (type) {
      case "EMAIL" -> new EmailNotificationFactory();
      case "SMS" -> new SMSNotificationFactory();
      default -> throw new IllegalArgumentException("Unknown type");
    };
  }
} 

Використання:

NotificationFactory factory = NotificationFactoryProvider.getFactory("EMAIL");
NotificationService service = new NotificationService();
service.send(factory, "Hello!"); 

Заключні думки

Вам не потрібно одразу переходити до складних патернів. Але коли ваші блоки if-else починають розмножуватися, це ознака того, що вам потрібен Factory Method.

Цей патерн допомагає підтримувати ваш код гнучким, тестованим і легким для розширення — і це велика перевага.

Оригінал статті тут: medium.com/...​java-example-5c9cecc58ee1

Відгуки та коментарі вітаються! Буду дуже радий зворотньому зв’язку!

👍ПодобаєтьсяСподобалось1
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Професійно програмую вже трохи більше ніж 3 роки на C# і я розумію, що ООП до мене так і не дійшло. Ми дійсно зробили NotificationService краще — тепер його можна відтестувати, він не буде, або майже не буде змінюватись при додаванні нових нотіфікейшн методів і т.д. Все завдяки Стратегії і Фабричному методу.

АЛЕ — ми просто взяли наші проблеми і перемістили вище по стеку. Тепер наші if-else-switch у факторі провайдері(таке ще називають Simple Factory патерном) і знову ж таки нам треба буде модифікувати цей метод у випадку, якщо потрібно буде додати новий нотіфікейшн метод. І це у кращому випадку, якщо ж новий нотіфікейшн сервіс буде мати якусь специфічну поведінку і семантично відрізнятись від інших реалізацій нотіфікейшина — нам потрібно буде викинути ту абстракцію на нотіфікейшн і написати нову. За мій короткий час програмування викидування і переписування абстракцій, які розбивались в хлам об нові вимоги стало доволі частим явищем.

Тобто код не став зрозумілішим, навпаки — тепер у нас повно абстракцій і треба розбиратись яка абстракція за що відповідає. Просто глянувши на метод ти не можеш з повною впевненістю сказати що він робить і ти лізеш шукати реалізації своїх абстракцій. Далі тестування — ми дійсно зробили наш клієнтський код зручним для тестування. До такого ступеня, що там тепер нічого тестувати — ви будете тестувати чи мок викликається з переданим параметром? Хіба це має сенс, коли у тебе такий простий метод? Що тут треба тестувати це NotificationFactoryProvider чи він повертає потрібну факторі при певному параметрі, також саму факторі чи вона повертає потрібний нотіфікейшн.

Що можна з цим всім зробити? А я не знаю, от просто не знаю. Шукаючи рішення цих проблем я почав до ООП ще додавати функціональну парадигму — з деякими задачами вона справляється набагато краще ООП. Десь, де є стейт і депенденсі ООП справляється добре. Поки зробив для себе висновок, що комбінуючи ці дві парадигми для вирішення різних проблем в проекті працює для мене найкраще і не просто так C# до ООП з кожним релізом додає все більше функціональних фіч.

Чого я не розумію, то це чому люди в 2025му році показують штуки, які були придумані в 1980их(точніше не знаю, але десь там) для вирішення проблем з 1980их і розповідають про прекрасний, тестабл, читабельний і масштабований код, коли насправді це не зовсім так і результат набагато складніший з купою нюансів, плюсів і мінусів. Я ніби прочитав чергову книжку по ООП, соліду і патернах, прийоми з якої дадуть збій при вирішенні чергової реальної задачі з комплексними вимогами.

А можливо це просто я відстій і так і не навчився програмувати чи чогось не зрозумів...

До автора притензій не маю, просто коментую зміст статті і свій суб’єктивний досвід

А класно ви оце звичайний метод, який достатньо було переписати на switch і enum-ми перепатрошили, вбивши читабельність і непотрібно ускладнивши логіку.


Microsoft Certified (Azure Fundamentals)

Звучить як жарт)

Ну і по темі, нижче вдало написали

Бо ви просто if...else замінили на switch...case.

Динамічно це легко зробити передаючи всі повідомлення в кожен процесор повідомлень. А він вже собі вирішує щось робити чи ні. Робиться в кілька рядків (в спрігну та інших ДІ фреймворках).
Розширення (добавляння) відбувається лише за рахунок нового класу (біна).

Якщо ви все ще хочете якусь динамічну поведінку

То ми її не отримаємо.
Бо ви просто if...else замінили на switch...case.

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

Дуже дякую за відгук. З назвами мабуть дійно треба допрацювати. Це планувалось більше як навчальна стаття для новачків.

Це більше схоже на інший патерн — Abstract Factory.
Factory method реалізується набагато простіше:

public class NotificationService {
  public void send(Notification notification, String message) {
    notification.notifyUser(message);
  }

  public static void main(String[] args) {
    NotificationService service = new NotificationService();
    Notification n = createNotification("EMAIL");
    service.send(n, "Welcome to the system!");
    Notification n2 = createNotification("SMS");
    service.send(n2, "Your code is 1234.");
  }

  static Notification createNotification(String type) {
     return switch (type) {
      case "EMAIL" -> new EmailNotification();
      case "SMS" -> new SMSNotification();
      default -> throw new IllegalArgumentException("Unknown type");
    };
  }
}

Всі класи, в яких є слово Factory — в даному випадку непотрібні.

Так, ваша версія абсолютно робоча — і для більшості продакшн-завдань такий підхід (з createNotification(...) як утилітним методом) цілком достатній. Але тут є нюанс: це не Factory Method у сенсі патерну з GoF, а просто зовнішня фабрика або «ручна інстанціація з логікою вибору».

Factory Method як патерн — це не просто метод, що повертає об’єкт. Це метод, який є частиною абстрактного класу або інтерфейсу, і який переозначується в підкласах для контролю створення конкретного об’єкта.

У вашому прикладі createNotification(...) — це procedural-style фабрика (схоже на Simple Factory або static factory), а от класична реалізація Factory Method виглядає саме так:

abstract class NotificationFactory {
public abstract Notification createNotification();
}

class EmailNotificationFactory extends NotificationFactory {
public Notification createNotification() {
return new EmailNotification();
}
}

І вже клієнт викликає factory.createNotification() без знання про конкретний клас.

Основна користь цього — інверсія залежностей + можливість розширення без модифікації коду, що відповідає принципу відкритості/закритості (OCP з SOLID). А в деяких випадках — ще й впровадження додаткової поведінки під час створення (логування, валідація, lazy init тощо).

Дякую за коментар. Про абстрактну фабрику буде стаття завтра в 11:20. Там теж цікаво і чекаю ваші коментарі. Паттерн абстрактної фабрики використовується для створення сімей пов’язаних об’єктів (наприклад, Notification + Template + Logger). Тут в Factory Method створюється один тип продукту.

КГАМ
Тут цікавіше чи автор вирішив погратися в маркетолога чи адміністрація доу проявила ініціативу і опублікувати статтю, що демонструє якраз нерозуміння того, що таке Factory Method.

Factory Method вирішує просту проблему — приховування логіки створення об’єктів від коду, що єє використовує.

В статті взято випадок спагетті коду.
Потім проведена його декомпозиція (Кроки 1-2). Це вже вирішує 90+% проблеми початкового коду, а з 3 пунктів, що виділив автор, 99%.
Крок 3 вирішує 1%, що лишився.
Кроки 4-5 — додають непотрібну складність і розкривають назовні деталі реалізації — тепер наш код знає не лише про NotificationService і строковий енум тип повідослення, а і про NotificationFactory та 2 його реалізації.
Якби автор розумів цей шаблон (між іншим один з найпростіших), то NotificationFactory були б інкапсульована в NotificationService. А якщо дуже не подобаються строки, то їх можна було б замітити енумами, або викликами спеціалізованих методі у сервіса, або як це роблять зараз в джаві — ДІ-фреймворком (до речі як там використовується Фабрика теж не розкрито).

Дякую за коментар — видно, що ви добре розбираєтесь у темі.

Власне, мета прикладу була не побудувати ідеальну продакшн-архітектуру, а показати базову суть патерну Factory Method «в чистому вигляді» — з Product, Creator, конкретними фабриками і розділенням створення та використання. Це типова демонстрація структури з книжки GoF, без DI, енумів і автоматичної магії.

Так, погоджуюсь — уже на етапі виділення інтерфейсів (Notification) і реалізацій (EmailNotification/SMSNotification) ми вирішуємо більшість проблем початкового коду.
А далі вже йде трохи «показушна» декомпозиція — щоб новачки зрозуміли, де в цьому патерн, а не просто «як краще зробити код».

Про DI — абсолютно згоден, у реальних проєктах зараз або Spring, або інші контейнери вирішують створення об’єктів фабриками за нас. Але для цього теж треба спочатку зрозуміти, що саме вони приховують.

Тому цей приклад — це радше «навчальний стенд», ніж готовий шматок для реального застосування. Але слушна думка — можна зробити окремий матеріал, як Factory Method виглядає в сучасному DI-контейнерному оточенні. Дякую!

Це типова демонстрація структури з книжки GoF, без DI, енумів і автоматичної магії.

Ну тобто книжку ви навіть не прогортали перед написанням топік. В книжці якраз описаний в основному приклад, який ви назвали бонусним, просто замість енумів там «Create (Productld id)».
Приклад з фігурами та маніпуляторами має значно більше сенсу, але він і вирішує реальну проблему (не дуже актуально для скчасного джава розробника)

Тому цей приклад — це радше «навчальний стенд

Так моя претензія була якраз до його навчальної цінності — ви створюєте його призначення і створюєте додаткову складність, що якраз для джунів збільшує ризик того, що вони почнуть вважати перевантажений абстракціями код за приклад «гарного коду».

Про DI — абсолютно згоден, у реальних проєктах зараз або Spring, або інші контейнери вирішують створення об’єктів фабриками за нас. Але для цього теж треба спочатку зрозуміти, що саме вони приховують.

І знову комедія. ДІ фремворки в джаві якраз працюють не через факбрики в основному. В спрінгу «фабрики» в конфігураційних класах — не фабрики, а метакод (що станеться коли викликати бін метод з іншого бін метода?).
В джусі фабрики якраз використовуються для одного специфічного сценарія. Чому це не було згадано в статті? Простота — слабка відмовка, бо ваш приклад складніше за їх сценарій.
Здається даггер працює через фабрики, але в статті про це не згадано.

Підписатись на коментарі