Migration from Primitive Types to Value Objects
When designing software systems, it is essential to choose the right data types to represent the concepts and behaviors of the problem domain accurately. Primitive types, such as integers, strings, and booleans, are often used to model simple data structures. However, they lack the expressiveness and rich behavior required to model complex business rules and invariants. Value Objects, on the other hand, are immutable, have a well-defined behavior, and can enforce complex business rules and invariants.
In this article, we will explore the possible options for migrating from primitive types to value objects, using the project “stevedunn/vogen/” as a reference.
Option 1: Replace Primitive Types with Value Objects
The most straightforward option is to replace primitive types with value objects explicitly. This approach involves creating a new value object class that encapsulates the primitive type and provides a rich behavior that enforces the business rules and invariants.
For example, in the project “stevedunn/vogen/”, the file “samples/Vogen.Examples/Types/IntoVo.cs” shows how to replace a primitive type (string) with a value object (StringVo
). The StringVo
class encapsulates the string primitive type and provides a behavior that enforces the business rules and invariants.
public class StringVo
{
private readonly string _value;
public StringVo(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("Value cannot be null or whitespace.", nameof(value));
}
_value = value;
}
public static implicit operator StringVo(string value) => new StringVo(value);
public static implicit operator string(StringVo value) => value._value;
public override string ToString() => _value;
}
To use the StringVo
class, you can create an instance of it and pass it to methods or properties that expect a string value. The StringVo
class will ensure that the business rules and invariants are enforced.
Option 2: Use Value Object Attributes
Another option is to use value object attributes to decorate the primitive types with metadata that defines the behavior and invariants of the value object. This approach involves creating a new attribute class that encapsulates the behavior and invariants of the value object.
For example, in the project “stevedunn/vogen/”, the file “src/Vogen.SharedTypes/ValueObjectAttribute.cs” shows how to create a ValueObjectAttribute
class that can be used to decorate primitive types with metadata that defines the behavior and invariants of the value object.
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class ValueObjectAttribute : Attribute
{
public ValueObjectAttribute()
{
}
}
To use the ValueObjectAttribute
class, you can decorate a primitive type with the attribute and define the behavior and invariants of the value object in the attribute class.
[ValueObject]
public class StringVo
{
private readonly string _value;
public StringVo(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("Value cannot be null or whitespace.", nameof(value));
}
_value = value;
}
public static implicit operator StringVo(string value) => new StringVo(value);
public static implicit operator string(StringVo value) => value._value;
public override string ToString() => _value;
}
Option 3: Use Code Generation
A third option is to use code generation to generate the value object classes automatically from the primitive types. This approach involves creating a code generation template that can generate the value object classes based on the metadata and behavior defined in the attribute class.
For example, in the project “stevedunn/vogen/”, the file “src/Vogen/Generators/Conversions/GenerateEfCoreTypes.cs” shows how to generate the value object classes automatically from the primitive types using a code generation template.
public class GenerateEfCoreTypes
{
public static string Generate(Type type)
{
var builder = new StringBuilder();
builder.AppendLine($"public class {type.Name}Vo");
builder.AppendLine("{");
builder.AppendLine($"\tprivate readonly {type.FullName} _value;");
builder.AppendLine($"\tpublic {type.Name}Vo({type.FullName} value)");
builder.AppendLine("\t{");
builder.AppendLine($"\t\tif ({type.FullName}.IsNullOrWhiteSpace(value))");
builder.AppendLine($"\t\t{{");
builder.AppendLine($"\t\t\tthrow new ArgumentException(\"Value cannot be null or whitespace.\", nameof(value));");
builder.AppendLine($"\t\t}}");
builder.AppendLine($"\t\t_value = value;");
builder.AppendLine($"\t}");
builder.AppendLine($"\tpublic static implicit operator {type.Name}Vo({type.FullName} value) => new {type.Name}Vo(value);");
builder.AppendLine($"\tpublic static implicit operator {type.FullName}({type.Name}Vo value) => value._value;");
builder.AppendLine($"\tpublic override string ToString() => _value;");
builder.AppendLine("}");
return builder.ToString();
}
}
To use the GenerateEfCoreTypes
class, you can call the Generate
method and pass the primitive type as a parameter. The Generate
method will return a string that contains the generated value object class.
Option 4: Use Serialization and Conversion
A fourth option is to use serialization and conversion to convert the primitive types to value objects automatically. This approach involves creating a serialization and conversion library that can convert the primitive types to value objects and vice versa.
For example, in the project “stevedunn/vogen/”, the file “src/Vogen/Templates/Short/Short_LinqToDbValueConverter.cs” shows how to create a serialization and conversion library that can convert the primitive types to value objects and vice versa using the LinqToDb
library.
public class ShortValueConverter : ValueConverter<short, int>
{
public ShortValueConverter() : base(
v => (int)v,
v => (short)v)
{
}
}
To use the ShortValueConverter
class, you can register it with the LinqToDb
library, and it will automatically convert the primitive types to value objects and vice versa.
Conclusion
In conclusion, migrating from primitive types to value objects is an essential step in designing software systems that accurately model the problem domain. The four options presented in this article provide a range of approaches to migrating from primitive types to value objects, from simple replacement to complex code generation and serialization and conversion. By choosing the right option for your project, you can ensure that your software system accurately models the problem domain and enforces the business rules and invariants required for correct behavior.
Sources:
- K8s ASA: The Storage Interface · Daniel Mangum
- From Object-Oriented To Functional-Domain Modeling (Mario Fusco) | Red Hat Developer
- Support for the ClickHouse database in Tremor | Tremor
- From object oriented to functional domain modeling by Mario Fusco | Red Hat Developer
- [Announcing the Hack Transpiler - Engineering at Meta](https://