Factory
Sissejuhatus
Eelnevates peatükkides oleme korduvalt kokku puutunud olukorraga,
kus üks koodiosa peab looma objekti mõnest kindlast klassist.
Tavapäraselt on see üsna otsekohene protsess - new SomeClass(...) ning klass saab loodud.
Kuid mis juhtub siis, kui ei ole ette teada millist klassi täpselt tuleb luua? Selline küsimus võib tekkida näiteks olukorras, kus otsus sõltub mõnest parameetrist, konfiguratsioonist või sisendandmetest.
Naiivne lahendus oleks kasutada if- või switch-lauseid otsuste langetamiseks.
Tehasemuster (Factory) pakub puhtamat lähenemist.
Tehasemuster koondab otsustusloogika ühte kohta, mille ainus ülesanne on luua õiget tüüpi objekti.
Probleemist lähemalt
Vaatleme veebipoodi, mis toetab mitut erinevat makseviisi: krediitkaart, PayPal ja pangaülekanne.
Igal neist on oma teostus ühise PaymentMethod liidese jaoks, maksmisel kasutaja näeb ja viib tehingu läbi ainult ühega neist.
Millisega makset läbi viia, see valik peab tehtud olema kusagil varasemas etapis ning koodi vaates tähendab seda,
et kasutaja valik peab olema teisendatud õigeks objektiks.
Ilma tehasemustrita satub see loogika otse kasutavasse koodi:
public class CheckoutService {
public void checkout(String methodType, BigDecimal amount) {
PaymentMethod method;
if (methodType.equals("CARD")) {
method = new CreditCardPayment();
} else if (methodType.equals("PAYPAL")) {
method = new PayPalPayment();
} else if (methodType.equals("BANK")) {
method = new BankTransferPayment();
} else {
throw new IllegalArgumentException("Unknown method: " + methodType);
}
method.process(amount);
}
}
Selline lähenemine toimib, kuid selles esineb mitu probleemi:
- Loomisloogika pole taaskasutatav. Kui mõni teine klass peab samuti sarnast loogikat kasutama,
tuleb sama
if/elseahel uuesti luua teises failis. CheckoutServiceklass teab kõiki konkreetseid makseklasse. Uue makseviisi lisamine tähendabCheckoutService-i (ja kõigi teiste seda loogikat kasutavate klasside) muutmist.- Ühes klassis on mitu vastutust.
CheckoutServicepeaks tegelema ostuprotsessi haldamisega, mitte otsustama, millisest makseklassist instanss luua.
Tehasemuster
Tehasemustri mõte seisneb selles, et sellise otsustusloogika eest vastutab eraldi klass, millel on üks avalik staatiline meetod:
public class PaymentMethodFactory {
public static PaymentMethod getPaymentMethod(String type) {
return switch (type) {
case "CARD" -> new CreditCardPayment();
case "PAYPAL" -> new PayPalPayment();
case "BANK" -> new BankTransferPayment();
default -> throw new IllegalArgumentException(
"Unknown payment method: " + type);
};
}
}
Loogika kolis CheckoutService klassist välja, selle asemel kutsutakse seal nüüd PaymentMethodFactory-t:
public class CheckoutService {
public void checkout(String methodType, BigDecimal amount) {
PaymentMethod method = PaymentMethodFactory.getPaymentMethod(methodType);
method.process(amount);
}
}
CheckoutService ei tea enam ega pea teadma, millised konkreetsed klassid eksisteerivad.
Selle eest vastutab PaymentMethod, mida CheckoutService lihtsalt kasutab.
Kui tulevikus näiteks lisanduks Apple Pay, tuleb muuta ainult PaymentMethodFactory klassi.
Kogu ülejäänud kood töötab muutmata kujul edasi.
GoF tehasemustrid
Eelnevalt näidatud tehasemuster ei kuulu ametlikult GoF disainimustrite alla. GoF raamatus kirjeldatakse Factory Method ja Abstract Factory mustreid, mis on veidi keerukamad. Antud näite täpsem termin oleks staatiline tehasemeetod, mis asub eraldi klassis.
Sellegipoolest tasub sellest alustada, sest see väljendab tehaste põhiideed kõige selgemini, millele formaalsemad mustrid tuginevad:
Kood, mis objekti kasutab, ei peaks teadma, kuidas see objekt luuakse.
Kui see põhimõte on selge, siis järgmises peatükis käsitletav Factory Method on pigem selle loomulik edasiarendus, mitte täiesti uus kontseptsioon.
Kasutusjuhud
Tehasemustrit on mõistlik kasutada, kui:
- Koodibaasis on mitu klassi, mis implementeerivad sama liidest või pärivad samast baasklassist.
- Otsus, millist klassi instantsida, sõltub parameetrist, konfiguratsioonist või sisendist, mis ei ole kompileerimise ajal teada.
- Sama loomisloogikat on vaja kasutada mitmes erinevas kohas.
Tehast ei ole mõistlik kasutada siis, kui antud liidesele eksisteerib ainult üks teostus.
Samuti muutub see ebamõistlikuks, kui selle väljakutsujal on täpselt teada, millist tüüpi objekti on vaja.
Tehast tuleks kasutada ainult siis, kui sarnast tüüpi objektide loomiseks vajaminev if/else loogika hakkab korduma.