Introducing the View Interface Pattern, maybe.
A design pattern has emerged on our team, which we call it the View Interface pattern. It is a variation of the Adapter pattern This article will describe the pattern and explain the motivation behind it as well as explore some of the benefits. We will also briefly compare it to some existing patterns.
The pattern is directly motivated by access restriction. Specifically, we have a stateful object to which different components need different access. The pattern is further informed by the Interface Segregation Principle.
A simple example is a CheckBox
; a CheckBox
can have its state set to true or false, and it can have its state read. The former is a mutator method on the CheckBox
(i.e., it changes the internal state of the object), the latter is a non-mutating query method.
In a particular system, we have one specific component, we’ll call it the Controller
, that is in charge of the checkbox, that means it is solely responsible for setting its state correctly. Many other components of the system need to know the state of the checkbox, but none of the others need to be able to set the state.
The Controller
needs access to the mutator method of the CheckBox
, but none of the other components do; components that only need to know the state of the checkbox only need access to its query method.
The View Interface Pattern provides one or more interface that present different views of the same object. In the example of the CheckBox
, it would provide an interface that includes the mutator method, and a separate interface that only includes the query method.
A key characteristic of the View Interface Pattern is that the underlying object does not implement any of the view interfaces, but instead supplies them. An example for the checkbox example is shown here:
CheckBox
type.We will further refine this implementation below, but first let’s make sure we understand it.
The CheckBox::getMutator()
method provides a view of the checkbox which allows for the underlying object (the CheckBox
) to be mutated, through the MutableCheckbox::setState(boolean)
method on the returned object.
Likewise, the CheckBox::getReadOnlyView()
method provides a view which does not permit the object to be mutated, but does allow the state to be known through the ReadOnlyCheckbox::getState()
method on the returned object.
What makes these returned objects view objects is that all of their methods delegate to the underlying CheckBox
object. The interfaces are implemented with anonymous inline classes that call through to the CheckBox
object. This ensures that any changes to the underlying object are immediately known to all the view objects.
The above implementation might be utilized as in the following example:
As mentioned, a key point of this pattern is that CheckBox
does not implement either of the view interfaces. Consider the same example usage as above, but in this case, the CheckBox
class implements the two interfaces directly:
We haven’t changed the signatures of the CheckBoxController
or the CheckBoxViewer
constructors, they still take a MutableCheckbox
and ReadOnlyCheckbox
respectively. However, in this case, the checkBox
object directly satisfies these interfaces itself.
Granted, this alternative code without using the pattern is a little bit simpler, and certainly the implementation of the CheckBox
class is less code. However, it means that the CheckBoxViewer
actually has direct access to a CheckBox
object; it’s typed as a ReadOnlyCheckbox
, but it’s runtime type is CheckBox
and the CheckBoxViewer
can easily type-check and cast the object back to a CheckBox
, at which point it has access to the mutator method, setState(boolean)
. With the View Interface pattern, this is not possible because the CheckBox
itself does not implement the view interfaces, so it can’t be passed into a constructor which expects them.
One question that has arisen from this: do we really need to protect ourselves in this way? In an enterprise situation, we’ve determined that the answer is yes. If this is a small application with one or two long term dedicated developers, maybe it’s not necessary; maybe it’s sufficient to agree by convention (and remind by comments) that only the Controller will mutate the check box.
In a large scale development environment, this system might see dozens of developers over its lifetime, who will have varying levels of experience and exposure to the system. In this sort of environment, the thinking is generally along the lines: “if the code allows it, it’s probably ok”. Which isn’t wildly invalid: if I give you an object that has a mutator method on it and you feel like you have a use case for mutating it, why wouldn’t you use it?
Hiding the CheckBox
object behind a ReadOnlyCheckbox
interface, as in the alternative example, should send a pretty strong signal that it’s not okay to mutate it. Specifically, doing a runtime type check and casting in order to access a mutator method should probably send up some red flags. None the less, an eager developer who discovers a use case for mutating this object might feel compelled to change the signatures so that a CheckBox
is accepted, and their problem is solved.
The View Interface pattern is meant to send a much stronger signal about what operations are appropriate, even if they all operate on the same underlying object. Since the underlying object doesn’t implement any of the view interfaces itself, it would take a fair amount more code change to be able to pass in a CheckBox
where a ReadOnlyCheckbox
is expected.
Refining the Implementation
Recall the initial implementation of our pattern for the CheckBox
, as shown above and repeated here:
There are several improvements we can make to streamline this code without changing the basic functionality of it. For one thing, the two view interfaces, MutableCheckbox
and ReadOnlyCheckbox
are both single method interfaces, so they can be implemented using lambdas in Java 8:
In fact, since each of these lambdas are perfect delegates for other methods, we can replace the lambdas with method references:
Another thing to note is that the implementations of the view interfaces are entirely stateless: all they ever do is call through to the underlying object. In this case, they can be singletons within the viewed object. In other words, we can create a single instance of each view and store those objects as instance variables in the CheckBox
, no need to create a brand new object each time the view is asked for:
You could also lazy-load these view objects the first time they are asked for, so that if a particular object is never asked for a particular view, it doesn’t have to waste time creating it:
Notice that there’s no strict need to synchronize the creation of these view objects, since they are stateless; if it so happens that getMutable()
is called concurrently and two instances of MutableCheckbox
are created, maybe even two different instances are returned, it really doesn’t matter except that you created an extra object that you didn’t need. Functionally, there is no harm in it.
Lazy loading the view objects is only useful if there will be some instances of the object for which the view isn’t needed; if your code creates the object specifically to get at the view, then don’t waste time with lazy loading.
A final modification is somewhat specific to this example, but is a reminder to consider using existing general purpose interfaces if you want maximum flexibility, and to avoid type proliferation:
In this case, the mutator method could be realized through the Consumer interface, and the query method could use the Supplier interface. There are advantages and disadvantages to using general purpose interfaces versus specific purpose interfaces which won’t be discussed in this article, but keep in mind that it’s an option.
Comparing to Other Patterns
The View Interface pattern appears to be a special case of the Adapter pattern, where the adaptee is also providing the adapters. As with the Adapter pattern, the implementation of the view interfaces follows the Delegation pattern.
In the Adapter pattern, a separate adapter class would implement the view interface (often referred to as the target interface) by wrapping the underlying object (the adaptee) and delegating to its methods. The View Interface pattern does the same thing, except that the adapter objects are created by the underlying object itself.
There are some advantages and disadvantages to implementing it this way. An upside of the View Interface pattern is that everything can be fully contained inside the underlying class (e.g., CheckBox
). Internal state and mutation can be entirely private and completely unaccessible outside of the class except through the provided views. With an external adapter class, that class would need access to appropriate methods on the underlying object in order to delegate to them.
Another upside is that you’re reducing class proliferation: even though your view object implementations are technically classes of their own, they are generally anonymous classes, or at least private inner classes that cannot be accessed externally. That means they can’t proliferate through your code and no one outside of this class needs to know or care about them.
One downside is that it is less flexible: it only supports adapters that are provided by the class itself; if you want to create new adapters without modifying the class, you only have the provided views to work with.
Another downside is that it adds some code complexity to your underlying class and arguably overloads it with the responsibility to implement multiple interfaces, where as external adapters remove the knowledge of specific interfaces from the underlying class.
Conclusion
The View Interface pattern is something that has emerged organically in our work on enterprise software, as a way of honoring interface segregation and restricting access to dangerous mutator methods. It follows the general pattern of the Adapter design pattern, with a key distinction that the adapter classes are entirely internal to the adapted object. This pattern is relatively new and we are still feeling it out, so please leave your thoughts and feedback in the comments.