Очистіть свій Java код за допомогою патерну Abstract Factory

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

Припиніть дублювати створення об’єктів і розблокуйте масштабовані комбінації пов’язаних об’єктів, таких як Email + SMS сповіщення.

Проблема: Жорстка і повторювана логіка сповіщень

Припустимо, ви створюєте систему, яка надсилає різні типи сповіщень. З часом впроваджуються нові типи сповіщень, які поєднують різні канали:

  • ALERT: Надсилає і Email, і SMS
  • NEWSLETTER: Надсилає лише Email
  • PROMOTION: Надсилає лише SMS

Спочатку ви можете написати щось подібне:

public class NotificationService {
  public void send(String type, String message) {
    if (type.equals("ALERT")) {
      Notification email = new EmailNotification();
      Notification sms = new SMSNotification();
      email.notifyUser(message);
      sms.notifyUser(message);
    } else if (type.equals("NEWSLETTER")) {
      Notification email = new EmailNotification();
      email.notifyUser(message);
    } else if (type.equals("PROMOTION")) {
      Notification sms = new SMSNotification();
      sms.notifyUser(message);
    } else {
      throw new IllegalArgumentException("Unknown type: " + type);
    }
  }

  public static void main(String[] args) {
    NotificationService service = new NotificationService();
    service.send("ALERT", "Server is down!");
    service.send("NEWSLETTER", "June updates are here!");
  }
} 

Що тут не так?

  • Код повторюється — створення сповіщень вручну кожного разу.
  • Порушується принцип відкритості/закритості — нові типи вимагають модифікації методу send.
  • Важко тестувати — тісне зв’язування ускладнює створення моків або розширення.
  • Низька масштабованість — що, якщо пізніше потрібно буде надсилати Slack, Push або WhatsApp?

Рішення: Використовуйте патерн Abstract Factory

Abstract Factory допомагає, коли потрібно створювати сімейства пов’язаних об’єктів — наприклад, «комбо» сповіщень, які йдуть разом.

Крок 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 interface NotificationAbstractFactory {
  Notification createPrimaryNotification();
  Notification createSecondaryNotification(); 
} 

Крок 4: Створіть конкретні фабрики

public class AlertNotificationFactory implements NotificationAbstractFactory {
  public Notification createPrimaryNotification() {
    return new EmailNotification();
  }

  public Notification createSecondaryNotification() {
    return new SMSNotification();
  }
} 

public class NewsletterNotificationFactory implements NotificationAbstractFactory {
  public Notification createPrimaryNotification() {
      return new EmailNotification();
  }
  
  public Notification createSecondaryNotification() {
      return null;
  }
} 

public class PromotionNotificationFactory implements NotificationAbstractFactory {
    public Notification createPrimaryNotification() {
        return new SMSNotification();
    }

    public Notification createSecondaryNotification() {
        return null;
    }
}

Крок 5: Використовуйте фабрику у вашому сервісі

public class NotificationService {
  public void send(NotificationAbstractFactory factory, String message) {
    Notification primary = factory.createPrimaryNotification();
    Notification secondary = factory.createSecondaryNotification();
    primary.notifyUser(message);
    if (secondary != null) {
      secondary.notifyUser(message);
    }
  }

  public static void main(String[] args) {
    NotificationService service = new NotificationService();
    service.send(new AlertNotificationFactory(), "Server is down!");
    service.send(new NewsletterNotificationFactory(), "Check out our new features!");
    service.send(new PromotionNotificationFactory(), "50% OFF this weekend!");
  }
} 

Чому це працює краще

  • Масштабованість: Додавайте нові комбінації, просто додаючи нову фабрику — без змін у логіці.
  • Тестованість: Ви можете створювати моки або заглушки для фабрики та сповіщень для юніт-тестів.
  • Слідує принципу відкритості/закритості: Ваша основна логіка залишається недоторканою.
  • Чисте розділення обов’язків: Логіка створення живе у фабриках, а не в сервісних класах.

Бонус: Ви все ще можете використовувати провайдер фабрик

Якщо ви хочете динамічно вибирати фабрику на основі рядка або конфігурації:

public class NotificationFactoryProvider {
  public static NotificationAbstractFactory getFactory(String type) {
    return switch (type) {
      case "ALERT" -> new AlertNotificationFactory();
      case "NEWSLETTER" -> new NewsletterNotificationFactory();
      case "PROMOTION" -> new PromotionNotificationFactory();
      default -> throw new IllegalArgumentException("Unknown type: " + type);
    };
  }
} 

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

NotificationAbstractFactory factory = NotificationFactoryProvider.getFactory("ALERT");
service.send(factory, "System is overheating!"); 

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

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

  • Системах сповіщень
  • Темізації UI (наприклад, створення узгоджених темних/світлих віджетів)
  • Крос-платформних компонентах

Якщо ваш код починає виглядати як ліс блоків if-else зі створенням об’єктів, захардкодженим всередині них — можливо, настав час для патерну Abstract Factory.

Оригінал статті тут: medium.com/...​tory-pattern-0f0c48e0ef96

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

👍ПодобаєтьсяСподобалось2
До обраногоВ обраному1
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
ALERT: Надсилає і Email, і SMS
NEWSLETTER: Надсилає лише Email
PROMOTION: Надсилає лише SMS

Та вільміть і почитайте ту блядську кнужку.
Abstract Factory вирішує зовсім іншу проблему. Визначення логіки чи правил коли щось відправляти, а коли ні — не його задача.
Цей патерн вирішує проблему створення груп реалізацій певних інтерфейсів. Саме групування реалізацій, а не формування набору поведінок. Різниця в тому, що в своїй ідеї він не містить логіки, у ваших же прикладах (кроки 4 та 5) ви розмизуєте логіку між фабрикою (Фабрикою, фабрикою з логікою, Карле!) та сервісом.

Приклади з «зе блядської книжки»:
— набір УІ контролів для різних лук-н-філ
— створення частин гри, де кімнати, стіни щось ще мають різну поведінку, умовно фабрика безпечні кімнати та фабрика небезпечних — це 2 конкретні фабрики однієї абстрактної.

Ваш випадок міг би бути ок, якби
ALERT, NEWSLETTER та PROMOTION мали спеціфічні імплементації Email та SMS, і сервіс, що відправляє Email та SMS треба було б дистанціювати від знань про деталі ALERT, NEWSLETTER та PROMOTION

Дякую за детальну критику! Ви абсолютно праві щодо теорії.
Але я свідомо вибрав цей підхід для навчання. Ось чому:
Чому так краще для засвоєння:

Від практичної проблеми до рішення — так людина краще розуміє навіщо патерн потрібен. Замість абстрактного «створюй сімейства об’єктів» показую реальний біль: «дивись, як твій код перетворюється на лапшу»
Еволюція мислення — показую весь шлях від копіпасти до структурованого коду. Людина бачить не просто «ось правильний код», а процес покращення
Не лякає новачків складними прикладами з UI або ігровими кімнатами. Сповіщення — це те, з чим всі стикалися. Зрозуміло з першого погляду
Мотивація через біль — спочатку показую як погано, потім як добре. Це створює «ага-момент»

Ваш чистий приклад з AlertEmail/NewsletterEmail — теоретично правильний, але для початківця незрозуміло навіщо робити різні реалізації Email. Він подумає «а чим AlertEmail відрізняється від звичайного Email?»
Мій підхід — поетапний:

Показую конкретну проблему (дублювання коду) — людина одразу розуміє біль
Демонструю механіку винесення створення в фабрики — основна ідея патерну
Після засвоєння можна переходити до класичних прикладів з правильними сімействами

Психологія навчання: краще спочатку зрозуміти навіщо і як працює патерн на простому прикладі, а потім вивчати правильні нюанси і терміни. Ніж одразу давати «правильний» але складний код, який відлякує.
Моя мета — щоб після читання людина подумала: «Ого, а в мене теж така лапша в коді! Треба виправити!» А не «Складно, не зрозуміло, пропускаю».
Після засвоєння цього прикладу людина легше зрозуміє і ваші класичні варіанти з UI темами чи ігровими кімнатами.

Але я свідомо вибрав цей підхід для навчання.

Який підхід? Взяти синє і сказати, що воно рожеве?
Ключове — ви подаєте невірне трактування патерна. Фактично інший патерн, що надихався тим, який ви винесли в заголовок.

Ваш чистий приклад з AlertEmail/NewsletterEmail — теоретично правильний, але для початківця незрозуміло навіщо робити різні реалізації Email. Він подумає «а чим AlertEmail відрізняється від звичайного Email?»

Берете чатГпт та пишете йому «Опиши в 2 речення різницю між AlertEmail та NewsletterEmail».
Ось приклад відповіді:

AlertEmail — це електронне повідомлення, яке надсилається для негайного інформування користувача про важливу або критичну подію (наприклад, підозріла активність або помилка системи).
NewsletterEmail — це регулярне інформаційне розсилання з оновленнями, новинами або корисним контентом, спрямоване на підтримку зв’язку з користувачем.

Таке початківець зрозуміти не зможе?

І ключове:

Ваш випадок міг би бути ок, якби
ALERT, NEWSLETTER та PROMOTION мали спеціфічні імплементації Email та SMS, і сервіс, що відправляє Email та SMS треба було б дистанціювати від знань про деталі ALERT, NEWSLETTER та PROMOTION

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

Їй богу «зе блядська книжки» і так створила купу непорозумінь, а ви ще накидаєте додаткові зверху.

Код повторюється — створення сповіщень вручну кожного разу.

Ну... якщо брати код з фабриками, там також багато повторів: три класи, по два методи у кожному, частина повертає null це повтор. Трохи на іншому рівні, але повтор.

нові типи вимагають модифікації методу send.
що, якщо пізніше потрібно буде надсилати Slack, Push або WhatsApp?

Ну.. якщо з’являється новий тип, який буде містити три нотифікації (sms, email, WhatsApp), то треба переробляти всю архітектуру. primary/secondary прибиті цвяхами.

тісне зв’язування ускладнює створення моків або розширення

Ну... якщо брати метод send, то там особливо нічого й тестувати. Але... щоб його протестувати треба буде городити цілу ієрархію класів. Це не ускладнення?

import qualified Data.Map as M

email :: String -> IO ()
email msg = putStrLn $ "[Email] " ++ msg

sms :: String -> IO ()
sms msg = putStrLn $ "[SMS] " ++ msg

handlers :: M.Map String [String -> IO ()]
handlers = M.fromList
  [ ("ALERT",      [email, sms])
  , ("NEWSLETTER", [email])
  , ("PROMOTION",  [sms])
  ]

send :: String -> String -> IO ()
send kind msg =
  case M.lookup kind handlers of
    Just actions -> mapM_ ($ msg) actions
    Nothing      -> error $ "Unknown type: " ++ kind

main :: IO ()
main = do
  send "ALERT" "Server is down!"
  send "NEWSLETTER" "June updates are here!"

А тут що не так? test it!

Код ніби-то не повторюється.
Треба новий тип повідомлень? Додай рядок.
Чи важко тестувати? Тут без прикладів важко, це окремий холівар, як на мене то не важко.
Пізніше потрібно буде надсилати Slack, Push або WhatsApp? Напиши відповідну функцію.

def email(msg):
    print(f"[Email] {msg}")

def sms(msg):
    print(f"[SMS] {msg}")

handlers = {
    "ALERT":      [email, sms],
    "NEWSLETTER": [email],
    "PROMOTION":  [sms],
}

def send(kind, msg):
    if kind in handlers:
        for action in handlers[kind]:
            action(msg)
    else:
        raise ValueError(f"Unknown type: {kind}")

def main():
    send("ALERT", "Server is down!")
    send("NEWSLETTER", "June updates are here!")

if __name__ == "__main__":
    main()

test it!

kind — має бути sum type, а не string
send має вертати помилку, якщо IO error (не вдалось відіслати), а не коли kind не коректний.

Це все деталі, чи треба нам динамічно створювати нові типи чи ні... До речі, якщо така вимога з’явилася, співчуваю тим, хто має фабрику класів :-) Привіт, клас DynamiclyConfiguredNotificationFactory.

Чи треба обробляти, коли не вдалося відіслати? Не знаю, це бізнес-логіка. У більшості випадків треба зробити алерт, що алерт не пройшов.

гарна стаття для початківця, але у вас реалізації notificaition не мають ніяких станів. в таких випадках простіше замінити об’єкти функціями. щось на кшталт такого (псевдокод)

void send_email(string msg) {...}
void send_sms(string msg) {...}

void send_alert(string msg) {
  send_email(msg);
  send_sms(msg);
}

void send_news(string msg) {
  send_email(msg);
}

void send_promo(string msg) {
  send_sms(msg);
}

void send(string type, string msg) {
  if (type == "ALERT") return send_alert(msg);
  if (type == "NEWSLETTER") return send_news(msg);
  if (type == "PROMOTION") return send_promo(msg);
  throw error;
}

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

об’єкт це функція з ефектом. паттерни це просто комбінації функцій з т.з. фп. з т.з. ооп do notation в хаскелі це якийсь паттерн. ці речи вони доволі ортогональні, відрізняються лише базовою концепцією і кількістю коду

Який жах, do-нотація в Haskell це синтаксичний цукор а не паттерн. Усе інше поток свідомості.

це синтаксичний цукор над... комбінацією функцій.

Патерни ж для С++ писали

Так, перше видання 1994 рік це C++ та Smalltalk.
Але все рівно, у мене таке відчуття, що спочатку це набуло популярності в Java, а вже потім перекинулося на інші мови. Чи то була ідея перенести гнучкість Smalltalk у статично типізований C++? :-) Тому що різні Turbo Vision, OWL, MFC та інші писалися без формалізованих паттернів, там були свої, часто простіші архітектурні рішення. Так, окремо COM, але то взагалі була ідея зробити ООП доступним для всіх мов програмування.

Кажуть, на OOPSLA черга стояла за тою книжкою, щоб купити. Тому, думаю, воно одразу понеслося.

Але я більше за те, що в С++ одразу були вказівники на функцій.

Хоча, в старих книжках з С++ трапляється, що клас використовують замість неймспейсу — і просто групують в нього статичні функції — ніколи не створюючи інстанс цього класу. Може, звідти й Джава росте)

Кажуть, на OOPSLA черга стояла за тою книжкою, щоб купити.

Ні, ідеї як раз були дуже популярні. Віддавав (смішно казати) останні гроші, щоб купити книгу Греді Буча про ООП. Інша ситуація, що то відчувалося як комунізм, який колись наступить. Про це можна почитати, а потім піти писати як і раніше.

Перші фреймворки, побудовані активно з паттернами, це Swing, J2EE, Spring. Точніше я так думаю, можу збрехати. Пам’ятаю випадок, коли до нас в Delphi команду тимчасово на підсилення перевели Java розробника. Було задача заімпортувати csv файл кудись там. Очікували 10 рядків кода. А він зник. Ну мало, чи може Delphi вивчає. Потім з’являється, і там замість 10 рядків кода 10 різних класів: парсер, абстрактний парсер, фабрика парсерів, абстрактний споживач даних, ... Тоді я відчув, що комунізм, про який так довго писали, настав, але щось там не чисто... Хоча дзвіночки були й до цього.

До речі, абстрактна фабрика, певне, відрізняється від структури з вказівниками на функції (стратегії) наявністю даних. Мені якось довелося ліпити ІП-телефонію з підтримкою різних видів локальних ЮСБ-пристроїв: аналогового телефона та радіотелефона.

От система на старті дивилася, що стирчить в ЮСБ, створювала відповідну фабрику, передаючи в конструктор хендл ЮСБ пристроя, а далі вже з фабрики виймалися класи з логікою та транспортом для шматку телефонії, що переймався локальною апаратурою.

Хоча і в стратегію можна запхати дані — але то вже таке...

public Notification createSecondaryNotification() {
return null;
}

Як взагалі люди без Null safety писали код?
Жодному обʼєкту не можна довіряти, кожен може бути Null. 👀

Як взагалі люди без Null safety писали код?

А тепер ржака:
Якби ТС замість null повертав ДамміНотіфікейшн, то це було б ближче до суті шаблону. Бо хоча і не було б принципово різних реалізацій, то хоча б були «групи компонентів». хоч і однотипних

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