What you have here between your models and viewmodels is a mapping.
No amount of code sorcery1 is going to change the fact that you're going to have to map pairs of (model+viewmodel) in order to decide which viewmodel to use for your model. As this is a given, the only real discussion left is how we want to define this mapping.
There are myriad ways to do so; most of which can be argued pro and con. We've already seen a few in answers, which I'll include in the list. Others are my suggestions.
- (suggested by Lee) Letting the model implement a generically typed interface for the viewmodel (
class Hamburger : IHasViewModel<HamburgerViewModel>
)
This is the first thing I thought of too. However, I do agree that it feels a bit dirty for your model to have to know its related viewmodel (especially if you have several viewmodels for a given model).
- (suggested by Felix Castor) Setting a
Dictionary<ModelType,ViewModelType
defined by letting the viewmodel implement a generically typed interface for the model (class HamburgerViewModel : FoodViewModelBase<Hamburger>
)
The interface implementation has the same pros and cons as the previous bullet point, but now you have an added responsibility of having to use reflection to compose a dictionary on the fly?
I'm not a fan of this added complexity. The previous bullet point seems objectively better (due to less management) with no additional drawbacks.
- (as a counter to Felix Castor's idea): why not simply maintain the type dictionary yourself, and do away with the interfaces and reflection?
Something like this:
public static Dictionary<Type,Type> ModelToViewModelDict = new Dictionary<Type,Type>()
{
{ typeof(Hamburger) , typeof(HamburgerViewModel) }
{ typeof(Taco) , typeof(TacoViewModel) }
}
This way, neither your model nor viewmodel class definitions are dirtied, because the mapping is maintained externally to the classes.
Having to maintain the dictionary isn't really an issue (as discussed in the beginning, the mapping needs to be maintained somewhere, in some shape anyway).
Unless I'm misunderstanding the example, this would require a similar generically typed interface and, on top of that, also require you to design a visitor who needs to be updated for every newly added food.
I don't see the benefits of this approach compared to the others. You end up with the same "dirty" model/viewmodel link by implementing an interface from one one the other; while also having to maintain an extra definition.
However, I'll admit that I have little experience with the visitor pattern in general, so I may be unaware of additional pro's (or cons).
Edit
The Visitor answer has been slightly updated since I wrote this, and I understand it a bit better now.
I still don't like it. It requires an overloaded method per entity type (which is very similar to your original FoodViewModelExtensions
class); yet also requires every entity type to implement an interface (granted, it doesn't contain a generic type used for mapping purposes, but you are going to be copy/pasting the same interface method implementation all over your entities).
This feel like double work. A mapping can be achieved with only doing one of the two tasks that the visitor requires.
- Don't overgeneralize your method parameters.
If you need to know the specific type of your passed object, then don't declare it as a shared base type. Instead, rely on overloading:
public static ViewModelBase GetViewModel(this Taco obj)
{
return new TacoViewModel(obj);
}
public static ViewModelBase GetViewModel(this Hamburger obj)
{
return new HamburgerViewModel(obj);
}
This comes with the added benefit that you can choose whether a default option (when the type is not known) can be given, or instead throw a compile-time error. If you want no default option to exist, then keep the code as above. However, if you want to allow the passing of types that are not explicitly mapped, you can add a default:
public static ViewModelBase GetViewModel(this EntityObject obj)
{
return new EmptyViewModel(obj);
}
Note for the default method:
This is a bit of a pitfall. If you declare your entity as EntityObject myTaco = new Taco();
and then call GetViewModel(myTaco)
, you're going to end up in the default GetViewModel(this EntityObject obj)
method; which is not what you'd want. This is because the type of the variable is used to decide which overloaded method to use.
To avoid this, make sure you declare your entity by its proper type: Taco myTaco = new Taco();
and then you will end up in the correct GetViewModel(this Taco obj)
method.
Maintenance-wise, it's not all that different from your if
chain. Originally, you needed to add a new if
for every type, whereas now you need to add a new method for every type.
I like this version better, but I do concede that it at least partially relates to coding style preferences.
Note
Similar to how I advised against overgeneralizing your Hamburger
and Taco
objects as EntityObject
s; there may be a similar argument to be made about having every method return a ViewModelBase
as opposed to the specific HamburgerViewModel
and TacoViewModel
.
Even if you handle the resulting viewmodel the same way, you can still have the method return an object of type HamburgerViewModel
but choose to store the return object in a ViewModelBase
variable (thus not changing anything). So I would suggest keeping the return type sufficiently specific, and not overgeneralized.
This would be very similar to your current FoodViewModelExtensions
class; with only a minor syntax change.
1I really wanted to come up with a suggestion that wouldn't require any manual maintenance when new entities get added to the codebase.
However, the best I could come up with is:
- Using standardized naming across your model/viewmodel names, e.g. a
Foo
entity must have a viewmodel that is named FooViewModel
.
You'll have to rely on reflection to find an object's corresponding viewmodel.Not having to manually maintain this is nice; but this is essentially a stringly typed approach; which I'm generally opposed to.
I'm less offended by the stringly typed approach than for other use cases; but it still feels a bit wrong to me. However, if you're really wanting to automate your mapping, this would effectively prevent you from having to manually maintain it.
To conclude
There are many ways to approach this. Each method has its own benefits, drawbacks, and applicability to the developer's preferred coding style (or the company's coding standards).
The most important thing to consider here is that you want your mapping to have a minimal footprint.
- At an absolute minimum, using naming conventions to auto-map models and viewmodels is viable; but comes with some drawbacks in that the structure of an application shouldn't be defined by the names that are used.
- A middle-of-the-road approach would require you to define the mapping once. There are two possibilities here:
- Spreading the mapping over many locations (e.g. by having each model class definition list a
IHasViewModel<HamburgerViewModel>
)
- Keeping the mapping contained in a single location (e.g. by using the dictionary, overloaded extension methods, or the factory). I much prefer this approach as it keeps the mapping centralized (inspired by SRP).
- What you should definitely avoid is having the mapping be defined more than once. The posted example of the visitor comes pretty close to this by having you maintain both an interface mapping and a visitor class definition, both of which need to be updated when a new entity is added into the mix.
In the end, it's really just dealer's choice. Company coding standards (or those of your team lead) will likely take precedence. If not available, then it's up to you to decide which approach's drawbacks you're most willing to live with.
IHasViewModel<out T> { T GetViewModel(); }
IHasViewModel
be in the model's namespace? Could you provide an example of this?class Taco : IHasViewModel<TacoViewModel> { ... }
etc.ViewModel
in myModel
. That is a solution but if I do it that way myModel
would be dependent on myViewModel
.IModelVisitor<TResult> { }
which your models implement to replace the dynamic type switching. Then create aViewModelVisitor : IModelVisitor<ViewModelBase>
to create the view models.