Encapsulation

A recently read a book where a contributor was arguing the importance of Encapsulation in Software. I agree. Encapsulation is fundamental to code being maintainable and extensible. But, what does this actually mean in practice? What should be encapsulated? Where do we draw the boundaries? I don’t think there’s a hard and fast rule that can be applied to all scenarios to given someone an indisputable answer. However, I do think that experience, pragmatism, and leaning on a set of good foundational principles can help someone get it right – most of the time.

The contributor’s position was that encapsulation should be around behavior and state. In theory I agree – sort of. Personally, I would have phrased it as: Encapsulate state and expose behavior through interfaces. He further says that an object encapsulates both state and behavior, where the behavior is defined by the actual state. An example about a door object follows where it provides an open and close operation where the behavior would depend on the current state. This does make sense and would be how I would describe the primitives involved.

Next came the example where they used Customer, Order, and Item to illustrate an example of “good” encapsulation. The Order object would know it associated Customer object and its addItem() method would encapsulate functionality around validation like ensuring the Customer had sufficient credit or funds to pay for the additional item. From the description I assume they were implying something like this in the Order class:

void addItem(Item item) {
    if (customer.canAffordIt(item.getPrice()) {
        items.add(item);
    }
    throw new InsufficientFundsException();
}

Then it goes on to say that some engineers would choose to put that sort of validation logic into a OrderManager or OrderService and that would be “wrong” because it would turn Customer, Order, and Item into just record type and introduce a single class that would contains a procedural method with a lot of internal if-then-else constructs which would be easily broken and almost impossible to maintain. I think we really need to be careful with statements like that. They oversimplify (and trivialize) the thought process that should go into how we define boundaries and introduce logical separation that allow for functionality to be extended and grow organically over time. Let’s not do that …

I often recite the SOLID code design principles to junior engineers where I emphasize the Single Responsibility Principle which simply states that a class/method/function should encapsulate a single piece of functionality. You need to think the system through and ensure that your encapsulation makes sense and provides the right boundaries for future expansion that won’t result in spaghetti code or highly coupled code that requires tons of branching and high cyclomatic complexity. The previous example, for instance, introduces coupling between the Customer and Order classes. What if an order now has multiple customers attached to it? What if order validation needs to be performed conditionally? What if we had more complex validation that would only be applied for specific customer types? There are lots of other scenarios I could conjure up for how the code in the addItem() method would become more complex and tightly coupled over time. How would you even effectively unit test the functionality over time?

My advice is to prioritize encapsulation BUT also prioritize logical separation and interfaces. One good measure of success here would be the ease (or difficulty) of unit testing. If you need to write overly complex code to test a simple change, you need to rethink your architecture.

I may follow up this post will an example of this in action. I will update this with a part two if I do. I could even post the code on GitHub =)

Till then, cheers!

1 Comment

  1. Write your unit tests first people!

Leave a Reply

Your email address will not be published.