Generic Value Adapter

Dmitri Nesteruk
A free video tutorial from Dmitri Nesteruk
Software/Hardware Engineering • Quant Finance • Algotrading
4.4 instructor rating • 21 courses • 128,056 students

Lecture description

Unlike C++, C# does not allow us to use literal values (numbers, strings) as generic arguments. Generic Value Adapter is a pattern that helps us deal with this. This lecture also uses the Factory Method design pattern and recursive generics.

Learn more from the full course

Design Patterns in C# and .NET

Discover the modern implementation of design patterns with C# and .NET

17:57:13 of on-demand video • Updated May 2020

  • Recognize and apply design patterns
  • Refactor existing designs to use design patterns
  • Reason about applicability and usability of design patterns
English [Auto] In this lecture we're going to take a look at an adapter variation we're going to take a look at yet another implementation of the adapter pattern. But this one is completely different this one is unlike anything you've ever seen because it's called a generic value adapter and it's actually something very silly. As you'll see in a moment it's something that is completely unnecessary in languages like C++ but in C sharp. Unfortunately it is necessary. And we're going to take a look at how to construct it. And we're also going to kind of explore this approach a bit further and look at some of the other patterns as well so this is going to be a rather feature packed demo but essentially the idea is as follows let's suppose that you want to implement vectors you want to represent the idea of a vector and now a vector can be in two dimensional space or three dimensional for the mental space whatever. It can also be made up of different types so the components of the vector like X Y and Z for example they can be integers they can be floating point values or double values or Decimal or something else and you want to have some sort of generic approach to implementing all of this using generics using generic types at the end what you want is you want to have types like vector to F and vector three eyes so you want to have a class for a two component vector of floating point values the three component vector of integer values and so on and so forth. So you want all of this. And the question is how can you actually get it using generic programming. Well a very naive approach would be to just make a class called vector where you have two generic parameters you specify the type of the elements that you just store and then you specify the dimensions how many dimensions does this vector actually have. So you would kind of specify like this and then you would have some maybe array of data where those components are actually stored and then you would have the constructor. So in the constructor you have to initialize the data you would say data equals nudes here Ray. And here is the problem here is where it's entirely uncertain what you have to put inside these square brackets so you obviously want the array to match the number of dimensions like put these here but that's not going to work because in C sharp you cannot put liberals inside generic types you can only have actual types in there. So we cannot write something like var. So actually it's not so much a VaR. You cannot make a class called let's say vector to F which inherits from vector of float comma too because you are not allowed to put the two in here. And this leads to a design pattern called the generic value adapter basically what you do is you adapt a literal literal value to a type and it's actually very simple to do what you do is you make an abstract class or an interface which just yields that particular value. So I can say for example public interface high integer which just gives us a value so we only have a getter and then we can implement this interface in classes and have those classes implement the interface and actually yield that particular value. So instead of doing the whole thing at compile time you're doing that same thing at runtime. So here I can build a class called 2 which implements the I integer interface. And here all I would have to do is implement a value and just return the value to and we can sort of duplicate this for example so if you want the three components something you would call the class three and here you would return the number three. We can actually make it a bit nicer by wrapping or sort of creating a synthetic class around these classes or indeed you could use a namespace here if you wanted to but I'll just make a class just to because basically these classes they kind of exist at the top level of the namespace that we have and that's not always a good thing. So I might make something like the following public static class dimensions so the dimensions are two three and you can add other things here. So we've basically taken a value like two or three and we've adapted the value using a class. So it's a different kind of an adapter but now this whole thing becomes roughly legal because here we can say the D is an integer so we can say the D is an integer and furthermore it has a default constructor. That's another requirement in order to be able to actually initialize it. So here instead of using d you would make a new instance of the and then you would get its value because well remember it's an I integer so it has a value. And this is now legal. So this whole construct is now legal you can now start actually manufacturing time so for example if you wanted let's say you wanted a two component integer you would make a class called vector to Y and you would inherit from a vector where the first argument is the type. So in this case in obviously and the second argument is the number of dimensions so you say dimensions dot two. There we go. So. So this is something that's completely valid and there is no problem in actually getting it to work. We can actually start using it so we can say things like var the equals new vector to I and you know you can you can customize the data here. But in order to customize the data of course you have to give the vector additional features like for example you might want to give it an indexer so people can actually address the different elements. So coming back to the vector here what you can do is you can say public see this with an index and here you will for the getter you say you get the data add this particular index and for the setter. Well it's obvious stuff you say data at index equals value. So this is how you would set the whole thing up and this would allow you to actually manipulate the different commanders so you can say that the first component of the IS EQUAL TO ZERO or if you want the whole X Y and Z business like like we have in geometry you can have that as well except this is where you really lose flexibility because we're not doing co-generation we're doing generic programming. So here for example for the vector where you can do is you can say well let's have a public property of type T called acts and that's going to be the first element. So for the getter we return data at position 0. And for the center we have data position zero equals value. However this quickly becomes kind of weird because if you add also the Y and Z those y and Zs are going to be there even if you have a two dimensional vector. So you have a Z but with a two dimensional vector it doesn't really make any sense and it's actually dangerous to use it. So as you can see you're kind of losing some of the functionality here already in terms of you know losing some of that flexibility. But on the other hand you're gaining quite a lot as well. So what might you want to do with this kind of construct. Well you might want to for example add two vectors together. It seems like a sensible thing you have one vector you have let's say another vector factor too high which also has a bunch of values and by the way let's suppose you want to constructor. Once again if you want the constructor this is something that can be set up in the base class so you can have it in the vector but then you'll have to propagate the constructors and let's actually take a look at how this is done so. Suppose you want to take a bunch of values and you want to initialize the vector out of those values. The question is how many arguments should you take because you cannot write you cannot write some sort of a vector where you take t x t y z and so on because you don't know how many arguments there are. So the only situation the only construct that solves this problem is paradigms parameter is what solved this problem. So here you specify the number of values that you want to assign to a particular vector when you initialize it. And then of course you have to do the actual work you have to figure out that's what data you've got and how to assign. So it looks something like this you can see there's plenty of code here. So there is a required size which is the size of the array you're going to store obviously relating to the dimensions of the data structure but also the provided size can be different so for example you have a three dimensional vector but you've only provided two elements because you find it convenient to just initialize the X and the Y leaving the Z alone. So here we take the minimum we take the minimum of the required size and then provide that size and basically assign only those values that makes it kind of the safest approach out there and what you can now do is you can you can sort of add the constructor arguments here except it's not going to work just yet. So in order to get this to work you have to jump into a vector to Y and you have to you have to basically create the appropriate constructors. Now cogeneration is very easy nowadays using various developer tools but this is one of the two approaches. So there is an alternative approach which takes us into the realm of factory methods and recursive generics. Now this is a more advanced approach so it's going to look particularly complicated. But I do want to talk about it but before we talk about that we need to talk about something else which is also very important and that is how to perform actual operations on these vectors. Because what I want to be able to do is I want to be able to say var result equals V Plus V V. And unfortunately at the moment this is completely impossible because these things don't have an operator plus. Furthermore what you cannot do is you cannot go back into the vector class that we've made here and you cannot give it an operator plus because remember an operator plus cannot operate on generic types. This could be a string or. Well strings also have operator pluses could be something that doesn't support operator plus like I you know a good for example. So in this particular case what you have to do is you have to expand the inheritance hierarchy and kind of do what you would call partial specialization in C++ you would specialize the vector class to integers and then you would inherit from that. So let's actually do that. Let me just show you how this is done because it's an interesting study. So you'd make an in-between class and this inheritance hierarchy. Let's go to the vector of it. So you take the dimensions but you don't take the type argument because it's an integer factor. So this vector of and obviously inherits from vector where you specify int as well as the number of dimensions you have to have the same generic constraints as well so you say this has to be an integer and has to have a default constructor. And in here you would maybe replicate all of those constructors that we have from the base class because you have to propagate them throughout the entire hierarchy so I've generate both the empty constructor as well as a parameter constructor but what we're interested in is getting that operator plus to work because we want to get this expression to be legal and we can we can have our cake and eat it too. In this particular location. So here we want to add the two vectors so public static vector of int with certain dimensions operator plus. Okay. So we take the left hand side and the right hand side left hand side right hand side. And then we basically perform the operation so we make the result basically a new vector event D we get the dimensions. So once again Vadim equals new the value. That's our generic value adapter work here and then we just make a for loop where we perform the copying. So we basically say result at I equals left hand side at I plus right hand side that I like. So and then we can return the result and similarly we could cut and paste this entire method in order to support operator times for example if you wanted to do element y's multiplication or division or subtraction or whatever. But basically what this this chunk of code this operator plus implementation now would make this thing legal. But we have to modify the inheritance hierarchy because remember vector to y at the moment is still just an ordinary vector of int and dimensions to it now has to be a factor of INS and specifying the dimensions like so and now everything is legal of all of a sudden the operator plus is a valid operator everything everything is working basically there is absolutely no problem. So as you may have noticed we've had to perform lots of co-generation for generating the constructors because I really like the parameter constructor. I really like being able to call the basic parameter constructor but you have to kind of jump from here to here and then all the way up to here and it's a lot of work just taking each of these derived classes and just just giving every single one of these two constructors maybe maybe we can avoid this because if we didn't have to generate this constructor then the default constructor would be generated automatically even if you didn't have it. So the question is how can we actually avoid this entire approach. Now I'll leave the default constructs here but let's make another vector. Public Class vector let's show we have a two dimensional one again. Well let's have a three dimensional one vector three f so vector three f is going to be a vector of float. So once again as soon as you introduce a new type you have to basically have a new class such as like here we have vector event you would simply have a vector a float. So without the operator plus it's actually very easy to construct. You would just copy it like so you would change off into two off float here and then you would put a float here and obviously that's well that's pretty much it because you don't have to do anything now. So this vector flow just uses floating point numbers you specify the dimensions as three like so. And that's pretty much it. Now let's suppose that we want to avoid the generation of the constructors and constructor propagation maybe we want our base class to just provide a factory method because methods can be inherited and vector 3 F would inherit any factory methods of the base vector class. So how can we do this. Is it is it even possible to make a factory method up above here. So let's let's try this let's go into vector. So here we are in vector and we're going to try to we have a parameter constructor but we want something different. We want a parameter factory method so we basically want some sort of public static vector of T comedy create which takes Paramus to values. Now you'll see it all go completely wrong in just about a second. But then they even implementation would be something like the following you basically return new vector of DDR where you specify the value. So you call the constructor which is right here and you return the result of that constructor. Now unfortunately this approach isn't going to work. It simply isn't going to work. Let me show you why it's not going to work. Let's suppose that we actually decide to use this approach and from the outset it would look like everything is OK. Var U equals vector 3 F dot create and provides some floating point values. Three point five after point to have one. So this is legal. Now what is the problem. If everything works like what is the problem. The problem is that as soon as you customize vector three f in any way this entire thing just fails because if you look at this far here and if you actually specify the type explicitly this is not a vector 3 F this is a vector of float and dimensions dot three. Now why is this important it's important because let's suppose we decide to go into vector 3 F and we decide to override it just make a custom to string for example. So you make a custom to string where you do something fancy like you try to you try to maybe I don't know string dot join the components using a comma. So take the data string that join. So now you've customized this type. Unfortunately if you do you like to string you're not going to be calling this method at all. Because like I said the result of the variable is a vector of float in dimensions 3 whereas this type inherits from vector of flow dimensions 3 so it's not even a vector a float it's just an ordinary factor. Another thing by the way is that you cannot write something like you plus you. It's no longer valid because once again a vector three of DOT create does not give you a vector a float and it's precisely the vector of flow that has the operator plus. So it looks like a terrible approach. It looks like the factory method isn't going to work but in actual fact we can make it work and the thing that makes it work is recursive generics not because of generics are it's a it's a really kind of rare approach is an approach that is used in some of the other design powders that we discuss here. And basically what this means is that when you inherit from vector afloat which in turn inherits from a vector you have to propagate additional type information up to the base class about the return type of your factory. So here instead of returning vector of T.D. you have to return some special type let's call it T self. So this is the kind of typical approach of factory methods which try to return the type of the derived object the most derived object but of course this requires quite a bit of work. So if we go into the vector class we now need to add P self as one of the generic arguments here. We also have to have additional constraints. So t self here is going to have the recursive generic constraint so t self remember has to inherit from us. It has to inherit from the whole vector. T the thing so we just cut and paste it here. It also has to have a default constructor as well. So coming back here now this entire approach basically changes this and we can no longer do it like this because well it's not a vector it's easy that we are constructing and it would be better to simply replicate the kind of behavior that we have inside the parameter constructor buds in on a temporary variable essentially. So how do we do this. Well we need to return a t self so we may as well start by creating it var result equals new T self. So far so good. And then we can basically perform all of these operations so all of the operations on prams. But when you have data you just prefix it with the result of data and the same goes for a result of data here and then you return the result. So as you can see it's kind of difficult to actually reuse that constructor that we have. So it's maybe easier to just go with the factory method approach altogether and have everything kind of self-contained. But the key the key thing here is that we're returning t self returning t self and because of this we now have to change the entire hierarchy like every single time that we have here we have to change every single type to propagate this idea of type information up above. So when you how for example vector of float in D. What you have to do is you also have to modify basically this type to also have additional type information now different ways of doing it may. Maybe you want to sort of specify it here or baby you want to specify at this particular point so here you could say vector of float V being the underlying type. So if somebody calls it on vector float D then you get a vector of float D but you know the point is that you inherit further from this and the same will go for vector event. Same recursive generic approach so you would specify it here but really you have to go down then you have to specify it in in these types you have to have this vector to Y and so on then all the rest of it you have to have basically all of this stuff working. So remember we wanted to avoid it. In fact a three f.. So that's perhaps where we would specify both that and the propagated type as well. So the modification will go something like this. You would go back to vector float and you would modify this to also have a T self. So that's particularly unpleasant because now you basically have to kind of propagate t self across the entire things so to speak. So you so you take a vector of yourself and then actually this has to be this entire thing passed in here. So as you can see it's kind of getting really complicated. It's getting really convoluted. I mean I can sort of see what's going on here but it's getting less and less readable but we are going to get some benefits from it. Of course now this part is invalid. Now this part has to also be modified because you basically have to propagate vector three f here as well before you get the dimensions. But now what we have is we have this wonderful situation where if I take this var and I specify the type explicitly you'll see that we are still getting vector of float stuff and that's just not particularly good is it because we want the create invocation to actually give us a vector 3 F and at the moment where we're almost there we're almost there but we're not quite there so let's try tracing through the time. So we have a vector three f a vector three f is it inherits from a vector of offload and it specifies vector three f in here so vector three f is the T self that is specified here and then the vector here is a vector of float to yourself. But in reality what happens is you have to specify I think the self here you'll have to specify T S here and then you get additional constraints on top of that because we don't know if that t self is actually valid so we add an additional constraint that t self is new but even that is not enough because T self must also be convertible to a vector of T self a float of D which as it stands it is in fact convertible everything is valid but you can sort of see the complexity rise as we do this. So with all of these modifications let's go back into our Declaration of vector three f. Take a look at what the type is. And finally we have our final result. So this is what we wanted in the first place. We wanted the factory method to return the actual time. So if I call it and vector three f I get a vector three f if I call this on something else and we have added support for this yeah. But if I call it on something else then I get that something else has a result. So recursive generics are particularly helpful in here but they do introduce quite a bit of complexity because now every single declaration like a vector three f declarations just becomes this really messy inheritance thing. I mean this is quite hard to understand for somebody who just wants to expand this hierarchy like adding you type it now becomes fairly complicated but this is a legitimate approach. This is something that does work and doesn't allow you to have this this nice little setup. So the purpose of this entire lecture was to first of all talk about the generic value data which is a rather silly thing that we have to do in C shop in order to propagate integer values or you know any kind of literal values as arguments of generic type. So that is one thing but then we also discussed a couple of other things like how to expand the hierarchy so that you get operations on numeric types in which case you basically have partial specialization you have like if you want to operate or plus for example you have to partially specialize the thing you have to say that here will space specify explicitly that this is an in thing and this loses some of that generality because now if you want a vector of flowed you have to have the vector flow class effect of double class blah blah blah so you're not as generic as you wanted to be. And then of course the icing on the cake here is to take a look at how to make a factory method which uses recursive generics to propagate type information across the inheritance hierarchy. So that's my example of the generic value adapter.