pondělí 26. října 2009
Hledání země nezemě
Kódy zemí, měn, bank, názvy měst a vesnic, poštovní směrovací čísla… Většina aplikací pracuje s nějakými číselníky. Kde ale vzít jejich hodnoty a nekrást? Pro firemní blog jsem napsal článek o několika zdrojích dat, které se mi osvědčily na nedávném projektu.
středa 30. září 2009
Práce s číselníkovými hodnotami
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 {
private Long id;
private String code;
private String label;
...
}
Tento text neřeší:- 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 { }
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;
...
}
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).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 {
public static final String CZECH_REPUBLIC = "CZ";
}
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í.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 {
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());
}
}
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.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 {
String RESIDENCE = "RESIDENCE";
String INVOICE = "INVOICE";
String BRANCH = "BRANCH";
}
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ů.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 {
CZ;
public boolean match(Country country) {
return country != null && name().equals(country);
}
}
Místo podmínek typuAssert.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 {
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());
}
}
Vyhodnocení číselníkových hodnot potom konečně vypadá celkem elegantně.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.
čtvrtek 10. září 2009
Závislosti polí ve formuláři a Drools
Ve druhé části blogu o závislostech polí ve formuláři bych rád navázal na předchozí úvahy a ukázal, jak postup popsaný v první části implementovat s pomocí knihovny Drools. Tento příspěvek nechce být obecnou úvahou nad vhodností použití rule engines. Na příkladě závislostí polí ve formuláři chci ukázat řešení s pomocí technologie, která je v mém okolí stále chápána jako okrajová a netradiční, ale která je podle mě pro řešení tohoto problému velmi výhodná.
Použiju stejný příklad jako minule a to parcelu evidovanou na katastru nemovitostí. Atributy parcely a jejich vztahy pro čtyři typy parcel jsou zachyceny následující tabulkou.
První tři atributy můžeme chápat jako „diskriminant“ parcely – jejich hodnoty podmiňují přístupnost, povinnost a přednastavené hodnoty jejich dalších atributů.
Dále budu pracovat s třídou Plot, která reprezentuje parcelu, s třídou FieldState, která obecně definuje stavy polí a s třídou PlotFieldState, která definuje stavy polí odpovídající atributům parcely. Dále si připomeňme interface PlotDependencySolver s metodou solveDependencies(Plot, PlotFieldState), která reprezentuje algoritmus řešení závislostí. Přesnou definici těchto tříd můžete nalézt v první části blogu.
| Popis parcely | České KÚ? | Má číslo? | Typ? | Ověřitelné | Číslo | Číslo LV | Pomocná identifikace | Zdroj PZE |
| Parcela katastru nemovitostí | ano | ano | KN | ano | přístupné, povinné | přístupné, povinné | nepřístupné, null | nepřístupné, null |
| Parcela zjednodušené evidence | ano | ano | ZE | ano | přístupné, povinné | přístupné, povinné | nepřístupné, null | přístupné, povinné |
| Budoucí parcela | ano | ne | nepřístupné, null | ne | nepřístupné, null | nepřístupné, null | přístupné, povinné | nepřístupné, null |
| Zahraniční parcela | ne | nepřístupné, null | nepřístupné, null | ne | nepřístupné, null | nepřístupné, null | přístupné, povinné | nepřístupné, null |
První tři atributy můžeme chápat jako „diskriminant“ parcely – jejich hodnoty podmiňují přístupnost, povinnost a přednastavené hodnoty jejich dalších atributů.
Dále budu pracovat s třídou Plot, která reprezentuje parcelu, s třídou FieldState, která obecně definuje stavy polí a s třídou PlotFieldState, která definuje stavy polí odpovídající atributům parcely. Dále si připomeňme interface PlotDependencySolver s metodou solveDependencies(Plot, PlotFieldState), která reprezentuje algoritmus řešení závislostí. Přesnou definici těchto tříd můžete nalézt v první části blogu.
Řekněme, že chceme napsat pravidlo (ve smyslu Drools), které popíše stav, kdy je parcela evidovaná v katastru nemovitostí (tedy stav popsaný prvním řádkem tabulky). Pravidlo "Land register" může vypadat třeba takto:
rule "Land register"Aplikovatelnost pravidla je popsána klauzulí "when", která porovnává hodnoty atributů v instanci třídy Plot, tedy hodnoty, které byly vyplněné ve formuláři. Další parametr říká, že pravidlo musí mít k dispozici i instanci třídy PlotState, aby stav formuláře mohlo aktualizovat. V těle pravidla potom dojde k aktualizaci stavu a nastavení některých hodnot atributů parcely, tak jak to u parcel z katastru nemovitostí chceme.
when
plot : Plot(locatedInCR == true, numberAssigned == true, plotType == "KN")
state : PlotFieldState()
then
state.setNumberAssigned(FieldState.ENABLED);
state.setPlotType(FieldState.ENABLED);
state.setVerifiable(FieldState.ENABLED);
state.setNumber(FieldState.ENABLED_MANDATORY);
state.setOwnershipNumber(FieldState.ENABLED_MANDATORY);
state.setAuxiliaryId(FieldState.DISABLED);
state.setSimplifiedEvidenceSource(FieldState.DISABLED);
plot.setAuxiliaryId(null);
plot.setSimplifiedEvidenceSource(null);
end
Podobně potom vypadá další pravidlo, tentokrát popisujicí zahraniční parcelu.
rule "Foreign plot"
when
plot : Plot(locatedInCR == false)
state : PlotFieldState()
then
state.setNumberAssigned(FieldState.DISABLED);
state.setPlotType(FieldState.DISABLED);
state.setVerifiable(FieldState.DISABLED);
state.setNumber(FieldState.DISABLED);
state.setOwnershipNumber(FieldState.DISABLED);
state.setAuxiliaryId(FieldState.ENABLED_MANDATORY);
state.setSimplifiedEvidenceSource(FieldState.DISABLED);
plot.setNumberAssigned(false);
plot.setPlotType(null);
plot.setNumber(null);
plot.setOwnershipNumber(null);
plot.setSimplifiedEvidenceSource(null);
end
Všechna pravidla můžeme udržovat ve zdrojovém souboru PlotRules.drl.
package PlotRules
import dependency.*;
import plot.*;
rule "Land register"
...
end
rule "Simplified evidence"
...
end
rule "Future plot"
...
end
rule "Foreign plot"
...
end
Příklady ukázaly, že pravidla v Drools většinou píšeme v DRL (Drools Rule Language) a přistupujeme přitom k Java třídám.
Máme pravidla, ale co je třeba udělat, aby se správně aplikovala? Implementace metody PlotDependencySolver.solveDependencies, která bude používat Drools a pravidla, je jednoduchá.
public void solveDependencies(Plot plot, PlotFieldState state) {
setUp();
StatelessKnowledgeSession ksession = kbase.newStatelessKnowledgeSession();
ksession.execute(Arrays.asList(plot, state));
}
Vytvoříme novou stateless session pro aplikaci pravidel a do její working memory vložíme instanci třídy Plot (data) a instanci PlotFieldState (stav formuláře). Je na Drools, jakým způsobem a v jakém pořadí pravidla aplikuje. Metoda setUp ukazuje, jak vytvořit instanci KnowledgeBase. Jinými slovy, jak inicializovat runtime knihovny Drools, abychom mohli aplikovat naše pravidla. protected void setUp() {
KnowledgeBuilder kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder();
kbuilder.add(ResourceFactory.newClassPathResource("PlotRules.drl"), ResourceType.DRL);
KnowledgeBuilderErrors errors = kbuilder.getErrors();
if (errors.size() > 0) {
for (KnowledgeBuilderError error: errors) {
System.err.println(error);
}
throw new IllegalArgumentException("Could not parse knowledge.");
}
kbase = KnowledgeBaseFactory.newKnowledgeBase();
kbase.addKnowledgePackages(kbuilder.getKnowledgePackages());
}
Tato ukázka kódu není pro náš příklad příliš zajímavá, ale pro úplnost ukazuje, jak načíst soubor s pravidly a vytvořit runtime Drools (v tomto případě instanci KnowledgeBase). Ve většině aplikací budeme typicky spoléhat na nějakou factory, které jako parametr předložíme resource odpovídající souboru PlotRules.drl a necháme náš DI framework, aby provedl injection očekávané instance KnowledgeBase.
- Deklarativní pravidla - Nepopisuji algoritmus vyhodnocení závislostí, definuji množinu pravidel, které reagují na stavy formuláře. Možnost popsat řešení problému deklarativně vidím jako velkou výhodu tam, kde se pracuje s větším množstvím samostatných podmínek a kde je někdy obtížné sestavit algoritmus, který by všechny podmínky zohlednil.
- Přehlednost a udržovatelnost - Tohle je dost subjektivní faktor, ale já považuji deklarativní pravidla za stručnější, přehlednější a dlouhodobě lépe udržovatelná než Java implementaci popsanou v předchozím příspěvku. Znamená to, že doporučuji, aby se obchodní logika na různých systémech vždy implementovala pomocí pravidel? To samozřejmě netvrdím, záleží na konkrétních požadavcích.
- Samostatný životní cyklus pravidel - Životní cyklus pravidel nemusí být stejný jako životní cyklus Java zdrojových kódů a verzí aplikace. Pokud se například pravidla mění výrazně častěji než zbytek aplikace, je možné je udržovat a distribuovat nezávisle na zbytku systému. Není to ale nutné, u některých systémů by takový přístup mohl být považovaný i za nevýhodu.
Co to stojí?
- Integrace dalšího nástroje s naší aplikace - Integrace Drools s DI frameworky jako Spring nebo Google Guice je jednoduchá, představuje jenom malou investici času a úsilí.
- Výkonnost - Rule engines a deklarativní pravidla se samozřejmě nehodí na všechny problémy. Nevhodné použití může znamenat výkonnostní problémy. V případě závislostí polí ve formuláři bych je ale nečekal. Drools nabízí několik možností, jak výkonnost testovat a optimalizovat. Pravidla mohu například indexovat (možno chápat podobně jako indexy v relačních databázích) a vyhodnocení tím výrazně zrychlit.
- DRL - Drools Rule Language představuje nový programovací jazyk pro část projektu a vyžaduje tedy určitý čas na pochopení a zvládnutí. Není to ale žádné drama, struktura pravidel je často velmi jednoduchá a těla pravidel, to už je stará známá Java.
- DRL a udržovatelnost kódu - Jako samozřejmost dnes bereme, že naše IDE umí hledat použití tříd, atributů, metod, ... nad všemi zdrojovými soubory projektu. V rámci JBoss Tools je k dispozici sada pluginů pro Eclipse, které nabízí editor se zvýrazňováním syntaxe a code completion, vizualizaci pravidel, debugger pravidel, ale některé vlastnosti tato podpora stále nenabízí. Chybí mi např. vyhledávání referencí, které by uvažovalo i pravidla a také refactoring, který by zahrnoval pravidla. Na blogu Drools se lze dočíst, že se na těchto dvou vlastnostech IDE už pracuje, ale aktuální stav je, že tato podpora stále chybí. Pro IntelliJ Idea jsem žádnou podporu Drools nenašel.
Tradiční implementace v Javě je diskutovaná v první části blogu. Zájemci o vyzkoušení a porovnání příkladů mohou uvítat možnost stáhnout si testovací projekt (konfigurace v pom.xml).
Příspěvek si kladl za cíl představit stále málo používané, ale v mnohých aspektech výhodné řešení problému závislostí polí ve formuláři pomocí frameworku Drools. Navazuje na první část, která popisuje obecný problém závislostí polí ve formuláři a diskutuje různé přístupy k jeho řešení.
Jaký názor na rule engines máte vy? Použili jste už Drools nebo jiný framework na komerčním projektu? Podělte se o své zkušenosti v diskuzi pod článkem. Na téma konceptu rule engines a dalších vlastností knihovny Drools bych rád v budoucnu napsal samostatnou sérii článků (příklady, integrace se Springem, testy výkonnosti a podobně).
pátek 10. července 2009
Závislosti polí ve formuláři
Ve svém vůbec prvním blogu bych se rád podíval na řešení závislostí polí ve formuláři. Nechci popisovat konkrétní technologii pro psaní webových aplikací nebo například tlustých klientů. Zajímá mě obecný problém, kdy máme aplikaci pracující s množstvým formulářů, které jsou „složité“ a jejich složitost je tvořena mimo jiné i závislostmi mezi poli, která obsahují. Závislostí polí ve formuláři myslím některou z následujících možností:
Většina frameworků a aplikací má dnes spolehlivě vyřešené problémy jako binding formulářových dat na doménový model aplikace, validace jednotlivých polí, lokalizace a další problémy, které musí typické GUI řešit. Ještě jsem se ale nesetkal s aplikací, která by závislostí polí ve formuláři řešila způsobem, který by byl přehledný, do budoucna udržovatelný a hlavně, který by byl testovatelný (ve smyslu unit testů). Domnívám se, že závislosti polí jsou často v návrhu aplikací opomíjeny, a chci proto představit jednoduchý návod, jak závislosti řešit. Jistě nejde o žádnou převratnou inovaci, ale podobný postup jsem zatím nikde zdokumentovaný a hlavně použitý nenašel. Smysl tohoto blogu tedy vidím v tom, že některým čtenářům pomůže uvědomit si problém a jeho nepříjemným důsledkům se vyhnout.
- Viditelnost nebo přístupnost pole závisí na hodnotě jiného pole nebo polí.
- Seznam přípustných hodnot pro nějaké pole závisí na hodnotě jiného pole nebo polí.
- Změna hodnoty pole (nebo polí) způsobí změnu stavu (tzn. přístupnosti nebo hodnoty) jiného pole nebo polí.
- Libovolnou jinou závislost.
Většina frameworků a aplikací má dnes spolehlivě vyřešené problémy jako binding formulářových dat na doménový model aplikace, validace jednotlivých polí, lokalizace a další problémy, které musí typické GUI řešit. Ještě jsem se ale nesetkal s aplikací, která by závislostí polí ve formuláři řešila způsobem, který by byl přehledný, do budoucna udržovatelný a hlavně, který by byl testovatelný (ve smyslu unit testů). Domnívám se, že závislosti polí jsou často v návrhu aplikací opomíjeny, a chci proto představit jednoduchý návod, jak závislosti řešit. Jistě nejde o žádnou převratnou inovaci, ale podobný postup jsem zatím nikde zdokumentovaný a hlavně použitý nenašel. Smysl tohoto blogu tedy vidím v tom, že některým čtenářům pomůže uvědomit si problém a jeho nepříjemným důsledkům se vyhnout.
Příklad
Budu používat příklad převzatý z bankovní aplikace, která řeší evidenci nemovitostí pro účely pořízení a schvalování žádosti o hypoteční úvěr. Aplikace eviduje tři typy nemovitostí – parcely, budovy a bytové jednotky. Pro svůj příklad se zaměřím na parcely. Atributy parcely a jejich závislosti jsou popsány následující tabulkou:
Sloupce tabulky popisují atributy a vlastnosti parcely, řádky popisují čtyři možné typy parcel. První tři atributy můžeme chápat jako „diskriminant“ parcely – jejich hodnoty podmiňují přístupnost, povinnost a přednastavené hodnoty jejich dalších atributů. Závislosti polí nemusí být vždy specifikovány tabulkou, asi nejčastěji jsou podobné závislosti popsány sadou podmínek („if – then“). Bohužel není výjimkou, že podobná specifikace úplně chybí a předpokládá se nějaká implicitní znalost. V reálné aplikaci jsou definice parcely a závislostí jejich atributů mírně složitější. Snažil jsem se zadání zjednodušit, ale přitom ho ponechat dostatečně ilustrativní.
Jak by takové zadání řešila typická aplikace? Nejčastěji se setkávám s tím, že se podobné závislosti neřeší systematicky, že jsou řešeny množstvím podmínek na různých místech nebo vrstvách aplikace. Podmínky mohou být často rozhozeny mezi view a controllery (ve smyslu MVC návrhu). Například událost změny katastrálního území parcely (KÚ) může vyvolat akci na serveru (ve smyslu volání metody controlleru Springu MVC nebo volání akce Struts 2), která změní KÚ a pokud nová hodnota znamená:
Budu používat příklad převzatý z bankovní aplikace, která řeší evidenci nemovitostí pro účely pořízení a schvalování žádosti o hypoteční úvěr. Aplikace eviduje tři typy nemovitostí – parcely, budovy a bytové jednotky. Pro svůj příklad se zaměřím na parcely. Atributy parcely a jejich závislosti jsou popsány následující tabulkou:
| Popis parcely | České KÚ? | Má číslo? | Typ? | Ověřitelné | Číslo | Číslo LV | Pomocná identifikace | Zdroj PZE |
| Parcela katastru nemovitostí | ano | ano | KN | ano | přístupné, povinné | přístupné, povinné | nepřístupné, null | nepřístupné, null |
| Parcela zjednodušené evidence | ano | ano | ZE | ano | přístupné, povinné | přístupné, povinné | nepřístupné, null | přístupné, povinné |
| Budoucí parcela | ano | ne | nepřístupné, null | ne | nepřístupné, null | nepřístupné, null | přístupné, povinné | nepřístupné, null |
| Zahraniční parcela | ne | nepřístupné, null | nepřístupné, null | ne | nepřístupné, null | nepřístupné, null | přístupné, povinné | nepřístupné, null |
Sloupce tabulky popisují atributy a vlastnosti parcely, řádky popisují čtyři možné typy parcel. První tři atributy můžeme chápat jako „diskriminant“ parcely – jejich hodnoty podmiňují přístupnost, povinnost a přednastavené hodnoty jejich dalších atributů. Závislosti polí nemusí být vždy specifikovány tabulkou, asi nejčastěji jsou podobné závislosti popsány sadou podmínek („if – then“). Bohužel není výjimkou, že podobná specifikace úplně chybí a předpokládá se nějaká implicitní znalost. V reálné aplikaci jsou definice parcely a závislostí jejich atributů mírně složitější. Snažil jsem se zadání zjednodušit, ale přitom ho ponechat dostatečně ilustrativní.
Jak by takové zadání řešila typická aplikace? Nejčastěji se setkávám s tím, že se podobné závislosti neřeší systematicky, že jsou řešeny množstvím podmínek na různých místech nebo vrstvách aplikace. Podmínky mohou být často rozhozeny mezi view a controllery (ve smyslu MVC návrhu). Například událost změny katastrálního území parcely (KÚ) může vyvolat akci na serveru (ve smyslu volání metody controlleru Springu MVC nebo volání akce Struts 2), která změní KÚ a pokud nová hodnota znamená:
- České KÚ - vynuluje ještě hodnoty pomocné identifikace a zdoje PZE;
- Zahraniční KÚ - nastaví typ parcely na null, stejně tak její číslo a číslo LV a vynuluje také hodnotu zdroje PZE.
- Kde všude se závislosti polí vyhodnocují?
- Dochází někde k duplikacím kódu pro vyhodnocování závislostí?
- Jaký dopad to má na doménový model aplikace?
- Kde všude se vyhodnocuje přístupnost polí formuláře?
- Jak si s tím později poradí validace?
- … a podobné otázky.
Návrh řešení
Představme si, že parcelu máme reprezentovanou třídou Plot.
Pokud budeme uvažovat formulář pro editaci dané parcely, můžeme stav jeho polí reprezentovat třídou PlotFieldState. Třída popisuje přístupnost jednotlivých polí.
Explicitní reprezentaci stavu polí formuláře (reprezentovanou třídou PlotFieldState) můžeme použít pro následující činnosti:
Následující ukázka kódu demonstruje možný postup při validaci parcely.
Představme si, že parcelu máme reprezentovanou třídou Plot.
public class Plot {
private boolean locatedInCR;
private boolean numberAssigned;
private String plotType;
private String number;
private String ownershipNumber;
private String auxiliaryId;
private String simplifiedEvidenceSource;
...
}
Obecně pro každé pole formuláře můžeme uvažovat čtyři stavy:- Přístupné, nepovinné
- Přístupné, povinné
- Nepřístupné
- Skryté
public class FieldState {
public static final FieldState ENABLED_OPTIONAL = new FieldState("ENABLED_OPTIONAL", true, true, false);
public static final FieldState ENABLED_MANDATORY = new FieldState("ENABLED_MANDATORY", true, true, true);
public static final FieldState ENABLED = ENABLED_OPTIONAL;
public static final FieldState DISABLED = new FieldState("DISABLED", true, false, false);
public static final FieldState HIDDEN = new FieldState("HIDDEN", false, false, false);
private String code;
private boolean visible;
private boolean enabled;
private boolean mandatory;
public boolean isVisible() { return visible; }
public boolean isEnabled() { return enabled; }
public boolean isMandatory() { return mandatory; }
@Override
public String toString() { return code; }
private FieldState(String code, boolean visible,
boolean enabled, boolean mandatory)
{
this.code = code;
this.visible = visible;
this.enabled = enabled;
this.mandatory = mandatory;
}
}
Jako „zkratka“ je ve třídě ještě reprezentovaná hodnota „přístupné“ (ENABLED), která vlastně přesně znamená „přístupné, nepovinné“.Pokud budeme uvažovat formulář pro editaci dané parcely, můžeme stav jeho polí reprezentovat třídou PlotFieldState. Třída popisuje přístupnost jednotlivých polí.
public class PlotFieldState {
private FieldState numberAssigned = FieldState.ENABLED;
private FieldState plotType = FieldState.ENABLED;
private FieldState verifiable = FieldState.ENABLED;
private FieldState number = FieldState.ENABLED;
private FieldState ownershipNumber = FieldState.ENABLED;
private FieldState auxiliaryId = FieldState.ENABLED;
private FieldState simplifiedEvidenceSource = FieldState.ENABLED;
...
}
Předpokládejme, že existuje mechanismus, který pro každou kombinaci hodnot parcely (tzn. libovolnou instanci třídy Plot), umí vyhodnotit stav formuláře parcela (tzn. přístupnost a povinnost jeho polí). Mějme tento výpočet reprezentovaný rozhranním PlotDependencySolver.public interface PlotDependencySolver {
void solveDependencies(Plot plot, PlotFieldState state);
}
Interface PlotDependencySolver obdrží jako parametry aktuální hodnoty parcely a stav odpovídajících polí formuláře. Provede vyhodnocení a výsledek - tzn. změněné hodnoty a změněný stav - reflektuje do těchto parametrů.Explicitní reprezentaci stavu polí formuláře (reprezentovanou třídou PlotFieldState) můžeme použít pro následující činnosti:
- Zobrazování (konstrukci view) formuláře.
- Stačí pouze procházet pole formuláře, kde pro každé z nich mám k dispozici stav, který říká, jestli dané pole vykreslit, resp. jestli bude přístupné. Důležité je, že viditelnost a přístupnost polí nemusím řešit složitými podmínkami (např. v JSP, u mnoha elementů typicky v atributu disabled), ale jednoduchým dotazem na stav (PlotFieldState).
- Validace hodnot z formuláře.
- Opět stačí procházet hodnoty polí z formuláře, pro každé z nich ihned vím, jestli je povinné / nepovinné, tedy jestli ho mám validovat.
- Pokud by bylo požadavkem validovat i kombinace hodnot polí (např. kombinace hodnot combo boxů – „auto Škoda, model Octavia“), mohu třídu PlotFieldState rozšířit i o reprezentaci přípustných hodnot pro pole formuláře. Snahou je přípravit si takovou stavovou informaci, která mi dovolí iterovat přes pole formuláře a validovat jedno po druhém.
- Testování závislosti polí.
- Mám-li stav formuláře explicitně reprezentovaný třídou a mechanismus pro zjišťování stavu, mohu tento mechanismus testovat (tzn. testovat závislost polí formuláře).
Hlavní myšlenka je tedy v tom mít explicitní reprezentaci polí formuláře a mechanismus, jak ho vyhodnotit. Výrazně nám to zjednoduší jinak potenciálně složité akce jako je zobrazování formuláře, validace hodnot a testování závislosti polí.
public class PlotValidationHelper {
public void validate(Plot plot, PlotFieldState state) {
if (state.getNumber().isMandatory() && !isNumber(plot.getNumber())) {
// report error
}
if (state.getOwnershipNumber().isMandatory() && !isNumber(plot.getOwnershipNumber())) {
// report error
}
if (state.getAuxiliaryId().isMandatory() && plot.getAuxiliaryId() == null) {
// report error
}
if (state.getSimplifiedEvidenceSource().isMandatory() && plot.getSimplifiedEvidenceSource() == null) {
// report error
}
}
...
}
Další ukázka kódu představuje jednoduchý JUnit test case pro testivání závislostí polí.public class Test1 extends TestCase {
public void testLandRegister() {
Plot plot = new Plot();
plot.setLocatedInCR(true);
plot.setNumberAssigned(true);
plot.setPlotType("KN");
PlotFieldState state = new PlotFieldState();
getSolver().solveDependencies(plot, state);
assertEquals(FieldState.ENABLED, state.getVerifiable());
assertEquals(FieldState.ENABLED_MANDATORY, state.getNumber());
assertEquals(FieldState.ENABLED_MANDATORY, state.getOwnershipNumber());
assertEquals(FieldState.DISABLED, state.getAuxiliaryId());
assertNull(plot.getAuxiliaryId());
assertEquals(FieldState.DISABLED, state.getSimplifiedEvidenceSource());
assertNull(plot.getSimplifiedEvidenceSource());
}
…
}
Implementace
Zbývá už jen zamyslet se nad implementací mechanismu pro vyhodnocování nového stavu formuláře, tedy implementaci rozhranní PlotDependencySolver. Pro implementaci můžeme zvážit dvě možnosti - přímou implementaci prostředky Javy a implementaci pomocí rule engine.
Zbývá už jen zamyslet se nad implementací mechanismu pro vyhodnocování nového stavu formuláře, tedy implementaci rozhranní PlotDependencySolver. Pro implementaci můžeme zvážit dvě možnosti - přímou implementaci prostředky Javy a implementaci pomocí rule engine.
Nebude žádným překvapením, že prostředky Javy můžeme metodu solveDependencies implementovat prostě jako sadu if – then statementů.
public class JavaPlotDependencySolver implements PlotDependencySolver {
@Override
public void solveDependencies(Plot plot, PlotFieldState state) {
if (plot.isLocatedInCR() && plot.isNumberAssigned() && "KN".equals(plot.getPlotType())) {
// Land register
state.setNumberAssigned(FieldState.ENABLED);
state.setPlotType(FieldState.ENABLED);
state.setVerifiable(FieldState.ENABLED);
state.setNumber(FieldState.ENABLED_MANDATORY);
state.setOwnershipNumber(FieldState.ENABLED_MANDATORY);
state.setAuxiliaryId(FieldState.DISABLED);
state.setSimplifiedEvidenceSource(FieldState.DISABLED);
plot.setAuxiliaryId(null);
plot.setSimplifiedEvidenceSource(null);
} else if (!plot.isLocatedInCR()) {
// Foreign plot
state.setNumberAssigned(FieldState.DISABLED);
state.setPlotType(FieldState.DISABLED);
state.setVerifiable(FieldState.DISABLED);
state.setNumber(FieldState.DISABLED);
state.setOwnershipNumber(FieldState.DISABLED);
state.setAuxiliaryId(FieldState.ENABLED_MANDATORY);
state.setSimplifiedEvidenceSource(FieldState.DISABLED);
plot.setNumberAssigned(false);
plot.setPlotType(null);
plot.setNumber(null);
plot.setOwnershipNumber(null);
plot.setSimplifiedEvidenceSource(null);
} else { ... }
}
}Takový přímočará implementace má ale i některá omezení:- Musíme sestavit deterministický postup (algoritmus), kdy všechny podmínky a závislosti vyhodnotíme a reflektujeme do nového stavu.
- Pokud si vyhodnocování závislostí představíme v nějakém "vyhodnocovacím stromě" (uvádím v uvozovkách a doufám, že intuitivní chápání pojmu tady postačí), může a bude se pravděpodobně stávat, že některé podstromy výpočtu se budou opakovat, protože musí reflektovat, že stav formuláře se během vyhodnocování mění (každá jednotlivá podmínka může do stavu něčím přispět). Mířím k tomu, že sestavit takový algoritmus nemusí být vždy jednoduché.
Z uvedených důvodů je elegantní a preferovanou možností zapojení rule engine (např. Drools), které nám dovolí většinu zmíněných výhrad jednoduše překonat. Protože vidím tuto možnost jako velmi vhodnou a zajímavou, budu jí věnovat další samostatný blog.
Shrnutí
V tomto blogu snažím klást důraz hlavně na myšlenku explicitní reprezentace stavu formuláře, ne na její implementaci. Důležité je dosáhnout toho, že závislosti polí budou vyhodnocovány na jednom místě a stav formuláře bude důsledně používaný. Pojďme si nakonec shrnout výhody a nevýhody řešení s explicitní reprezentací stavu polí formuláře. Postup přináší následující výhody:
- Závislosti polí ve formuláři jsou definovány na jednom místě.
- Abych závislosti zmapoval, pochopil nebo upravil, nemusím je hledat na různých místech / vrstvách aplikace.
- Odpadávají duplicity vyhodnocování závislostí. Stejný kód pro vyhodnocení stavu formuláře mi poslouží pro vykreslení polí ve view jako i pro validaci jejich hodnot.
- Přehledný, snadno čitelný kód pro vykreslování view a pro validace hodnot z formuláře.
- Procházím pole jedno po druhém, vyhodnocuji jenom, jakým způsobem (ne)zobrazit a jak validovat.
- Testovatelnost závislostí.
- Testovatelnost závislostí vidím jako největší výhodu tohoto řešení.
- Režie navíc.
- Samostatná reprezentace stavu polí formuláře jako i práce s explicitně definovanými stavy může být pro jednodušší aplikace zbytečná. Vedle doménových tříd reprezentujích strukturu informací tu mám ještě další, speciální třídy pro zachycení stavu polí ve formuláři.
- Kombinace přístupu explicitního stavu polí s jiným řešením může smazat výhody explicitních stavů a vytvořit nepřehledný, těžko udržovatelný kód.
- To je ale obecná poznámka aplikovatelná snad na jakékoliv řešení. Pokud někdo nedodrží domluvené návrhové vzory, přehlednost a udržovatelnost kódu tím samozřejmě trpí vždy.
Přihlásit se k odběru:
Příspěvky (Atom)