Creating a tree of classes
Classes are used to provide object-oriented features in D. To explore how they work, you're going to write a small inheritance hierarchy to evaluate basic addition and subtraction operations.
Getting ready
Before writing a class, step back and ask yourself whether it is the best tool for the job. Will you be using inheritance to create objects that are substitutable for their parent? If not, a struct may be more appropriate. If you plan to use inheritance for code reuse without substitutability, a mixin template may be more appropriate. Here, you'll use classes for substitutability, and a mixin template for some code reuse.
How to do it…
Let's create a tree of classes by executing the following steps:
Create a class, with the data and methods it needs. For your expression evaluator, you'll create two classes:
AddExpression
andSubtractExpression
. They will need variables for the left and right-hand side of the expression, and a method to evaluate the result.Move common methods from substitutable classes out to an interface, and make the classes inherit from it by putting a colon after the class name, followed by the interface name. In both
AddExpression
andSubtractExpression
, you will have an evaluate method. You'll move this function signature, but not the function body, to the interface, calledExpression
.If there is still a lot of code duplication, move the identical code out to a mixin template, and mix it in at the usage point.
Tip
If you want to use most, but not all, of a mixin template, you can override specific declarations by simply writing your own declaration below the
mixin
statement.Functions should operate on interface parameters, if possible, instead of classes, for maximum reusability.
The following is the code you have so far:
interface Expression { // this is the common method from the classes we made int evaluate(); } mixin template BinaryExpression() { // this is the common implementation code from the classes private int a, b; this(int left, int right) { this.a = left; this.b= right; } } // printResult can evaluate and print any expression class // thanks to taking the general interface void printResult(Expression expression) { import std.stdio; writeln(expression.evaluate()); } class AddExpression : Expression { // inherit from the interface mixin BinaryExpression!(); // adds the shared code int evaluate() { return a + b; } // implement the method } class SubtractExpression : Expression { mixin BinaryExpression!(); int evaluate() { return a - b; } }
Let's also add a
BrokenAddExpression
class that uses inheritance to override theevaluate
function ofAddExpression
:class BrokenAddExpression : AddExpression { this(int left, int right) { super(left, right); } // this changes evaluate to subtract instead of add! // note the override keyword override int evaluate() { return a - b; } }
Finally, you'll construct some instances and use them as follows:
auto add = new AddExpression(1, 2); printResult(add); auto subtract = new SubtractExpression(2, 1); printResult(subtract); // same function as above!
The usage will print 3
and 1
, showing the different operations. You can also create a BrokenAddExpression
function and assign it to add
as follows:
add = new BrokenAddExpression(1, 2); printResult(add); // prints -1
How it works…
Classes in D are similar to classes in Java. They are always reference types, have a single inheritance model with a root object, and may implement any number of interfaces.
Class constructors are defined with the this
keyword. Any time you create a new class, it calls one of the constructors. You may define as many as you want, as long as each has a unique set of parameters.
Note
Classes may have destructors, but you typically should not use them. When a class object is collected by the garbage collector, its child members may have already been collected, which means that they cannot be accessed by the destructor. Any attempt to do so will likely lead to a program crash. Moreover, since the garbage collector may not run at a predictable time (from the class' point of view), it is hard to know when, if ever, the destructor will actually be run. If you need a deterministic destruction, you should use a struct instead, or wrap your class in a struct and call the destructor yourself with the destroy()
function.
Object instances are upcasted implicitly. This is why you could assign BrokenAddException
to the add
variable, which is statically typed as AddExpression
. This is also the reason why you can pass any of these classes to the printResult
function, since they will all be implicitly cast to the interface when needed. However, going the other way, when casting from interface or a base class to a derived class, you must use an explicit cast
. It returns null if the cast
fails. Use the following code to better understand this:
if(auto bae = cast(BrokenAddExpression) expression) { /* we were passed an instance of BrokenAddExpression and can now use the bae variable to access its specific members */ } else { /* we were passed some other class */ }
In classes, all methods are virtual by default. You can create non-virtual methods with the final
keyword, which prevents a subclass from overriding a method. Abstract functions, created with the abstract
keyword, need not to have an implementation, and they must be implemented in a child class if the object is to be instantiated. All methods in an interface that are not marked as final or static are abstract and must be implemented by a non-abstract class.
When you override a virtual
or abstract
function from a parent class, you must use the override
keyword. If a matching function with any method marked override cannot be found, the compiler will issue an error. This ensures that the child class's method is actually compatible with the parent definition, ensuring that it is substitutable for the parent class. (Of course, ensuring the behavior is substitutable too is your responsibility as the programmer!)
The mixin template is a feature of D that neither C++ nor Java have. A mixin template is a list of declarations, variables, methods, and/or constructors. At the usage point, use the following code:
mixin BinaryExpression!();
This will essentially copy and paste the code inside the template to the point of the mixin
statement. The template can take arguments as well, given in the parenthesis. Here, you didn't need any parameterization, so the parentheses are empty. Templates in D, including mixin templates, can take a variety of arguments including values, types, and symbols. You'll discuss templates in more depth later in the book.
There's more…
Using interfaces and mixin templates, like you did here, can also be extended to achieve a result similar to multiple inheritance in C++, without the inheritance of state and avoiding the diamond inheritance problem that C++ has.
See also…
The Simulating inheritance with structs recipe in Chapter 6, Wrapped Types, shows how you can also achieve something subtyping and data extension, similar to inheritance with data, using structs.
The official documentation can be found at http://dlang.org/class.html and it goes into additional details about the capabilities of classes.