Recently I’ve been thrust back into web development after a 5 year hiatus. Sadly I’ve discovered that not much has changed. We’re still forcing an inappropriate pattern onto web development.
Why not MVC?
The pattern we all know and love has been great for many years, and certainly in a thick client environment, it and its brethren seem appropriate. In these cases, the UI’s are often being developed by the same folks that are developing the M and the C. With web development, this is often not the case.
On my first significant web project back in 1999, the workflow was a designer delivering HTML files, and then a developer hacking that into a JSP/Servlet combo. This results in your controller and views being tightly coupled together. What happens if the designer needs to deliver completely new screens and workflows? My guess is you’d have to throw out the views and the controllers and start again. How diligent is your team at keeping the controllers thin and free of business logic? Most of us know that’s a good practice, but how often have you seen that violated?
Introducing a new TLA: SVF
Service, View, Flow. The last one is Workflow, but W just has too many syllables. Let’s partition a new way.
A service takes data in, and returns data out. A service might fetch a customer or a customer’s orders, or it might create a new order for a customer. This has been common in software development for some time now, and it continues to make perfect sense in web development. Nice, simple, reusable, beautiful.
A view should only require some data, and be able to render that data. Imagine a view template that specifies the data it needs. A Customer. A list of orders for that customer. The template specifies it needs that info be specifying the services needed. An incoming web request/get doesn’t go to a controller, instead it goes straight for the view. The view will specify what it needs. It will work to render itself.
What about performing operations? Let the form or the link specify a target service and bind data to be passed. Now the designer can compose the views however they want, without impacting code much, if at all.
Flow, or workflow if you like, is the traffic cop. The designer can specify much of the workflow through the views, but occasionally some more complex logic may be necessary. In this case, your workflow can be a simple codified state machine that can determine the next view based on some logic.
What does it mean?
Developers can focus on building small simple services, designers can not only build the views, but actively maintain them over time (as opposed to painful markup merges or outright rewrites). We are minimizing the development of “throwaway” code, and in this case I’m referring to throwaway as code developed for a single purpose of gluing/coupling components together. In the realm of MVC, Controllers are essentially throwaway. To some extent the Views are too, since they are so tightly coupled to their controllers.
To visualize, imagine web services, with HTML documents that interact with those web services directly. All we need is a framework to tie them together.
It seems that Twitter style micro-blogging is the Hello World of web apps, so let’s see what this might look like. We’ll take a look at a user’s home page, which will provide a form for a new micropost, as well as a list of our own posts. Such a page will need two primary services, one for the User, and one for the user’s Posts.
@Fulfill User = User.Current
@Fulfill Posts = Posts.ByUser(User.Id)
<title>Hello Microblogger World</title>
Here, we see a couple template markups in our invented template language that are binding variables to services. First, the service identified by User.Current is bound to the variable User. Likewise, the service Posts.ByUser is passed a parameter (the current users ID), and the results are bound to the Posts variable. These are pre-requisites for displaying this view.
Next, we’ll have a nav bar that includes links to various other views (not controllers, not actions, but the actual views).
<nav class="round" >
<li><a href="@View()" >Home</a></li>
<li><a href="@View('Users')" >Users</a></li>
<li><a href="@View('Users', User.Id)" >Profile</a></li>
<li><a href="@View('Users', User.Id, 'Edit')" >Settings</a></li>
<li><a href="@View('Help')" >Help</a></li>
<li><a href="@Service('User.Logout', 'Login')">Sign out</a></li>
Most of our links are pointing straight at other views. We are maintaining RESTy-ness with the links that would be generated by @View. The final link for signing out actually calls a User.Logout service, and specifies Login as the followup view. By doing this, the designer can maintain control of workflow instead of the developer coding this into a controller.
Now let’s see how we would handle posting a new microblog message.
<h1>Tell me what you're doing:</h1>
<textarea cols="40" id="newpost" name="@Bind('message')" rows="20"/>
<input id="post_submit" name="commit" type="submit" value="Post" />
We’re going to bind the HTML form to a service called Posts.Create. The textarea element is bound to the input parameter on the service, “message.”
Finally, we’ll iterate the user’s own messages
<a href="@View('Users', User.Id)" >
<img class="gravatar" src="...."/></a>
<td class="post" >
<span class="content" >@this.Message</span>
<span class="timestamp" >@this.Timestamp</span>
We build a link much like before, and we iterate over the posts with the @ForEach tag.
We can cleanly build our services now. In this case, we need Users.Current (which would handle the authenticated user), User.Logout, Posts.ByUser, and Posts.Create. These services are very general, and once built the views can be endlessly changed and redesigned and reused without further code necessary.
class PostsService < Service
user_id = request[:user_id]
... fetch return posts for user_id
user_id = context[:user].user_id
message = request[:message]
... create new post for user
It’s interesting to note that these services are practically web services if the inputs and outputs were transformed from/to JSON or XML.
Getting with the Flow
For the sake of discussion, we’ll say that our Help views can be initiated from anywhere in the application. The help contains numerous individual views to assist the user, but when the user clicks a Done button, they should return to the screen they were on when they first clicked Help.
class HelpFlow < Flow
We are changing the roles and responsibilities now. Developers are focusing on developing code to process and return data. Designers are focusing on building the user experience. While offloading some responsibilities from developers, we are adding some for the designers. Communication will be necessary to work out what services will be needed, what data needs to go in and what data is coming out. In other words, the developers and designers will be coming up with an API between them.
Just today the value of this became apparent. The team lead and the designer were trying to figure out how to refactor the partitioning of the views. It turns out that partial views and layouts had been pulled out by some developer along the way to suit his purposes. Unfortunately, how he had partitioned them was damaging and resulted in considerable duplication. If the designer had been wholly responsible for views, they could have properly partitioned the different headers, footers, and so on in a logical manner that would minimize duplication.