Programming in D – Tutorial and Reference
Ali Çehreli

Other D Resources

Encapsulation and Protection Attributes

All of the structs and classes that we have defined so far have all been accessible from the outside.

Let's consider the following struct:

enum Gender { female, male }

struct Student {
    string name;
    Gender gender;
}

The members of that struct is freely accessible to the rest of the program:

    auto student = Student("Tim", Gender.male);
    writefln("%s is a %s student.", student.name, student.gender);

Such freedom is a convenience in programs. For example, the previous line has been useful to produce the following output:

Tim is a male student.

However, this freedom is also a liability. As an example, let's assume that perhaps by mistake, the name of a student object gets modified in the program:

    student.name = "Anna";

That assignment may put the object in an invalid state:

Anna is a male student.

As another example, let's consider a School class. Let's assume that this class has two member variables that store the numbers of the male and female students separately:

class School {
    Student[] students;
    size_t femaleCount;
    size_t maleCount;

    void add(Student student) {
        students ~= student;

        final switch (student.gender) {

        case Gender.female:
            ++femaleCount;
            break;

        case Gender.male:
            ++maleCount;
            break;
        }
    }

    override string toString() const {
        return format("%s female, %s male; total %s students",
                      femaleCount, maleCount, students.length);
    }
}

The add() member function adds students while ensuring that the counts are always correct:

    auto school = new School;
    school.add(Student("Lindsey", Gender.female));
    school.add(Student("Mark", Gender.male));
    writeln(school);

The program produces the following consistent output:

1 female, 1 male; total 2 students

However, being able to access the members of School freely would not guarantee that this consistency would always be maintained. Let's consider adding a new element to the students member, this time directly:

    school.students ~= Student("Nancy", Gender.female);

Because the new student has been added to the array directly, without going through the add() member function, the School object is now in an inconsistent state:

1 female, 1 male; total 3 students
Encapsulation

Encapsulation is a programming concept of restricting access to members to avoid problems similar to the one above.

Another benefit of encapsulation is to eliminate the need to know the implementation details of types. In a sense, encapsulation allows presenting a type as a black box that is used only through its interface.

Additionally, preventing users from accessing the members directly allows changing the members of a class freely in the future. As long as the functions that define the interface of a class is kept the same, its implementation can be changed freely.

Encapsulation is not for restricting access to sensitive data like a credit card number or a password, and it cannot be used for that purpose. Encapsulation is a development tool: It allows using and coding types easily and safely.

Protection attributes

Protection attributes limit access to members of structs, classes, and modules. There are two ways of specifying protection attributes:

Protection attributes can be specified by the following keywords. The default attribute is public.

Additionally, the export attribute specifies accessibility from the outside of the program.

Definition

Protection attributes can be specified in three ways.

When written in front of a single definition, it specifies the protection attribute of that definition only. This is similar to the Java programming language:

private int foo;

private void bar() {
    // ...
}

When specified by a colon, it specifies the protection attributes of all of the following definitions until the next specification of a protection attribute. This is similar to the C++ programming language:

private:
    // ...
    // ... all of the definitions here are private ...
    // ...

protected:
    // ...
    // ... all of the definitions here are protected ...
    // ...

When specified for a block, the protection attribute is for all of the definitions that are inside that block:

private {
    // ...
    // ... all of the definitions here are private ...
    // ...
}
Module imports are private by default

A module that is imported by import is private to the module that imports it. It would not be visible to other modules that import it indirectly. For example, if a school module imports std.stdio, modules that import school cannot automatically use the std.stdio module.

Let's assume that the school module starts by the following lines:

module school.school;

import std.stdio;    // imported for this module's own use...

// ...

The following program cannot be compiled because writeln is not visible to it:

import school.school;

void main() {
    writeln("hello");    // ← compilation ERROR
}

std.stdio must be imported by that module as well:

import school.school;
import std.stdio;

void main() {
    writeln("hello");   // now compiles
}

Sometimes it is desired that a module presents other modules indirectly. For example, it would make sense for a school module to automatically import a student module for its users. This is achieved by marking the import as public:

module school.school;

public import school.student;

// ...

With that definition, modules that import school can use the definitions that are inside the student module without needing to import it:

import school.school;

void main() {
    auto student = Student("Tim", Gender.male);

    // ...
}

Although the program above imports only the school module, the student.Student struct is also available to it.

When to use encapsulation

Encapsulation avoids problems similar to the one we have seen in the introduction section of this chapter. It is an invaluable tool to ensure that objects are always in consistent states. Encapsulation helps preserve struct and class invariants by protecting members from direct modifications by the users of the type.

Encapsulation provides freedom of implementation by abstracting implementations away from user code. Otherwise, if users had direct access to for example School.students, it would be hard to modify the design of the class by changing that array e.g. to an associative array, because this would affect all user code that has been accessing that member.

Encapsulation is one of the most powerful benefits of object oriented programming.

Example

Let's define the Student struct and the School class by taking advantage of encapsulation and let's use them in a short test program.

This example program will consist of three files. As you remember from the previous chapter, being parts of the school package, two of these files will be under the "school" directory:

Here is the "school/student.d" file:

module school.student;

import std.string;
import std.conv;

enum Gender { female, male }

struct Student {
    package string name;
    package Gender gender;

    string toString() const {
        return format("%s is a %s student.",
                      name, to!string(gender));
    }
}

The members of this struct are marked as package to enable access only to modules of the same package. We will soon see that School will be accessing these members directly. (Note that even this should be considered as violating the principle of encapsulation. Still, let's stick with the package attribute in this example program.)

The following is the "school/school.d" module that makes use of the previous one:

module school.school;

public import school.student;                  // 1

import std.string;

class School {
private:                                       // 2

    Student[] students;
    size_t femaleCount;
    size_t maleCount;

public:                                        // 3

    void add(Student student) {
        students ~= student;

        final switch (student.gender) {        // 4a

        case Gender.female:
            ++femaleCount;
            break;

        case Gender.male:
            ++maleCount;
            break;
        }
    }

    override string toString() const {
        string result = format(
            "%s female, %s male; total %s students",
            femaleCount, maleCount, students.length);

        foreach (i, student; students) {
            result ~= (i == 0) ? ": " : ", ";
            result ~= student.name;            // 4b
        }

        return result;
    }
}
  1. school.student is being imported publicly so that the users of school.school will not need to import that module explicitly. In a sense, the student module is made available by the school module.
  2. All of the member variables of School are marked as private. This is important to help protect the consistency of the member variables of this class.
  3. For this class to be useful, it must present some member functions. add() and toString() are made available to the users of this class.
  4. As the two member variables of Student have been marked as package, being a part of the same package, School can access those variables.

Finally, the following is a test program that uses those types:

import std.stdio;
import school.school;

void main() {
    auto student = Student("Tim", Gender.male);
    writeln(student);

    auto school = new School;

    school.add(Student("Lindsey", Gender.female));
    school.add(Student("Mark", Gender.male));
    school.add(Student("Nancy", Gender.female));

    writeln(school);
}

This program can use Student and School only through their public interfaces. It cannot access the member variables of those types. As a result, the objects would always be consistent:

Tim is a male student.
2 female, 1 male; total 3 students: Lindsey, Mark, Nancy

Note that the program interacts with School only by its add() and toString() functions. As long as the interfaces of these functions are kept the same, changes in their implementations would not affect the program above.