Abstract Factory
Sissejuhatus
Factory Method lahendas probleemi, kus tuli luua üks toode, mille konkreetne tüüp sõltus kontekstist. Abstract Factory viib selle sama mõtte ühe sammu võrra edasi: mis siis, kui on vaja luua terve kogumik omavahel seotud objekte?
Näiteks, kui Factory Method otsustab, millist makseviisi peaks antud tellimustüübi puhul kasutama, siis Abstract Factory arvestab sisse ka erinevaid reegleid ja komponente, mis selle tellimusega kaasnevad.
Probleemist lähemalt
Jätkame eelmise peatüki tellimuse näidet.
Seni pidi iga Order alamklass langetama otsuse ühe asja kohta - millist PaymentMethod-it kasutada.
Päris süsteemid on kompleksemad.
Korduvtellimused, hulgimüügitellimused ja annetused võivad erineda ka järgmistes punktides:
- Valideerimine - hulgimüügitellimuse puhul peab kontrollima ärinumbrit ja minimaalset kogust; annetuse puhul piisab kehtivast e-posti aadressist.
- Allahindlus - hulgimüügi puhul rakendatakse allahindlust; korduvtellimustel näiteks hinnatakse lojaalsust; annetustel allahindlus puudub.
- Maksustamine - B2B-hulgimüügil rakendub pöördmaksustamine; korduvtellimustel käibemaks; annetused on maksuvabad.
Naiivne lähenemine oleks lisada olemasolevale Order klassile rohkem tehasemeetodeid:
public abstract class Order {
protected abstract PaymentMethod createPayment();
protected abstract Validator createValidator();
protected abstract DiscountPolicy createDiscountPolicy();
protected abstract TaxCalculator createTaxCalculator();
public final void process(BigDecimal amount) {
createValidator().validate(this);
BigDecimal discounted = createDiscountPolicy().apply(amount);
BigDecimal withTax = createTaxCalculator().apply(discounted);
createPayment().process(withTax);
recordReceipt();
}
}
Selline lahendus töötaks ning seda liigitataks jätkuvalt kui Factory Method-ina.
Samas kerkib esile uus probleem, milleks on järjepidevus.
Iga Order alamklass peab nüüd meeles pidama, et tehasemeetodid tagastaksid vastava sobiva variandi:
public class WholesaleOrder extends Order {
protected PaymentMethod createPayment() { return new BankTransferPayment(); }
protected Validator createValidator() { return new WholesaleValidator(); }
protected DiscountPolicy createDiscountPolicy() { return new BulkDiscountPolicy(); }
protected TaxCalculator createTaxCalculator() { return new B2BTaxCalculator(); }
}
Kui muuta nüüd näiteks hulgimüügi loogikat, lisades uue valideerija ja kalkulaatori, siis
tekib oht, et vajalikud muudatused jäävad sisse viimata vajaminevates kohtades.
Selle tulemusena näiteks tekib WholesaleOrder klass, mida valideeritakse vastavalt uutele reeglitele,
aga maksustamine toimuks jätkuvalt vana jaemüügi reeglite järgi.
Kompilaatori vaates on kõik korrektne kuna kõik neli meetodit on teineteisest sõltumatud.
Need neli osapoolt ei ole tegelikult iseseisvad valikud. Nad moodustavad kogumiku, mida tuleb alati koos kasutada.
Abstract Factory muster
Abstract Factory seob omavahel kokku kõik loomise meetodid ühte liidesesse. Iga konkreetne tehas rakendab seda liidest ühe kogumiku jaoks:
public interface OrderPolicyFactory {
Validator createValidator();
DiscountPolicy createDiscountPolicy();
TaxCalculator createTaxCalculator();
PaymentMethod createPayment();
}
Seejärel on iga tellimusetüübi jaoks oma teostus:
public class WholesalePolicyFactory implements OrderPolicyFactory {
public Validator createValidator() { return new WholesaleValidator(); }
public DiscountPolicy createDiscountPolicy() { return new BulkDiscountPolicy(); }
public TaxCalculator createTaxCalculator() { return new B2BTaxCalculator(); }
public PaymentMethod createPayment() { return new BankTransferPayment(); }
}
public class SubscriptionPolicyFactory implements OrderPolicyFactory {
public Validator createValidator() { return new SubscriptionValidator(); }
public DiscountPolicy createDiscountPolicy() { return new LoyaltyDiscountPolicy(); }
public TaxCalculator createTaxCalculator() { return new StandardVatCalculator(); }
public PaymentMethod createPayment() { return new CreditCardPayment(); }
}
public class DonationPolicyFactory implements OrderPolicyFactory {
public Validator createValidator() { return new DonationValidator(); }
public DiscountPolicy createDiscountPolicy() { return new NoDiscountPolicy(); }
public TaxCalculator createTaxCalculator() { return new TaxExemptCalculator(); }
public PaymentMethod createPayment() { return new PayPalPayment(); }
}
Nüüd ei vaja Order klass isegi alamklasse.
See saab tehase ette ja usaldab, et see tagastab omavahel sobivate klasside komplekti:
public class Order {
private final OrderPolicyFactory policies;
public Order(OrderPolicyFactory policies) {
this.policies = policies;
}
public final void process(BigDecimal amount) {
policies.createValidator().validate(this);
BigDecimal discounted = policies.createDiscountPolicy().apply(amount);
BigDecimal withTax = policies.createTaxCalculator().apply(discounted);
policies.createPayment().process(withTax);
recordReceipt();
}
}
Visuaalselt ilmnevad paralleelsed hierarhiad - üks tehaste jaoks, üks iga tellimustüübi jaoks - ning need on omavahel kokku seotud nii, et iga konkreetne tehas loob ainult sobivaid tooteid:
Pane tähele, et creates nooled ei ristu kunagi perekondade vahel:
WholesalePolicyFactory toodab ainult hulgimüügi komponente, SubscriptionPolicyFactory ainult tellimuse komponente.
See visuaalne eristus ongi mustri antud lubadus - struktuuriliselt ei ole võimalik, et tehas tagastaks segamini komplekti komponentidest.
Kasutav kood valib tehase ühel korral ning kõige muu eest vastutab vastav tehas:
public class Checkout {
public void placeWholesaleOrder(BigDecimal amount) {
Order order = new Order(new WholesalePolicyFactory());
order.process(amount);
}
public void placeSubscription(BigDecimal amount) {
Order order = new Order(new SubscriptionPolicyFactory());
order.process(amount);
}
}
Seos Factory Method mustriga
Abstract Factory mustrit võib võtta kui mitme Factory Method’ina, mis on koondatud ühte objekti:
| Factory Method | Abstract Factory |
|---|---|
| Loob ühe toote | Loob kogumiku omavahel seotud toodetest |
| Variatsioon alamklasside kaudu | Variatsioon tehase objekti vahetamise kaudu |
| Üks üle kirjutatav meetod | Mitme meetodiga liides |
| Alamklass valib ühe toote | Tehas tagab, et kõik tooted sobivad kokku |
Tegelikult on iga OrderPolicyFactory meetod eraldi Factory Method.
Abstract Factory tuleb mängu siis, kui need tehasemeetodid kuuluvad kokku ja peavad alati toimima ühtse komplektina.
Samuti tasub tähele panna, et Order klass ei pea enam abstraktne olema.
Factory Method puhul paiknes variatsioon klassihierarhias (SubscriptionOrder, WholesaleOrder, ...).
Abstract Factory puhul paikneb variatsioon tehase objektis ning
Order muutub üheks konkreetseks klassiks, mida saab vastava tehase parameetrina.
See on sageli puhtam lahendus olukorras, kus variant ei ole tegelikult erinev tüüp, vaid sama protsessi jaoks rakendatav erinev reeglistik.
Kasutusjuhud
Abstract Factory sobib olukorras, kus:
- Süsteem peab töötama mitme omavahel seotud toodete kogumikuga
- Erinevatesse komplektidesse kuuluvad komponendid ei tohi üksteist segada
- Soovid, et kasutajaliides ei peaks teadma, millist komplekti parasjagu kasutatakse.
See on ebamõistlik olukorras, kus on tegemist ainult ühe tootetüübiga - sellisel juhul on Factory Method lihtsam lahendus. Samuti on see üleliigne siis, kui komplektid ei muutu kunagi käituse ajal. Kui kasutusel on alati ainult üks variant, tekitab abstraktsioon rohkem segadust kui selgust.