In our company, managers used Excel to plan the operational load of employees. There was no need to use something complex like MS Project. But for some time the tables have ceased to meet their requirements, and the expansion of the capabilities of scripts embedded in spreadsheets is not the most pleasant task.
Therefore, we were asked to write an application. But it is known that they first ask for a tractor, and in the end it turns out that they need a fighter. So we decided to approach the development creatively. In this article I want to tell you what we managed to create in the end, which architecture we chose and what advantages it has.
At first it was planned that the basis of the Tula interface would be two tables: employees and projects. You can drag employees to projects with the mouse, thereby assigning working hours.
')
It was decided to write the application on Flex (+ AIR runtime). The framework contains everything we need. The AdvancedDataGrid component serves as the basis for employee and project tables. It is very flexible thanks to renderers and has convenient drag-n-drop support. AIR has the support of multi-window applications and can work with files.
To begin to highlight the basic requirements.
Primary requirements
1. Flexibility to customize the view
From the very beginning of development, ideas for improving the convenience of the interface began to occur. Group projects by customers, add drag and drop links between projects, drag employees and projects into folders, then there was a wish to open several documents simultaneously in different windows. It became clear that it was necessary to separate the presentation from the control logic as best as possible and to ensure its flexibility.
2. The ease of independent inclusion of different logical blocks and the reliability of the transfer of commands between them
Surely they will then want to support hot buttons, new data storage formats, and other data sources, such as server, export, import, history, and a bunch of other additional functionality. So it happened.
3. The architecture should provide reliable operation.
Adding new features should be as simple as possible to avoid new errors. It should not affect the already implemented functionality, if this is not required explicitly.
For the development of the requirements obtained, it would be possible to use a ready framework based on events, such as mate, but when you know the patterns, it is more interesting to assemble the system yourself.
1. Flexibility to customize the view
We use a scheme similar to MVC. We have 3 components similar to the model, view and controller:
DataBuilder ,
View and
Engine .
DataBuilder (model) stores data and provides the basic logic of working with them. Only he knows how to create a data element from the initial set of parameters. DataBuilders are used by all engines as data element factories. This ensures that elements are created correctly using the same method.
View has access to dataBuilder's data. For convenience, this data is stored in static variables of dataBuilders, and is accessible from anywhere. Also the view is updated at the command of its engine and displays the data as it should. In addition, the view sends commands to the engines in response to user actions.
Engine (controller) deals with business logic.
It turns out the following scheme of work:
Thus, there are 2 dataBuilder-engine-view triples: for employees and for projects. Also in the architecture there are systems where view or dataBuilder are missing. This will be written below.
2. The modular structure of the program with a reliable connection between its parts
It is necessary to achieve the ease of independent inclusion of different engines and, at the same time, the reliability of the transfer of commands between them.
Implement the Chain of responsibility pattern. Engines will be connected in a chain. The first engin in the chain is sent commands that advance along the chain to their addressee.
First we add to each endgin an identifier
public static const TYPE: String . All identifiers must be different. Their uniqueness is verified in the factory factory, through which they must be created.
Commands will be passed using the
CommandEvent class inherited from
Event . It has a
commandType: String field to which the type of the engine is transferred to which this command is addressed. Also, the event has a set of fields to refine the command, so that the engineer can perform various actions. For example, adding an item, changing some of its properties, updating your
view , reading or writing to a file.
Now we collect the chain. All concrete endgins are inherited from the AbstractEngine abstract class, which has several specific fields and methods.
-
Next stores a link to the next item in the chain.
- The
activate (event) abstract method. It is redefined in subclasses where specific behavior is implemented for each engine.
- The
handle (event) method checks if this engine is processing a command with the incoming type. If yes,
activate () is executed
; if not, the
handle () method of the next engine in the chain is called.
Thus, in order to transfer a command to a chain, it is only necessary to call
handle () on the root element. In order for the chain to work, the
Application root engine subscribes to the
CommandEvent :
rootEngine.addEventListener (CommandEvent.ACTIVATE, rootEngine.handle);
All the blocks used in the application are shown in the picture below.
As already mentioned, there are 2 engine-dataBuilder-view triplets: for employees and for projects.
There is a system without data:
CommandPanel (view) and
CommandEngine to it. This is the panel at the top of each window. She only deals with sending commands.
There is a system without
view : for objects that define the relationship between the employee and the project. This is a pair of
BindsBuilder-BindsEngine . The relationship view is included in both the
EmployeesView and
ProjectsView .
Finally, there are several engines without views and data:
InputOutputEngine (working with XML files),
CSVEngine (working with tables),
ShortCutsEngine (working with hot keys).
This architecture makes it easy to add new blocks. For example, at first it was not supposed to do support for external formats. But by the end of the development of Tula, the need to add the export / import function of CSV was revealed. To do this, add
CSVEngine to the chain. It is almost the same as
InputOutputEngine , only it contains a different data parser. Then we add buttons to the
Command Panel with sending the appropriate events. Now the application can work with CSV!
Since the whole system works asynchronously, there are no big obstacles for the realization of the possibility of receiving data from the server.
The merits of our architecture
First, for the chain to work, it’s enough to sign the root engine for the event. After that, you can turn on / off engines simply by adding and removing them from the chain.
Secondly, each engine can modify the command passing through the chain, which gives greater flexibility. Any engine can send several additional commands to other engines before skipping the current command down the chain. In this way, one command can trigger a sequence of actions. For example, a command to open a new file may save the current one, read the new one, parse it, and then update the view.
Another advantage of this architecture is to simplify the development process. The logic of each engine is encapsulated. Due to this, several people can work on one application at once without creating conflicts.
It is very easy to send commands to the chain from the objects of the display list.
An event with
bubbles == true pops up to
Application and enters the chain. Here is the scheme of work, where it is assumed that the user clicks a button in one of the
view :
3. Support history and hotkeys
How in modern software without the possibility of undoing the last action?
The architecture makes it easy to add support for the story.
At first, the story was made on the basis of the Memento pattern. The state of this application after each action is uniquely converted into an xml object that can be saved in Memento. But it turned out that in the case of real data a full update of the xml tables is too slow, about 1-2 seconds. Therefore, we had to use a more complicated way: to create and memorize reverse actions to each of the user's actions, and then call them sequentially when pressing undo.
The history manager stores sets of direct and reverse actions in two arrays. When Engines receive commands, they add direct and reverse commands to their history. Now, when you click undo or redo, it is enough to take the necessary command from the array and call
dispatchEvent () with it. All engines will take it as a regular command that comes from the view, and perform the appropriate actions.
Working with
CommandEvent is very similar to the Command pattern. The difference is that in the “receiver” pattern it is encapsulated into a command, and in
CommandEvent the command only has the type according to which the engine (playing the “receiver” role) determines whether to process it or not. In principle, any engine can start processing other commands if you add them manually using
addHandableCommand () .
Hotkeys are implemented just as easily. Their manager stores instances of the
Shortcut class, which contain a
CommandEvent and a key combination that invokes this command. When you click on the buttons viewed combinations. If the combination is in the array, the corresponding task is performed.
The application with the source code and instructions lies on the Google code:
code.google.com/p/easyworkloadtool
findings
We have designed an application that fully meets the requirements. It works reliably and has almost unlimited potential to expand and complement new features. Its architecture makes it convenient to develop and maintain it both for a single programmer and for a team, since each employee can work independently with others on his own engine.
The described architecture can be used in the development of virtually any client application. With various modifications, it is successfully used in a number of our projects.