Book Image

D Cookbook

By : Adam Ruppe
Book Image

D Cookbook

By: Adam Ruppe

Overview of this book

Table of Contents (21 chapters)
D Cookbook
Credits
Foreword
About the Author
About the Reviewers
www.PacktPub.com
Preface
Index

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:

  1. Create a class, with the data and methods it needs. For your expression evaluator, you'll create two classes: AddExpression and SubtractExpression. They will need variables for the left and right-hand side of the expression, and a method to evaluate the result.

  2. 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 and SubtractExpression, you will have an evaluate method. You'll move this function signature, but not the function body, to the interface, called Expression.

  3. 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.

  4. 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; }
    }
  5. Let's also add a BrokenAddExpression class that uses inheritance to override the evaluate function of AddExpression:

    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; }
    }
  6. 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.