For this application, a model-view approach has been followed. The engine of the application has been developped with C++11, while the presentation is fully based on the Qt library.
Sometimes the division may seem artificial (e.g. when having to
continously convert from std::string
to QString
and vice versa), but if hopefully well done, it can allow to use
a completely different library for the view with moderate effort.
I would also hope that Qt would have a deeper integration with the
C++11 and STL standards, but maybe it is soon and convergence will
happen in the future.
Another library is also used to draw the ontology dependency tree: Graphviz. I didn't want again to rely on the graph capabilities and internal structures of the library and depend on any future change. The price to pay is that the same structure is stored twice in memory, first time in the Graphviz library and again in the internal structures of the engine, based on the STL containers.
I have to also mention the lack of a database backend. I guess it wouldn't be difficult to add it if need be. The .loc file if an XML formatted file. The idea was that that file could be edited by a person in case of necessity or, as it is the case in the version of the application at the time of writing this, if certain functionality isn't provided. The most important functionality the application lacks now is dealing with ontologies. At this moment, the only way to change ontologies is editing the .loc file.
To have a general view of the design of the model part of the application
a shallow class diagram is provided. This diagram was produced with
Dia and the file,
locator.dia
can be found in the source directory.
Let's see with an example, in the following sections, the different components of the diagram.
These are perhaps the most important classes of the application. Containers have the ability of holding other objects or containers inside, but they are also objects themselves.
Objects belong to one or several classes. Classes give objects hints about the properties they can define. An example of an object is My dear cat Trino. This object belongs to class Cats. Cats class gives one property: Number of mice eaten by week, but Trino doesn't define it since he is a strict vegan, safe from flies, which he likes the most.
For each property an object defines, there is an instance of the
Datum
class.
Objects can define custom properties that don't come from any class they belong to. Finally, objects are in a location. The main goal of the application is to record and find where objects are.
I think that I've written this application only to be able to write the header of this section. This class represents classes. Shall it represent itself? Fortunately not!
Classes are intended to hold a list of properties that will be provided to objects as hints, just in case they want to store that information.
But classes also form a hierarchy, with more general classes at the top. For example, class Cats can be the descendant of class Animals and sibling of class Dogs. If class Animals has the property (attribute) Height then an object belonging to class Cats, like Trino, will be suggested to fill in its height, if he wants to.
Classes belongs to a ClassGraph that is used to draw the class diagram that shows the hierarchy of classes in the ontology.
Attributes are named properties in the program interface. They express something that can be said about an object. For example, Height can be an attribute. Although classes has associated attributes, objects can also define their own. For example, Trino, the cat, belongs to class Cats and, therefore, belongs to class Animals. Attribute Height is associated with class Animals. Thus, when editing the properties of Trino, a height input is suggested. You can input Trino's height there, but always optionally. Besides, Trino, as an object, can define additional attributes and give values for them. For instance, Trino can define a property: Date of last headache, not quite usual for a cat, but for Trino.
All attributes have a type that defines the kind of value they can hold. Possible types are fixed and quite obvious. For instance, Height attribute is of type length and Date of last headache is of type date
Attributes have types. Although class Type
is subclassed in
different descendants, real types are defined in the static map
types
. An attribute can choose among a limited amount
of types. At the time of writing this, these are: text, weight,
power, money, date, length, rectangle,
and box. This list will surely grow in the future. Each one of
these is an instance of one of the classes of the family of Type
.
Of all subclasses, Measure Type
is perhaps the more difficult
to understand. It holds all types that have something equivalent to
physical units. For instance, typle length belong to class
ScalarType
, which is a one dimensional MeasureType
.
The length can be expressed in several units: meters, inches, and so on.
An attribute has default units but the user can express the data in the
unit he likes more among the available, and conversions are done automatically.
mainUnit
is a property of the ancestor of all this family.
For classes that don't have units, it is the empty string. For classes
that use them, it is the unit in which the stored values are expressed.
For instance, I can express and want my length shown in inches, but
the data is stored internally in meters, if meter is the mainUnit
of length. Although it may seem at first sight that
mainUnit
ought to be a property of MeasureType
,
it isn't the case. There can be units outside this class: currencies, groups
(dozens), or even general classes with arbitrary conversions in
TextType
The picture is completed with this class. Each property an object defines
is an instance of the Datum
class. Imagine that object
Trino(*) defines property Height
to be 43 cm. An instance of Datum
, in this case, more
exactly, of FloatDatum
is created by factory method
createDatum
of class Type
. This instance
can hold a float value that will express the height of Trino in
meters (0.43 m), although it will always be shown in centimeters.
The Attribute
involved is, of course, Height, which
is of type length.
Some data associated with properties have units. One has to consider
model-view structure when talking about units. Regarding the model,
all data stored in files, or eventually a database, is stored in
Type::mainUnit
unit.
Regarding presentation, there is a great deal more of flexibility. First, not only the unit in which I want my property to be expressed can be chosen, but also the number of decimal places, if necessary. And for the unit part, there are several defaults.
First, you can choose arbitrary units for the data you input. But
if not, if the attribute comes from a class the object belongs to,
there is a default unit and decimal places. And if not, the very
Attribute
object provides a default value.
Let's see it with an example: think of an attribute named Height of class Animals. Trino is an animal, so he has that attribute. If I don't leave the height of Trino blank, I can express it in, let's say inches. If I don't set a unit, the default unit would be the default for Height of Animals, for instance, cm. And if Animals doesn't set a default unit, the default unit of the attribute is used. In the case of Height, it could be meters.
The rationale is that the same attribute Height could be reused to record the height of a tower or a mountain and adapted depending on the class that uses it.
Access to model classes from the view is easy. There is a singleton
AppModel::app
, accessible everywhere.
In order to preserve view independence, whenever the model needs to signal something like a change of state to the view, it is done by callback machanics. A class of the view registers one of its methods in the model and, from then on, it will be notified.
An example: MainWindow
object wants to be notified
whenever the application is dirty, i.e. data contents differ
from what is stored in the disk. Here are the steps:
MainWindow
where
objects of the class will be notified:
class MainWindow { [...] public: void dirtyStateChangedCallback(bool dirty); [...] };
MainWindow
constructor, registe it in
the model:
MainWindow::MainWindow() { [...] app.setDirtyStateChangedCallbackp( std::bind(&MainWindow::dirtyStateChangedCallback,this, std::placeholders::_1)); [...] }
AppModel
class, the infrastructure to do this
is present:
class AppModel { [...] private: std::function<void(bool)> dirtyStateChangedCallbackp=0; public: void setDirtyStateChangedCallbackp(decltype(dirtyStateChangedCallbackp) p) {dirtyStateChangedCallbackp=p;} [...] };
class AppModel { [...] private: bool dirty=false; public: bool isDirty() const {return dirty;} void setDirty(bool b=true) {dirty=b; if (dirtyStateChangedCallbackp) dirtyStateChangedCallbackp(dirty);} [...] };
With all this we get view independence. We can use whatever library for the view and register the relevant methods to be notified. An easy alternative would be to use the nice SIGNAL-SLOT mechanics of Qt, but that would tie the application to the library.
_________________________