Types
Problematic
Most (if not all) software projects use data. They need to structure it as well as validate it.
Information structure
All software projects structure its information. It is necessary to describe, manage and distribute it.
There is a lot of means to write this structure. Here some cases in different languages that could be found in projects:
case class Article(
title: String,
content: String,
tags: Seq[String]
)
interface Article {
title: string
content: string
tags: Array<string>
}
class Article {
private $title;
private $content;
private $tags;
public function __construct(string $title, string $content, array $tags) {
$this->title = $title;
$this->content = $content;
$this->tags = $tags
}
public function getTitle(): string {
return $this->title;
}
public function setTitle(string $title) {
$this->title = $title;
}
// other getters and setters
}
public class Article {
private final String title;
private final String content;
private final List<String> tags;
public Article(String title, String content, List<String> tags) {
this.title = title;
this.content = content;
this.tags = tags;
}
public String getTitle() {
return title;
}
// other getters
}
Of course, each solution can be used in other languages and is not always representative of its developer community.
All of these solutions do the same thing: describe some information as a list of attributes. Some solutions are more verbose because of the language restrictions or conventions.
How could we see that these elements are data structures?
There is a list of attributes
All attributes are accessible from outside (directly or indirectly)
For each attribute, the same pattern appears: attribute definition, declaration inside constructor, getters and setters (when a pattern is inexistent, it is inexistent for all attribute)
So we have a common pattern. We could use for instance the version with the least characters to write.
Information validation
We looked at how to structure the information. For most of the structures, we can pass to the language syntax. However, there is another notion to look at.
Look at the following code:
case class Period(
start: Date,
end: Date
)
What is the issue about this code? Should we consider this type valid as soon as we have two dates?
To ensure the data validation, we could do in several ways:
class Period private (val start: Date, val end: Date)
object Period {
def apply(start: Date, end: Date): Either[String, Period] = {
if (start.isBefore(end)) {
Right(new Period(start, end))
} else {
Left("start > end 😱")
}
}
}
interface Period {
start: Date
end: Date
}
function isPeriodValid(period: Period): boolean {
return period.start < period.end
}
class Period {
private $start;
private $end;
public function __constsruct(Date $start, Date $end) {
if ($start->isBefore($end)) {
$this->start = $start;
$this->end = $end;
} else {
throw new Exception("start > end 😱")
}
}
public function setStart(Date $start) {
if ($start->isBefore($this->end)) {
$this->start = $start;
} else {
throw new Exception("start > end 😱")
}
}
// other setters and getters
}
class Period {
private Date start;
private Date end;
public Period(Date start, Date end) {
if (start.isBefore(end)) {
this.start = start;
this.end = end;
} else {
throw new RuntimeException("start > end 😱")
}
}
// getters
}
One more time, you can use some solutions in other languages.
It answers our question of how to validate data consistency. However, did you see that we check that start < end
and not that start <= end
? What happens if this restriction was added afterward and some data in your database are now invalid (where start == end
) but you need to accept the value for any reason? Your code and your data are not compatible anymore and will fail each time you query these values.
To avoid this case, you will surely define another function to deserialize data from your database and remove the validation. It is not an issue as long as you accept it. However, it creates some boilerplate that could be avoid.
Type syntax
Type declaration
You can declare a type by writing the following:
type Article {
title: String
content: String
tags: List[String]
}
You can also give an alias to an existing type:
type ArticleId = String
For more information about attribute declaration, please refer to part Attributes.
Verification reference
If you want to specify a verification that your type must validate, you can do it with a verification reference:
type Period verifying IsValidPeriod {
start: Date
end: Date
}
// Could also be in another file
verification IsValidPeriod {
"start > end 😱"
(period: Period) => {
period.start.isBefore(period.end)
}
}
You can also do it with alias types:
type NonEmptyString = String verifying IsNonEmptyString
verification IsNonEmptyString {
"Please give a non empty string"
(value: String) => {
value.nonEmpty()
}
}
Please see page Verifications for more information about using them.
Inline verification
If you want to define the verification inside the type, you can also do it:
type Period {
start: Date
end: Date
verify {
"start > end 😱"
(period) => {
period.start.isBefore(period.end)
}
}
}
Or with alias types:
type NonEmptyString = String {
verify {
"Please give a non empty string"
(string) => {
string.nonEmpty()
}
}
}
Note that you do not need (and cannot) to give the type of the verification parameter. It is induced by the surrounding type.
Type parameters
If you want to declare parameters to your types in order to reuse their structure and logic, you can do it this way:
type User(minAge: Number) {
name: String
age: NumberHigherThan(minAge)
roles: ListOfMaxSize[String](5) // Maximum 5 roles
}
type NumberHigherThan(min: Number) = Number {
verify {
message("number.higher.than", Number, Number)
(number) => {
if (number > min) {
ok
} else {
ko(number, min)
}
}
}
}
In concrete usage, top-level structures has few value to have type parameters. They mostly come in handy for alias types to increase the level of abstraction of common elements (NumberHigherThan
, ListOfSize
…).
Type verification parameters
You can also specify that you type verification need an external value. In this case, you will declare a named verification with parameters.
type Order {
id: String
items: List[OrderItem]
verify withConfiguration(orderIdFormat: String) => {
"The id should respect the configured format"
(order) => {
order.id.matches(orderIdFormat)
}
}
}
type OrderItem {
product: String
quantity: Number
verify withConfiguration(allowedQuantity: Number) => {
"The quantity is too important"
(orderItem) => {
orderItem.quantity <= allowedQuantity
}
}
}
When you check the full aggregate of Order by using withConfiguration
, all verifications with this name will be included into the verification. So in above code, both withConfiguration
of Order
and OrderItem
will be call.
The top-level function in targeted language will require all parameters identified by their name. An error will occur if you use a name with different type in the same aggregate.
Type parameters or type verification parameters?
You could do what you want in both cases. However, these features were designed for two different cases:
Type parameters were designed for static invariants inside code, top-level aggregates should not have type parameters
Type verification parameters were designed for external parameters (configuration, user settings, options taken from an API…)
When you use one or the other, please think about where the value comes from and why you define this parameter.
Attributes
We talked about types but not their attributes specifically.
Attribute declaration
Their default usage is:
type Article {
title: String
content: String
tags: List[String]
}
Attribute verifications
You can add verification references to attributes:
type Article {
title: String verifying IsNonEmptyString // Will use default error message
content: String verifying IsNonEmptyString("Please give a content")
tags: List[String] verifying IsListOfMinSize(3) verifying HasNoDuplicatedElements
}
If you want to add a constraint to each tag, you need to define an alias type:
type Article {
title: String
content: String
tags: List[Tag]
}
type Tag = String verifying IsValidTag
Also, if alias type has parameters, you can specify them:
type Article {
title: String
content: String
tags: ListOfMinSize[Tag](3)
}
Attribute usage
In the rest of the code, to access to an attribute, just name it:
type Period verifying IsValidPeriod {
start: Date
end: Date
}
// Could also be in another file
verification IsValidPeriod {
"start > end 😱"
(period: Period) => {
period.start.isBefore(period.end)
}
}
All attributes are public and immutable.
Enumerations
Like most language, Definiti has an enumeration system.
You can describe them like:
enum Days {
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday
}
Line returns are not required. You can also write the enumeration like:
enum Days {
Monday Tuesday Wednesday Thursday Friday
Saturday Sunday
}
You can compare enumeration values between them but you cannot use them another way.
Last updated