Object Encapsulation

Thiago Cordeiro
Mollie
Published in
7 min readFeb 24, 2021

--

Once upon a time, we had just finished lunch and, as usual, I was heading to prepare a cappuccino at the office coffee machine, running away from Pietro (our Italian mate), I could hear him saying “Hey… you are not allowed to have a cappuccino after 11 am”. Well, this day Albert and Robin were putting on their jackets and Albert invited me to have a coffee outside the office, a slightly different one, made of coconut milk. Of course, I’m always in for a good coffee and joined them, so we went down the stairs and walked around the block to reach the place.

Once there, we did what we usually do when ordering a coffee, said hi to the barista, Albert ordered his coffee, Robin was next, and finally myself. I tasted and woooow, that’s amazing. I will definitely come back again I thought, we paid, and headed back to the office.

That’s a normal life story, not very detailed but something like this happens every day and everything goes very smoothly. The process is very simple: put on the jacket, go downstairs, walk to the coffee place, order, be happy, pay, and get back to work, easy to understand as well as easy to repeat, we can do it every day unless we run out of money.

Let’s change the perspective…

They invited me, we put on our jackets and headed to the place, then… We arrived there, said hi to the barista, Albert went to the bar, prepared his coffee, Robin was next, and finally myself, prepared, tasted, and the coffee was just amazing. While we enjoyed our coffee the barista picked up our wallets and got the amount to pay for the coffee, put our wallets back in our pockets, we said bye, and went back to the office.

Same story, our goal was to have a coffee and we achieved it, we had the coffee, we paid, and we got back to work.

Why does the second sound so weird?

Because it loses encapsulation: We own the wallet, so it’s our responsibility to take the money and pay. We are a framework, we can do a bunch of different things and we are sometimes mindful of risks, responsibilities and boundaries, so it’s our duty to grab the money and pay. A barista is an external interface, they don’t care how you’ll pay, they just care about receiving the money no matter which card we use, whether it’s debit, credit, or whether we pay by cash. They only care about receiving the amount for the coffee and hopefully some tip.

Usually, those actions are very clear in our daily life, but are they clear when we write code?

Let’s write some code:

That could be a normal code in your engineering life and, maybe it’s not obvious, but do you see the issue with structuring your code this way? This is the same as the barista picking up our wallets and taking the money out, but can we guarantee the wallet has enough money and we don’t have to wash dishes? Can we guarantee the right credit card will be chosen? Or is the right amount being charged and are we not paying for a kopi luwak?

So we should write our code differently, but first, let’s try to draw a real-life scenario and how it actually happens:

Step by step the flow would look like this:

  • The customer orders a coffee
  • The barista gets the bill summary
  • The barista asks the customer for the payment
  • The customer grabs his wallet and pays
  • The barista deposits the payment into their bank account

This process is correctly encapsulated, each one has the right responsibility and the chance to have a safe and reliable process is higher.

Now let’s write some code to represent this process:

As we can see in the second example, now the wallet balance is not exposed and it guarantees the right amount is being deducted, also now we have the Customer playing its role, in this way the barista will not have direct access to the wallet, and the customer could have some additional logic in it, for example, transferring money from saving to checking account or running away as fast as possible ¯\_(ツ)_/¯

Now let’s explore how these encapsulations can make our life better.

Better testing, more confidence

This approach is known as Rich Model. Marcin Dźwigała describes it in his post Encouraging use of a Rich Domain Model, by explaining the Anemic model:

Rich Domain Model is what Object Oriented Programming is most suitable for. As we know, OOP helps us, developers, to link behavior (methods) with data (properties) using classes. Unfortunately, often Entities end up as adapters for a database with lots of getters and setters modifying private properties with almost no business logic […] instead of implementing business logic where it would fit the most, in the Model […] especially in web applications, all of the business logic goes directly into a controller […] And that is the opposite of a Rich Domain Model, an Anemic Domain Model.

All the business logic is moved to different entities. Services become handlers driving the application, connecting multiple entities, aggregates, and interfaces with external dependencies. Testing these entities and aggregates will become much easier since we just need unit tests, they are faster, easier to change and extend.

Let’s look closer into this example. At some point we might identify bugs, for instance, PHP has a proper math library that prevents issues with floating-point. Once it happens we can get the input value (causing the bug) and extend our testing scenarios with it, of course, the new test will fail. Then we can change the implementation to use the library, the new test will pass and all the other scenarios should pass. We fixed the bug and we are confident that nothing will break.

Better understanding

Martin Fowler once said:

Any fool can write code that a computer can understand. Good programmers write code that humans can understand.

Classes, method names, and Exceptions try to imitate the real-life actions and expressions, and the real-life scenario is very clear to anyone, even for non-engineers. Besides the process, we follow the Single Responsibility Principle where classes are small and holder their isolated responsibility.

We also should try to follow the (Dependency Inversion Principle). All the database connection and parsing logic, or all the HTTP call stuff and error handling are not part of the business logic. Those are external implementation and we can put everything behind an interface, which can represent a business action, “deposit the money” for instance.

Easy to change

Imagine we have implementation details in our classes:

  • Albert wants to pay his cappuccino using a credit card, the ChargeHandler must be aware of a credit card payment flow (grab the card, authorize the payment, capture the money) and deal with all its edge cases.
  • Robin wants to pay with cash, then the ChargeHandler must be aware of a cash payment flow, (collect the cash, check that it’s not fake, deposit into the cash register, return the change, deposit into the bank account).
  • I want to pay with bitcoins (of course), so the ChargeHandler needs to generate an address and wait for the confirmation.

All these implementation details will make the ChargeHandler a very big class, and they have many more steps than a payment flow requires because each method works differently. Can you imagine what this class would look like? Many ifs and many different possible execution flows, it would just make this class hard to change.

In the real-world there are different interfaces to handle different payment methods, for a credit card there is the machine, which authorizes and captures the money, for cash, there is the cash register, which opens up so the barista can receive the money and return the change, for bitcoins they will have a bitcoin address. The process is the same: Ask for some money, wait for it and deposit somewhere.

Our objects don’t need to know about these details, they just need to receive a payment, and we can take advantage of the Strategy Pattern to handle different methods. TheChargeHandler will contain only payment receiving logic connecting to this interface. We can very easily change and add new business rules to it, for instance: support multi-currencies, or discount/cashback policy, or we can publish an event and have other handlers listening to it.

Conclusion

By encapsulating objects we guarantee scope limits, in the example, the Barista interacts with the customer, while only the customer can interact with its wallet. Objects have a valid state from the very beginning, and they are aware of how the state can be changed, in the example we check whether the balance is enough.

We benefit a lot when testing, we can write unit tests without much set up overhead or mocking, all test cases will be very specific and a few lines of code will cover all possible scenarios.

Any internal change will not cause much trouble or side effects, because the same input will always return the same output.

And finally, we will represent our code in a much more real-life approach, AKA as DDD, where it reflects situations that can be easily understood by anyone.

--

--

More important than where we are is who we meet and what we learn on the way, I’m a software engineer trying to share a bit of what I learned with people I met