Learn encapsulation by applying it in a practical scenario.
Learn encapsulation by applying it in a practical scenario.
Encapsulation is grouping all related functionality in one place. Apart from being cohesive (related fields and methods being together), it helps in securing the data fields of an object from being altered directly by external methods or objects. Interactions (reading or changing) with data fields of the object should happen only via methods that the object provides. This safeguards the data stored in the object from system-wide access.
In this Byte, we’ll see why encapsulation is a good design principle and a practical example of where and how it can be applied.
Learn why encapsulation is needed
Understand where and how to apply Encapsulation
Learn encapsulation by applying it in a practical scenario.
Encapsulation is grouping all related functionality in one place. Apart from being cohesive (related fields and methods being together), it helps in securing the data fields of an object from being altered directly by external methods or objects. Interactions (reading or changing) with data fields of the object should happen only via methods that the object provides. This safeguards the data stored in the object from system-wide access.
In this Byte, we’ll see why encapsulation is a good design principle and a practical example of where and how it can be applied.
Learn why encapsulation is needed
Understand where and how to apply Encapsulation
You can download the source code from gitlab by executing one of the following commands:
git clone https://gitlab.crio.do/crio_bytes/me_encapsulation.git
git clone git@gitlab.crio.do:crio_bytes/me_encapsulation.git
If you don’t have Git already installed. Use this as a reference to help yourselves do that.
Verify by running the main.py function
A well-known online shopping site offers the users multiple ways to access it. The users can use either the Web Browser, an Android App or an iOS App to access it. These are termed Clients.
Each of the Clients have their own Handlers on the Backend Server to handle requests. These Handlers read and update common structures so that any change made from one of the clients is accessible by the other.
The User Preference object is one such common structure. It stores the user’s preferences.
Currently, User Preferences include two fields - country and preferred language.
The options to choose a country and preferred languages are provided on the client. The clients then send the user selection to the Backend Server to be saved.
Fetch the code from this git repo as specified in the Setup & Getting Started task.
Let’s look at how we might go about implementing the User Preference class & the Handler classes for two of the Clients (iOS and Android).
user_preferences.json → file where we store user preferences. Any user preference updates have to be written to this file.
user_preferences.py → contains utility methods to read existing user preferences from user_preferences.json and write back updates to this file.
ios_client_handler.py → handler function for iOS application queries
android_client_handler.py → handler function for Android application queries
main.py → calls the Android/iOS handler code for demonstration
Let’s understand the code a bit more.We’ll start with user_preferences.py
Two of the user preferences we support now are the user’s country & their preferred language. The user_preferences.py file contains methods to update these fields:
update_user_country(user_name, user_country)
→ read current user preferences from json file, update country of user_name
with user_country
if the user already exists, else add a new entry for user_name
with user_country
and update the json file.
update_user_language(user_name, user_language)
→ read current user preferences from json file, update language of user_name
with user_language
if the user already exists, else add a new entry for user_name
with user_language
and update the json file.
Note: We are currently storing the user preference data in a json file for demonstration. In the real world, it may get stored in a database.
Coming to the Handler functions, the ios_client_handler.py has some methods, including one to change the user’s country preference and another to change the user’s language preference. It uses the update_user_country()
function provided by user_preferences.py to change the user’s country and the update_user_language()
function to change the user’s language.
Can you check the android handler at android_client_handler.py, which supports similar methods?
We are going with a simpler implementation for our current purpose and will use the main.py file as a proxy to call the handlers.
The user preferences are stored in JSON format in user_preferences.json file. Each entry can contain upto three fields with language & country preference along with the username. It gets populated when we run main.py (as specified in the Setup & Getting Started task). Try deleting its contents and see what happens when you run the main.py file again.
With the existing user preferences, some users end up changing the language to one that they don’t know (by mistake or maybe to fool around!). Then, they struggle to change it back resulting in a few customer complaints.
To address this, a new feature has been requested by the customer service team - to restrict the languages based on the country chosen.
If the user chooses a language that is not supported for his/her choice of country, an error is thrown and no update is made.
2 countries are currently supported - India and USA.
3 languages are currently supported - English, Hindi and Spanish.
When the clients send the country and preferred language to the backend server, it needs to do the following validation, before updating the user preferences:
If a user chooses India, the user can only choose between English and Hindi as the preferred language.
If a user chooses the USA, the user can only choose between English and Spanish as the preferred language.
This feature was implemented as requested and we now have a bug!
The user preference updates made from an iOS mobile client works correctly - setting the user preferences with validation.
However, updates made from the Android client are not consistent. The Preferences don’t get updated correctly.
Our updated implementation is available inside the non_oops_solution directory. We’re getting this error when we run the non_oops_solution/main.py file which utilizes the handler methods to change user preferences.
The error is trying to tell us that an error occurred when this line of code in the main.py file was run
change_user_language_android('luis', 'COUNTRY_USA', 'LANGUAGE_SPANISH')
That’s weird. Americans do speak Spanish!
As the change_user_language_android()
method is inside our newly implemented Android handler, that is where we need to check. Can you figure out the bug by looking at the implementation of change_user_language_android()
in non_oops_solution/android_client_handler.py?
Were you able to find the bug?
Aha, incorrect language combination was the culprit. Americans don’t speak Hindi, right? The code was missing the LANGUAGE_SPANISH validation under COUNTRY_USA and was using LANGUAGE_HINDI instead.
Taking a step back to understand the cause of the issue, we can see that similar functionality (eg: to update language) is present in both of the handlers.
This can lead to inconsistent behavior.
Results in code duplication which makes maintenance difficult. Whenever a new change is to be implemented related to the user preferences, the developer needs to work out the changes for each of the handlers separately.
We saw what encapsulation is in the Background section earlier. Is our current implementation encapsulated?
Nope. The handlers are triggering an update to the country and language fields directly. Each one is implementing its own validation logic.
Repeating code is never good. It is better to have common methods in one place, which can be used by all other methods.
What is the best place to put a method?
A method should be placed closed to its relatives. If you are using classes, all methods that work on the data fields in that class should be within the class itself, making it a cohesive unit. This is precisely what encapsulation offers!
The main advantage is this. The data fields of the class will not be modified from outside methods. This puts the state of the class object in the hands of the methods it exposes. So, the object stays in control. It is its own master.
Note: In Java, the private
access specifier can be used to hide the data fields from outside access.
We have answered the why
about encapsulation, let’s look at the how
in the next section.
Let’s see how to re-design our current implementation to satisfy encapsulation.
We have an Encapsulation based implementation of our country-language validation inside the oops_solution/user_preference.py file as the UserPreference
class. Class? Yeah, we’ve enclosed everything related to user preferences in a class. Let’s check the methods provided by the class
__init__(self)
- empty constructor for the class i.e, we’ll be able to create objects without passing any parameters. But, isn’t self
a parameter? Hmm, not really! self
denotes the object itself.
__init__(self, user_name, user_country=None, user_language=None)
- constructor that takes in parameters. We can see that user_name
is a mandatory parameter and the other two defaults to None
if not provided
update_user_language(self)
- wraps our validation logic which the handlers will now be utilizing for updating the user’s language preference
update_user_country(self)
- utility method to update user’s country preferences
_update_user_preferences_in_file(self)
- stores the user preferences to the oops_solution/user_preferences.json file. The leading _ in the method name denotes the method isn’t to be used outside of the class.
_read_preferences_from_file(self)
- Hidden method to read current user preferences from the oops_solution/user_preferences.json file.
You might be thinking, "Why are the variable names starting with __, don’t you like normal names?"
This is just a naming convention in python, which signifies that the field is not supposed to be accessed directly from outside the class.
If you check our new handler implementations, you’d see that both of them are using the same method, update_user_language()
of the UserPreference
class to update the user's language preference. The handler doesn't need to worry about how the user language is updated anymore.
Run the oops_solution/main.py file & verify the contents of the user_preferences.json file is correct
What are the different places we now need to check when the update language feature doesn’t work as expected? Just one, right?
Not only was code duplication avoided but also the developers working on the handlers can do their job and let the class methods (who know their own class variables better), do their job.
The new feature is clearly becoming a hit. With data available for the user’s country & preferred language, we are able to provide a much more personalized experience to the users. It’s time to open up our application to a much wider audience - add support for 2 more countries and 4 more languages with specific validation.
With the encapsulation based code, isn’t it much easier to implement than before?
Welcome to the encapsulation enlightened world! :)
Practical scenarios
Data gets passed between method/function calls all the time. With increasing complexity, we will end up passing more and more variables between methods which are related to a common cause. It is best to model these variables as a single object and pass the object between the methods. Encapsulation ensures only the necessary sections of the object are modifiable across all the methods.
Simple example: Passing variables to represent a matrix - a 2D array, number of rows, number of columns - can be replaced by a matrix object.
Another example: A product for sale has these fields - price, category, discount percentage, vendor name etc. These can be modeled as an object with methods to change the price, discount etc.
Having encapsulation ensures that situations like renaming of a variable doesn’t impact the external methods that are using the class method to get or set this variable.
Summary of Encapsulation
Encapsulation is achieved by the object making its state (data fields) restricted to private access. External objects cannot access them directly. They can only access the state through the methods exposed by the objects.
Why is it useful? - Sets the stage to restrict access to data fields and keeps the object in control of what fields can be modified and how. Ensures consistent behavior across users of this object.
Where to apply? - While designing any class that has data fields and needs methods to be supported for their reading or modification.
How to apply? - Provide public methods for reading or updating the data fields in the class only as needed. E.g. get() or set() methods.
What is the drawback if we don’t use encapsulation? - Duplication of code. Low maintainability. Inconsistent behavior. Difficult to test. Possible inappropriate access/modification from external methods.
Language specific notes
In Python, we name our variables starting with dunders(__) to explicitly mark them as hidden variables as Python doesn’t support this behaviour by default.
More OOPS aligned programming languages like C++/Java/Ruby provide access modifiers like public/private that we can utilize effectively to achieve encapsulation. Here, getter/setter methods are portals to the class state.
General encapsulation notes
Restricts direct access to data members of a class.
Fields are set to private if language supports it, else treated as such
Each field has a getter and setter method
Getter methods return the field
Setter methods let us change the value of the field
Does prefixing variables with dunders (__) in Python like we did really make them hidden from outside access? Is there some way to still access & manipulate them?
How does C provide support for OO programming?
Think about encapsulated code every time you start writing some new code. Design first, implement next.
Answer those interview questions
Explain one OO concept
What is Encapsulation?
When or why should you use private variables?