ByteIntroduction

A new requirement was implemented in a well-known online shopping platform (like amazon). That requirement needed changes across different modules. Post that change, we see a bug in the repeated code. In this byte, you will improve the design and fix the issue using encapsulation.

Skills:

OOPS

Objective

Learn encapsulation by applying it in a practical scenario.

Background

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.

Primary goals

  1. Learn why encapsulation is needed

  2. Understand where and how to apply Encapsulation

Objective

Learn encapsulation by applying it in a practical scenario.

Background

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.

Primary goals

  1. Learn why encapsulation is needed

  2. Understand where and how to apply Encapsulation

Getting Started

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

image alt text

Current design to store User Preferences

image alt text


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.

Code structure

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:

  1. 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.

  2. 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.

Validate Country and Language combination

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.

The problem

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.

Can you find the problem in the below implementation

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.

image alt text

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?

image alt text

What was the root cause of the bug?

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.

  1. This can lead to inconsistent behavior.

  2. 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.

How can we design for more maintainable code?

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.

Redesigned Code

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

  1. __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.

  2. __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

  3. update_user_language(self) - wraps our validation logic which the handlers will now be utilizing for updating the user’s language preference

  4. update_user_country(self) - utility method to update user’s country preferences

  5. _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.

  6. _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.

image alt text

image alt text


Run the oops_solution/main.py file & verify the contents of the user_preferences.json file is correct

image alt text

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.

New Requirement

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! :)


Debrief

  • 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

Curious Cats

  • 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?

Newfound Superpowers

  • You have experienced Encapsulation! And it is here to stay with you.

Now you can

  • 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?