images/logo.png
SHWB na blogspot  | Uživatel: Nepřihlášen

O programování 06 - Návrhové vzory - síla i slabina Javy

Pro účely otestování různých implementací cyklu foreach v klasické i funkcionální podobě jsem implementoval čtyři varianty téhož algoritmu, každý jako samostatný program, viz předchozí díly. Tyto čtyři programy dělaly samozřejmě úplně to stejné a množství společného kódu bylo značné, ve skutečnosti se lišily pouze třemi řádky kódu, totiž vlastním cyklem. Inicializace, příprava měření času, výpočet času a jeho výpisy, to vše bylo vždy úplně stejné. Je jasné, že jsem nenapsal čtyři rozdílné programy metodou Ctrl-C/Ctrl-V, ale použil jsem dědičnost a návrhový vzor strategy, resp. to, co si myslím, že je strategy (kdysi jsem měl totiž učenou rozpravu s kolegou, který tvrdil, že moje chápání strategy není strategy podle GOF).

Každopádně nejde o název ale o princip a ten spočívá v tom, že mám abstraktní třídu, která obsahuje všechny společné části - v tomto případě přípravné a měřící. Neobsahuje však žádnou konkrétní implementaci a místo ní se volá abstraktní metoda. Potomci této třídy implementují pouze tuto metodu, takže se jednotlivé implementace liší tím, co mají opravdu odlišné a společný kód je jednotný a společný.

Tento návrhový vzor se používá pro eliminaci ifů, které v OOP nemají co dělat, alespoň podle OOP puristů typu dr. Kravála z objects.cz. Toto tvrzení je sice poněkud silné, protože nelze nahradit všechny ify, ale když se nad tím člověk zamyslí, tak si uvědomí, že existují (minimálně) dva typy ifů. První je spojený s algoritmem jako takovým, např. s jeho hraničními podmínkami. Druhý typ ifů je "strukturální" ve smysl výběru vhodného algoritmu nebo jeho varianty a právě o eliminaci těchto "strukturálních" podmínek je návrhový vzor strategy, aspoň jak ho chápu já.

Síla strategy vynikne v případě, kdy konkrétní implementační třída neimplementuje jen jednu metodu, ale více logicky svázaných metod. Bez použití strategy je kód plný ifů a switchů a často je problém zajistit, aby všechny varianty byly na všech místech správně vyhodnoceny a hlavně, aby se někde na nějakou variantu nezapomnělo. Další problém nastane při přidání nové varianty, kdy se musí všechny podmínky projít a doplnit o novou variantu. V případě strategy je vše potřebné v jedné třídě a přidání nové varianty spočívá v implementaci nové třídy a všech jejich abstraktních metod. Nevýhodou je horší čitelnost kódu, kdy se často z algoritmu "odskakuje" do konkrétních implementací, ale při správném pojmenování to není velký problém.

Nicméně vraťme se od teorie návrhových vzorů ke konkrétní implementaci. Abstraktní třída LoopRunner obsahuje metodu run(), která inicializuje prostředí a v cyklu volá metodu testedMethod(), ta počítá časy pro konkrétní implementace. Vlastní testovaný algoritmus je v abstraktní metodě _testedMethod(), resp. v jejích implementacích u jednotlivých potomků.



public abstract class AbstractLoopRunner {
final int EXEC_COUNT = 10; final int WARMUP_COUNT = 10; final int SIZE = 100000; private final String name;
public AbstractLoopRunner(String name) { this.name = name; }
public void run() { final List<Integer> inputList = new ArrayList<>(SIZE);
for (int i = 0; i < SIZE; i++) { inputList.add(i); }
//Warmup Long warmUpActual; for (int i = 0; i < WARMUP_COUNT; i++) { warmUpActual = testedMethod(inputList); System.out.println(String.format("%d: %d ", i, warmUpActual)); }
//Exec Long sum = 0l; Long min = Long.MAX_VALUE; Long max = Long.MIN_VALUE; Long actual; for (int i = 0; i < EXEC_COUNT; i++) { actual = testedMethod(inputList); min = Math.min(min, actual); max = Math.max(max, actual); sum += actual; } Double avg = (double) sum / EXEC_COUNT; System.out.println(String.format("%s: avg: %f min: %d max: %d", name, avg, min, max)); }
public abstract void _testedMethod(final List<Integer> inputList, List<Integer> outputList);
public Long testedMethod(List<Integer> inputList) { final List<Integer> outputList = new ArrayList<>(inputList.size());
final Date start = new Date(); _testedMethod(inputList, outputList); final Date end = new Date();
return end.getTime() - start.getTime(); }
}

Jednotlivé testovací programy se liší pouze v rozdílné implementaci _testedMethod(), pro jednoduchost uvádím jen jeden z nich.



public class RunForeach extends AbstractLoopRunner {
public static void main(String[] args) { RunForeach runner = new RunForeach("run standard forEach"); runner.run(); }
public RunForeach(String name) { super(name); }
@Override public void _testedMethod(final List<Integer> inputList, List<Integer> outputList) { for (Integer integer : inputList) { outputList.add(++integer); } }
}

Z hlediska OOP a GOF je to celkem přímočaré a jednoduché řešení, ovšem na to, jak jednoduchý problém řešíme je to docela dost kódu a elegantní kód bych viděl přeci jen trošku jinak. Zřejmě by šel použít i návrhový vzor dekorátor pro dodání funkcionality před a za algoritmus, ale s tím jsem se nikdy prakticky nesetkal a nechce se mi ho studovat. Další variantou je použít návrhový vzor factory, který se se strategy často kombinuje, factory totiž zajistí vrácení správné implementační třídy. Pro implementaci factory nejdříve definuji interface.



public interface ILoopMethod {
void testedMethod(final List<Integer> inputList, List<Integer> outputList);
}

Jedna z variant implementované třídy je:



public class ForEach implements ILoopMethod {
@Override public void testedMethod(final List<Integer> inputList, List<Integer> outputList) { for (Integer integer : inputList) { outputList.add(++integer); } } }

Příslušná zjednodušená factory pak vypadá následovně. V ukázce nejsou implementovány všechny 4 třídy, místo String ve switch bych mohl použít Enum, místo vracení null bych měl vyhodit výjimku a místo new() bych mohl použít dependency injection. Také porušuji princip jednoho výstupního bodu z metody (=jeden return) a chybí mi default, ale na demonstraci principu návrhového vzoru factory strategy to nic nemění.



public class LoopFactory {
public static ILoopMethod get(String name) {
switch (name) { case "foreach": return new ForEach(); case "streamForeach": return new StreamForeach(); } return null; }
}

Vlastní implementace a použití se od předchozí varianty příliš neliší. Do konstruktoru předávám instanci implementující ILoopMethod a ta implementuje testovaný algoritmus. Vlastní spuštění výpočtu má na starosti metoda run(), ta se prakticky neliší od předchozí varianty pouze místo _testedMethod() volá metodu deklarovanou interfacem, tedy method.testedMethod(inputList, outputList);



public class LoopRunner {
public LoopRunner(ILoopMethod method) { this.method = method; }
public void run() {
...
method.testedMethod(inputList, outputList);
...
}
public static void main(String[] args) { LoopRunner runner = new LoopRunner(LoopFactory.get("foreach")); runner.run(); }
}

Výhoda tohoto řešení spočívá mimo jiné i v tom, že pokud si pomůžu již zmíněným DI, např. Springem, tak jsem docela pěkně odstíněn (ode všeho možného) a výsledkem je takové pěkně sofistikované Javovské řešení. Jenže se mi může stát, že na něj někdo (třeba ne-Javista nebo někdo se zbytky selského rozumu) kouká jak tele na nové vrata, protože používám několik návrhových vzorů na úplnou trivialitu. Protože co já vlastně tímto kódem dělám? Když se nad tím trochu zamyslím, tak vlastně předávám metodě funkci jako argument. A na to, jak to lze udělat elegantně, i když ne čistě objektově, se podívám v dalším díle.

12.01.2017
Diskuse k článku:
Počet příspěvků: 2 - zobrazeno v: 22.08.2019 - 02:09:32
David (25.02.2017 - 19:54:07):
P.S. samozrejme "Context" mel byt "Client" .. To jsem jen narazil na nejak divne pojmenovany diagram.. (Context je samozrejme trosku neco jineho...)
David (25.02.2017 - 19:48:42):
To k cemu jsi dosel (tedy implementace LooperRunner + ILoopMethod), tak je ta prava Strategy by GoF :) To co je tam dulezite je ze Context (LooperRunner) je oddeleny od Strategy (ILoopMethod), nestane se tedy ze by se zmenou Strategy zmenila i implementace Context. Napr. Apache Collections jsou plne Strategy vzoru (Predicate, Transformer .. to vsechno je Strategy).
Přidat názor:
Vyhrazuji si právo libovolný komentář smazat bez udání důvodu. Kritika mi nevadí, ale chci omezit anonymní výkřiky, které nemají s tématem nic společného.
V textu je možné používat HTML tagy a tuto zjednodušenou MarkDown syntaxi
Jméno
Text
Postřehy:
31.07.2019: Arduino 01 - Motivace k elektrotechnice
To jsem se jednou, nechci říct nudil, ale zkrátka jsem narazil na knihy "Porty, bajty, osmibity" a "Hradla, volty, jednočipy" od Martina Malého z produkce sdružení NIC.CZ, které jsou volně dostupné na knihy.nic.cz.
extravaganza.controverso@seznam.cz: Zdravím, krásný a informacemi nabitý blog. Musím pochválit. Plánuji rozjet undergroundový zin, co by se týkal black matalu, ambientu, satanismu, left hand
Poslední diskuse Postřehy
O programování 06 - Návrhové vzory - síla i slabina Javy
P.S. samozrejme "Context" mel byt "Client" .. To jsem jen narazil na nejak divne pojmenovany diagram.. (Context je samozrejme trosku neco jineho...)
...
David | 25.02.2017
O programování 06 - Návrhové vzory - síla i slabina Javy
To k cemu jsi dosel (tedy implementace LooperRunner + ILoopMethod), tak je ta prava Strategy by GoF :) To co je tam dulezite je ze Context (LooperRunner) je oddeleny od Strategy (ILoopMethod),
...
David | 25.02.2017
O programování 03 - Přehlednost funkcionálního zápisu v Java 8
Máš pravdu, to map je tam zbytečné. Odněkud jsem to opsal a nezkontroloval. Tím ovšem trochu padá pointa celého článku.
...
Saha | 14.12.2016
O programování 03 - Přehlednost funkcionálního zápisu v Java 8
Jen takova otazka k tomu druhemu prikladu:
Proc tam tu cast s "map" ktera de facto s prvky toho streamu nic nedela? Nestacilo by
list.stream().reduce(0, Integer::sum);
?
Ja teda moc
...
David P. | 13.12.2016
Paleo na půl - 01 - První tři dny bez mléka
Kvalitní hořké čokolády jsou bez mléka.. :) (a to i ty méně "kvalitní"). Mléko bývá součástí jen těch "sladkých".
...
David | 04.05.2015
Statistiky
Aktualizováno: 21.08.2019
Počet článků/fotek: 1374/13795
(C) Saha - 1990 - 2019 - Verze 1.3.32 - 11.05.2019 - Generated by SHREC 2.214
Veškeré zde uvedené materiály vyjadřují pouze moje soukromé názory (s výjimkou knihy návštěv a diskusí, kam může přispívat kdokoliv), a pokud s nimi někdo nesouhlasí, tak je to jeho problém, nikoliv můj.