Introduction
Note: For a more technical description of Models see BlueprintEngine.
A Model
is a formal description of an object which includes
ModelAttributes
ModelActions
ModelStates
A Model
can (and must) derive from other Models
, but only from one Model
at a time. The Model
from where we derive is called the Parent. Each Parent in turn has one other Parent. By deriving from a Parent a Model
tells us, that it shares some similarity with it.
Any customer (or Tenant
which is the technical term in Appclusive to represent a separate legal entity in the inventory) has a name and domain name which serves as the base identifying information for all objects.
Let's try an example (where we work as the Tenant
example.com):
Presume we want to model a Circle and a Square. Now these two objects do not have that much in common, except that both have an common ModelAttribute
named Area (in the unit acres, as we have very large shapes). We would therefore define a Shape as the basis for our Circle and Square.
Shape
So we can start defining a Model
Shape like this:
public class Shape : BaseModel
{
}
In this case our Shape is a completely new Model
that does not want to have any similarities with other Models
. But as any Model
must derive from a Parent it derives from BaseModel
which serves as a common base for all models (so they can be identified as such). Deriving from a Model
is always expressed by specifying the Parent after our new Model
delimited by a colon (:
).
ModelAttributes
In order to define our common ModelAttribute
Area we extend the Shape like this:
public class Shape : BaseModel
{
[Unit("acre")]
[AttributeName("com.example.Shape.Area")]
public float Area { get; set; }
}
Here we notice a few interesting things:
public float Area { get; set; }
By describing our Area as public we tell Appclusive that the ModelAttribute
Area should be visible everyone. In addition, we make the Area a float so it can contain floating point numbers (like 1.32). If we only wanted odd or even numbers we could have defined Area as int. This float is called the Type of the ModelAttribute
. Furthermore, to have the Area visible to everyone we tell Appclusive that it is potentially readable and writable (as long as we have enough permissions to do that - remember: Appclusive has a fine grained permission model).
Most of the time we use public and { get; set; } in our ModelAttributes
, we nevertheless have to write it down.
[AttributeName("com.example.Shape.Area")]
As Area is a relatively common name we want to uniquely describe that ModelAttribute
, so others can identify it was defined by us. We therefore mark Area with a full qualified name (or FQCN for short) that is constructed from our Tenant
name example.com. At Appclusive these names are always created in reverse order (we use com.example.Shape.Area instead of example.com.Shape.Area, so it can be nicely ordered by each Tenant
name). By using the Tenant
name as a name prefix (which is unique throughout the system) we guarantee uniqueness of the ModelAttributes
as well.
[Unit("acre")]
As noted before we want the Are to be calculated and count in acres so we give a hint to any user and possible designer be specifying a unit on our ModelAttribute
. Of course this is optional and we do not have to define a unit.
What we learned so far:
Any ModelAttribute
we define on a Model
consists of at least two elements/lines: an AttributeName
and type definition and its visibility.
Circle and Square
Now back to our example. We actuall want our additional shapes Circle and Square, so let's define them:
public class Circle : Shape
{
}
public class Square : Shape
{
}
Again this looks familiar to the definition of Shape. But there is a difference: as we want to derive from Shape we use Shape
instead of BaseModel
after the colon. And by deriving from Shape we tell Appclusive that we want to use the ModelAttribute
Area as well on our two new models.
ModelAttributes
Now as our two models are different we define additional ModelAttributes
on them:
public class Circle : Shape
{
[AttributeName("com.example.Circle.Radius")]
public float Radius { get; set; }
}
public class Square : Shape
{
[AttributeName("com.example.Square.Length")]
public float Length { get; set; }
}
There is nothing new in that for us. We used the same mechanism as with our basic Shape.
ModelActions
Now we realise that we want to be able to resize our shapes, and we therefore want to add a common ModelAction
to all shapes. We therefore extend Shape once more:
public class Shape : BaseModel
{
[Unit("acre")]
[AttributeName("com.example.Shape.Area")]
public float Area { get; set; }
public class Resize : ModelAction
{
}
}
As we can see, all we need to do is to define our Resize ModelAction
(followed by a colon (:
) and the name ModelAction
) and Appclusive knows we want a ModelAction
in that model. Even better, now every model that derives from Shape also has that same ModelAction
.
But wait, something is missing: if we want to resize something, we have to know by what extend we want to resize it. So let's add another ModelAttribute
, but this time inside the ModelAction
directly:
public class Shape : BaseModel
{
[Unit("acre")]
[AttributeName("com.example.Shape.Area")]
public float Area { get; set; }
public class Resize : ModelAction
{
[Required]
[AttributeName("com.example.Shape.Resize.NewSize")]
public float NewSize { get; set; }
}
}
Note: in order to force our users to supply a value for NewSize when calling the Resize ModelAction
we mark the ModelAttribute
as being Required
. Appclusive will not process any Resize requests unless the user specifies a value for NewSize.
This is all good and fine but what does NewSize mean for a Circle or a Square? The answer to this is: it depends. For the Circle we decide to use NewSize as the new Radius and for the Square we decide to use NewSize as the new Length.
This is something we call polymorphism. Both, Circle and Square, are a Shape, which means they have common ground. But in detail they might be more specialised and differ in their use and operation. But as long as we know how to resize a Shape (i.e. knowing the name of the ModelAction
and its ModelAttributes
) we will also know how to resize any other derived shape.
Advanced Modelling
After happily playing around and resizing our shapes for a while we realise we would also like to have a new Rhomboid Model
instead of only a Square. So what do we do?
We define a new Model
that also derives from Shape:
public class Rhomboid : Shape
{
[AttributeName("com.example.Rhomboid.Length1")]
public float Length1 { get; set; }
[AttributeName("com.example.Rhomboid.Length2")]
public float Length2 { get; set; }
}
Overriding ModelActions
We gave the Rhomboid two ModelAttributes
Length1 and Length2 so our it can have two differing lengths. But what about our Resize ModelAction
? It only has a single NewSize ModelAttribute
. How do we resize both ModelAttributes
of our Rhomboid? The answer is simple: We override the existing ModelAction
:
public class Rhomboid : Shape
{
[AttributeName("com.example.Rhomboid.Length1")]
public float Length1 { get; set; }
[AttributeName("com.example.Rhomboid.Length2")]
public float Length2 { get; set; }
public new class Resize : Shape.Resize
{
[AttributeName("com.example.Rhomboid.Resize.Length2")]
public float Length2 { get; set; }
}
}
Here we see that we derived the existing ModelAttribute
NewSize from the parent Shape as Length1 (as we did not specify it at all) and added a new ModelAttribute
Length2. The former attribute was marked as Required
(on its Parent) and Length2 is effectively optional (as it is not marked as Required
). Our Rhomboid will then use the value NewSize for Length1 and Length2 if no "Length2 was specified by the user. The benefit of this is that any user who knows how to Resize a Shape still knows how to resize a Rhomboid*.
And now we see that we can further re-model our existing Square as it has something in common with Rhomboid:
public class Square : Rhomboid
{
public new class Resize : Rhomboid.Resize
{
}
}
Here we see that the definition of our Square has been simplified. We do not need any special Length property any more and the Resize ModelAction
is directly taken from the Rhomboid. A user who knows a Shape and only supplies the NewSize ModelAttribute
can resize a shape and a user who supplies the second ModelAttribute
Length2 from the Rhomboid Model
can also resize a Square (of course we will have to inform the user if both of the lengths do not match as a Square must have all the same lengths).
By this we can see that we can simplify and re-use components if we design our Models
carefully.
This example could be further extended by supporting a Rectangle as well (which would lead to yet another Parent in our Model
chain).
ModelStates
WIP
Behaviours
WIP should be better described in a separate file