Various aspects of controller data dependency in ROBOTLEGS
The problem
Several weeks ago, while implementing one sick game...:)... I dealt with a minor mind-job that I try to explain now. It's about Command's role in Model-Controller relationship of the MVCS architecture.
Commands, according to the original design pattern and RobotLegs documentation, should serve as a short-term clients, encapsulating the operational logic of our application. Commands are often used for calling an injected service and that service, according to Robotlegs diagram, should directly pass the data payload into a Model. Commands shouldn't be used as mediators between Model and Service, but for handle the Service only. This also cleans Command from responsibility of carrying data from Service to Model like in the following example of downloading simple XML file and store its content into the Model:
public final class CommandExampleLoadXML extends AsyncCommand { [Inject] public var _service:IXMLService; [Inject] public var _filename: String; // Signal payload public override function execute():void { _service._onSuccess.add(_onSuccessHandler); _service._onFault.add(_onFaultHandler); super.execute(); _service._load(m_filename); } private function _onSuccessHandler():void { _removeListeners(); } private function _removeListeners():void { _service._dispose(); } }
And the corresponding service:
public final class ServiceExampleLoadXML implements IService { [Inject] public var _model:IModel; private const m_success:ISignal = new Signal(); // NO PAYLOAD! private const m_fault:ISignal = new Signal(); private const m_loader:URLLoader = new URLLoader(); public function _load(filename:String):void { m_loader.addEventListener(Event.COMPLETE, _onComplete); m_loader.addEventListener(IOErrorEvent.IO_ERROR, _onFault); m_loader.dataFormat = URLLoaderDataFormat.TEXT; m_loader.load(new URLRequest(filename); } private function _onComplete(event:Event):void { m_model._setData(XML(event.target.data); // DATA SET TO MODEL HERE m_success.dispatch(); } private function _onFault(event:IOErrorEvent):void { m_fault.dispatch(); } public function get _success():ISignal { return m_success; } public function get _fault():ISignal { return m_fault; } public function _dispose():void { m_success.removeAll(); m_fault.removeAll(); m_loader = null; } }
What we can see in this example is that Command handles the Service, but the data flow only happens directly between the Service and the Model. No payload is attached with the success signal. So this is the "recommended" way. It lacks the Command's responsibility for handling the payload making it clear and simple. But it ties the Service with the Model.
Note: If you are not familliar with Signals, take a quick look to understand all the examples.
Another view
When I was talking about this principle with my colleagues, we found a perspective of using command as the payload handler. It might come handy when we want to have highly reusable services and independent models at a time. We implement the service without internal model dependency and leave the Command as the bridge for the payload exchange. It seems logical, right? But... There is the dilemma. Bond the service with the particular model or with a particular payload+command instead? Let's see an example:
public final class CommandExampleLoadXML extends AsyncCommand { [Inject] public var _service:IXMLService; [Inject public var _model:IModel; [Inject] public var _filename: String; // Signal payload public override function execute():void { _service._onSuccess.add(_onSuccessHandler); _service._onFault.add(_onFaultHandler); super.execute(); _service._load(m_filename); } private function _onSuccessHandler(payload:XML):void { m_model._setData(payload); // DATA SET TO MODEL HERE _removeListeners(); } private function _removeListeners():void { _service._dispose(); } }
And the altered service:
public final class ServiceExampleLoadXML implements IService { private const m_success:ISignal = new Signal(XML); // PAYLOAD NOW! private const m_fault:ISignal = new Signal(); private const m_loader:URLLoader = new URLLoader(); public function _load(filename:String):void { m_loader.addEventListener(Event.COMPLETE, _onComplete); m_loader.addEventListener(IOErrorEvent.IO_ERROR, _onFault); m_loader.dataFormat = URLLoaderDataFormat.TEXT; m_loader.load(new URLRequest(filename); } private function _onComplete(event:Event):void { m_success.dispatch(XML(event.target.data); } private function _onFault(event:IOErrorEvent):void { m_fault.dispatch(); } public function get _success():ISignal { return m_success; } public function get _fault():ISignal { return m_fault; } public function _dispose():void { m_success.removeAll(); m_fault.removeAll(); m_loader = null; } }
Final view
The first (and according to the docs the proper) way messes the design of the Service and make it closely tied to the particular Model. I assume that in clean design we all want to have reusable services. To achieve that, we have to look at the Service as a pair of Service-Signal and take this option as generic and final solution in most cases. From my own experience, the cleanest way is to return the payload within a Signal and set the data in Command's success handler into a Model. You can also post-process the data here or use another service to do so. Nothing more. Clean and reusable.
References
Comments
Please feel free to leave a comment related to the topic. Posts violating the netiquette will be deleted.
comments powered by Disqus