System Architecture

From Heureka Wiki
Jump to navigation Jump to search


Architectural Goals and Quality

For Heureka, the two most important quality goals are modifiability and performance. The system should be flexible enough to allow maintenance and further development in a cost effective manner. At the same time performance should be good enough to allow simulation on large amounts of data. To a certain extent, these goals are contradictable.

In addition to this, testability is an important quality aspect. As many parts as possible should be possible to test automatically, e.g. with unit testing tools such as NUnit. This sometimes means that sub functions must be made public so that they can be tested from external code. Over time, this goal has been proven more important than originally expected. This means that this goal has not always been met in the existing code, for instance many classes has a lot of dependencies on other classes making them harder to test.*

Other quality goals are less important:

  • Security. There are no specific security requirements. The system will use security functions available in the database and operating system.
  • Availability. The system is a single user application, so availability is not considered in the architecture.
  • Portability. Portability is not required.

Logical View

Forest model and Calculations

Forest and Calculations.png

The key entity in Heureka is the Treatment Unit, that represents a stand, i.e. an area restricted in space, with the same type of forest. A Treatment Unit is the smallest unit treatments are applied for. Each treatment unit contains one or more predictions units. A prediction unit represents a plot or a cell inside the treatment unit. Each prediction unit contains trees. Trees have different states, identifying if the tree object represents a sapling, a tree, is dead and so forth. Each of these entities handle different time periods.

Treatments are simulated in Heureka with different type of treatment models such as regeneration, fertilization, cleaning, thinning, and final felling.

Growth and yield models calculates tree related values such as volume, basal area growth, height, age, and bark thickness. With help of the growth models, a treatment unit can grow, i.e. the state in a future time period can be calculated.

Result models are used to calculate different types on results. Based on the state of a treatment unit in a given time period, different types of results can be calculated, such as cost and revenue of treatments, amount of biomass in the living forest, or amount of litter added to the ground. A result model may also produce a result based on the results in other result models (e.g. the recreation model uses input from the dead wood model).

Simulation models or frameworks, allows treatment programs to be calculated for a treatment unit. A simulation model utilizes the growth and yield models, the treatment models, and sometimes also result models, to simulate different alternatives for managing a treatment unit. When growth prognosis and treatments has been simulated for a number of periods, the treatment unit (together with its prediction units and their trees and saplings) contains information about the forest state in all periods that were included in the simulation. The results can be saved in a treatment program, which contains results for all periods with the desired result models. Currently, three simulation models are implemented in Heureka: the (strategic) Treatment Program Generator, the Tactical Treatment Program Generator, and the Regional Framework.

An optimization model is used to create a plan. A plan consists of a selection of treatment programs (one for each treatment unit in the plan), that gives the best result according to the optimization model. An optimization model is defined using the results from the result models. Any variable produced by a result model can be used in an optimization model. Optimization models differs from other models in Heureka, these are the only models that are defined by the user (all other models are programmed into the system).

Treatment units may be classified in forest domains. A user may define a forest domain, based on variables from the result models. The forest domain is then used to configure a simulation model.

Data Import

Data Import.png

A TreatmentUnit can be created from a NFI plot (Riksskogstaxeringen). In this case, aTreatmentUnit and a PredictionUnit is created for each NFI plot (if the NFI plot is splitted a TreatmentUnit and a PredictionUnit is created for each part of the plot).

Similarly FMPP data can be imported into Heureka (not shown in figure).

Another option is to import a stand register. From StandObjects in the stand register, TreatmentUnits can be simulated. In this case, trees or saplings are created for the TreatmentUnit, based on stand level variables in the StandObject. The simulation process is stochastic, meaning that the result will be slightly different each time if repeated several times for the same StandObject.

It is also possible to get data to Heureka by making a sample design, choosing StandObjects to be inventoried, and then to make a field inventory using Ivent, to get real plot data and inventoried trees and from that data create TreatmentUnits.

The last option to get data into Heureka is to manually enter data using StandWise (not shown in figure).

All forest data is stored in a relational database.

Projects

Projects.png

A Project contains ControlCategories and ForestDomains. Each ForestDomain refers to a ControlCategory, and each ControlCategory contains a number of ControlTables, each of which contains user settings for a model or a group of related models in the system.

Also, a Project contains the TreatmentUnits that the user is working with (the TreatmentUnits are actually kept in an AnalysisArea, not shown in the figure).

A Project may also contain other items, such as optimization models (not shown in the figure).

The project information is saved as files in the file system.

Design Principles

Layering

Layering.png

The system is built with a layered architecture (see for example Bass et al: Software Architecture in Practice. The layering is based on the system logic, where each layer adds functionality from a lower layer.

The three layers are:

Application Layer
Each application is a separate subsystem. An application connects the domain specific subsystems into the functionality provided by the application.
Domain Layer
A large number of subsystems with different types of domain specific functionality. Each subsystem has its own logic, and in many cases also presentation and persistence services. By having the presentation in the domain layer, UI functionality can be reused between applications.
Base Layer
General purpose support functions.

The purpose of the layering is to achieve modifiability by isolation of changes. Changes in a higher layer does not affect a lower layer at all.

The layering is not strict, meaning that a higher layer can access any lower layer.

A similar layering is proposed in IBM Rational Unified Process, and in Domain Driven Design. The advantage of layering by generality as opposed to layering by type (as proposed by Microsoft) is lower coupling and higher cohesion.

Inside a subsystem, layering by type, i.e. UI -> Logic -> DB should be done (if a subsystem has a presentation and/or persistence service). A subsystem should be able to use the logic of another subsystem without refering to the presentation or persistence service of that subsystem. The persistence service should be encapsulated inside the subsystem and not be used directly by other subsystems. The presentation service of a subsystem should be available for composition of UI.

Unfortunately the subsystem layering has not always been used, in many cases several subsystems access the same tables in the database directly. Some subsystems are also poorly layered, e.g. with database code inside the UI, or UI code inside the logic.

MVC

Linking between user interface and the domain model is done with the architectural pattern Model-View-Controller (Reenskaug 1979).

The model is a domain entity. When the entity is changed, it signals that with an event so that all views rendering the entity can update themselves. The event PropertyChanged is used when a property is changed, the interface IBindingList is used when a list or collection is changed. In some cases other events are also used, in Slu.Heureka.BaseLayer.ComponentModel there are a few more EventArgs that can be used. That namespace also contains the class ExtendedBindingList which is recommended for collections that can be bound.

The view is any kind of graphical control, such as a TextBox, a DataGrid, a TreeView, or a Map. The view can show and change some parts of the entity.

The controller is the object that reacts on user inputs, typically event handlers in a form.

When developing with MVC it is important not to short-circuit the view and the controller when they are implemented in the same class. The event handler should only update the model, and not trigger changes in the view directly.

Dependency inversion

To reduce dependencies between classes and subsystems, should the subsystems that need a particular service define an interface for that service. For example, the Forest subsystem needs biomass to be calculated. To avoid a direct (and circular) dependency from Forest to Biomass, the Forest subsystem declares an interface, IBiomassService, and the Biomass subsystem implements that interface.

With dependency inversation, a new problem arises, how does an object locate the needed services? There are two common approaches to solving this problem: either Dependency Injection is used, or a Service Locator is used. For Heureka, the latter approach has been used.

The subsystem Slu.Heureka.BaseLayer.Services implements a service locator. Key classes in interfaces in that subsystem are:

IService
Interface that must be implemented by all services
Service<T>
Generic class for accessing a service
ServiceAttribute
Attribute that should be set on all services that should be auto loaded
ServiceBase
Base class for implementing a service
ServiceManager
The service locator

The ServiceManager is a Singleton. It supports auto loading of services (all services marked with the service attribute will be loaded at application startup) as well as explicit loading of services (very useful in unit tests).

The Services subsystem was introduced relatively late in the project but has proven to be very flexible and useful. In future development, it is recommended to use it more frequently to reduce dependencies in the system. The recommended approach is that when:

  • A class needs to instantiate other classes (except .Net classes), a FactoryService should be implemented
  • A class needs to use another subsystem, an interface to that subsystem should be implemented

Static classes and singletons should never be used, with the ServiceManager as the single exception to this rule.

Persisting Data

The system uses both the file system and RDBMS:s (SQL Server) to store data.

  • Forest data (stand registers, inventory data, GIS data, treatment units etc) are stored in a RDBMS, the "ForestDb"
  • Calculated results and optimization results (plans) are stored in another RDBMS, the "ResultDb"
  • Project information and templates are stored in the file system (typically in the user's document folder)
  • Application configuration is stored in the file system (typically in the user's application data folder)

Storgate to files are mainly done via serialization. However, a major drawback with serialization is that versioning can be a problem. Also, performance can be an issue. The subsystem Slu.Heureka.BaseLayer.SerializationManagement provides helper classes to resolve some of these issues. The classes SerializationReader and SerializationWriter improves performance drastically. A recommended practice is to always serialize a version number first. By doing this, deserialization of different versions can easily be implemented. A different problem is when a class changes its name or becomes obsolete. A solution for that problem has not been implemented in the Heureka system, but it should be possible to do it using the [|SerializationBinder] class in the .Net framework.

For simple configuration data, the class ConfigurationStore should be used.

For control tables, a helper class that serializes control tables has been developed. Thus it is not necessary to implement serialization for control tables.

For interaction with RDBMS the following techniques are used:

Data readers
For fast loading of objects, mainly used for forest data
Typed data sets
Used when the user is updating data
Custom OR-mappning
Used for the result database

Note that for the result database, custom OR-mapping is used. The result database are created automatically based on the Result Models in the system, and existing result databases will automatically be extended if new result variables and models are introduced in later versions of the system. Queries against the result database are created by SQL queries generated with reflection from the result models.

Error Handling

When an error is discovered an exception is thrown. Built-in exceptions can be used if appropriate, such as ArgumentException, ArgumentNullException, and InvalidOperationException. In other cases Heureka specific exceptions, inheriting from ApplicationException, are used.

Code that depends on exceptions in the normal flow should be avoided. For instance, if a string should be checked for a valid number it is better to use int.TryParse() than int.Parse() as the latter throws an exception of the string not is numeric. When designing classes, consider implementing similar methods allowing clients to determine beforehand if an operation is valid or not.


Exceptions should only be caught if they are to be handled, and only those exceptions that are relevant should be caught. Do not just catch and re-throw exceptions. This leads to poor performance and also destroys the call stack of the exception.

In the user interface there must be an exception handler for all code that might throw exceptions. That code should show an error message to the user if an exception is caught. Use Slu.Heureka.BaseLayer.HeurekaForms.ErrorDialog to show error messages.

The exception message should be brief. If a longer description is needed, set the HelpLink for the exception.

Kommandon

The Command design pattern is used to encapsulate actions that the user performs into separate objects. There are several benefits with this simple pattern, so it should be used frequently:

  • Command makes it possible to implement Undo and Redo
  • Command moves logic from forms into separate classes, and thus reduces the complexity of forms
  • The logic in a Command can easily be used in separate forms and applications

Technically, commands belongs to the presentation services. Commands are allowed to launch dialogs and message boxes, guiding the user through a flow.

Reports

Standrd reports are developed in Visual Studio as Microsoft Client Report Definitions (rdlc). These are shown in the applications with the ReportViewer component.

Help

The applications should have support for online help.

Technical Environment