The Object-Oriented Programming is a paradigm that instructs us, how to organize data and function into objects. Those objects are structures that should be designed to reflect the real-world object. Fortunately, we have four principles defined: abstraction, polymorphism, inheritance and encapsulation, that will lead us towards good code quality. Unfortunately, for years I understood them improperly, even if I was really convinced that I know them well. This article has to shorten your way to an understatement of OOP paradigm.
During this article, we will focus mostly on practice with a little theoretical background. Even if all code samples are included, you can clone GIT repository with sources and follow each change. At the end of the article, you can continue learning by doing kata. To simplify work, we will skip unit testing. Let’s check, what are we going to work on.
The Washing Machine
Application, that we are working, is a representation of a washing machine. Seems like it is nothing special, just put clothes inside and start it, so the laundry is being done. On the other hand, when we start to gather requirements that ordinary washing machine has to do, it grows into at least a few point list.
- It must wash any laundry that we have
- Must provide dedicated programs for a different type of fabric
- Programs for fabrics must be adjustable in the matter of temperature and spin speed
- It should not be possible to insert too many laundry into the washer
We could add more and more requirements to the list, but let’s stop for now, and make those four a little bit more precise.
- Supported fabrics are wool, cotton and silk
- Wool can be washed only in temperature up to 40°C, with spin speed between 100 and 400 rounds per minute
- Cotton can be washed in temperature between 30 and 95°C, with spin speed up to 1200 RPM
- The maximum temperature for silk is 30°C and up to 300 RPM
- The default value for temperature and spin speed will be its maximum value
- The washer should have protection for putting over 7 kilograms of laundries
Development of the first version of the application
We are going to code the application in a way that some controller will provide laundry to the core of the application — the Washing Service. Washing Service will use the Washing Machine to perform wash. This version of the code will be a base for applying OOP principles. On purpose, we are going to design and implement it in a way that I have seen in multiple projects and companies. Writing Object-Oriented code is not only about using classes, but we also need to model it in a very precise way. For now, we will start with the following diagram.
We will start with defining two, the most valuable elements of the whole application:
WashingMachine. They are mentioned multiple times in the requirements, so let’s take a look at code that we are going, to begin with. You can also clone the GitHub repo and follow all the changes in your favourite IDE.
Base class that will keep laundry.
Supported fabric types are defined in the requirements, so we decided to keep them as an enum and hope it will not change pretty often.
Currently, our application will support three types of fabric.
The last thing from model classes is the washing machine itself. From the description, we may conclude that it will spin with different speeds and will do laundry at different temperatures. Also, it will, of course, perform wash and clean up the laundry.
Main, currently agnostic, Washing Machine class.
Model classes are fairly simple and represent object attributes pretty well. What we do not have here is the logic described in requirements for the application. We are going to implement it as a
WashingService. The logic that will manage the washing process is represented by three
We will focus on one of the methods, let’s say,
washWool and later will duplicate the solution of all of the programs. Wool should be by default wash at a temperature of 40 degrees and with the spin speed set up to 400 RPM. Before we set up our laundry, we also have to sort it by fabric type and get only the ones that we are currently interested in. We need to remember also required check for each of the parameters, so we will not exceed values defined for a program.
The whole logic is currently in Service class, together with value checks.
Having washing logic in place, we need the last piece of the puzzle — something that will invoke the washing process. The controller here is responsible for obtaining all laundry and providing it to the
WashingService so it will do the laundry. This part of the application can be changed, depending on the presentation layer, so we are not going to keep here any logic. Keeping logic apart from this class ensures us that all possible controllers will use the same process.
How hard we broke OOP?
Even if you do not see it yet, we already violated Object-Oriented Principles pretty hard. A lot of logic is now in the service, that is very specific to washer programmes. In the matter of encapsulation, we are taking out the internals of the washing machine and perform some operations on it. It is equivalent to pouring water with a bucket into the washer, instead of letting washing machine to take up the right amount — it is clearly violating OOP’s principles. What would happen, if new requirements would appear, like no drain function or double rinse? Adding such features to the service will quickly make it hard to maintain. Pretty easily we will reach a dead-end when code becomes unmaintainable.
Problems with lack of Object-Oriented approach
Till now, we have implemented an application that does not follow OOP principles, even if it might look to do. Our
WashingService is on a good way to become a big class that handles all washing-related logic. It is visible, that we are facing some problems:
- Moving value checks from Washing Machine to Service may lead to inconsistency. All services using Washing Machine needs to know, what are its limitations. What will happen, if we will have another, smaller Washing Machine, that may operate only on a maximum of 5 kg of laundry? We would have to update all services that are using it.
- Adding new service will require to implement exactly the same checks. If we would need to add a
DryingService, then it is needed to duplicate the code. Logic centralized in service instead of the object makes it hard to be reused.
- Extensive code is not easy to test. A lot of possible ways of execution for every method makes automated tests heavy (a lot of preparation) and overlapping (a common part of methods is tested by multiple tests)
Without following OOP, classes and tests may become heavy. Right now, to fully test all paths within
washWool method, we will need up to 32 unit test — just for this one method. I find it one of the core reasons for developers not writing unit tests — they are too big and require a lot of data preparation. Now we know, what is the problem with our code, and we do know how it should work. Time to make this code Object-Oriented!
Inheritance of clothes
While going into a clothing store, you can choose from multiple fabrics that clothes are made, yet all clothes have the same abilities — they can be put on, worn, stained and cleaned. Depending on the way you are looking at clothes, they can be categorized by different characteristics. You can distinguish them by type of clothes (shirts, jackets, trousers), but they are still clothes.
Inheritance in OOP is something, that allows for generalisation of specific things. Depending on that with who you are talking to, you might need to be more specific, or perform conversation on a general level. For example, imagine a situation when you are walking into a store, and shop assistant approaches you. It would be more valuable to say that you need a pair of trousers than saying you need some clothes. On the other hand, while talking to a friend that you meet on a street, it is perfectly fine to say that you are going to buy some clothes. Inheritance allows you to refer to jackets and trousers as clothes.
Inheritance vs polymorphism
While inheritance is about copying behaviours and attributes, polymorphism is about changing behaviour in subclasses. In Java, they are strictly connected — if
Clothes has a
cleanUp() method, to change its behaviour we have to extend
Jacket class. So, in Java, the OOP polymorphism is fulfilled by extension.
Applying inheritance to washing elements
Inheritance is clear
is-a relation. If
Jacket is-a Clothes. While thinking of laundry, we usually do not care if we are washing jackets or anything else. More important is the type of fabric – in our case they were distinguished by enum property. It works, but it is promoting direct access to the internals of the object. This may lead to breaking the encapsulation and gives the possibility to put the object into an invalid state. Having it in a property inside class requires you to search for this information inside the object – it is like looking for a clothes tag and reading it every time you want to wash this piece of clothes.
Let’s change our code. To make
LaundryElement class more OOP focused, we will have to do multiple things. Let’s start with making it an abstract class, so it will become the base for inheritance. Another small, but important step is making fields immutable where possible. This will prevent instances of this class being vulnerable to direct field changes. The only field that is mutating is the
dirty flag, we will not use
set method. Instead of that, we will hide it behind a little abstraction of
clean() methods. It promotes hiding internals of object and protects its data integrity by preventing anyone from directly providing value for object internals. This change will simplify future changes – in the case if it would be needed for clothes to keep information about how dirty it is (instead of just boolean flag), it will be only needed to change
dirty() method – all its calls will remain unchanged. Another change that we will introduce, is Creation Methods, as described by Joshua Kerievsky in “Refactoring to Patterns” (chapter 6).
Extended base class for laundry, with dedicated classes per each fabric type.
Final steps for this class is to apply OOP inheritance. We will extend
LaundryElement with three, dedicated classes, one for each fabric type. Because they are really small ones, they are implemented as nested classes. When classes are named, we can remove the
enum totally – now it contains obsolete information about the type of the fabric. Before that, we need to replace all usages of
Fabric, e.g. in Washing Controller.
Where the laundry comes from?
While modelling the real world with OOP in mind, we are trying to reflect reality the best we can. Right now, our laundry is represented by
LaundryElements, but it is taken from nowhere. It is equivalent to clothes laying all over the apartment, and please, tell me you do not do that in real life! Usually, they are kept in some basket, from which we are taking laundry to be washed.
Laundry, as a term that we used a lot in this article, we defined something that is a group of laundry, with the possibility of its modification. Having real-word terms that are commonly used is a great way of simplifying code and documenting it. The second object, that we are going to introduce, is
Basket which we decided to be a good place for storing laundry.
Wrapper that provides clothes for laundry.
Having those types defined, we can refactor existing code (controller and washing machine) to use it. In the beginning, while learning the OOP approach, it might seem overcomplicated to create so many types. Many questions might be raised, like “Why cannot I use List directly in the Basket?” or “Why objects are immutable?” or “Where are the getters?”.
We are doing it for few reasons, like concept separation (classes are small, methods are trivial), documentation (the new person reading code is learning naming and processes in the business) or maintainability (small classes with simple methods are very easily testable). Imagine, if someone reports the bug, and describes it as “I cannot take out the laundry from a basket” — you already know, where you should look for it. To learn more about naming and separating objects, take a look into Vaughn Vernons “Implementing Domain-Driven Design”, especially on chapters regarding Value Objects and Standard Types.
Wrap up: inheritance vs polymorphism
By now, we discussed OOP inheritance and polymorphism principles. We also started a talk regarding encapsulation. From this part, you should remember:
- inheritance is about copying behaviours and attributes
- inheritance might be not the best choice, especially if you need
- polymorphism is about changing behaviours in subclasses
- you should try to reflect real-world when modelling classes, focusing on its behaviour
- wrap meaningful information into well-named Value Objects
The encapsulation that we were taught
Encapsulation is one of OOP principles, that has to protect the integrity of the data. In multiple sources usually it is told, that encapsulation is achieved by hiding the data in a small, single unit, like class. We can think of integrity as about a consistent state of object internals, so no one can change it without letting the object knowing it. It is usually told to expose
getters/setters and change the visibility of fields to private. Does it help to keep object integrity?
Let’s think about the imaginary type
HomeAddress. It is composed of few fields, including
zipCode. All fields are private and encapsulated with
Between the invocation of methods, there is a small interval, during which the object will remain inconsistent. Encapsulation from OOP is not only hiding data but also that how object achieves some desired state. We should not even know, that to update the address we have to change two properties. It would be much better if this code will contain an
City value object with properties
zipCode that is provided to
HomeAddress as a single object in a single method call. It will allow
HomeAddress changing its internal state according to the provided data, without exposing information on how it should be done. That is really an object internal business.
Applying encapsulation to the Washing Machine
Encapsulation in OOP is focusing that how the object is working, therefore we should try to hide internal logic to protect its consistency. Best place to start will be setting up temperature and spin speed. Right now, Washing Service is responsible for setting those values individually. Similarly to the
Basket, we will create a dedicated class called
Programme that will store
Storing values in separate object simplify other parts of the application.
Note, that for simplicity we skipped checks for minimal and maximum values (your task will be to implement those checks while doing kata). With this approach, Washing Service has one responsibility less to maintain, as its code will be simplified.
A service class that violates OOP a little bit less.
Abstraction on top of encapsulation
The final step is to understand abstraction, the last Object-Oriented Principle that we are going to cover. Similarly to encapsulation, it secures the object from leaking its internals. The main difference is the level that it covers — encapsulation works more on the internals of object and that, how it works on data. Abstraction on the other hand focuses on what the object can do.
Applying this definition into the washing machine, encapsulation hides, how temperature is stored, and how it is set up in the washing programme. While using a washing machine, we are more focused on what it can do, like adding laundry, selecting a programme or starting it. Those are clearly behaviours, that washing machine offers. Applying abstraction on the washing machine will hide all the complexity behind it. This time, we will not have a lot of work in implementing. Let’s rename some methods to reflect the real world.
Change is really small, but it focuses user of a washing machine on its behaviours and capabilities. Literally, the user does not have to know anything, how complex washing process is — it can be composed of supplying water, heating, spinning, rinsing and draining, but while we are standing in front of washing machine — it is just happening.
Wrap up: encapsulation vs abstraction
Summing up what we learnt about OOP’s encapsulation and abstraction:
- Both are hiding internal of object
- encapsulation focuses more on how the object is working
- hiding private fields behind simple setters and getters does not protect its integrity
- abstraction focuses on what the object can do
- abstraction can be perceived as a perspective of the user that is going to interact with the object
- abstraction is often achieved by interfaces, as those allow to focus on behaviours
Train your skills with OOP Code Kata
Till now, you should gain a solid background for understanding Object-Oriented Principles, but it is just a theory. Without practice you will quickly forget about those principles, so you need additional training. You can use kata based on this article, starting from the clean Washing Machine project. Below you will find steps that you can perform to apply OOP into this project. You can also find sample solution on this branch. Remember, that your solution does not need to look exactly the same, as there are multiple ways of implementing it.
Steps to follow
- Create a class for each type of clothing
- Add method that will allow changing state of clothes (clean/dirty)
- Create a class for basket and laundry
- Make a new class for representation of each programme and add validation to it
- Rename methods from
WashingMachineso they will reflect real-world action
After those steps, you should have a similar structure to the one used in this article. For better OOP practice, you could also make further changes:
- Try to remove all direct passing of
Listas method parameter –
Listshould be only used internally inside of a class
Programmeclass so it will contain behaviours to adjust parameters
- Temperature can be changed only every 5 degrees
- The spin speed can be changed only every 100
- The programme can be set up to perform an additional rinse
- Create dedicated classes for temperature and spin speed
- Move check if the value is in a valid range to
- Laundry, that does not fit into Washing Machine should be put back into Basket
- Try to remove Service class or name it properly — naming class with ‘Service’ suffix promotes adding all, barely connected code into this class instead of splitting responsibilities. It’s an easy way to violate OOP
Remember, writing OOP code is constant work, that you have to train all the time. At every step, you need to think, if principles are fulfilled and take care of proper naming. This additional work will pay back with easier maintenance of the codebase. Code kept in good shape will shorten the time needed for developing new features.