Avoiding breaking encapsulation can sometimes be difficult. The most common encapsulating breaking habit is using field accessors (properties/getters and setters), these can break encapsulation as the object looses control over how it's information should be interpreted and also a language is never built up. Here is an example: in a banking application the system needs to know if the account is overdrawn, it would be common to do:
if(account.balance < 0) ... do something
This code could be scattered across the system: however, what if a new business rule came in to say that if an account is frozen it shouldn't be marked as overdrawn? All those statements are incorrect and have to be changed. If behaviour had been put on the account it would have been simpler to do:
if(account.is_overdrawn) ... do something
Then the Account is in complete control over what it considers as being overdrawn. This is a simple example and easy to solve but what if there is an exchange of information between two objects? Breaking encapsulation seems a hard thing to avoid. For example:
if(loan.amount + account.balance < 0) ... do something
This is a classic case of "I'll show you mine if you show me yours": the loan exposes it's amount and the account exposes it's balance, both objects have lost control of their data. In the real world this would create scenarios of ridiculous bureaucratic proportions. Let's prove this with an application which models a host offering a guest a drink. Using the non-encapsulated method is the equivilant of having a third party ask both of you for private information and then doing the selection for you. Here it is in code:
class IllShowYouMineIfYouShowMeYours
def main
host.drinks.eachType | drinkType | do
if(guest.drinks.containsType(drinkType)) do
drink = drinks.get(drinkType)
if(drink.is_consumed) do
throw AlreadyConsumedException.new
end
guest.energy.add(drinks.get(drinkType).calories)
drinks.remove(drink)
drink.is_consumed = false
end
end
end
end
A better way to solve this is to use "You tell me and I'll tell you". In the real world the host would ask "what would you like to drink?" and the stock reply is "what have you got?" they would then tell you what drinks they have and you'd tell them which drink you want: neither of you expose any data: there is no case of the guest rummaging through the host's cupboards, the host can select their drinks themselves and the guest is allowed to decide what they'd like. By telling the Host to offer the drinks the Host can tell the Guest to choose one and encapsulation isn't broken. Here is the alternative:
class YouTellMeAndIllTellYou
def main
drink = host.give_drinks_to(guest)
if(drink.is_consumed) do
. . .
end
end
class Host
def give_drinks_to(guest)
drink = drinks.get(guest.which_drink(drinks.getTypes))
end
end
class Guest
def which_drink(drinkTypes)
drinkTypes.each |drinkType| { return drinkType if drinks_i_like.contains(drinkType) }
end
end
There is room for further encapsulation: the consumption of the drink still relies on outside manipulation, we have another case of "I'll show you mine" but even worse as it's not only showing it but it's letting just about anyone touch it too! So let's tell the guest to take the drink and tell the drink to give nutrition to the guest.
class YouTellMeAndIllTellYou
def main
drink = host.give_drinks_to(guest)
end
end
class Host
def give_drinks_to(guest)
drink = drinks.get(guest.which_drink(drinks.getTypes))
guest.give(drink)
drinks.remove(drink)
end
end
class Guest
.
.
.
def give(drink)
drink.consume(self)
end
def increase_energy(increase_by)
calories = calories + increase_by
end
end
def Drink
def consume(consumer)
if(is_consumed) do
throw AlreadyConsumedException.new
end
consumer.increase_energy(calories)
is_consumed = true
end
end
By encapsulating the behaviour we have given ourselves a number of advantages. The first and most obvious of them being testing: the encapsulated code is much easier to test as the behaviour of each class can be independantly tested and verified. We also stop spread: without those getters and setters it is very difficult to add behavior centric to those classes in any other parts of the application: all behaviour to do with the class is in one place, this means less repetition, less bugs and less maintenance. We also allow Single Responsibility: if we wanted to change the way the drinks worked (say from a list to a hash) then we can do safelty without breaking any code. Lastly we have code which supports polymorphism: for example if we wanted to add alcoholic drinks to the system, we can polymorphically add a different type of drink which causes a guests to become drunk:
class AlcoholicDrink
def consume(consumer)
if(is_consumed) do
. . .
end
consumer.increase_alcohol_levels(units)
. . .
end
end
The guest can also be made polymorphic:
def TeaTotalGuest
def give(drink)
if(drink.is_alchoholic) throw IDontDrinkException.new
end
end
def LightWeight
def increase_alcohol_levels(units)
total_units += units
if(total_units > 3) spew
end
end
def PartyAnimal
def increase_alcohol_levels(units)
total_units += units
if(total_units > 10) start_dancing
end
end
All of the above polymorphic behavior can be easily added without ever changing the code of any consumers, in the non-encapsulated version there would be a nightmare or nested if's and switch statements which would make even our TeaTotalGuest dizzy and want to spew. Or to quote Neal Ford:
"getters/setters != encapsulation. It is now a knee-jerk reaction to automatically create getters and setters for every field. Don't create code on auto-pilot! Create getters and setters only when you need them 10 Ways to Improve Your Code".
As a final note to readers: please drink responsibly!
1 comment:
I've been looking for a name for this pattern ever since I began eliminating getters and setters from my code. I've been thinking of it as 'mutual disclosure', or 'pass the parcel'. Often two base type values need to be combined in some way but are wrapped up in multiple layers of owner objects. One object passes itself to a second object, the second object calls back the first, passing in the required value without itself as a layer. This back and forth and unwrapping continues until finally the naked values can be combined.
I like this style of programming, though it does lead to a proliferation of small methods and deepening of the call stack, which some might find confusing. I prefer to have the many benefits of encapsulation as pointed out in your post.
Post a Comment