Ik ben bezig met een project waarin een aantal eigenschappen van grafen bewezen moeten worden. De details zijn niet zo van belang. Waar het echter om gaat is dat er een aantal verschillende algoritmen zijn die op verschillende niveaus met elkaar samenwerken, en ik heb wat moeite dat op een nette manier in code om te zetten.
Heel abstract omschreven is de invoer een Probleem en de uitvoer een Oplossing. Om een Probleem op te lossen bestaat er een zogenaamde Universele Solver die het probleem opsplitst in subproblemen, elk stukje afzonderlijk oplost met een specifiek algoritme (een Solver) dat speciaal voor dit subprobleem wordt geïnstantieerd, en de resultaten samenvoegt tot een complete oplossing. Deze Solvers zelf kunnen op hun beurt weer geparametriseerd worden met verschillende parameters, waaronder de te gebruiken heuristieken. Deze heuristieken hebben een gemeenschappelijke interface, maar de implementatie verschilt per heuristiek, en de heuristieken hebben zelf ook weer (verschillende) parameters.
Het probleem waar ik nu mee zit, is de vraag hoe ik de boel kan structureren zodanig dat ik een universele solver kan instantiëren, die op zijn beurt solvers kan instantiëren voor subproblemen, die op hun beurt weer heuristieken kunnen instantiëren, waarbij ik van te voren kan configureren welke strategie en heuristieken er gebruikt gaan worden.
Momenteel heb ik alleen de Solvers (dus niet de Universele Solver) geparametriseerd en dan nog wel op een weinig geraffineerde manier: ik krijg een stringomschrijving van een heuristiek op de command line mee en die geef ik simpelweg mee aan de solver. De base class van de Heuristiek classes heeft een factory method om op basis van zo'n stringbeschrijving een specifiek klasse en (eventueel) specifieke constructor aan te roepen.
De huidige situatie in semi-pseudo-code samengevat:
Nu wil ik dus de universele solver implementeren, op zo'n manier dat ik 'm kan instantiëren met een solver-beschrijving ongeveer net zoals het nu met heuristieken werkt. Het probleem is dan dat je vrij ingewikkelde strings krijgt die je overal moet parsen. Echt netjes vind ik dat niet. Ik zou liever het parsen één keer doen in main() (aangezien ik command line arguments toch een keer moet parsen) en verder de configuratie van heuristics en solvers op een nettere manier geäbstraheerd hebben.
Een manier die ik hiervoor al bedacht had, is om naast de abstract classes voor Heuristic en Solver, ook een abstract factory class voor elk van beiden te maken. Ik heb dan een abstracte HeuristicFactory en een abstracte SolverFactory, en instanties daarvan als HeuristicAFactory, die ik kan instantiëren met de gewenste parameters. Het voordeel daarvan is dat de universele solver een SolverFactory instance kan accepteren, die al geparametriseerd is met alle argumenten, en die één methode (create() ofzo) heeft om een solver te instantiëren voor een subprobleem.
Een mogelijk nadeel van zo'n aanpak is dat ik allerlei factory objecten dynamisch moet instantiëren (anders dan singletons); volgens mij is dit niet zo gebruikelijk. Verder zit je met een wildgroei aan klassen: behalve de base Heuristic en HeuristicFactory heb je voor elke heuristiek ook een HeuristicX klasse en een HeuristicXFactory klasse. Er zit gelukkig wel structuur in, maar heel eenvoudig is het niet.
Vandaar mijn vraag hier: zijn er wellicht andere patterns die beter op deze situatie van toepassing zijn? Of hebben jullie andere suggesties hoe dit aangepakt zou kunnen worden?
Heel abstract omschreven is de invoer een Probleem en de uitvoer een Oplossing. Om een Probleem op te lossen bestaat er een zogenaamde Universele Solver die het probleem opsplitst in subproblemen, elk stukje afzonderlijk oplost met een specifiek algoritme (een Solver) dat speciaal voor dit subprobleem wordt geïnstantieerd, en de resultaten samenvoegt tot een complete oplossing. Deze Solvers zelf kunnen op hun beurt weer geparametriseerd worden met verschillende parameters, waaronder de te gebruiken heuristieken. Deze heuristieken hebben een gemeenschappelijke interface, maar de implementatie verschilt per heuristiek, en de heuristieken hebben zelf ook weer (verschillende) parameters.
Het probleem waar ik nu mee zit, is de vraag hoe ik de boel kan structureren zodanig dat ik een universele solver kan instantiëren, die op zijn beurt solvers kan instantiëren voor subproblemen, die op hun beurt weer heuristieken kunnen instantiëren, waarbij ik van te voren kan configureren welke strategie en heuristieken er gebruikt gaan worden.
Momenteel heb ik alleen de Solvers (dus niet de Universele Solver) geparametriseerd en dan nog wel op een weinig geraffineerde manier: ik krijg een stringomschrijving van een heuristiek op de command line mee en die geef ik simpelweg mee aan de solver. De base class van de Heuristiek classes heeft een factory method om op basis van zo'n stringbeschrijving een specifiek klasse en (eventueel) specifieke constructor aan te roepen.
De huidige situatie in semi-pseudo-code samengevat:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| class Heuristic { virtual int guessNext() = 0; // dit is de methode die een solver gebruikt Heuristic *create(const Problem &p, string desc) { if (descr matcht "a:(float)") return new HeuristicA(p, parse_float_arg(descr, 1)); if (descr matcht "b:(int):(int)") return new HeuristicB(p, parse_int_arg(descr, 1), parse_int_arg(descr, 2)); return null; } }; class HeuristicA : public Heuristic { HeuristicA(const Problem &p, float param_x) { .. }; int guessNext() { .. }; }; class HeuristicB : public Heuristic { HeuristicB(const Problem &p, int param_y, int param_z) { .. }; int guessNext() { .. }; }; class Solver { Solution solve(const Problem &problem) = 0; }; class SolverX { Solver(string descr) { .. } Solution solve(const Problem &p) { Heuristic *h = Heuristic::create(p, descr); // solve met behulp van h .. } }; |
Nu wil ik dus de universele solver implementeren, op zo'n manier dat ik 'm kan instantiëren met een solver-beschrijving ongeveer net zoals het nu met heuristieken werkt. Het probleem is dan dat je vrij ingewikkelde strings krijgt die je overal moet parsen. Echt netjes vind ik dat niet. Ik zou liever het parsen één keer doen in main() (aangezien ik command line arguments toch een keer moet parsen) en verder de configuratie van heuristics en solvers op een nettere manier geäbstraheerd hebben.
Een manier die ik hiervoor al bedacht had, is om naast de abstract classes voor Heuristic en Solver, ook een abstract factory class voor elk van beiden te maken. Ik heb dan een abstracte HeuristicFactory en een abstracte SolverFactory, en instanties daarvan als HeuristicAFactory, die ik kan instantiëren met de gewenste parameters. Het voordeel daarvan is dat de universele solver een SolverFactory instance kan accepteren, die al geparametriseerd is met alle argumenten, en die één methode (create() ofzo) heeft om een solver te instantiëren voor een subprobleem.
Een mogelijk nadeel van zo'n aanpak is dat ik allerlei factory objecten dynamisch moet instantiëren (anders dan singletons); volgens mij is dit niet zo gebruikelijk. Verder zit je met een wildgroei aan klassen: behalve de base Heuristic en HeuristicFactory heb je voor elke heuristiek ook een HeuristicX klasse en een HeuristicXFactory klasse. Er zit gelukkig wel structuur in, maar heel eenvoudig is het niet.
Vandaar mijn vraag hier: zijn er wellicht andere patterns die beter op deze situatie van toepassing zijn? Of hebben jullie andere suggesties hoe dit aangepakt zou kunnen worden?