V tomto příspěvku s lehce nudným názvem bych se rád pokusil o svěží zamyšlení nad prací s číselníkovými hodnotami. Konkrétně mě bude zajímat porovnávání číselníkových hodnot a rozhodování podle číselníkových hodnot v obchodní logice. Mám na mysli otázky typu „Jak zpracovat adresu typu 'sídlo firmy'?“ nebo například „Jak vypočítat provizi podle schématu 'fixní procentní podíl'?“ Je to další profláknuté a docela jednoduché téma a na většině projektů asi není nutnost „zamýšlet se“ nad ním. Zároveň ale platí, že na většině projektů, u kterých jsem byl, by se porovnávání číselníkových dalo řešilo o dost přehledněji a čitelněji, což mě nakonec po troše váhání vedlo k napsání tohoto příspěvku.
Co je číselník
Číselníkovou položkou myslím třídu,
- která je perzistentní
- a má minimálně atributy reprezentující kód, se kterým může pracovat obchodní logika,
- a text, případně odkaz na jazykový text (tzn. českou, anglickou, … verzi), který se zobrazuje uživateli.
public abstract class AbstractCatalogEntity {Tento text neřeší:
private Long id;
private String code;
private String label;
...
}
- Jestli je o hodnotách, které chci porovnávat, lepší mluvit jako o „kódech“ nebo třeba „klíčích“.
- Jestli má být odkaz na číselník realizován přes „id“ nebo „code“.
- Jestli mají mít číselníkové položky platnost a jestli ji má porovnávání hodnot zohledňovat.
- Další související otázky.
Příklad
Budu používat jednoduchý příklad s číselníkem států, číselníkem typů adres, adresou a vazbou na adresu.
public class Country extends AbstractCatalogEntity { }Adresa má vlastnost country ukazující do číselníku zemí. Číselník zemí je příklad toho, kdy v obchodní logice potřebujeme vyhodnocovat pouze jednu nebo několik položek číselníku a ostatní nás víceméně nezajímají. Příklady budou zjišťovat, jestli je daná country rovna České republice (tedy jestli je daná adresa v Česku).
public class AddressType extends AbstractCatalogEntity { }
public class Address {
private Long id;
private Country country;
private String city;
private String street;
private String postcode;
...
}
public class AddressLink {
private Long id;
private AddressType addressType;
private Address address;
private String note;
...
}
Vazba na adresu má vlastnost addressType určující typ adresy – tzn. jestli je daná adresa adresou sídla firmy, adresou fakturační nebo třeba adresou pobočky. Odpovídající číselník typů adres je příkladem toho, kdy se budou v obchodní logice vyhodnocovat všechny jeho položky. Podle typu adresy se může zpracování adresy lišit.
Stringové literály bez konstant
Přímočarým řešením je porovnávat číselníkové kódy na očekávanou hodnotu všude tam, kde je to třeba.
Assert.assertEquals(“CZ”, address.getCountry().getCode());Java kód plný stringových literálů ale není můj ideál, proto od tohoto způsobu radši rychle pryč. Každé řešení je ale dobré nebo špatné vždy v určitém kontextu, proto jsem pro úplnost zmínil i tuto variantu.
Stringové konstanty v doménové třídě
Dalším krůčkem k lepšímu řešení může být zavedení konstant v doménových třídách reprezentujících číselníky:
public class Country extends AbstractCatalogEntity {Samotný kód tedy máme definovaný na jednom místě. Jeho případné změny jsou bezbolestné, snadno můžeme vyhledávat všechna jeho použití.
public static final String CZECH_REPUBLIC = "CZ";
}
Assert.assertEquals(Country.CZ, address.getCountry().getCode());Stále ale porovnáváme řetězce a to nemusí být vždy vhodné.
Má smysl porovnávat kódy?
Má tedy porovnávání kódu vůbec smysl? No, číselníkové kódy máme reprezentované jako řetězce, takže někde se ty řetězce porovnávat musí. Otázka byla míněna jinak – má smysl porovnávat kódy v obchodní logice? Domnívám se, že ne a proto se dostáváme k dalšímu řešení.
Doménová třída s isX metodami
V obchodní logice je porovnávání číselníkových kódů typicky velmi „malým“ problémem a jeho realizace by teda mohla být podobně úsporná a čitelná. Typicky tam nejsou důležité otázky typu „jestli je daný kód rovný jinému kódu“, ale spíš jestli daný kód nebo instance doménové třídy splňuje vlastnost reprezentovanou daným číselníkovým kódem. Můžeme si proto představit následující drobné rozšíření doménových tříd.
public class AddressLink {IsX metody rozhodnou, jestli daná doménová třída splňuje vlastnost reprezentovanou číselníkovým kódem. Na pozadí samozřejmě musí dojít k porovnání číselníkových kódů, obchodní logiku tím ale nemusíme zatěžovat.
public static final String RESIDENCE = "RESIDENCE";
public static final String INVOICE = "INVOICE";
public static final String BRANCH = "BRANCH";
….
public boolean isResidence() {
return addressType != null && RESIDENCE.equals(addressType.getCode());
}
public boolean isInvoice() {
return addressType != null && INVOICE.equals(addressType.getCode());
}
}
Assert.assertTrue(addressLink.isResidence());Alternativně můžeme tyto konstanty a metody umístit do třídy AddressType. Záleží víceméně na osobním vkusu.
Oddělení definice číselníku od jeho hodnot
Jsou případy, kdy je nutné oddělit definici číselníků od jejich hodnot. Někdy není žádoucí aby např. třída AddressType znala svoje možné hodnoty, to znamená obsahovala konstanty s možnými hodnotami číselníkových kódů. Příkladem mohou být projekty, kdy jsou číselníkové a doménové třídy součástí nějakého frameworku nebo komponenty a mohou mít na různých zákaznických projektech různé hodnoty. Tomuto požadavku se bude věnovat zbytek článku.
Stringové konstanty v k tomu určeném rozhraní
Pokud nechceme, aby doménové třídy znaly číselníkové kódy, můžeme kódy umístit třeba do k tomu určenému rozhraní.
public interface IAddressTypeCodes {Po předchozí argumentaci je jasné, že nejsem nakloněn porovnávání kódů přímo v obchodní logice. Toto řešení ale uvádím jako nejčastěji používané (z toho, co jsem měl možnost vidět) a jako řešení, které jsem dříve automaticky používal taky. 'I' je v názvu rozhraní jenom proto, abych ho odlišil od dalších příkladů.
String RESIDENCE = "RESIDENCE";
String INVOICE = "INVOICE";
String BRANCH = "BRANCH";
}
Rozhraní?
Někdo může namítnout, že rozhraní reprezentuje kontrakt mezi jeho implementací a klientským kódem a že proto není vhodné používat jej jako kontejner na stringové konstanty. A bude mít pravdu. Jsou číselníkové kódu součástí takového kontraktu? Ano i ne. Tento faktor chápu jako dost subjektivní, ale kloním se k tomu, že do rozhraní číselníkové kódy nepatří. Otázka typu „A proč to máš v rozhraní?“ vlastně vedla ke vzniku tohoto příspěvku.
Enum s match metodou
Od Javy 1.5 je přirozeným prostředkem k evidování konstant typ enum. Pouhé vyjmenování číselníkových kódů ale mnoho neřeší. Je vhodné vyžadovat, aby obchodní logika volala metody typu Country.getCode nebo AddressType.getCode? Jinými slovy, je vhodné, aby obchodní logika „znala“ vnitřní strukturu číselníků? Rozhodně to není nutné. Enum může vedle číselníkových hodnot obsahovat i prostředky k jejich porovnání. Takovým prostředkem může být metoda match:
public enum ECountryCodes {Místo podmínek typu
CZ;
public boolean match(Country country) {
return country != null && name().equals(country);
}
}
Assert.assertTrue(ECountryCodes.CZ.equals(address.getCountry().getCode()));tak můžeme dostat výrazně přehlednější kód
Assert.assertTrue(ECountryCodes.CZ.match(address.getCountry()));
Opět, 'E' je v názvu enumu jenom proto, abych ho odlišil od ostatních příkladů.
Abychom se úplně vyhnuli porovnávání a mohli vyhodnocovat jenom to, jestli daná instance doménové třídy splňuje vlastnost reprezentovanou číselníkovým kódem, můžeme opět použít isX metody.
public enum EAddressTypeCodes {Vyhodnocení číselníkových hodnot potom konečně vypadá celkem elegantně.
RESIDENCE, INVOICE, BRANCH;
public static boolean isResidence(AddressType addressType) {
return RESIDENCE.match(addressType);
}
public static boolean isInvoice(AddressType addressType) {
return INVOICE.match(addressType);
}
public static boolean isBranch(AddressType addressType) {
return BRANCH.match(addressType);
}
public boolean match(AddressType addressType) {
return addressType != null && name().equals(addressType.getCode());
}
}
Assert.assertTrue(EAddressTypeCodes.isResidence(addressLink.getAddressType()));Navíc, isX metody samozřejmě nemusí mít jako vstupní parametry jenom samotné číselníkové třídy, pro pohodlné použití a úspornější volání mohou obsahovat například i metody isCzechRepublic(Address) nebo isResidence(AddressLink).
Závěr
Tolik jednoduché zamyšlení nad porovnáváním číselníkových hodnot. Nekladl jsem si za cíl přinést úplný výčet všech myslitelných řešení, šlo mi o to popsat posloupnost úvah, které vedou k mnou preferovanému řešení – enumu s isX metodami.
Jak porovnávání číselníkových hodnot řešíte vy? Podělte se o své nápady a zkušenosti v diskuzi pod článkem.