Hello there! The team I work in, jStage, develops a module-based eCommerce platform. This article will summarize some thoughts about composable and typesafe extensions to our view model classes without using inheritance. Our shop system is used by different business clients, each having their own requirements. We can customize our shop system to supply business clients the required functionality through our module-based architecture. This article gives a quick glance on how we achieved a stable API on our View-Models in the frontend, while being able to extend them for each business client specific requirements.
In our architecture, the frontend is decoupled from the backend by the so-called View-Models. The result of each request is a rendered view. To render a view, we need the view template (HTML + Freemarker) and the ViewModels (Java objects containg the data).
A simple view template in Freemarker may look like this:
<#if articleViewModel.isOnWatchlist>
${articleViewModel.watchlistUrl}
</#if>
The corresponding ViewModel class may look like this:
public class ArticleViewModel {
private boolean isOnWatchlist;
private String watchlistUrl;
public SomeViewModel(boolean isOnWatchlist, String addToWatchlistUrl) {
this.isOnWatchlist = isOnWatchlist;
this.addToWatchlistUrl = addToWatchlistUrl;
}
// getter and setter omitted
}
Our shop system is following a module-based architecture. The views may be programmed by our business clients or by us. To support the frontend developers, we need to have a nice way to document the information that view models provide. We use JavaDoc and Doclets to generate the documentation out of the source code. It is crucial that this documentation only contains the methods and variables of our View-Models that are present in the corresponding business client specific build of our shop system.
The module based approach is crucial to support multiple business clients. Each business client has distinct requirements. We try to map functionality required by our business clients to existing modules. Sometimes, we therefore have to develop new modules if the requirements do not fit in existing modules. A module is therefore used by at least one business client, but may also be used by more than one. Crucial modules, like the authentication module, are used by all business clients.
Lets discuss how that module based approach works. Our shop provides a module with the ability to display articles. Articles do have some basic information, like a translatable name. To render an article, we need to provide a view (customer specific) and the information to display in form of a View-Model (not customer specific). In some of the shops of our business clients, an article may be added to a watchlist. Customers get notifications if an article on the customers watchlist changes, e.g. if the price changes. The crucial part to understand here is, that the watchlist feature is an optional module. The composition of modules is customer specific, and some modules provide features that extend the ability of other modules.
Lets have a look on how to model that. First, we need a corresponding ArticleViewModel
class.
This could be located in a module called article
, which handles other article-related operations, e.g. database lookups.
Because not all shops do need a watchlist, the watchlist management is provided by a separate watchlist
module.
This module knows what an article is (and has therefore a dependency on the article
module), but the article module
does not know the watchlist module.
This is common in modular architecture, but notable is, that other modules provide operations, which are centred around
models of other modules, e.g. the watchlist allows to watch articles.
The really interesting question is now, how can other modules add information to a given view model?
One could try to solve this problem using inheritance (WatchlistableArticleViewModel extends ArticleViewModel
), but you
fail with this approach as soon as you have more than one module trying to extend a view model.
You could still try to solve this issue by writing an ArticleViewModel
class per business client, but this would clearly
lead to much duplicated code.
The proposed solution
I will present the classes necessary first and describe the usage afterwards.
// this class contains the information which shall be added to other view models
public class WatchlistArticleViewModelExtension {
private boolean isOnWatchlist;
private String watchlistUrl;
public WatchlistArticleViewModelExtension(boolean isOnWatchlist, String addToWatchlistUrl) {
this.isOnWatchlist = isOnWatchlist;
this.addToWatchlistUrl = addToWatchlistUrl;
}
// getter and setter omitted
}
// this interface must be implemented to extend ViewModels of type VM (see the first generic paramter).
// The information added is an instance of type T (see the second generic paramter).
// Do not worry if you do not immediately understand this, the examples should clarify this soon.
public interface ViewModelExtensionDescriptor<VM, T> {
/**
* The key used in the extension map (and therefore the name of the extension for accessing in templates).
* @return the extension map key.
*/
String getKey();
}
// This class contains the meta data to link the extension (WatchlistArticleViewModelExtension) with
// the class which shall be extended (ArticleViewModel).
public class WatchlistExtensionDescriptor implements
ViewModelExtensionDescriptor<ArticleViewModel, WatchlistArticleViewModelExtension> {
@Override
public String getKey() {
return "watchlist";
}
}
// This is the base class of all extensible View-Models (like the earlier mentioned ArticleViewModel)
// inheriting classes need to set the VM-Parameter to their class, so the ArticleViewModel
// would extend AbstractExtensibleViewModel<ArticleViewModel>.
public abstract class AbstractExtensibleViewModel<VM> {
private Map<String, Object> extensions = new HashMap<>();
// this method allows the extension of this view model instance
public <T> void addExtension(Class<? extends ViewModelExtensionDescriptor<? extends VM, T>> viewModelExtension,
T value) {
try {
extensions.put(viewModelExtension.newInstance().getKey(), value);
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
// This method is used by freemarker to have easier access to the properties provided by extensions
// have a look at http://freemarker.org/docs/pgui_misc_beanwrapper.html#beanswrapper_hash
public Object get(String key) {
return extensions.get(key);
}
public <T> T getExtension(Class<? extends ViewModelExtensionDescriptor<? extends VM, T>> viewModelExtension) {
try {
return (T) extensions.get(viewModelExtension.newInstance().getKey());
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
Usage
To extend a ViewModel
, it has to inherit from AbstractExtensibleViewModel
. After that, you can declare an extension
in Java like this:
// set the extension:
WatchlistArticleViewModelExtension extension = new WatchlistArticleViewModelExtension(false, "some url here");
ArticleViewModel articleViewModel = new ArticleViewModel();
articleViewModel.addExtension(WatchlistExtensionDescriptor.class, extension);
// get the extension:
WatchlistArticleViewModelExtension extension = articleViewModel.getExtension(WatchlistExtensionDescriptor.class);
The corresponding Freemarker-Template could look like this:
<#if articleViewModel.watchlist.isOnWatchlist>
${articleViewModel.watchlist.watchlistUrl}
</#if>
Summary
To recap: This solution may not be as lightweight as a simple extension-Map-approach with naive Key-Value-Extensions, but it provides the following benefits:
- programmers do not write ad hoc extensions (create lots of keys and throw in values needed somewhere, making it hard to see which extensions are available)
- it is easier for the programmer to see all existing extensions (just show inheriting classes of
ViewModelExtensionDescriptor
) - this approach is documentable and can be used for automatic documentation generation
- you have type-safety in the Java world
- you have easy access in the template world, especially with Freemarkers “generic get methods”
- it maximizes composability between modules
- it is more flexible than simple inheritance
- extensions and view models are as reusable as possible
Contact me in the comments if you have questions, critique or other ideas regarding this approach.