Object encapsulation and properties

published: Sat, 4-Mar-2006   |   updated: Sat, 4-Mar-2006
Copper Mountain in summer

I'm sure we can all rattle off the three tenets of object-orientation without even thinking about it: encapsulation, inheritance and polymorphism. Easy-peasy. I'm sure we could, without even the slightest sheen of sweat appearing on our brow, knock off a quick description of what they mean as well. So why don't we pay attention to them when we write some object-oriented code?

Let's just take one: encapsulation. Good grief, this is the simplest of the lot. It means? Well, dear reader, think about what it means before you read on. We'll wait.

If you're like me, you will have made up something about enclosing a bunch of data and behavior with some code that hides the internals of that data and behavior. A black box in other words. You know how to create one, you know how to poke at it so that it does some work, and that's about it. Without looking at the code that implements it, you're pretty much in the dark.

And therein lies its benefit: because the black box has a well-defined interface for its use and hides the internal details of how it's implemented, we can replace the black box with another whenever we want; so long as the new black box has the same interface, of course. That's a very powerful and useful concept.

There is an extremely valuable construct in modern OO languages that is a pure realization of encapsulation, but in my experience tends to be little or badly used: the interface type. With an interface you can define pretty strictly what the behavior of an instance of that interface should be. Furthermore there are absolutely no hints as to how the behavior has been implemented. It's encapsulation at its most virginal in a sense. Breaking encapsulation that has been defined by an interface tends to be quite hard and generally not worth it. It's easy to create classes that satisfy the interface (known as "implementing the interface"), but it tends to be hard to design and define the interface in the first place. Instances of the implementing classes can be used for an interface instance without the calling code being any the wiser. You can swap them in and out ad infinitum, the ultimate black box.

The visibility keywords like public and private in object-oriented programming languages are expressly designed for encapsulation of data and behavior in classes. There's whole newsgroup threads out there about whether the protected keyword should be allowed, whether inheritance (another of the pillars of OOP, remember) or friend classes break encapsulation. Delphi for example has suffered until recently from the implicit friend relationship for classes in the same unit (I remember jabbering on about this at TurboPower with the developers once).

There is another concept that breaks encapsulation, but we tend to ignore it or we don't realize that it does break encapsulation. It's another of those things that comes from a procedural programming mindset, and is used by a programmer who has migrated to OO programming from old-style procedural programming. Like me.

The concept even has a law that frowns on it: the Law of Demeter. There's also a principle we should apply: the "Tell, Don't Ask" rule. Yes, I'm talking about properties (and accessors and mutators, or getters and setters).

The problem is that properties break encapsulation. They are a window into the internals of your black box. Worse than that, they encourage the developer to inadvertently move behavior that should be internal to the class outside into another. Since I got such a lot of opprobrium last time for discussing someone else's code, this time I'm going to use some of mine.

This is the definition of a binary tree node, excerpted from my book:

PtdBinTreeNode = ^TtdBinTreeNode; {binary tree node}
TtdBinTreeNode = packed record
  btParent : PtdBinTreeNode;
  btChild  : array [TtdChildType] of PtdBinTreeNode;
  btData   : pointer;
  case boolean of
    false : (btExtra  : longint);
    true  : (btColor  : TtdRBColor);

Quick: what's wrong with it? I can come up with a couple of things straight away: it's not a class, it's just a record (or a struct, in C-language parlance); any encapsulation here is mostly about gathering a bunch of data in one packet, there's no data hiding here at all. This is typical Julian code from 5 years ago, essentially.

Here's some code (from a method in the splay tree class) that uses this node definition, together with a call to it.

function TtdSplayTree.stPromote(aNode  : PtdBinTreeNode)
                                       : PtdBinTreeNode;
  Parent : PtdBinTreeNode;
  {make a note of the parent of the node we're promoting}
  Parent := aNode^.btParent;

  {in both cases there are 6 links to be broken and remade: the node's
   link to its child and vice versa, the node's link with its parent
   and vice versa and the parent's link with its parent and vice
   versa; note that the node's child could be nil}

  {promote a left child = right rotation of parent}
  if (Parent^.btChild[ctLeft] = aNode) then begin
    Parent^.btChild[ctLeft] := aNode^.btChild[ctRight];
    if (Parent^.btChild[ctLeft] <> nil) then
      Parent^.btChild[ctLeft]^.btParent := Parent;
    aNode^.btParent := Parent^.btParent;
    if (aNode^.btParent^.btChild[ctLeft] = Parent) then
      aNode^.btParent^.btChild[ctLeft] := aNode
      aNode^.btParent^.btChild[ctRight] := aNode;
    aNode^.btChild[ctRight] := Parent;
    Parent^.btParent := aNode;
  {promote a right child = left rotation of parent}
  else begin
    Parent^.btChild[ctRight] := aNode^.btChild[ctLeft];
    if (Parent^.btChild[ctRight] <> nil) then
      Parent^.btChild[ctRight]^.btParent := Parent;
    aNode^.btParent := Parent^.btParent;
    if (aNode^.btParent^.btChild[ctLeft] = Parent) then
      aNode^.btParent^.btChild[ctLeft] := aNode
      aNode^.btParent^.btChild[ctRight] := aNode;
    aNode^.btChild[ctLeft] := Parent;
    Parent^.btParent := aNode;
  {return the node we promoted}
  Result := aNode;

  {determine the kind of double-promotion we need to do}
  if ((Grandad^.btChild[ctLeft] = Dad) and
      (Dad^.btChild[ctLeft] = aNode)) or
     ((Grandad^.btChild[ctRight] = Dad) and
      (Dad^.btChild[ctRight] = aNode)) then begin
    {zig-zig promotion}
  else begin
    {zig-zag promotion}

Ugly, eh? Look at all those bloody carets for a start that muck up your scanning and reading ability. Look at how the code delves deep into the node record to get at items of information. Gasp at how I chain from the node to its parent to get at one of the parent's children (a violation of the Law of Demeter). Notice how from this class (the TtdSplayTree class) I need to know intimate details of how the node is constructed: in effect I'm asking the node for internal data so that I can manipulate it outside the node.

Man, looking at that lot, I'm sorry for all those people who bought the book. Anyway...

Here's some code I'm writing for an article that will derive balancing algorithms for binary search trees, together with how it's called (warning: this code is still being developed; don't even use at your own risk):

  INode = interface
    function GetData : TObject;
    function GetKey : IKey;

    function Next(aKey : IKey) : INode;
    function Attach(aKey : IKey; aItem : TObject) : INode;

    function RelationshipWith(aChild : INode) : TRelation;
    procedure RotateAbout(aParent : INode);

    function This : TObject;

procedure TSimpleTreeNode.RotateAbout(aParent : INode);
  parent : TSimpleTreeNode;
  Assert(aParent.This is TSimpleTreeNode);
  parent := TSimpleTreeNode(aParent.This);

  if (parent.FLeft = self) then
    parent.FLeft := FRight;
    FRight := parent;
  else begin
    parent.FRight := FLeft;
    FLeft := parent;

  gparent, parent, walker : INode;  
  if (gparent.RelationshipWith(parent) = parent.RelationshipWith(walker)) then begin
  else begin

This code covers the same functionality, but I would venture is much easier to read in the calling code. The calling code needs to know nothing about how the node is constructed internally. It just knows that, conceptually, there's a left and right child in a node and that nodes have parent nodes so that a binary tree could be constructed. Those nodes could be individual objects (so that somewhere there's some code that news up (creates) a node), or it could be that the entire tree is stored as an array of nodes and they're pointed to by indexes and not references (much as a heap is built).

In essence, knowledge about the internals of the node are stored within the TSimpleNode class, and further hidden behind the INode interface. The binary tree class knows nothing about all this (indeed, the way I've designed it, the splay algorithm is written using the Strategy Pattern and not by inheriting from a base tree class as I did it in olden times).

In writing this later code I was extremely attentive to not exposing the left and right child references, either as public fields or as properties. If I had, I'd have produced the same code as before, where the tree class in essence becomes a controller and manipulator of dumb nodes.

What I'm trying to get at here is that the first code example exposes way too much and because I wrote it that way, in a procedural fashion, it meant that I then violated encapsulation all over the place. The tree is intimately linked to the node definition with a very high coupling; they are in essence a single class that happens to have been written as a class and a struct. Replacing the node record with a node class with Left and Right properties would only get rid of the carets in the code. Nothing much else would change; the node is a dumb class, a data container, with no behavior worth speaking of.

Now, it may be argued that this is OK ("how can you define a binary tree without referring to nodes, eh?") and that the tree having intimate knowledge of its nodes is perfectly acceptable ("that's how all the algorithm implementations do it, anyhow"). And in this case, yes, maybe it is. But I've seen lots of code in my time where properties of an object are deliberately used outside the object in order to manipulate the object in some way, manipulation that can and should be part of the object's behavior.

Here's a tiny example from an internal code review (names changed, etc).

string originalNodeName =
    busObj.State.Name.Substring(0, busObj.State.Name.IndexOf(":"));

To me this one line of code raises several flags. There's the code duplication for a start, there's the violation of the Law of Demeter (which leads to the code duplication), there's the built-in assumption that the Name string (of the State object, of the business object) cannot be null, and there's that niggling thought wondering what originalNodeName is going to be used for. If you could ignore the latter point, you really would like to write this:

string originalNodeName = busObj.getOriginalNodeName();

And then you read the next couple of lines:

string proposedNewNodeName = newName.Substring(0, newName.IndexOf(":"));
return originalNodeName == proposedNewNodeName;

So, yes, indeed, there is some decisions about an object being made outside the object. You should be writing something like this instead:

return busObj.IsSameName(newName);

Notice that the actual code doing the checking for equality hasn't gone away: it's just been refactored into the business object class (wherever that may be).

Anyway, in my "improved" second code sample the tree code (actually an algorithm class) just knows about the INode interface. The INode interface defines some basic behavior ("given a key, return the child node where the key might be found", "given a key and an item, attach a new node in the correct child position and return it"). The TSimpleNode class that implements the INode interface could be replaced at a moment's notice with something that has the same interface and the tree class would still work just fine. (Note that the INode interface does define a couple of getters: the GetKey and GetData functions. This goes along with the idea that a node carries the data and the key, and has behavior that defines how it acts in a tree.)

I think I'll stop here for now (this article is already pretty long), but I'll continue with this thought in some future postings.