Factory Method
Sissejuhatus
Eelmises peatükis tutvisime tehasemustriga - klass, millel on staatiline meetod, mis otsustab parameetri põhjal, millist konkreetset objekti luua. Selline lahendus toimib hästi olukorras, kus valikute hulk on fikseeritud ning asuvad ühes kohas.
Samas on olukordi, kus üks keskne klass ei peaks teadma kõiki võimalikke implementatsioone. Samuti see tähendab ikkagi seda, et uute tüüpide lisamisel peame muutma olemasolevat koodi.
Nende probleemide lahendamiseks on välja mõeldud Factory Method muster. Selle asemel, et üks tehas teaks kõiki valikuvõimalusi, otsustab iga alamklass ise, millist objekti luua.
Probleemist lähemalt
Jätkame eelmises peatükis tutvustatud näitega.
Seal loodud PaymentMethodFactory töötab hästi olukorras, kus makseviisi valik on kasutaja teha.
Samas makseviis ei sõltu ainult kliendi soovist, vaid võib ka sõltuda tellimuse tüübist.
Näiteks:
- Korduvtellimus vajab krediitkaardi andmeid, millele on igakuiselt võimalik makseid esitada.
- Hulgimüük liigub alati pangaülekannete kaudu, sest B2B-arveldus nõuab seda.
- Annetused käivad läbi PayPal-i.
Esimene mõte oleks lisada OrderService klassi switch-avaldise, mis valib makseviisi tellimuse tüübi põhjal:
public class OrderService {
public void process(String orderType, BigDecimal amount) {
// Shared logic: validation
validateCart();
PaymentMethod payment = switch (orderType) {
case "SUBSCRIPTION" -> new CreditCardPayment();
case "WHOLESALE" -> new BankTransferPayment();
case "DONATION" -> new PayPalPayment();
default -> throw new IllegalArgumentException(orderType);
};
payment.process(amount);
// Shared logic: receipt
recordReceipt();
}
}
Esmasel pilgul see näeb välja nagu tavapärane tehasemuster, kuid siin on üks oluline erinevus:
orderType ei ole lihtsalt parameeter, mis valib makseviisi, vaid see määrab kogu tellimuse käitumise.
Korduvtellimusel on omad valideerimisreeglid, hulgimüügil oma allahindlusloogika, annetustel oma maksureeglid jne.
Lõpuks muutub OrderService üheks suureks switch-avaldiste rägastikuks, kus iga haru teeb juba omaette asju.
Factory Method muster
Factory Method läheneb tehase põhimõttele teise nurga alt.
Selle asemel, et üks klass teaks kõiki tellimusetüüpe, defineerime abstraktse Order klassi,
mis määrab töövoo kuju, jättes makse loomise alamklasside hooleks:
public abstract class Order {
// The factory method — each subclass decides which payment to create.
protected abstract PaymentMethod createPayment();
// The shared logic — same for every order.
public final void process(BigDecimal amount) {
validateCart();
PaymentMethod payment = createPayment();
payment.process(amount);
recordReceipt();
}
protected void validateCart() { /* ... */ }
protected void recordReceipt() { /* ... */ }
}
Order ei tea midagi CreditCardPayment või BankTransferPayment klassidest.
Ainus asi, mida Order klass teab on see, et process käivitamisel tekib mingi PaymentMethod, ning sellest piisab, et kõik töö ära teha.
PaymentMethod-i loomise eest vastutavad alamklassid:
public class SubscriptionOrder extends Order {
@Override
protected PaymentMethod createPayment() {
return new CreditCardPayment();
}
}
public class WholesaleOrder extends Order {
@Override
protected PaymentMethod createPayment() {
return new BankTransferPayment();
}
}
public class DonationOrder extends Order {
@Override
protected PaymentMethod createPayment() {
return new PayPalPayment();
}
}
Visuaalselt näeb struktuur välja selline - kaks paralleelset hierarhiat, kus iga konkreetne Order on seotud vastava PaymentMethod-iga:
Antud skeemil pange tähele noolt Order-ist PaymentMethod-ini.
Baasklass sõltub ainult liidesest, mitte ühestki konkreetsest makseklassist.
Konkreetse tüübi valiku teevad alamklassid.
Kutsuv kood kasutab tellimust ilma makseviisi pärast muretsemata:
public class Checkout {
public void placeSubscription(BigDecimal amount) {
Order order = new SubscriptionOrder();
order.process(amount);
}
public void placeWholesaleOrder(BigDecimal amount) {
Order order = new WholesaleOrder();
order.process(amount);
}
}
Kuidas see erineb tavalisest tehasemustrist
Põhimõte mõlemal mustril on sama - luua mingit tüüpi objekt. Erinevus seisneb peamiselt selles, milline osapool otsustab, mis objekti luua.
| Factory. | Factory Method |
|---|---|
| Üks klass teab kõiki valikuid | Iga alamklass teab oma konkreetset valikut |
| Uue valiku lisamine = factory muutmine | Uue valiku lisamine = uue alamklassi loomine |
| Otsus põhineb parameetril | Otsus põhineb kasutataval alamklassil |
| Loob objekti eraldiseisvalt | Loomine on osa suuremast ühisest algoritmist |
Selles tabelis viimane rida on kõige olulisem. Tavaline tehasemuster sobib siis, kui vajad lihtsat objekti. Factory Method sobib olukorras, kus objekti loomine on osa suuremast protsessist, mis on kõigi variantide puhul sama. Selles peatükis käsitletud näite puhul siis:
validateCart -> payment -> recordReceipt
Factory Method järgib ka avatud/suletud printsiipi:
süsteem on avatud laiendamiseks (saad lisada uusi tellimusetüüpe), kuid suletud muutmiseks (olemasolevaid klasse ei pea muutma).
Näiteks GiftCardOrder lisamine tähendab ainult ühe uue klassi loomist - Order, SubscriptionOrder ja teised jäävad puutumata.
Kasutusjuhud
Factory Method sobib hästi, kui:
- Sul on protsess, mis on sama sõltumata sellest, millist konkreetset objekti kasutatakse.
- Võimalike konkreetsete objektide hulk on avatud - eeldad, et neid lisandub ajas juurde.
- Soovid, et iga variant oleks omaette klassis, mitte ühe
switch-lause haruna.
Kui valikute hulk on väike ja tõenäoliselt ei kasva, on tavaline tehasemuster enamasti sama tulemuse saavutamiseks lihtsam ja vähem kohmakas lahendus.