I want to implement the value object pattern in D. That is, I want to have mutable reference variables to immutable objects. T
variables should be assignable, but T
objects should never change their state.
I am confused about the difference between const
and immutable
in D. Let me illustrate my doubts with a skeleton Rational
class:
class Rational
{
int num;
int den;
Should I declare num
and den
as const
or immutable
? Is there a difference for integers?
invariant()
{
assert(den > 0);
assert(gcd(abs(num), den) == 1);
}
Should I declare invariant
as const
or immutable
? Marking it as immutable
results in a compile-time error, but that may be due to other members not being marked immutable
.
this(int numerator, int denominator) { ... }
Should I declare the constructor as const
or immutable
? What would that mean?
string toString()
{
return std.string.format("(%s / %s)", num, den);
}
}
Should I declare toString
as const
or immutable
?
Instead of marking individual members, it seems I can also mark the entire class:
class Rational
const class Rational
immutable class Rational
Which of these make the most sense for the value object pattern?
What about pure
? In the value object pattern, the methods should be free of side effects, so does it make sense to declare every member as pure
? Marking toString
as pure
does not compile, unfortunately, because std.string.format
is not pure; is there any particular reason for that?
It seems I can also declare the class itself as pure
, but that does not seem to have any effect, because the compiler does not complain about toString
calling an impure function anymore.
What does it mean to declare a class as pure
then? Is it simply ignored?
A value object is a design pattern in which an object is made to represent something simple, like currencies or dates. A value object should be equal to another value object if both objects have the same value despite being two different objects.
Value Object is an object that represents a concept from your problem Domain. It is important in DDD that Value Objects support and enrich Ubiquitous Language of your Domain. They are not just primitives that represent some values - they are domain citizens that model behaviour of your application.
To implement a value object, we simply wrap a value into an immutable class with an equals/hashcode pair that compares the objects by values.
DTO is a class representing some data with no logic in it. On the other hand, Value Object is a full member of your domain model. It conforms to the same rules as Entity. The only difference between Value Object and Entity is that Value Object doesn't have its own identity.
The value object pattern is best represented in D by simply using a struct and its in-built value semantics.
To my understanding, the value object pattern is usually employed in Java due to Java's current lack of in-built aggregates with value semantics.
D's structs work similarly to structs in C and C#, as well as structs and classes in C++. The comparison is perhaps best for the latter, as D structs have constructors and destructors, but with one important exception: there's no inheritance and virtual functions; those features are delegated to classes, which work much like classes in Java and C# (they are implicit reference types, hence they never exhibit the slicing problem).
struct Rational
{
int num;
int den;
/* your methods here */
}
Instances of Rational are then always passed by value (unless the parameter explicitly specifies otherwise, see ref and out) to functions and copied on assignment.
Pure functions cannot read or write to any global state. Pure functions are allowed to mutate explicit parameters as well as the implicit this
parameter for methods; methods on Rational are thus probably always pure
.
std.string.format
not being pure
is a problem with its current implementation. It will use a different implementation in the future that is pure
.
If you want to express that the method is pure and also doesn't mutate its own state, you can make it both pure
and const
.
Both mutable (Rational
) and immutable (immutable(Rational)
) instances can be implicitly converted to const(Rational)
, hence const
is the best choice when you don't need the immutable guarantee but you still don't mutate any members.
In general, struct methods that don't need to mutate member fields should be const
. For classes, the same applies but you also have to think about any derived methods that may override the method - they are bound by the same restriction.
Putting const
or immutable
on a struct
or class
declaration is equivalent of marking all its members (including methods) const
or immutable
respectively.
If all your constructor does is assign the num
and den
fields to their respective constructor parameters, then this functionality is already present on structs by default:
struct S { int foo, bar; }
auto s = S(1, 2);
assert(s.foo == 1);
assert(s.bar == 2);
const
on a constructor doesn't make a lot of sense because any constructor regardless of constancy can construct a const instance since everything is implicitly convertible to const.
immutable
on a constructor does make sense and is sometimes the only way to construct an immutable instance of a struct or class. A mutable constructor could create aliases for the this
reference through which the instance could later be mutated, so its result cannot always be implicitly converted to immutable.
However, an immutable constructor is not needed in your case because Rational does not have any indirection, so a mutable constructor can be used and the result copied over. In other words, types with no mutable indirection are implicitly convertible to immutable. This includes primitive types like int
and float
as well as structs satisfying the same condition.
Attributes put on declarations where they don't have any effect are ignored by all current compilers. This can make sense, because attributes can be applied to multiple declarations at once, with the attribute { /* declarations */ }
and attribute: /*declarations*/
syntaxes:
struct S
{
immutable
{
int foo;
int bar;
}
}
struct S2
{
immutable:
int foo;
int bar;
}
In both of the above examples, foo
and bar
are of type immutable(int)
.
Sometimes value semantics are not desired, such as for performance reasons associated with frequent copying of large structs. It's possible to explicitly pass structs by reference, such as using ref
and out
function parameters or by using pointers, but when value semantics are the default it's easy to make mistakes, and the syntactic overhead can be grinding. Pointers also have a number of other pitfalls.
Classes are reference types and it's impossible to treat them like values. They are typically instantiated with new
, which always creates a GC-allocated instance of the class (overloading of new
is deprecated). These two points make classes in D very similar to classes in Java and C# (another notable point is that there are interfaces instead of multiple inheritance). However, classes have the overhead of hidden fields (currently size_t.sizeof * 2
bytes for all classes) and the ABI of fields is not specified, but classes are also the only option when inheritance and virtual functions are desired.
Here's Rational implemented for the Value Object Pattern:
class Rational
{
immutable int num;
immutable int den;
this(int num, int den)
{
this.num = num;
this.den = den;
}
/* methods here */
}
This is the implementation most faithful to Java implementations. It uses immutable to prevent mutation of num
and den
regardless of the mutability of the instance itself. Methods should be const
and typically pure
as with the struct.
Since immutable constructors are not currently fully implemented (read: don't use them at all), the above constructor will actually allow you to create immutable instances of the class (e.g. new immutable(Rational)(1, 2)
) even though the constructor is free to make mutable aliases of the this
reference, breaking the immutable guarantee.
A slightly more D-like way would be to leave immutability decisions to user code, implementing it plainly like this:
class Rational
{
int num;
int den;
this(int num, int den)
{
this.num = num;
this.den = den;
}
/* immutable constructor overload would be here */
/* methods here */
}
The user can then choose whether to use Rational
or immutable(Rational)
. The latter can be safely passed between threads using the std.concurrency threading interface, while trying to send the former would be rejected at compile-time.
However, the latter has a glaring problem - because Rational
is implicitly a reference type, there's no way to type a mutable reference to an immutable instance of Rational. The current solution to this problem is to use std.typecons.Rebindable. There is a proposed solution for fixing this in the language.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With