How Statistics Can Enrich Domain-Driven Design’s Building Blocks? (Part-3)

Ruhollah Delpak
6 min readMar 2, 2023

In the previous part, we got acquainted with four categories of variables from the statistics perspective. Now, I will try to explain how this categories can lead us to enrich the key idea of Value Object and extend the building blocks of Domain-Driven Design.

Inspired by four categories of variables in statistics, I suggest that whenever it seems that a concept can be modeled as a Value Object, we could think more about which type of variables that concept is similar to, and then instead of modeling it as a typical Value Object, we can model that concept as one of the following Value Object sub types:

1- Nominal Value Object (NVO)
2- Ordinal Value Object (OVO)
3- Interval Value Object (IVO)
4- Ratio Value Object (RVO)

Extended Mode-Driven Design Building Blocks. (Other blocks removed for more brevity.)

All of these new building blocks, are inherited from the typical Value Object. (You can find implementations of a Value Object base class, for your favorite programming language easily).

Let’s take a closer look at each of the new building blocks, and see what features or differences, each one has and when we can use them.

1- Nominal Value Object

When you have found out that a variable is a Nominal Variable, why not model it as a Nominal Value Object?

Sometimes we just need the separation of qualitative properties and the classification of differences. For example, blood groups, gender, nationality, mother tongue and etc. According to Nominal Variable that was introduced in the first part, we can model qualitative properties as Nominal Value Object. A NVO acts as a label and its most important usage, is to categorize entities. Therefore, an applicable interface for a NVO looks like this:

public interface INominalValue
{
string Label { get; }
string[] Synonyms { get; }
}

What is the use of Synonyms property in the INominalValue interface?
In some domains, homonymous titles are used to categorize entities. For example, when classifying people according to gender, the words “man” , “sir”, “male” or “woman”, “lady”, “female” might be used interchangeably. If there was such a condition in domain, it would be better that our model reflected the same condition. Thus, the Synonyms property is embedded in the INominalValue interface to bring the model closer to the problem space.

Consider the abstract Nominal class that inherits from the Value Object class:

public abstract class NominalValueObejct : ValueObject, INominalValue
{
public abstract string Label { get; }

public abstract string[] Synonyms { get; }
}

Now suppose that in a domain we are going to model the favorite sport of students. Because the favorite sport is the same type of nominal variables, it can be modeled as follows:

public class Sport : Nominal, INominalValue
{
private readonly string _label;
private readonly string[] _synonyms;

public override string Label => _label;

public override string[] Synonyms => _synonyms;

public Sport(string label)
{
_label = label;
_synonyms = Array.Empty<string>();
}

public Sport(string label, string[] synonyms)
{
_label = label;
_synonyms = synonyms;
}

protected override IEnumerable<object> GetEqualityComponents()
{
yield return _label.ToLowerInvariant();
}
}

In the next step, assuming that we are going to model a set of sports which are valid choices in our domain. Therefore, any entity in the domain, could just have an instance of sport object which belongs to defined collection of sports. So, CollectionFactory classes will be added to model, in order to response to this simple question asking what are valid sports in this domain model?

Introduce CollectionFactory as a new building block.

Following code, shows the intent of CollectionFactory class.

public abstract class CollectionFactory<T> where T : INominalValue
{
protected ReadOnlyCollection<T> InitialValues;

public CollectionFactory(ReadOnlyCollection<T> initialValues)
{
InitialValues = initialValues;
}

public T? this[string label]
{
get
{
return InitialValues.Single(x => x.Label == label || x.Synonyms.Contains(label));
}
}
}

A sample CollectionFactory containing a collection of valid sports in imaginary domain:

public class Sports : CollectionFactory<Sport>
{
public Sports() : base(new ReadOnlyCollection<Sport>(new List<Sport>() { FootBall, Swimming, Haki }))
{ }

private static Sport FootBall => new("Football", new string[] { "Soccer" });
private static Sport Swimming => new("Swimming");
private static Sport Hockey=> new("Hockey");
}

Note well to Sports class usage:

class Person
{
public int Id { get; set; }
public string Name { get; set; }
public Sport? FavoriateSport { get; set; }
}

var sports = new Sports();

var person1 = new Person
{
Sport = sports["Football"]
};
var person2 = new Person
{
Sport = sports["Soccer"]
};

//Verifies that two person have same favorite sport.
Assert.Equal(person1.Sport, person2.Sport);

2- Ordinal Value Object

If you are not yet familiar with Ordinal Variables, read the first part. Ordinal Variables, in addition to having the usage of nominal variables, also have a natural order between their values. For example, you might ask patients to express the amount of pain they are feeling on a scale of 1 to 10. A score of 7 means more pain than a score of 5, and that is more than a score of 3. But the difference between 7 and 5 may not be the same as between 5 and 3. The values simply express an order.

Therefore, for each of the values, it should be specified what rank it has in comparison to other values. So, an applicable interface for an OVM would look like this:

    public interface IOrdinalValue : INominalValue
{
int Index { get; }
}

Note that IOrdinalValue contains properties of INominalValue.
Since in Ordinal variables, questions such as first-last, highest-lowest, largest-smallest and etc., are usual questions, the IOrdinalValue interface can optionally implement one or more of the following interfaces which are commonly used in domain:

public interface IHaveFirstLastValues<T> where T : IOrdinalValue
{
T First { get; }
T Last { get; }
}
public interface IHaveMaxMinValues<T> where T : IOrdinalValue
{
T Max { get; }
T Min { get; }
}
public interface IHaveLowHighValues<T> where T : IOrdinalValue
{
T Lowest { get; }
T Highest { get; }
}

Sample implementation of IOrdinalValue:

public abstract class Ordinal : ValueObject, IOrdinalVariable
{
private readonly string _label;
private readonly int _index;
private readonly string[] _synonyms;

public string Label => _label;

public string[] Synonyms => _synonyms;

public int Index => _index;

public Ordinal(string label,int index)
{
_label = label;
_index = index;
_synonyms = Array.Empty<string>();
}

public Ordinal(string label, int index, string[] synonyms)
{
_label = label;
_index = index;
_synonyms = synonyms;
}

public static bool operator <(Ordinal O1, Ordinal O2)
{
return O1.Index < O2.Index;
}

public static bool operator >(Ordinal O1, Ordinal O2)
{
return O1.Index > O2.Index;
}

protected override IEnumerable<object> GetEqualityComponents()
{
yield return Index;
yield return Label;
}
}

You can see that the Ordinal class inherits from the Nominal class. In addition, the comparison operators < and > are implemented in this class, so that two values of the same Ordinal class can be compared.
Another point in the code above is the GetEqualityComponents method, which states that two instances of the Nominal class are the same only if they have the same order and label.

See an example implementation of the Ordinal class that models the movie rating concept:

public class FilmRating : Ordinal
{
public FilmRating(string label, int index) : base(label, index)
{
}
}
public abstract class OrdinalCollectionFactory<T> : CollectionFactory<T>, IHaveLowHighValues<T>, IHaveFirstLastValues<T> where T : IOrdinalVariable
{
protected OrdinalCollectionFactory(ReadOnlyCollection<T> initialValues) : base(initialValues)
{
}

public T? this[int index] => InitialValues.ElementAt(index);

public T Lowest => InitialValues.OrderBy(x => x.Index).First();
public T Highest => InitialValues.OrderBy(x => x.Index).Last();
public T First => InitialValues.OrderBy(x => x.Index).First();
public T Last => InitialValues.OrderBy(x => x.Index).Last();

public override string ToString()
{
return string.Join(',', InitialValues.Select(x => x.Label));
}
}

class FilmRatings : OrdinalCollectionFactory<FilmRating>
{
public FilmRatings() : base(new ReadOnlyCollection<FilmRating>(new List<FilmRating>() {
new FilmRating("R", 0),
new FilmRating("PG-13", 1),
new FilmRating("PG", 2),
new FilmRating("G", 3)
}))
{ }
}

Summery:
In this part I explained how we can take new sub types of Value Object into account and extend DDD’s building blocks. Nominal Value Object and Ordinal Value Object were introduced and main specifications of each were showed as code.
In the next part, two new building blocks will be introduced as an extension to Value Object idea:

1- Interval Value Object
2- Ratio Value Object

--

--