The D Struct
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.
Purity
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
.
Const and Immutable
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.
Immutable Constructors
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 with no Effect
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)
.
Using a Class
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.