Lesson 3 - Encapsulation

by Zoran Horvat

Data Safety Considerations

Values contained within an instance are typically subdued to specific validity and consistency rules. If those rules are not obeyed, instance would quickly become broken and useless, leading to different kinds of problems and program execution errors. Suppose that some code desires to modify instance's content directly. First problem occurs if another caller comes into play, asking for the same operation on data. Would it be acceptable to code the same functionality another time? Off course, no - functionality required on more than one place should be coded as a separate function. Second issue occurs if person who designed the class suddenly decides to change it, so that it operates differently. If outer code was accessing internal data structures directly then it will fail after internals of the class have been redesigned. Programmers should be warned that design decisions that do not affect exterior of an object, like whether to use linked list or an array inside the object, are not something that surrounding code should bother with.

Way Out of Problems

Now that we have discussed dangers related to unrestricted access to object's internals, it looks quite safe to lock object-modifying code within methods. Not only that this dangerous code is now contained within class definition, but it also establishes certain kind of a facade to the caller. Now caller knows exactly how the method which provides a required service should be called, what is its name and arguments, and which type of result it returns. Such methods define a programmatic contract between calling code and an object being called upon.

This doctrine is called encapsulation. As the word says, we are putting object's internal content into a capsule, so that it is not exposed to the environment. Special methods that allow callers to initiate modifications on the object are a proper way in which internals are exposed. Those methods both allow callers to ask for object's modifications, and allow object to maintain its internal consistency rather than get broken due to erroneous changes made to it.

Accessors and Mutators

Just for a moment we will recall the Shape class declaration (shape.hpp file):

class Shape
{
    ...
private:
    float locationX;
    float locationY;
    char *name;
};

Fields of the class, variables that need to be protected from unrestricted access, have been declared under the private section of the class, which means that they are not accessible from outside the class. There is no parallel to private keyword in non-object-oriented languages, such as C. Principles of encapsulation can only be substituted by coding practices like this one: Never access a field directly; use specialized functions instead.

In our geometric shapes design we have already met Shape_SetName function, which was designed to safely set the given name of the geometric shape:

void Shape_SetName(struct Shape *_this, const char *name)
{
    if (_this->name != NULL)
        free(_this->name);
    _this->name = (char*)calloc(strlen(name) + 1, sizeof(char));
    strcpy(_this->name, name);
}

This kind of a function is called a mutator, due to its habit to modify values contained in the instance.

On an opposite side of the spectre there are accessors, functions that only return values from the object. We could define an accessor function for shape's name:

const char *Shape_GetName(struct Shape *_this);

Observe that this function returns constant pointer to the buffer containing shape's name so that caller cannot modify content of the buffer.

Implementing Accessors and Mutators

In many cases accessors and mutators are quite simple and typically come in pairs: accessor which reads one value has its corresponding mutator which modifies the same value. For this reason they are often called getters and setters. If class defines variable X, then it would expose accessor get_X and mutator set_X, with value X itself being private. When this idea is applied to our shapes example, the Shape type becomes something like this:

/* Partial listing of shape.h */
#ifndef SHAPE_H
#define SHAPE_H

struct Shape
{
    char *name;
    float locationX;
    float locationY;
};
...
const char *Shape_get_Name(const struct Shape *_this);
void Shape_set_Name(struct Shape *_this, const char * value);
...
#endif

Note that get_Name accessor receives pointer to constant Shape instance, so it cannot modify it - it is a proper accessor. Implementation of get_Name and set_Name from the following listing are now making sure that both the shape and the outer world are protected from misuse of null pointers. In terms of our geometric shapes model, null string is considered the same as empty string because in either case name of the instance is not set.

/* Partial listing of shape.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "shape.h"

...
const char *Shape_get_Name(const struct Shape *_this)
{
    const char *name = ""; /* Constant pointer to empty string */
    if (_this->name != NULL)
        name = _this->name;
    return name;
}

void Shape_set_Name(struct Shape *_this, const char * value)
{

    if (_this->name != NULL)
    {
        free(_this->name);
        _this->name = NULL;
    }

    if (value != NULL && strlen(value) > 0)
    {
        _this->name = (char*)calloc(strlen(value) + 1, sizeof(char));
        strcpy(_this->name, value);
    }

}
...

Getters and Setters in Object-Oriented Languages

We have explained getters and setters design without much help from the language constructs. First novelty, which we have already mentioned, were the access modifiers. Modifier private ensures that data fields are not accessible from outside. Now fields can only be accessed via accessors and mutators:

// Partial listing of shape.hpp
#ifndef SHAPE_HPP
#define SHAPE_HPP

class Shape
{
public:
    ...
    const char *get_Name() const;
    void set_Name(const char *value);
    ...
private:
    ...
    char *name;
};

#endif

First notice the public access modifier under which methods are declared, as opposed to private access modifier for fields. Second important detail is the const keyword following the get_Name method declaration. This keyword indicates that method will not change the object on which it is called, which is equal to constant _this pointer that we have used in C version of the code.

Now the implementation is straight-forward:

// Partial listing of shape.cpp
#include <iostream>
#include <stdlib.h>
#include <string.h>
#include "shape.hpp"

using namespace std;
...
const char *Shape::get_Name() const // This is the get_Name accessor with implicit _this argument
{
    const char *name = ""; // Constant empty string
    if (this->name != NULL)
        name = this->name;
    return name;
}

void Shape::set_Name(const char *value) // This is the set_Name mutator with implicit _this argument
{

    if (this->name != NULL)
    {
        delete[] this->name;
        this->name = NULL;
    }

    if (value!= NULL && strlen(value) > 0)
    {
        this->name = new char[strlen(value) + 1];
        strcpy(this->name, value);
    }

}
...

And as so many times before, we can realize that object-oriented code in C++ looks line-for-a-line the same as structured C code. We have strictly followed object-oriented methodology when programming in C. It is not the language, but programmer who makes the code object-oriented. Languages mostly help ease the coding process and help the code produced be more convenient to read and understand.

Some languages are offering a construct called property, which represents getter-setter pair for a variable of desired type. Languages that support properties are C#, Objective-C and some others. Languages like C++ and Java do not support properties. Syntax of the Name property in C# would look like this:

// Partial listing of Shape.cs
public class Shape
{
    ...
    public string Name
    {
        get
        {
            return name ?? string.Empty;
        }
        set
        {
            name = value;
        }
    }
    ...
    private string name;
    ...
}

When looked from the outside, property Name looks like a common variable of type string. Internally, however, it provides get and set methods which are conveniently called get_Name and set_Name.

Exercising Encapsulation

As a form of an exercise, we will apply encapsulation to other fields of Shape, Ellipse and Rectangle types.

/* Partial listing of shape.h */
#ifndef SHAPE_H
#define SHAPE_H

struct Shape
{
    char *name;
    float locationX;
    float locationY;
};

...
float Shape_get_LocationX(const struct Shape *_this);
void Shape_set_LocationX(struct Shape *_this, float value);
float Shape_get_LocationY(const struct Shape *_this);
void Shape_set_LocationY(struct Shape *_this, float value);
...

#endif

Implementation of getters and setters for location coordinates are straightforward.

/* Partial listing of shape.c */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "shape.h"

...
float Shape_get_LocationX(const struct Shape *_this)
{
    return _this->locationX;
}

void Shape_set_LocationX(struct Shape *_this, float value)
{
    _this->locationX = value;
}

float Shape_get_LocationY(const struct Shape *_this)
{
    return _this->locationY;
}

void Shape_set_LocationY(struct Shape *_this, float value)
{
    _this->locationY = value;
}
...

These getters and setters were quite simple. But Ellipse's radiusX field is preparing a twist (and same case is with radiusY field):

/* Partial listing of ellipse.c */
#include "ellipse.h"

...
void Ellipse_set_RadiusX(struct Ellipse *_this, float value)
{
    if (value > 0)
        _this->radiusX = value;
}
...

Note that setter is now performing a validity test on new value. We are aware that each radius of the ellipse must be a positive number. If caller has disobeyed this requirement, then setter refuses to continue with the operation. This example shows how setter protects the object from invalid input.

C++ versions of these functions are again exactly the same as in C, only with C++ notation applied. We will leave that to the reader to complete.

Yet most compact implementation of getters and setters is in C#, due to its property notions.

// Partial listing of Shape.cs
using System;

namespace Geometry
{
    public class Shape
    {
        ...
        public float LocationX
        {
            get
            {
                return locationX;
            }
            set
            {
                locationX = value;
            }
        }

        public float LocationY
        {
            get
            {
                return locationY;
            }
            set
            {
                locationY = value;
            }
        }
        ...
    }
}
// Partial listing of ellipse.cs

namespace Geometry
{
    public class Ellipse: Shape
    {
        public float RadiusX
        {
            get
            {
                return radiusX;
            }
            set
            {
                if (value> 0)
                    radiusX = value;
            }
        }
        public float RadiusY
        {
            get
            {
                return radiusY;
            }
            set
            {
                if (value> 0)
                    radiusY = value;
            }
        }
        private float radiusX;
        private float radiusY;
    }
}

Code in C# is quite similar to its older versions in C++ and in C, only a bit more compact due to properties which help us avoid cumbersome method declarations.


If you wish to learn more, please watch my latest video courses

About

Zoran Horvat

Zoran Horvat is the Principal Consultant at Coding Helmet, speaker and author of 100+ articles, and independent trainer on .NET technology stack. He can often be found speaking at conferences and user groups, promoting object-oriented and functional development style and clean coding practices and techniques that improve longevity of complex business applications.

  1. Pluralsight
  2. Udemy
  3. Twitter
  4. YouTube
  5. LinkedIn
  6. GitHub