Voor een bepaald project ben ik bezig met het ontwerpen van de klassestructuur. Zonder al te diep op details in te gaan, hebben we in dit project een aantal concepten en bij elk concept een afgeleid concept. Stel we hebben het concept van Foo met onderstaande eigenschappen
dan hebben we ook een bijbehorend concept AdjustedFoo:
Met andere woorden, een AdjustedFoo heeft naast de velden die Foo ook heeft (nl. X, Y en Z) de velden w_X, w_Y en w_Z plus een verwijzing naar een Foo.
Deze relatie tussen Foo en AdjustedFoo is in het project niet een eenmalig iets, maar een terugkerend patroon. Als we bijv. een Bar hebben met velden ω, φ en κ, dan hebben we ook een AdjustedBar met velden ω, φ, κ, w_ω, w_φ, w_κ en original.
Nu is mijn vraag, bestaat er in C++ een abstractie voor dit patroon? Of zit er niets anders op dan alles uit te tikken? Of loont het om bijv. een script te schrijven die tijdens het buildproces de code voor de Adjusted-klassen genereert (met mogelijk als nadeel dat de IDE dan niet altijd op de hoogte is van deze gegenereerde klassen en geen ondersteuning kan bieden)? Of zie ik andere opties over het hoofd?
(optionele bijlage: proof of concept in Python)
Als ik deze vraag trouwens zelf zou moeten beantwoorden in de context van Python, dan zou ik wat boilerplate kunnen schrijven waarmee ik vervolgens Foo en AdjustedFoo als volgt zou kunnen definiëren:
Zodat ik deze classes als volgt kan gebruiken:
C++:
1
2
3
4
5
6
7
8
| class Foo { public: Foo(double X, double Y, double Z) : X(X), Y(Y), Z(Z) {} double X; double Y; double Z; }; |
dan hebben we ook een bijbehorend concept AdjustedFoo:
C++:
1
2
3
4
5
6
7
8
9
10
11
| class AdjustedFoo : public Foo { public: AdjustedFoo(double X, double Y, double Z, double w_X, double w_Y, double w_Z, Foo* original = nullptr) : Foo(X, Y, Z), w_Z(w_Z), original(original) {} double w_X; double w_Y; double w_Z; Foo* original; }; |
Met andere woorden, een AdjustedFoo heeft naast de velden die Foo ook heeft (nl. X, Y en Z) de velden w_X, w_Y en w_Z plus een verwijzing naar een Foo.
Deze relatie tussen Foo en AdjustedFoo is in het project niet een eenmalig iets, maar een terugkerend patroon. Als we bijv. een Bar hebben met velden ω, φ en κ, dan hebben we ook een AdjustedBar met velden ω, φ, κ, w_ω, w_φ, w_κ en original.
Nu is mijn vraag, bestaat er in C++ een abstractie voor dit patroon? Of zit er niets anders op dan alles uit te tikken? Of loont het om bijv. een script te schrijven die tijdens het buildproces de code voor de Adjusted-klassen genereert (met mogelijk als nadeel dat de IDE dan niet altijd op de hoogte is van deze gegenereerde klassen en geen ondersteuning kan bieden)? Of zie ik andere opties over het hoofd?
(optionele bijlage: proof of concept in Python)
Als ik deze vraag trouwens zelf zou moeten beantwoorden in de context van Python, dan zou ik wat boilerplate kunnen schrijven waarmee ik vervolgens Foo en AdjustedFoo als volgt zou kunnen definiëren:
Python:
1
2
3
4
5
6
7
8
9
| @data class Foo: X: float Y: float Z: float @Adjusted class AdjustedFoo(Foo): pass |
Zodat ik deze classes als volgt kan gebruiken:
>>> foo = Foo(1, 2, 3) >>> adj_foo = AdjustedFoo(1.1, 1.9, 3.01, 0.1, 0.2, 0.3, foo) >>> print(foo) Foo(X = 1, Y = 2, Z = 3) >>> print(adj_foo) AdjustedFoo(X = 1.1, Y = 1.9, Z = 3.01, w_X = 0.1, w_Y = 0.2, w_Z = 0.3, original = Foo(X = 1, Y = 2, Z = 3))
Hierbij gebruik ik dus de dynamische eigenschappen van Python om de definitie van een class te kunnen inspecteren en daarmee op dynamische wijze een bijhorende class te genereren.(niet zo robuuste boilerplate om bovenstaande Python-snippet aan de praat te krijgen)
Python:
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 def data(cls): annotations = {} for ancestor in cls.mro()[::-1]: if hasattr(ancestor, '__is_data__') or ancestor == cls: annotations.update(ancestor.__annotations__) fields = list(annotations.keys()) name = cls.__name__ bases = cls.__bases__ def init(self, *args, **kwargs): if len(args) > len(fields): raise TypeError('__init__() takes {} positional arguments but {} were given'.format(len(fields), len(args))) else: for i, arg in enumerate(args): field_name = fields[i] if field_name in kwargs: raise TypeError("__init__() got multiple values for argument '{}'".format(field_name)) else: kwargs[field_name] = arg if len(kwargs) < len(fields): missing_args = ["'{}'".format(field_name) for field_name in fields if field_name not in kwargs] raise TypeError("__init__() missing {} required argument(s): {}".format(len(missing_args), ', '.join(missing_args))) for k, v in kwargs.items(): setattr(self, k, v) def representation(self): return '{}({})'.format(name, ', '.join( '{} = {!r}'.format(field_name, getattr(self, field_name)) for field_name in fields )) return type(name, bases, { '__annotations__': cls.__annotations__, '__is_data__': True, '__init__': init, '__repr__': representation }) def Adjusted(cls): annotations = {} for ancestor in cls.mro()[::-1]: if hasattr(ancestor, '__is_data__'): annotations.update(ancestor.__annotations__) for key in annotations.keys(): if key == 'original': continue w_key = 'w_{}'.format(key) cls.__annotations__[w_key] = annotations[key] bases = cls.__bases__ if len(bases) != 1 or not hasattr(bases[0], '__is_data__'): raise TypeError('This PoC of @Adjusted only works for classes that derive from a single @data class') cls.__annotations__['original'] = bases[0] return data(cls)
Ipsa Scientia Potestas Est
NNID: ShinNoNoir