Encapsulation is a Good Thing™

Created 28th June, 2006 12:55 (UTC), last edited 21st February, 2009 03:22 (UTC)

News flash:

Get and set accessors aren't a way of preserving encapsulation

Now that we have that out of the way maybe we can take a look at what encapsulation really is:

  • it has to do with keeping your privates private and only exposing those things that you can handle others playing with;
  • it has to do with only telling people how you will communicate with them and not about how you keep your notes;
  • it means that as you grow and change you still keep your trousers¹ [1That's pants in America, but this is one step further than I wanted to push the analogy in Britain.] on because you don't want to flaunt those changes.

Notice that nowhere did I add this point:

  • it has to do with scribbling all over your notes whenever somebody tells you to do it.

And I hope you would ridicule anybody who tried to put it in this way, but it is this last that the proliferation of get & set members means in practice. Now, I can hear the incoherent spluttering from here: your get & set accessors aren't like that, no not at all. To which I will just ask

“Do you know why you put the accessors on some members and not on others? What drives this decision?”

I hope that I can help you answer this with something more coherent than mere hand waving.

A history lesson

But, before I answer that we're going to take a quick travel through time in order to try to understand where this get/set thing came from in the first place.

My first program

Back in the cold and dark days at the dawn of home computer era² [2This is the home computer revolution in the UK in the early eighties. Sinclair, Memotech, Dragon, Amstrad, Commodore and Atari. We didn't have computer clubs where you could play with different computers, we had to use Laskeys instead.] when I was first programming we had BASIC.

At this time in my programming development I committed a number of sins. To start with I would use GOTOs with gay abandon. Of course every variable was a global and you would only rarely see a GOSUB.

Just as serious were the data structures, or more correctly the lack of data structures.

As a nostalgia piece I decided to work some of my global-variable-and-complex-data-types-in-arrays style coding into the timing function of an article on recursion (which uses functions that calculate powers as its example). Download the source and take a look at the results merrily bandied about.

The results array contains the metrics of each function as it is tested. As you de-index against the (there's no good way of explaining this) order that the functions appear in the source code () you find a two element array that contains the name of the function and then the timing results. This element is itself an array of two element arrays. Each of these is the power requested against the time taken. So:

  • results[ 0 ][ 0 ]—the name of the first function.
  • results[ 3 ][ 1 ][ 2 ][ 1 ]—the time taken to run the third test for the fourth function.
  • results[ 2 ][ 1 ][ 5 ][ 0 ]—the power requested as the sixth test for the third function.

This is why we don't muck about with this sort of nonsense any more. It works — you can see that it does when you run the code — but would you want to maintain it?

For my latest forray in these waters I was doing it on purpose because I wanted to show what I did when I wrote BASIC programs.

Why did I do it then? The prime reason was that I didn't know any better — I was only twelve — but a secondary reason was that the language I was using didn't allow me to do it any other way. There were no functions so there was no concept of a local variable and there was no concept of defining data types — you were jolly glad for the ones that came with your BASIC interpreter, thank-you very much.

But who notices that their language is limiting their knowledge when they don't know any better?³ [3Paul Graham introduced the idea of the blub programmer (and blub language) in his essay Beating the Averages]

Oh, and that first program for you:

10 PRINT “Hello! ”;
20 GOTO 10

Simple, elegant and produces pretty patterns.

Structured programming

Now, given the mess that early programming languages left all over the source code you can already see where we should head. But of course the early pioneers didn't have hindsight to work with. They were peering into the future and the future was all around them and full of possibilities.

The C programming language taught more people the way forward than any of the other languages around. It wasn't that it was more sophisticated, because it certainly wasn't, but it solved the mess of arbitrary data structures by showing that it was OK to let the language (and by extension the compiler) sort out where you were putting things. And more importantly it did this without putting any distance between the programmer and the CPU.

With early processor and compiler technology, programming in C was only half a step removed from programming the metal itself. Adding structure to the data didn't have to impose any overhead at all when the program was running — the compiler could handle it, and it could handle it better than most of us could when writing assembly (my early assembly code sucked — but you wouldn't believe how proud I was when I got my first interrupt working on a Z80).

The thing was that it wasn't doing anything that couldn't be done before. It just made doing those things easier. You could always build a complex data structure by just drawing a diagram of how it would work in memory and then accessing those offsets yourself, but now you didn't have to. You could let the compiler worry about it.

And all was right in the world.

Well, almost.

The thing was we could still implement a linked list as a single array of integers in memory and although the structure definitions allowed the access to the data structure to be self-documenting it was still far too easy to abuse. Look at the self-documenting nature first. Compare:

struct double_linked_list {
    int prev;
    int next;
    int value; };
double_linked_list_ptr *dllp = malloc( sizeof( double_linked_list ) * 100 );
dllp[ 0 ].prev = 0;
dllp[ 0 ].next = 1;
dllp[ 0 ].value = 42;
dllp[ 1 ].prev = 0;
dllp[ 1 ].next = 1;
dllp[ 1 ].value = 0;

With:

int *dllp = malloc( sizeof( int ) * 300 );
dllp[ 0 ] = 0;
dllp[ 1 ] = 1;
dllp[ 2 ] = 42;
dllp[ 3 ] = 0;
dllp[ 4 ] = 1;
dllp[ 5 ] = 0;

Now I'm sure you'll agree that the first is self documenting compared to the second, but it still leaves a little to be desired. The desired bit of course is controlled change on the structure.

What was the end-of-list and start-of-list marker? In this example it's an element that points to itself, but you'd be forgiven for missing it. And in any case, why should you care? It turns out that you have to care because you're responsible for getting this right every time you use the structure.

This introduced the second part of the structured programming paradigm which gave us a load of functions called things like:

double_linked_list *init_double_linked_list( int items );
int double_linked_list_head( double_linked_list *list );
int double_linked_list_insert( double_linked_list *list, int value, int position );
int double_linked_list_erase( double_linked_list *list, int position );

Apart from spending hours correcting typos in the long function names there was always the chance that somebody forget to call init_double_linked_list(). Then it got worse when somebody discovered that it was faster to bypass all of the checks done by double_linked_list_head() and just do this:

int head = list[ 0 ].value;

And once you start down that slippery path there is no turning back. The encapsulation of the data structure has been completely blown away, and what's worse is that the moment the access pattern of the list changes we suddenly find that we have a bug and have to go fix it everywhere because the correct way to access the head's value is this:

int head = list[ list[ 0 ].next ].value;

The brilliance of structured programming was that it imposed a discipline on the programmers to use specialised functions to access and manipulate the data structures. The problems were the terribly long names and the lack of enforcement. Luckily an idea borrowed from somewhere else could help with these issues.

Objects, objects everywhere

Sometime in the early nineties it seemed that the programming world suddenly discovered a way of programming that had only been around for the previous twenty-five odd years [4This delayed re-discovery happens a lot in computing. If you want to get an early start in the latest programming paradigms just read some research papers from between twenty and thirty years ago. Given the current rise in functional programming maybe you should be going even further back.].

Objects neatly solved the proliferation of ever longer function names by tying functions to the objects themselves. Now you could have as many functions called head() as you had classes, more if you used the right language. Even better you didn't have to worry about always passing the structure to your functions because the language would deal with it for you.

It was this ability to keep the functions tied closely to the data and the ability to use convenient names that mainly drove the adoption of object-oriented languages, paradigms and idioms in the nineties even though it has virtually nothing to do with the philosophy of object-orientation [5I'm only going to discuss object orientation as it relates to encapsulation. This is a pretty naïve way of using objects, but it is an important point in the context of this discussion. The improved encapsulation that the use of objects gives us is really a side-effect of the driving force behind its development, albeit an important one.What object-orientation is really about is a discussion for another time.].

This is why so many people think that encapsulation and objects are the same thing. Worse is that the requirement to add accessors in many of the languages (and poor teaching) is why so many think that adding an accessor to data members indiscriminately doesn't break encapsulation — for these programmers the object, by definition, is an encapsulation no matter what else you do to it.

So what's the real secret of encapsulation then?

When you look at the data that your class holds you'll see that it has one of two different purposes:

  1. There's information that you hold because it is useful to the client of the object. This may be some metric, configuration parameter or just a memo item that the client needs to associate with the class in order to keep track of it. This information always has read access and sometimes has write access. After all the object holds this information because it is useful to others, so it has to allow them to see it.
  2. There's information that the object holds because it needs it to fulfil its function. Without this information the object will fail to do its job so it must guard this data jealousy. The object will sometimes provide read access, but never write access. The concept here is self preservation.

It is this dichotomy between what attributes do for the class that ultimately drives our choice of whether or not an accessor should be provided or not and whether that accessor breaks the object's encapsulation [6For example, in our list example above accessors to the next and prev members, even if only readable, will clearly break the encapsulation. What these values contain and how they are used is the business of the list implementation and nothing else. value is different though and there should be accessors (or it could even just be public). See the appendix below for a deeper explanation of this example.]. The core point here is that providing accessors to a member may or may not break the object's encapsulation and whether it does or not depends on what that data member does for the object.

Very occasionally you will find that there is something that legitimately falls between these two rules, but it's far more likely that you've either:

  1. made a mistake in your design; or
  2. you're trying to plug a hole in your language (remember those BASIC days?).

If it is truly legitimate you will be able to justify it. If it's a problem with the language then… Change languages?

Appendix

I didn't want to leave this without giving some examples. I also wanted to put some of the rubbish code in the article in a slightly deeper context.

The list

In our C example we have a very simple double-linked list for storing ints based on an array of ints. There were three data members: prev, next and value. Clearly prev and next should only be indirectly accessible through functions that allow us to move backwards and forwards in the list. The value member on the other hand is the whole raison d'être of the list and is, and should be, read/write for the user of the list. The list doesn't care what those values are.

In the same example we also had an implicit length of 100 for the number of values that could be in the list before it was full. Our first impression is probably that this member should be writable, readable and has a direct impact on the implementation. It seems to fall between our two rules in that it is critical to the list function as well as being something a client of the list would need to read and write.

This is a fallacy. The reason is that the write operation isn't really a write operation, it is actually a suggest operation. The maximum length that the list can handle should be managed by the list implementation. As a client we can suggest a length if we have some idea of how long a list we need, but the list implementation itself is free to do whatever it feels with our hint. We cannot directly write to the value, we can only suggest.

A co-ordinate class

One classic reason given for hiding attributes behind get/set members is that we may later want to change a type. Let's say we start with this simple class for storing a two-dimensional co-ordinate:

class point {
public:
    int x, y;
};

We may say that we should hide the data members because if a client requires more accuracy we can then change the class. This means that we end up with:

class point {
public:
    int get_x() const;
    int set_x( int );
    int get_y() const;
    int set_y( int );
private:
    int x, y;
};

This supposedly improves the encapsulation because we can now alter the type we use internally. In practice this forces us to do something akin to the below (in a strongly typed language):

class point {
public:
    int get_x() const;
    int set_x( int );
    int get_y() const;
    int set_y( int );

    double get_x_ex() const;
    double set_x_ex( double );
    double get_y_ex() const;
    double set_y_ex( double );
private:
    double x, y;
};

This is a serious mistake. It looks obviously stupid when put like this, but in the real world there are all sorts of variants on this technique.

If there is an attribute and you're not sure what type it should be then you have two serious choices:

  1. delegate the type to another class;
  2. parametrise the type.

Weakly typed languages will naturally allow you to do the second (or better yet not worry about the issue at all) and strongly typed languages should allow either with equal simplicity. Unfortunately there are many unsophisticated languages that make this a much more tricky problem than it should be (I'm mostly thinking Java here, but I'm sure there are others). If you're stuck with a language like that then, first of all, you have my sympathy.

In practice though you have a hard choice. Delegating will solve the problem, but may introduce an unacceptable performance overhead unless you have a very good compiler. You may well end up doing the serious mistake above simply because your language allows no other path. Blame your language and know why you're doing it though. This falls squarely under the trying to plug a hole in your language category I explained earlier.

These days I tend much more towards having public members, but arranging for the syntax to match that of the accessors. I describe this particular example in a lot more detail in C++ killed the get & set accessors.

Switching between the rules

When you first start to work on a class you may well find that you discover a member that changes from one of these rules to the other. If you find yourself doing this a lot then it's probably because you're not experienced enough yet to get it right first time. Don't worry about it — experience comes with time and practice, and getting things wrong and understanding why is far more valuable than getting it right by accident.

In contrast, if you find yourself never changing between these then you've probably not understood the point. This is a far more serious problem.

If you find that you do it sometimes then the pattern that should emerge is that it occurs when you are still trying to understand the problem domain properly. This is normal.