A Cleaner MVC Inspired by Continuation-Passing Style
Join the DZone community and get the full member experience.
Join For FreeWith this article I'm starting a series derived from how I'm designing blueBill Mobile, an Android application, but in addition to technology-related topics (not only Android) I'll talk a lot about general design strategies. A relevant effort has been made to define a design that makes testing easier and also reduces the technology-relevant part of the code to a minimum.
In fact, the whole application is being developed as a core which is just plain Java and can be implemented by different UI solutions, such as Android, web apps and desktop; examples will be given in future articles which cover also Vaadin, Wicket (for the web part) and the NetBeans Platform (for the desktop part). The articles will focus on different topics case by case, moving from design considerations to lower level details concerning the implementation, even including tools, such as Maven, the NetBeans IDE, the maven-android-plugin and the NetBeans Android plugin.
In this first article we're going to deal with design considerations about the use of MVC together with some ideas inspired from a technique callled Continuation-Passing Style (CPS). The last part of this article deals with testing and assumes prior knowledge of Mockito.
I have to thank many friends at JUG Milano and at the JavaPosse for having helped me to better understand CPS and the correct way to refer to it.
Code examples are mainly taken from blueBill Mobile for Android, version 1.0-ALPHA-3.
Let's start with a simple thing. blueBill Mobile contains a small RSS feed reader, called "News", which is used to deliver to users posts from the project blog. The requirements are easy:
-
The user enters a specific screen and sees a list of topics (with caption and publication time).
-
Each topic can be marked as read or not read by means of a context menu and it's rendered in a different way to reflect its state.
-
A topic can be selected so it can be read in full (and will be marked as read).
-
The feed can be cached in memory in case there's no network connection, or the user doesn't want to connect.
-
A preference says whether the application is allowed to connect to the internet.
-
If network connections are allowed, the news service retrieves a fresh copy of the feed.
-
If network connections aren't allowed and a cached copy is stored in memory, news are rendered with a warning and no attempt is done to retrieve a fresh copy.
-
If network connections aren't allowed and no cached copy is available, the user is explicitly asked for the permission to connect. If it doesn't confirm, the interaction terminates.
-
Unexpected errors (such as network connection failures) are be properly reported.
It's pretty simple, but it makes it possible to explore some design directions about the control flow moving from the backend (the news service) and the user interface (confirmations), as well as strategies for testing.
So, let's start with a look at the UML diagram of the API:
Applying the standard patterns, we have a service providing data, an MVC and one support class:
-
NewsService: the headless «service» that retrieves the feed in backgroud and eventually caches it;
-
NewsView: the «view» that interacts with the user;
-
NewsViewController: the «controller» of the view;
-
RssFeed: a class representing a RSS feed and all its properties (the «model»);
-
NewsPreferences: an interface exposing the preferences needed by the NewsService
NewsService exposes two methods:
public interface NewsService { public void eventuallyCheckForUpdates(); public void getNewsFeed (@Nonnull AvailabilityNotifier availabilityNotifier); }
-
eventuallyCheckForUpdates() can be invoked at the initialization of the application and, if networking connections are allowed, it searches for a fresh copy of the feed in background. It allows to save time and eventually have already loaded the data when the user will ask for them. We can safely ignore it in the scope of this exercise as it doesn't interact with the other classes we've introduced.
-
getNewsFeed() is the method that retrieves the feed: it is called by the NewsViewController when it wants to render the data onto the NewsView.
There are two characteristics of getNewsFeed() that we must pay attention to:
-
it can have multiple outcomes (e.g. the feed might be available or not, for different reasons);
-
it can be completed in a very different amount of time (the feed might be available immediately, or it could be downloaded from the network, an operation that might require some time)
One of the first things that comes to mind is to implement that method as a plain "getter" so it returns different values representing the outcome. For instance, a RssFeed instance if data are available, null if not; or an enumerative type for the different outcomes, and the caller would then retrieve the actual feed with a getRssFeed() method in case of positive outcome; or perhaps negative outcomes would be represented by throwing an exception which describes the reason for which data are not available. In all of these cases the method would block until completion.
We should see some problems already on these premises. For instance:
-
null / non null outcomes must be handled with an if / else and an enumerative result encourages the use of a switch. Both approaches should be avoided in a good OO design since they don't scale: after an evolution of NewsService that added a new outcome we could easily forget to update all the switches in the application adding the required new case.
-
Blocking methods cannot be directly called by a view component in many technologies, for instance Swing, Android, or some web frameworks; other technologies (mostly web based) would have no problems. Usually there are specific library functions for dealing with this issue, such as the well known SwingWorker in Swing or the similar AsyncTask in Android, that split the sequence of operations in parts bound to the proper thread. These solution work, but the programmer must remember to apply them when needed (and somewhat create a strong coupling of the design to the technology); which in turn requires that services properly document that some exposed methods are blocking. If during the evolution of the project a method changes its blocking behaviour you'll likely have a user interface not behaving properly. In other words, I don't want to see a SwingWorker or an AsyncTask in portions of code that is independent of technology.
But there's a subtler issue. Reviewing the specifications, let's focus on the one saying that network connections must be enabled by means of a preference or the system must explicitly ask for a confirmation to the user. Let's imagine how we're going to write the NewsViewController: it calls getNewsFeed() and, if it is answered that a network connection is required, opens a confirmation dialog. The user eventually confirms that he wants to connect and at this point the NewViewController calls... what? Perhaps another method of NewsService, such as getNewsFeedWithoutConfirmation(), which connects without asking for a feedback.
Is it all right? In the end it would work and I've seen (and written) many pieces of code like that. The problem is: what prevents a distracted developer from directly calling getNewsFeedWithoutConfirmation(), thus bypassing any check and violating the specifications? The point is that we would expose methods in a service that shouldn't be called but under precise preconditions. I think you've seen lots of times, in the JavaDocs, comments warning you that "THIS METHOD SHOULDN'T BE CALLED UNLESS...". Of course, many don't read the JavaDocs and, to tell the truth, more often than not such preconditions aren't properly documented. This usually ends up with a lot of headaches. Sure, the application must be tested and a properly written test would detect the bug. But robust code enforces policies (for what is possible): preventing errors from occurring by construction is better than allowing errors to happen and later hunting for them.
Seen in another perspective, let's verify whether we have properly distributed responsibilities to classes. Which class should own the responsibility of enforcing the connection confirmation policy? Undoubtedly NewsService, which encapsulates the network connection; for sure it's not a responsiblity of NewsViewController, that only has to properly coordinate the control flow between NewsService and NewsView. Now, in the hypothetical implementation that we have sketched, the responsibility is split between the two classes: NewsService notifies the need for a confirmation (correct), but NewsViewController is capable to directly command a download without a further confirmation. Instead, it should just tell NewsService it has got the confirmation (how to get a confirmation is a UI matter), and let the latter to decide how to continue (not a UI matter).
Let's try a better design to deal with the issues we've found so far - for the sake of clarity, let's recall them:
-
we don't want to use a switch or a if / else in the NewsViewController implementation;
-
we don't want to take special care in case getNewsFeed() blocks for a long time and we don't want to introduce technology-specific solutions in modules designed to be independent of the UI technology;
-
we want to encapsulate the whole responsibility of the connection confirmation policy into NewsService.
Adopting an asynchronous message passing style is an elegant solution to the threading problem. In practice, we guarantee that getNewsFeed() completes immediately, even when the result is not ready; the caller will be notified later, by means of a callback that is passed as argument:
public static interface AvailabilityNotifier { public void notifyFeedAvailable (@Nonnull RssFeed newsFeed); public void notifyCachedFeedAvailable (@Nonnull RssFeed newsFeed); public void notifyFeedUnavailable(); public void notifyFeedCouldBeDownloaded (@Nonnull DownloadConfirmation confirmation); }
AvailabilityNotifier will be called back in one of its four methods for one of the four possible outcomes:
-
notifyFeedAvailable() notifies that a fresh feed is available and carries it as a parameter;
-
notifyCachedFeedAvailable() notifies that a cached feed is available (for some reason a fresh copy couldn't be retrieved) and carries it at as parameter;
-
notifyFeedUnavailable() notifies that it is not possible to retrieve any data;
-
notifyFeedCouldBeDownloaded() notifies that there's no data available, but it could be possible to download it by connecting to the network.
Of course, the presence of a specific callback method for each outcome also eliminates the need of a switch. Should a future version of NewsService allow a fifth outcome, our code won't be compilable unless we provide the implementation of a fifth method, forcing us to take care of the new behavior. Or, perhaps, it will be possible to provide a default implementation of the new method (thus turning AvailabilityNotifier into an abstract class) to have backward compatibility with existing code. We would be safe in both cases.
Now, let's focus on the fourth method, notifyFeedCouldBeDownloaded(): it's the one notifying that the data could be downloaded, upon confirmation. As you can see, it has an argument, called DownloadConfirmation. It's another callback:
public static interface DownloadConfirmation { public void confirmDownload(); }
The exposed single method confirmDownload() allows to complete the operation (of course, if the user cancels the operation, the method won't be called). doDownload() works again asynchronously, returning immediately (so it can be called by a UI thread); when the operation is completed, with a positive or negative outcome, the initial instance of AvailabilityNotifier will be notified again. So, instead of exposing a getNewsFeedWithoutConfirmation() into NewsService, warning that it shouldn't be called but under some circumstances, confirmDownload() is available on an object that only materializes, and thus can be called, precisely under those circumstances.
Let's think of these callbacks as objects representing a state in the the control flow of the application: each state only exposes methods doing things that are allowed to be done in that state. A great reduction of the chances of an error.
Note that the implementation of DownloadConfirmation could even enforce a check in confirmDownload() so it can be called only once per instance (and perhaps within a certain time window). It would prevent tricks such as keeping a reference to DownloadConfirmation to call confirmDownload() multiple times during other interactions. Sure, we can't automatically enforce the proper implementation in NewsViewController (after all it could just pretend the user has confirmed without asking anything), but we've made some reasonable efforts to avoid some trivial errors.
Below is a diagram of the News API, where you can see the relationships among the classes described so far:
We can now have a look at the implementation of the method getNewsFeed() in NewsService (I'm not showing the full listing, but methods names are self-describing):
@Override public void getNewsFeed (final @Nonnull AvailabilityNotifier availabilityNotifier) { try { availabilityNotifier.notifyFeedAvailable(cache.getRssFeed()); } catch (NotFoundException e) { ensureCacheIsInitialized(availabilityNotifier); if (!cache.getStatus().isDownloadNeeded()) { readNewsFeedAndNotifyAvailability(availabilityNotifier); } else if (preferences.get().isNewsDownloadAllowed()) { downloadNewsFeedAndNotifyAvailability(availabilityNotifier); } else { availabilityNotifier.notifyFeedCouldBeDownloaded(new DownloadConfirmation() { public void confirmDownload() { downloadNewsFeedAndNotifyAvailability(availabilityNotifier); } }); } } }
We earlier said that we can think of this design as asynchronous message passing: each interaction is represented by a message (the invocation of a callback method) which carries an object (AvailabilityNotifier and DownloadConfirmation) that represents the state at a certain point of the interaction. This design presents some similarities with a technique called Continuation-passing style (CPS) . While in direct functional style a subroutine just returns a value, with CPS the caller passes an object, called "continuation", to the subroutine and that object will receive the result of the subroutine and decide how to proceed. In our case, the callbacks AvailabilityNotifier and DownloadConfirmation are doing a very similar job to continuations. I say "similar" since CPS operates with stricter rules and with deeper implications; anyway it is probably correct to say that some ideas in the design described in this article have been inspired by CPS.
The use of continuations in a user interface has been first described in the paper User Interface Continuations (Dennis Quan, David Huynh, David R. Karger, Robert Miller), where the authors pointed out how they can be used to efficiently represent the control flow for the interaction between a user and a user interface. It makes sense to quote the abstract, as it describes the design requirements that have been introduced in the example before:
“Dialog boxes that collect parameters for commands often create ephemeral, unnatural interruptions of a program’s normal execution flow, encouraging the user to complete the dialog box as quickly as possible in order for the program to process that command. In this paper we examine the idea of turning the act of collecting parameters from a user into a first class object called a user interface continuation. Programs can create user interface continuations by specifying what information is to be collected from the user and supplying a callback (i.e., a continuation) to be notified with the collected information. ... Furthermore, user interface continuations, like other continuation-passing paradigms, can be used to allow program execution to continue uninterrupted while the user determines a command’s parameters at his or her leisure.”
In our previous example we have used continuations between a service and a view controller. Let's look at another example of the blueBill News API, this time between the controller and the view. First, let's recall the methods exposed by the view:
public interface LockableView { public void lock (@Nonnull UserNotification notification); public void unlock(); }
public interface NewsView extends LockableView { public void bindActions (@Nonnull Action markAllMessagesAsReadAction); public void populate (@Nonnull PresentationModel newsFeedPM); public void notifyFeedUnavailable (@Nonnull UserNotificationWithFeedback notification); public void notifyFeedIsCached (@Nonnull UserNotification notification); public void notifyAllMessagesMarkedAsRead (@Nonnull UserNotification notification); public void confirmToDownloadNews (@Nonnull UserNotificationWithFeedback notification); }
The view has been designed so it exposes methods representing all the possible interactions with the controller. They are typically:
-
requests for rendering data (populate());
-
requests for rendering a user notification (e.g. notifyFeedUnavailable());
-
requests for a feedback from the user (e.g. confirmToDownloadNews()).
There's nothing more, as views are dumb objects: in fact all the logic must stay within the controller. A view implementation is just the binding with the proper UI technology (Swing, Android, etc...) and it's up to this implementation to respect threading constraints. For instance, we can suppose that a Swing implementation will wrap all methods within a EventQueue.invokeLater(). I'll give more details on this in another article.
Now let's focus on the confirmToDownloadNews(), which asks for a confirmation to the user. Again, this method is asynchronous and it has a callback argument that represents the status at this point of the control flow: a UserNotificationWithFeedback, an object which contains something to render on the display (such as the question "Do you want to connect?", etc...) and exposes the methods confirm() and cancel() representing the user response. The view responsibility is to bind this continuation to a dialog box with "Ok" / "Cancel" buttons, so the user response is appropriately conveyed to the NewsViewController. The details of the implementation of UserNotificationWithFeedback will be given in a future article, but we can just recap the exposed operations:
On these premises, the implementation of the interaction by means of the NewsViewController in its method showNewsFeed() is easy and the code is very readable (the funny single underscore _ is just a shortcut for NewsViewController.class, to be passed to a Java resource bundle handler without polluting readability):
private final NewsService.AvailabilityNotifier availabilityNotifier = new NewsService.AvailabilityNotifier() { public void notifyFeedAvailable (final @Nonnull RssFeed newsFeed) { populateAndUnlockView(newsFeed); } public void notifyCachedFeedAvailable (final @Nonnull RssFeed newsFeed) { populateAndUnlockView(newsFeed); view.notifyFeedIsCached(notification().withText(_, "obsoleteNews")); } public void notifyFeedUnavailable() { markAllMessagesAsReadAction.setEnabled(false); view.unlock(); view.notifyFeedUnavailable(notificationWithFeedback().withCaption(_, "unavailableNewsTitle") .withText(_, "unavailableNewsMessage") .withFeedback(new Feedback() { @Override public void onConfirm() { flowController.finish(); } })); } public void notifyFeedCouldBeDownloaded (final @Nonnull NewsService.DownloadConfirmation confirmation) { view.confirmToDownloadNews(notificationWithFeedback().withCaption(_, "confirmDownloadTitle") .withText(_, "confirmDownloadMessage") .withFeedback(new Feedback() { @Override public void onConfirm() { confirmation.confirmDownload(); } @Override public void onCancel() { view.unlock(); flowController.finish(); } })); } }; @Nonnull public void showNewsFeed() { view.lock(notification().withText(_, "preparingNews")); newsService.get().getNewsFeed(availabilityNotifier); }
It's worth while having a look at how tests are implemented. Below there's an excerpt of the test for DefaultNewsViewController, the default implementation of NewsViewController:
public class DefaultNewsViewControllerTest { private DefaultNewsViewController fixture; private NewsView view; private NewsService newsService; private NewsService.DownloadConfirmation downloadConfirmation; private FlowController flowController; @BeforeMethod public void setupFixture() { view = mock(NewsView.class); flowController = mock(FlowController.class); newsService = mock(NewsService.class); downloadConfirmation = mock(NewsService.DownloadConfirmation.class); fixture = new DefaultNewsViewController(view, flowController); } @Test(timeOut=2000) public void showNewsFeed_must_start_downloading_when_the_news_feed_can_be_downloaded_and_the_user_confirms() throws Exception { doAnswer(notifyFeedCouldBeDownloaded()).when(newsService).getNewsFeed(any(AvailabilityNotifier.class)); doAnswer(confirm()).when(view).confirmToDownloadNews(any(UserNotificationWithFeedback.class)); fixture.showNewsFeed(); waitForViewInteraction(); inOrder.verify(view).lock(argThat(notification("", "Loading news..."))); inOrder.verify(newsService).getNewsFeed(any(AvailabilityNotifier.class)); inOrder.verify(view).confirmToDownloadNews(argThat(notificationWithFeedback("Confirmation", "Please confirm that you want to download the latest news."))); inOrder.verify(downloadConfirmation).confirmDownload(); verifyNoMoreInteractions(view, flowController, newsService, downloadConfirmation); } @Test(timeOut=2000) public void showNewsFeed_must_dismiss_the_view_when_the_news_feed_can_be_downloaded_and_the_user_cancels() throws Exception { doAnswer(notifyFeedCouldBeDownloaded()).when(newsService).getNewsFeed(any(AvailabilityNotifier.class)); doAnswer(cancel()).when(view).confirmToDownloadNews(any(UserNotificationWithFeedback.class)); fixture.showNewsFeed(); waitForViewInteraction(); inOrder.verify(view).lock(argThat(notification("", "Loading news..."))); inOrder.verify(newsService).getNewsFeed(any(AvailabilityNotifier.class)); inOrder.verify(view).confirmToDownloadNews(argThat(notificationWithFeedback("Confirmation", "Please confirm that you want to download the latest news."))); inOrder.verify(view).unlock(); inOrder.verify(flowController).finish(); verifyNoMoreInteractions(view, flowController, newsService, downloadConfirmation); } }
FlowController has not been described in this article (will be introduced in future), in any case just keep in mind that it's a navigation controller and its finish() method means that the current view must be dismissed (whatever it might mean with the specific UI technology used for the implementation).
As a general note, this design splits the behaviour implementation from a single class into multiple smaller classes. This is good, but the inconvenience is that to set up a unit test you have to prepare a few more mocks: for instance, mocking NewService is not enough as DownloadConfirmation is needed too. But usually it's not a problem.
The test uses TestNG, Mockito and a few Mockito custom classes for providing some syntactic sugar; in particular, notifyFeedCouldBeDownloaded(), confirm() and cancel() are implementations of Mockito's Answer to mock the behaviour of callbacks. As an example. here's the listing of one of these Answers:
@Nonnull private Answer<Void> notifyFeedCouldBeDownloaded() { return new Answer<Void>() { public Void answer (final @Nonnull InvocationOnMock invocation) { final AvailabilityNotifier notifier = (AvailabilityNotifier)invocation.getArguments()[0]; notifier.notifyFeedCouldBeDownloaded(downloadConfirmation); return null; } }; }
As you can see, the tests are very readable and perfectly describe their associated scenarios. Furthermore, the fact that our classes have no dependencies on a UI technology makes it easier the task of writing (and running) tests without setting up a complicated context.
Another article will give more details on the view implementation.
Opinions expressed by DZone contributors are their own.
Trending
-
Database Integration Tests With Spring Boot and Testcontainers
-
Send Email Using Spring Boot (SMTP Integration)
-
Mainframe Development for the "No Mainframe" Generation
-
Building a Flask Web Application With Docker: A Step-by-Step Guide
Comments