Защо шаблони
Накратко: Шаблонът за дизайн е назовано, проверено решение на често срещан проблем в кода; класическите 23 шаблона са в три семейства: създаващи, структурни и поведенчески.
Проблем
Два абзаца за една дума
Преглед на кода в онлайн магазин. Петър чете касата и оставя коментар на маркирания ред:
interface PaymentRule { double total(double amount);}
class Checkout { private final Map<String, PaymentRule> rules = Map.of( "карта", amount -> amount, "наложен платеж", amount -> amount + 2.50, "ваучер", amount -> Math.max(amount - 10, 0));
public double pay(String method, double amount) { return rules.get(method).total(amount); }}Checkout checkout = new Checkout();System.out.printf("%.2f €%n", checkout.pay("наложен платеж", 80));System.out.printf("%.2f €%n", checkout.pay("ваучер", 80));82.50 €70.00 €Петър: Не разбирам какво е това. Касата държи таблица, в която всеки начин на плащане е отделно правило. Всички правила имат една и съща форма: взимат сума и връщат сума. По време на работа касата взима правилото по името, което е избрал клиентът, и го вика, без да знае кое е. Нарочно ли е така? Ако утре дойде разсрочено плащане, трябва ли да пипам
pay?Мария: Нарочно е. Не пипаш
pay, добавяш ред в таблицата. Правилото само смята и нищо не помни, затова касата може да го пита по няколко пъти: за прегледа, за фактурата, за плащането. Иначе всяко питане би дало друга сума…
Двама души, два абзаца, а най-важното още не е казано: какво става, ако името го няма в таблицата?
Мария можеше да напише една дума: Strategy. Петър щеше да знае какво гледа и какво да провери. Правилото само смята. Новият начин е нов ред и касата не се пипа. Непознатото име се хваща, преди да стигне до таблицата. Тези въпроси вече са решени от много хора и са записани под това име.
Идеята
Шаблонът е име на решение
Шаблон за дизайн е проверено решение на задача, която се повтаря в много проекти, с име, по което всички го познават. Той не е библиотека и не е код за копиране: всеки проект го пише по своему. Шаблонът носи четири неща:
- име: една дума вместо два абзаца;
- задачата, която решава: например „правилото се сменя, а касата не трябва да се пипа“;
- ролите: кой какво държи и кой кого вика;
- цената: какво плащаш за него (още класове, още един скок при четене) и кога не си струва.
Повечето имена идват от книгата „Design Patterns“ (1994) на четирима автори, които програмистите наричат „бандата на четиримата“ (Gang of Four). Тя описва 23 шаблона в три семейства. Модулът минава през най-ползваните:
- Създаващи: как и къде се правят обекти. Factory Method и Simple Factory, Builder, Singleton и Dependency Injection.
- Структурни: как обекти се сглобяват в по-голямо цяло. Adapter и Facade, Decorator, Composite.
- Поведенчески: кой кого вика и кой какво решава. Strategy, Observer, Command, State, Template Method и Iterator.
Repository и слоевете не е от книгата. Той е архитектурен шаблон: къде в програмата живее базата данни.
Модулът завършва с упражнение, Магазинът: кой шаблон къде: разхвърлян магазин, който сам подреждаш.
Виж го
Капанът: шаблон заради шаблона
Задачата е една: 10% отстъпка за поръчки над 100 €. Някой току-що е чел за шаблоните:
interface DiscountStrategy { double apply(double amount);}
class PercentOverThresholdDiscountStrategy implements DiscountStrategy { private final double percent; private final double threshold;
public PercentOverThresholdDiscountStrategy(double percent, double threshold) { this.percent = percent; this.threshold = threshold; }
@Override public double apply(double amount) { return amount > threshold ? amount * (1 - percent / 100) : amount; }}
abstract class AbstractDiscountStrategyFactory { public abstract DiscountStrategy createStrategy();}
class DefaultDiscountStrategyFactory extends AbstractDiscountStrategyFactory { @Override public DiscountStrategy createStrategy() { return new PercentOverThresholdDiscountStrategy(10, 100); }}
class DiscountService { private final AbstractDiscountStrategyFactory factory;
public DiscountService(AbstractDiscountStrategyFactory factory) { this.factory = factory; }
public double discounted(double amount) { return factory.createStrategy().apply(amount); }}DiscountService service = new DiscountService(new DefaultDiscountStrategyFactory());System.out.printf("Сума: %.2f €%n", service.discounted(120));System.out.printf("Сума: %.2f €%n", service.discounted(80));Сума: 108.00 €Сума: 80.00 €Пет класа, два шаблона (Strategy и фабрика) и име AbstractDiscountStrategyFactory. Същата задача, без шаблон:
static double discounted(double amount) { return amount > 100 ? amount * 0.9 : amount;}System.out.printf("Сума: %.2f €%n", discounted(120));System.out.printf("Сума: %.2f €%n", discounted(80));Сума: 108.00 €Сума: 80.00 €Изходът е същият. Разликата е в четенето: за да разбереш колко е отстъпката, в първия вариант минаваш през пет класа, а във втория четеш един ред.
Шаблоните в първия вариант не решават нито един проблем. Отстъпката е една, никой не я сменя по време на работа и никой не избира между отстъпки. Когато дойде втората отстъпка и клиентът трябва да избира, тогава функцията става стратегия. Не по-рано.
Пробвай
Кое семейство и трябва ли шаблон
Идеята
Кога шаблонът вреди
Шаблонът е отговор на проблем. Без проблема остава само цената:
- „Може да ни потрябва.“ Интерфейс с една имплементация, фабрика за един клас, стратегия с един вариант. Всеки, който чете кода, плаща за това, а никой не го ползва.
- Името казва шаблона, а не работата.
AbstractDiscountStrategyFactoryказва как е направено,discountedказва какво прави. Ако не можеш да опишеш класа без думите на шаблона, вероятно не ти трябва. - Шаблон от друг език. Където стига ламбда, интерфейс и клас за всяко правило са излишни.
- Шаблонът преди болката. Добрият ред е обратният. Пишеш простото. Когато същият
switchсе появи на трето място или правилата станат пет, правиш refactor към шаблона. Тогава вече знаеш точно какво се сменя.
Учиш шаблоните, за да ги разпознаваш и назоваваш в чужд код и в прегледа. Не за да ги слагаш навсякъде.
Упражнение
Познай шаблона
Реши задачата в редактора си.
Три фрагмента от един магазин. Пусни ги и за всеки кажи: има ли шаблон, кой е (Strategy, Observer, Factory Method, Singleton или Adapter от урока „Шаблони за дизайн“ в модула „Модерна Java“) и с едно изречение: какъв проблем решава. Един от трите няма нужда от шаблон.
А.
class Order { private final List<Consumer<String>> listeners = new ArrayList<>();
public void onShipped(Consumer<String> listener) { listeners.add(listener); }
public void ship(String id) { for (Consumer<String> listener : listeners) { listener.accept(id); } }}Order order = new Order();order.onShipped(id -> System.out.println("Имейл: поръчка " + id + " е изпратена"));order.onShipped(id -> System.out.println("Склад: махни " + id + " от опашката"));order.ship("1042");Имейл: поръчка 1042 е изпратенаСклад: махни 1042 от опашкатаБ.
class CourierApi { public String sendParcel(String city, int grams) { return "Пратка за " + city + ", " + grams + " г"; }}
interface Shipping { String ship(String city, double kilograms);}
class CourierShipping implements Shipping { private final CourierApi api = new CourierApi();
@Override public String ship(String city, double kilograms) { return api.sendParcel(city, (int) Math.round(kilograms * 1000)); }}Shipping shipping = new CourierShipping();System.out.println(shipping.ship("Пловдив", 1.25));Пратка за Пловдив, 1250 гВ.
static String priceLabel(double price, boolean withVat) { if (withVat) { return String.format("%.2f € с ДДС", price * 1.20); } return String.format("%.2f € без ДДС", price);}System.out.println(priceLabel(25, true));System.out.println(priceLabel(25, false));30.00 € с ДДС25.00 € без ДДСОтговорите
- А е Observer. Поръчката не знае кой я слуша. Имейлът и складът се абонират, а
shipги вика по реда на абонирането. Нов слушател не пипаOrder. - Б е Adapter.
CourierShippingпревежда твоетоship(килограми) към чуждотоsendParcel(грамове), без да пипа нито едното. - В няма нужда от шаблон. Два варианта, на едно място, никой не ги сменя. Strategy тук би бил интерфейс и два класа вместо един
if.
Още задачи
- Кога В иска шаблон. Магазинът започва да продава в пет държави с различен ДДС, а клиентът избира държавата. Какво се променя? Напиши кой шаблон би помогнал и защо, без код.
- Шаблонът, който изчезва. Вземи петте класа от картата „Капанът“ и ги замени с една функция. Преброй редовете преди и след. Изходът трябва да остане същият.
- Твоят код. Намери в свой проект
switchили верига отif, която се повтаря на две места. С коя дума би я назовал в прегледа?
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Списъци · HashMap и HashSet · Ламбди · Шаблони за дизайн
- Задачи по темата: Шаблони за дизайн