Model-View-Adapter (MVA, Mediated MVC, Model-Mediator-View)

Motivation

Model-View-Adapter is a variation of the Triad where all communication between
Model and View must flow through a Controller, instead of interacting directly
as in a Traditional MVC Triad. The Controller becomes a communication hub,
accepting change notifications from Model objects and UI events from the View.

This approach might appear excessively strict, but has some advantages: the
communication network is artificially constrained, making it easier to evaluate
and debug. All action happens in the Controller, and the View can be created
from off-the-shelf widgets without any Model-specific variation.

Design

MVA is an implementation of the Mediator pattern. Controllers are
generally referred as Adapters or Mediators. The Model and the View
do not hold references to each other, they do not exchange data nor
interact directly.


Model View Adapter - 图1

The pattern of communication in MVA can be represented with the following
interaction diagram


Model View Adapter - 图2

which can be described as

  1. The View receives a UI event. It calls an appropriate method on the Controller.
  2. The Controller sets the value on the Model.
  3. The Model notifies its listeners of the change, among which is the Controller itself.
    As already pointed out, in a MVC approach this notification would be sent to the View.
  4. The notified Controller fetches information from the Model and updates the View.

With the Controller in full control on the dialog between the two remaining
parties, smart tricks can be performed on the “in transit” data: for example,
the Controller could be responsible for formatting, translating or ordering
the data from the Model.

Practical Example 1: With Subclassing of the Views

common in Apple OSX Cocoa Framework.

We examine a MVA implementation of the Engine example in two steps. In the first step, we will keep subclassing the Views, to present the general concept. In the second step, we will remove the need for subclassing the Views.

The Model is unchanged: it stores rotations per minute information and notifies about changes

  1. class Engine(BaseModel):
  2. def __init__(self):
  3. super(Engine, self).__init__()
  4. self._rpm = 0
  5. def setRpm(self, rpm):
  6. if rpm < 0:
  7. raise ValueError("Invalid rpm value")
  8. if rpm != self._rpm:
  9. self._rpm = rpm
  10. self._notifyListeners()
  11. def rpm(self):
  12. return self._rpm

The two View classes, Dial and Slider, are now unaware of the Model. Instead,
they know about the Controller, and accept changes to their content through the
setRpmValue() method. A matter of taste can decide the semantic level of this
method. Should it talk “domain language” (i.e. Rpm) or not (i.e. the method
should just be named setValue). In any case, Views behave differently with
respect to the issued value (the Slider needs its value scaled appropriately).
One could handle this difference in the Controller, but this will be considered
in the next step.

When the user interacts with the Dial, the Controller
changeRpm() method is directly invoked, in this case via the Qt Signal/Slot
mechanism

  1. class Dial(QtGui.QDial):
  2. def __init__(self, *args, **kwargs):
  3. super(Dial, self).__init__(*args, **kwargs)
  4. self._controller = None
  5. self.setRange(0, 10000)
  6. def setRpmValue(self, rpm_value):
  7. self.setValue(rpm_value)
  8. def setController(self, controller):
  9. self._controller = controller
  10. self.connect(self, QtCore.SIGNAL("valueChanged(int)"),
  11. self._controller.changeRpm)

For the Slider, the interface is the same, but the internal implementation is
different. Again, the setRpmValue allows the Controller to change the
View contents. In this case however, a proper transformation of the data is
performed to deal with the specifics of the Slider behavior, whose range is
from 0 to 10. Similarly, when the User interact with the Slider, the method
_valueChanged will be invoked, which in turn will issue a call to the
Controller changeRpm() method, after transformation of the parameter

  1. class Slider(QtGui.QSlider):
  2. def __init__(self, *args, **kwargs):
  3. super(Slider, self).__init__(*args, **kwargs)
  4. self._controller = None
  5. self.connect(self, QtCore.SIGNAL("valueChanged(int)"),
  6. self._valueChanged)
  7. self.setRange(0,10)
  8. def setRpmValue(self, rpm_value):
  9. self.setValue(rpm_value/1000)
  10. def setController(self, controller):
  11. self._controller = controller
  12. def _valueChanged(self, value):
  13. if self._controller:
  14. self._controller.changeRpm(value*1000)

The Controller class handles the Model and the two Views accordingly. It
registers for notifications on the Model, and it receives notification from the
Views on its changeRpm() method, where it modifies the contents of the Model.
When the Model communicates a change, it pushes the new value to the Views

  1. class Controller(object):
  2. def __init__(self):
  3. self._views = []
  4. self._model = None
  5. def setModel(self, model):
  6. self._model = model
  7. model.register(self)
  8. def addView(self, view):
  9. view.setController(self)
  10. self._views.append(view)
  11. def changeRpm(self, rpm):
  12. if self._model:
  13. self._model.setRpm(rpm)
  14. def notify(self):
  15. for view in self._views:
  16. view.setRpmValue(self._model.rpm())

Practical Example 2: With generic Views

In the previous example, removing the dependency of the Views on the Model did not allow us to use generic Views. In this example we will move the value translation logic
from the Views to the Controller, thus removing the need for subclassing the Views, an important advantage of MVA.

  1. class Controller(object):
  2. def __init__(self, model, slider, dial):
  3. self._slider = slider
  4. self._dial = dial
  5. self.connect(self._slider, QtCore.SIGNAL("valueChanged(int)"),
  6. self._sliderChanged)
  7. self.connect(self._dial, QtCore.SIGNAL("valueChanged(int)"),
  8. self._dialChanged)
  9. self._model = model
  10. model.register(self)
  11. def changeRpm(self, rpm):
  12. if self._model:
  13. self._model.setRpm(rpm)
  14. def notify(self):
  15. for view in self._views:
  16. view.setRpmValue(self._model.rpm())

Practical Example 3: With forwarding of Model events

If the Model emits qualified events, the Controller could simply forward them
to the View.