Published on

The ambiguity of encapsulation

Authors

A document without supervision

Imagine there is a guy named Jerry.

He is working in an organization in which he is responsible for a very important matter: ordering office equipment like pens, notebooks, stickies, etc. whenever there is a need.

Jerry is massively busy with his work so he's constantly absent in his room.

Other employees quickly understood and learned that Jerry is busy so that they need to take care of their needs themselves.

What they started doing was going to his room and filling in the "office equipment needs" sheet.

Jerry was confident that the format might change in the future so he considered this sheet as "version 1".

public class OfficeEquipmentNeed_Version_1 { /*...*/ }

public class OfficeEquipmentNeeds {
    public IEnumerable<OfficeEquipmentNeed_Version_1> Sheet { get; set; }
}

This wouldn't have been that worrisome, but this sheet started being passed here and there throughout the entire office.

Who has the document?!

Poor Jerry needed to track who is currently editting this document.

It took ages to find out where it is.

When finally Jerry found the document - it was full of unauthorized entires - people made jokes and ordered pizza, coca-cola.

Others put the actual needs, but in the wrong format. At least not the one that Jerry expected.

Jerry was pissed off. He was busy enough with coordinating shippings and ordering, so he couldn't stand this kind of situation.

He thought of hiring an assistant for doing this job, but his manager said "no".

Copy is all what you need

Suddenly, there was a corporate decision to change the format of the "office equipment needs" sheet.

Version 2 was rolled out.

//         ________ notice change: class into record
//        /
public record OfficeEquipmentNeed_Version_2 { /*...*/ }

public class OfficeEquipmentNeeds {
    public IReadOnlyList<OfficeEquipmentNeed_Version_2> Sheet { get; }
    /*...*/
}

public class Jerry {
    public OfficeEquipmentNeeds OfficeEquipmentNeeds { get; }
    public void AddOfficeEquipmentNeed(OfficeEquipmentNeed_Version_2 officeEquipmentNeed) { /*...*/ }
}

What's more, Jerry decided to introduce one new rule: no possibility for adding anything to the list without Jerry's knowledge.

Of course, people were able to ask about the existing needs - Jerry gave them the copy of such a sheet.

As they were using copies, no one was able to make jokes about pizza, etc.

Jerry was happy.

Even if an employee changed the copy, he had his own version.

But one thing he didn't anticipate.

Any pens?

Turned out that people started abusing the information stored in each copy:

/*...*/
IReadOnlyList<OfficeEquipmentNeed_Version_2> sheetCopy = needs.Sheet;
var whoIsAnOfficeThief = 
    sheetCopy
    .Where(need => need.Column === 6 && need.Contains("pen") && need.Count > 10)
    .Select(need => need.Requester)
    .ToList();

await gossipAbout(whoIsAnOfficeThief);
/*...*/

Soon, Jerry and the famous "thieves list" were in the spotlight.

Other employees used other information too and they made decisions based on that:

/*...*/
IReadOnlyList<OfficeEquipmentNeed_Version_2> anotherSheetCopy = needs.Sheet;
var anyExpensiveEquipmentOrdered = 
    anotherSheetCopy
    .Any(need => 
        (need.Column === 10 || need.Column === 2)
        && (need.Contains("printer") || need.Contains("scanner")))
    .ToList();

if(anyExpensiveEquipmentOrdered)
{
    await orderSomethingMoreExpensive();
}
/*...*/

One problem solved, other problems added.

What was even worse, soon a new version was required, so Jerry changed it:

public record OfficeEquipmentNeed_Version_3 { /*...*/ }

public class OfficeEquipmentNeeds {
    public IReadOnlyList<OfficeEquipmentNeed_Version_3> Sheet { get; }
    /*...*/
}

Chaos, chaos everywhere

Introducing new version made things more complicated.

Some people used copies of the previous version (version 2), others tried to use copies of the new version (version 3).

Jerry needed to go to each desk and check what version is there, replace it in case of an outdated one.

He became massively delayed with ordering which impacted company's KPIs.

Jerry's manager was hugely disappointed that he couldn't take his responsibility seriously.

Mind your own business

Jerry understood that even providing non-changeable copies didn't solve the problem.

He became even more restrictive - no one is able to see his sheet.

"Why they even need to know there is a sheet?!" - he thought.

Jerry decided to change the strategy - he would accept a need in a very universal format so that later on he can make a decision how to deal with potential corporate format changes.

Also, he thought of changing the language, so that no one would tell him to "add" something to a sheet.

He taught others to "request an equipment". As the language changed, the behaviors and results too.

Soon, people realized that a "request" might be rejected.

public class Jerry {
    public void RequestOfficeEquipment(StandardOfficeEquipmentNeed officeEquipmentNeed) { /*...*/ }
}

Also Jerry had a little secret - there was no spoon, uhm, sorry - sheet.

At least a paper one. Jerry was very innovative so he decided to write a web application that will enable him to additionally generates raports, send notifications, etc.

Of course he deployed it to a multi-node Kubernetes cluster so that everything was scalable and ready for a production-grade traffic of a single user.

And no one even knew - Jerry was keeping this information for himself and even Jerry's manager was amazed of the efficiency!

Details of...?

Encapsulation is one of the fundamental concepts in software engineering (generally in engineering too?).

In Object-Oriented Programming paradigm, encapsulation is typically used for protecting mutable state from being corrupted/changed externally (information hiding).

It also helps keeping the details of implementation hidden so that we might change our mind and use some different approach.

But what about Functional Programming paradigm? It is widely known that immutability is the one of its tenets.

OOP achieves "state evolution" through mutability (I tried to show differences in modeling in Many faces of DDD Aggregates in F# blogpost), whereas FP does it through immutability (is it not a "state" anymore?).

In OOP we must protect the mutable state so that it won't get "malformed", whereas in FP we are getting new one each time we apply some operation/behavior.

Does it mean that in FP we don't need to "hide the implementation"?

It's a copy, but still a detail!

One could be a bit relaxed and toss immutable records here and there. Spill them all over the "office" (I meant codebase, but why I said "the office"? Please check "Organization-Driven Design").

Such information could always be discarded, in case of becoming corrupted.

But what happens if we decide to change the internal structure of such a record? Or change the way we are implementing some operation and other kind of information is needed?

As we saw with Jerry's case, giving copies away resolves the problem partially. And it is not free from side-effects - "people" (functions, objects, etc.) might abuse/overuse copies (immutable records).

Even if we have immutability at our disposal, it's good to use "the second" trait of encapsulation - implementation hiding. It will help us with localizing changes and building cohesive units (of what?).

This means that we shouldn't be afraid of putting private modifiers (e.g. in F#, C# and TS) everywhere, use opaque types (you don't know what I am talking about? Please check Modeling Value Objects in TypeScript) so that we are able control the details.

What's more! We should be only using private modifiers and then question what is really needed to be visible publicly.

To abstract or to encapsulate?

Wait, can one have encapsulation without abstraction? Or abstraction without encapsulation?

Ask a developer, you will get an answer.

Ask another one, you will get another answer.

For me, at the time of writing, those are two different things. And surely you can have one without another (and vice versa).

How?

As you probably recall, when Jerry realized that people need to "mind their own business", he did two things:

  • he kept the "sheet" away from our sight
  • he changed the language of working with him (instead of "adding" he started using "requesting")

The main point of abstraction is to provide a certain language so that one can reason about on totally different level.

Reusability should not be the biggest "selling point" of abstraction (why? please check The ambiguity of abstraction). It might be a side-effect, but not the goal itself.

Could Jerry keep "adding" in his language, but hide the "sheet"?

Could Jerry change his language and use "requesting", but leave the "sheet" visible and accesible?

Both concepts are tools for other kinds of problems.

Tell, don't ask

Suddenly, when Jerry changed the language and when he made the details controlled by not mentioning the "sheet", no one was able to ask him about the needs of other people.

He was again "the boss" of ordering. He decided which requests should be rejected and which poeple need to be warned that they might be seen as thieves (because of excessive number of pens requested).

So he accepted requests, but not all of them were turned into orders.

Be aware of change in terms of words used! (What to know what I am taking about? Please check Three aspects of modeling)

A request might change into an order? That's interesting - it sounds like we might actually using Language of the problem, isn't it?

Nevertheless, from now on, Jerry doesn't need to provide any details.

Of course, he can given answers to a various questions, but without telling how he knows about it. It's again a combination of an abstraction (language) and details hiding (encapsulation).

Finally, Jerry got a responsibility assigned from his manager and he started operating in "tell, don't ask" mode - "tell me what you need and I will tell if it that's possible" (responsibility assignment? you might want to check A solid grasp of responsibility).

Keep everything private, expose only what's required

Even when you use code snippets from your favorite IDE - what class template does it use?

Are you getting public setters and getters by default?

When you create an object in TS - aren't all properties mutable and visible?

We are begging for trouble from an early start. Create an environment that will support the habit of encapsulation - make your objects/functions/types ignorant, apathetic and selfish with help of your tools.

Once created, it's hard to be changed - "nah, I'm lazy, I might need it at some point".

It might look innocent, but without the discipline and the supervision it will soon become a problem.

The same applies to using primitives all down the way - ints, strings, IEnumerables - it feels "easy" to use them and any other type or class might feel as "overengineering" or "accidental complexity".

But aren't we cheating ourselves? Aren't we lying that things are that simplistic? Maybe we are afraid of The cost of modeling?