About half-way down this page there is an excellent diagram which summarizes very nicely the responsibilities and interactions of the actors in MVC.
I've been teaching myself about MVC by applying it to two little projects of mine. The first is a password saver called Bob. It saves encrypted passwords on a web-server so that you can pick them up wherever you happen to be; it also keeps a local cache so that you still have access to it even if you are not online. The client is a typical desktop app written in Python/Tkinter.
The second is a shopping list builder. You have a list of recipes you can edit, each of which has a list of ingredients. To build a shopping list you choose a recipe, select the ingredients you'll need to buy, and click a button to add them to your list. I like the retro feel of this: back in the day when home computers were new and nobody really knew what to do with them, recipe databases were all the rage! The client in this case is a web page with JavaScript / DHTML list boxes and so forth. It talks to a back-end built on PHP and MySQL.
In this part I'll just be talking about Bob. In the second part I'll talk about what (I think) I've learned about applying MVC to a JavaScript / DHTML web application.
Originally Bob was just a command line script. In fact I can still run it that way if I need to, and the model part of the code is shared. The model is a
class PasswordDB(object):
def __init__(self, server, db_name):
# sets the server and database to sync with
def unlock(self, master_password):
# sets the encryption key to use
def set(self, account, name, password):
# save user-name and password for the account
def get(self, account):
# retrieve user-name and password for this account
def accounts(self):
# get list of all accounts
def sync(self):
# synchronize with the server
Mostly, this just behaves like an associative array mapping accounts to (name, password) pairs. It must be unlocked before get and set can be used. (The list of accounts is not encrypted, just the passwords.) For the command-line client this is all we need.
For the GUI we want a little bit more. We display a list of accounts so that clicking on the account allows us to see the user-name and password. When we add a new password or sync with the server the list of accounts may change and we want it to stay up to date in the UI.
If we just wrote what came naturally, we'd hook code up to the 'Save password' button (or the 'Sync' button) which first called
# non-MVC code
def handleSaveClicked(self): # bound to the Save button
account, name, password = ... # query input fields
self.model.set(account, name, password)
self.view.updateAccountList() # don't forget!
Why is this less than ideal? Simply because it means that every piece of code which modifies the model must also know about the UI. Not just about the account list but about any user-name and password currently on display which could be changed by a 'Sync' operation. Given
There is a much better way, which is to put the model and view into a publish-subscribe relationship. This is how the MV part of MVC works. We extend the model with 'save' and 'sync' events. The view can subscribe to these events by providing a callback which the model will call whenever 'set' or 'sync' are called on it. Here is a simple way to do that in Python:
class PasswordDB(object):
def __init__(self, ...):
self.set_listeners = []
...
def set(self, account, name, password):
...
for listener in self.set_listeners:
listener(account, name, password)
Now the code behind the 'Save password' button needs to do one thing only: call 'set' on the model with the appropriate parameters. The model tells the view and the view updates itself.
The view knows about the model: it has to call query methods on the model ('accounts' and 'get') but neither the model nor anything else which updates it need to know anything about the view. The way a view and a model interact is pleasantly stylized. The view gets a reference to the model. It subscribes to all relevant events on the model, providing callbacks which tell the view to query the model (if necessary) and update itself appropriately.
class AccountListView(Tkinter.Frame):
def __init__(self, model):
model.set_listeners.append(self.handleSet)
...
def handleSet(self, account, name, password):
if account not in self.accounts:
# insert account into the view
For the very simple case, we've written a bit more code than we had before. On the other hand we've conceptually simplified quite a lot. The model is responsible for storing our data, providing methods to query and update it, and telling us when anything has changed. Each view (and there may be several, and/or several components within a view) is responsible for tracking the state of the model. They subscribe to change events and calls query methods of the model.
Which brings us to the Controller. Our view has a 'Save password' button. Before we knew about MVC we'd probably just have the code behind the button get the selected account, and the name and password to save, and call 'model.set()' itself. Is this a problem? Well, maybe. The point is that a lot of different things could happen when we press a button. Suppose we wanted to check if the password was strong enough before letting it through. (Humour me.) We'd have some rather domain-specific code in the middle of our UI code. Another button, the 'New' button, makes the normally read-only account field writable so that a new account can be added; it changes the state of the UI.
We can take these functions out of the UI code and thereby reduce its set of responsibilities. That's a good thing to do! UI code is already (well, depending on the framework you're using, may be) responsible for layout and tracking the model. Rather than trying to do more, when the user does something -- selects a row, clicks a button, presses enter, or whatever -- the view will just fire an event and let someone else deal with it.
Although we don't want to prematurely generalize, notice that this approach gives us a wonderful opportunity to write reusable view components. A scrollable list of accounts is really just a scrollable list of short strings. Rather than give it our PasswordDB directly as its model, we can give it a small adaptor which views the PasswordDB as a collection of strings. All events and query methods can now have completely generic names, and the scrollable list can be used wherever we need such a thing. We can only do this if we don't have to know in the UI code what happens when the user clicks on a row: if it has to call
As with model and view, the key to decoupling is to put view and controller into a publish-subscribe relationship. The view is responsible for firing events to report user interaction. The controller listens for those events; in response it may query the view (what does this input field contain etc.) and then it may update the model or modify the view in some way. Those are the two responsibilities of the controller: model updates and view selection. It can carry out validation of input before updating the model. It can change what the view displays, including such things as moving through a series of screens, causing a modal dialog to be displayed, enabling various controls and so forth.
Note that when the controller updates the model, it does not have to update the view as well; that just happens.
Because the controller has to know about both views and models, it is the least reusable of the three. On the other hand, by divesting it of responsibility for appearance and the underlying data, we have at least minimized the amount of code that has to be so specific. We have also made it testable: by providing it with mock views and models we can write unit tests for it.
Here is what a part of Bob's controller looks like:
class Controller(object):
def __init__(self, model, view):
self.model = model
self.view = view
self.view.save_listeners.append(self.doSave)
...
def doSave(self)
account, user, password = self.view.getAccount() # queries input fields
self.model.set(account, user, password)
So how does it all look in the end:
- The model knows nothing about views or controllers. It is responsible for the integrity of the state it manages and for publishing notifications when the state changes.
- The view knows about a model (which may be a simple adaptor in the case of a generic component). It is responsible for its own appearance, for tracking the model, and publishing notifications about user interactions. It must also provide query methods for any user-modifiable state.
- The controller knows about the model and the view. It is responsible for turning user interaction events from the view into updates on the model and/or changing the view.
No comments:
Post a Comment