From: Stefan F. <ste...@we...> - 2012-07-08 20:53:23
|
This installment of the series on refactored classes in Rails 2.0 discusses States and Models. Those are the core of both the undo/redo mechanism and the data/view separation of Rails. I will cover the big picture of States and Models here, more details on the specific States and Models classes to follow in later mails and how to use them will follow in later mails. As it is still on the abstract level, most likely only Erik will comment ;-) Stefan *** Object Hierarchy Item (Interface) | Observable (Abstract Class) | | State (Abstract Class) Model (Abstract Class) | | BooleanState MoneyModel ... ... and many more and many more (in State package) (in Model package) (Remark: Observable above is not java.util.Observable) States being Items implies that they are elements inside the Rails Item tree. Example: hasFloated is a BooleanState of a PublicCompany. So it is possible to locate the hasFloated state of PRR from the root node by calling the locate method: root.locate("/companies/PRR/hasFloated"). And this bypasses the fact that hasFloated and/or its getter/setter methods are declared private. This makes implementing the "exception-to-the-general-rule" rules of some 18xx much easier. *** Core ideas of States and Models State classes usually take existing (or simple) constructs and wraps them with the mechanism required for undo/redo mechanisms. Examples are BooleanState (wraps Boolean), HashMapState (wraps HashMap). Model classes have no redo/undo mechanism of their own, they rely on embedded state objects for that. For this Models can add themselves to States using state.addModel(Model m). If the Model is the parent of the State this is done automatically during construction of the State. An example is a CashMoneyModel which embeds an IntegerState. It is also possible that Model itself is based on another Model, an example for this is PresidentModel which relies on the CertificateModel. By allowing those connections States and Models create a directed graph (in this case a forest == many disjoint trees) with states being the root nodes of each tree in the forest. *** Creation of States and Models The creation of States and Models usually uses the following patterns: State.create(Item parent, String id) to initialize with a (sensible) default value. State.create(Item parent, String id, [Object] init) to initialize with a specified initial value. In some cases (especially models) an id is a predefined and the parent class is specialized. Example: PresidentModel.create(PublicCompany parent) Here the id is automatically set to "PresidentModel" and the parent has to be a PublicCompany. *** Observable/Observer mechanism By deriving from Observable State and Model both share the property that they can be observed by other Rails (usually) GUI elements. Those have to implement the Observer Interface and add themselves to States/Models using state/model.addObserver(Observer o) Observers get only updated once per (Action)ChangeSet. Thus we avoid multiple updates of the GUI during the processing of an action. This update only occurs AFTER closing the open (Action)ChangeSet. This is different to when the State itself changes it value: This occurs immediately after the change of the State. The same is true for Models that are connected with those State, they are informed about the new values, thus could and should update their value as well. Examples: 1. hasFloated BooleanState If hasFloated.set(true) is executed the following happens: * hasFloated changes its value to true (thus hasFloated.value() returns true). * A BooleanChange is added to the current ChangeSet to store that change. * However no update is send to a potential Observer. This is delayed until the ChangeSet closes. 2. CashMoneyModel treasury This stores the amount of cash in companies treasury: It embeds an IntegerState called value to store the amount of money. If treasury.change(-100) is executed the following happens: * The IntegerState value is changed to -100. * An IntegerState change is added to the current ChangeSet. * The CashMoneyModel treasury is informed that the value has changed. * However no update is send to a potential Observers. Again this is delayed until the ChangeSet closes. The reason behind this is to avoid multiple updates of GUI elements during the processing of the GameAction. Only after all processing is finished, the Observers of all effected States and Models get informed only once. It is also possible to roll-back the ChangeSet without having intermediate updates of the GUI. This makes a potential client/server communication much more efficient. *** Comparison to Rails 1.x In Rails 1.x the general hierarchy was: java.util.observable | ModelObject | State So State was a specialized ModelObject. This has been changed, so that State and Model have clearly separated roles, but both are still Observables. Another issue was that several State subclasses did not inherit from State, but from ModelObject or from neither. *** Current State of Implementation Observable/State/Model classes are fully specified, implemented and have unit tests added. I consider the java.util.Observable/Observer implementation is not a perfect fit for Rails, I preferred to write my own instead. However the Observer interface could potentially change, as I am stil working on the needs and constraints of the existing GUI. Currently an Observer only provides an update(Observable obs, String text) method. The latter argument provides a text update for the standard cases. I am wondering if I should add a proxy approach, such that the Observer can specify a function which has to be called that can be specific for each Observable observed. Or if I should be more restrictive and allow each Observer to observe ONLY one observable, thus dropping the need to specify the Observable in the interface method. |