V tomto článku chci na reálném (byť zjednodušeném) příkladu ukázat aplikaci návrhového vzoru Dekorátor v Dynamics AX a souvisejícího refaktoringu.
Zadání bylo zhruba takové: „Při účtování Expense Reportu (modul Expense management) vyhledat maximální povolenou částku transakce, snížit částku na tento limit a vytvořit další transakci s přeslimitní částkou na jiný účet”. Samozřejmě v tom hrají roli nějaké parametry, ale ty tady nejsou podstatné.
Víceméně celá účtovací logika je implementována v metodě post() třídy TrvPost (statická metoda o 341 řádcích – a to není zdaleka jediná věc, co bych jí a celému Trv* modulu vytknul). Krom spousty jiného duplicitního kódu se zde šestkrát opakuje kód na zaúčtování transakce do hlavní knihy. Pokud bych na každé z těchto míst umístil požadované větvení, problém s duplicitou by se nejméně dvojnásobil. A to se ještě bude v budoucnu implementovat další rozpad transakcí (podle jiné logiky). Refaktoring je očividně nezbytný.
Poznámka: V Dynamics AX je třeba počítat s tím, že každá změna standardního kódu sebou nese určité náklady – zejména komplikuje upgrade na vyšší verze. Je tedy správné dělat refaktoring takového kódu?
Samozřejmě ANO, pokud je třeba ke správné implementaci. Tudíž pokud se mi nějaká část nelíbí, ale mohu ji korektně použít, neměl bych ji jen tak „opravovat“. Ale snaha vyhnout se zbytečným změnám nemůže být omluvou pro vytváření nekvalitního a neudržovatelného kódu. Ve výše zmíněném příkladu bych mohl za chvilku mít metodu o 600 řádcích a desítkách duplicitních účtování.
OK, takže refaktoring. První a nezbytný krok je rozdělení kódu do tříd a metod. Tuto dekompozici udělá každý trochu jinak, ale nabízí se udělat jednu třídu pro účtování celého expense reportu, která bude zodpovědná za způsob účtování jednotlivých řádků, a další jednoduchou třídu pro vlastní účtování řádků. Dále je možné vyjmout vytváření LedgerJournalTable do samostatné metody, převést SQL dotazy na parametrizované query a tak dále. Výsledkem by mělo být něco připomínající objektově orientovaný kód. 🙂
Já jsem skončil s třídou poměrně robustní třídou TrvExpTablePost (18 metod) a triviální třídou TrvExpTransPost (5 metod, jen jedna výkonná veřejná metoda).
Spousta lidí úplně nechápe, jak je refaktoring důležitý a proč je třeba o něm uvažovat samostatně, ne míchat s implementací nové funkcionality. My jsme teď získali jiný kód, který dělá přesně to samé jako ten původní (což je možné ověřit původními testy – pokud existují – včetně automatizovaných testů). A já teď ukážu, jak málo skutečně nové kódu stačí přidat.
Teď už přijde ke slovu ten zmiňovaný návrhový vzor Dekorátor. Pokud jste se s ním ještě nesetkali, lehký úvod naleznete třeba zde, jinak je na výběr spousta literatury. Dekorátor v zásadě umožňuje “nabalovat” na objekt další chování, aniž by se měnil způsob používání daného objektu.
Dekorátor naimplementujeme jako samostatnou třídu, která obsahuje dekorovaný objekt (v mém případě TrvExpTransPost) jako svou členskou proměnnou a má stejné veřejné rozhraní (což nejsnáze zajistíme typem interface
, který bude implementovat jak dekorovaná třída, tak dekorátor).
V případě mé implementace vypadá rozhraní takto:
interface TrvExpPostTransProvider { void postTrans(TrvExpTrans _trvExpTrans) //TrvExpTablePost je třída pro účtování celého expense reportu a má určité informace potřebné pro účtování řádků void setExpTablePost(TrvExpTablePost _expTablePost) void setLedgerJournalTable(LedgerJournalTable _ledgerJournalTable) }
To bylo snadné. Teď ještě vytvořím abstraktní třídu, implementující toto rozhraní, která poskytne defaultní implementaci pro obě set*() metody.
abstract class TrvExpPostTransDecorator implements TrvExpPostTransProvider { //Dekorovaný objekt TrvExpPostTransProvider decoratedPosting; void new(TrvExpPostTransProvider _posting) { decoratedPosting = _posting; } abstract public void postTrans(TrvExpTrans _trvExpTrans){} public void setExpTablePost(TrvExpTablePost _expTablePost) { decoratedPosting.setExpTablePost(_expTablePost) } public void setLedgerJournalTable(LedgerJournalTable _ledgerJournalTable) { decoratedPosting.setLedgerJournalTable(_ledgerJournalTable) } }
V konstruktoru dostaneme object typu rozhraní, tudíž můžeme dekorovat nejen třídu TrvExpPostTrans, ale i jiný dekorátor. Metody set*() jen předají hodnoty dále (ale dekorátor by mohl tyto hodnoty zpracovávat, kdyby je potřeboval) a metodu postTrans() si musí každý dekorátor naimplementovat po svém.
Stále ještě jsme nepřidali žádné nové chování, ale teď už se do toho pustíme. Vytvořím další třídu:
class TrvExpPostMySplitDecorator extends TrvExpPostTransDecorator { public void postTrans(TrvExpTrans _trvExpTrans) { //následuje pseudokód if (je nutné transakci rozdělit) { nastav částku transakce na hodnotu limitu a zaúčtuj vytvoř a zaúčtuj novou transakci, s přeslimitní částkou } else { zaúčtuj transakci } } }
Účtování není pochopitelně implementováno dekorátorem, nýbrž řešeno dekorovaný objektem (voláním decoratedPosting.postTrans()
). Pominu-li logiku pro výpočet limitu a způsobu vytváření přeslimitní transakce (což je naše interní věc), toto je veškerá nová logika, kterou bylo nutné přidat. Jestli lze snáze naprogramovat a otestovat tuto třídu nebo složitě větvenou metodu a mnoha stech řádcích, může zhodnotit každý sám.
Volání nové logiky zajistíme jednoduchou změnou. Řekněme, že máme ve třídě TrvExpTablePost následující kód:
TrvExpTransPost transPost = new TrvExpTransPost(); transPost.setExpTablePost(this); transPost.setLedgerJournalTable(ledgerJournalTable); transPost.postTrans(trvExpTransPost);
Změníme jediný řádek – vytváření instance:
TrvExpTransPostProvider transPost = new TrvExpPostMySplitDecorator(new TrvExpTransPost()); transPost.setExpTablePost(this); transPost.setLedgerJournalTable(ledgerJournalTable); transPost.postTrans(trvExpTransPost);
Místo zmatené metody se spoustou duplicitního kódu mám kód logicky rozdělený do tříd a metod a ten, kdo po mně bude přidávat ten zmiňovaný další rozpad řádků, musí prostě jen naimplementovat další dekorátor a přiřadit ho před nebo za ten můj (krásné je, že na pořadí teď nezáleží). Nemusí vědět nic o tom, jak se vybírají transakce k účtování, jak vznikají doklady atd., prostě naprogramuje jednu třídu zapouzdřující požadovanou logiku. A kdyby byl původní kód nějak rozumně napsaný, bylo by to celé ještě mnohem snažší.
PS: Jednu věc jsem výše (v zájmu jednoduchosti) zatajil. Při účtování vracené hotovosti vyplacené předem se provádí navíc (oproti účtování ostatních transakcí) jeden krok navíc – změna hodnoty v tabulce TrvCashAdvance. To jsem naprogramoval také jako dekorátor – na osm řádků kódu.