Abstraction is one of the key concepts of object-oriented programming (OOP) languages. Its main goal is to handle complexity by hiding unnecessary details from the user. That enables the user to implement more complex logic on top of the provided abstraction without understanding or even thinking about all the hidden complexity.
That’s a very generic concept that’s not limited to object-oriented programming. You can find it everywhere in the real world.
I’m a coffee addict. So, when I wake up in the morning, I go into my kitchen, switch on the coffee machine and make coffee. Sounds familiar?
Making coffee with a coffee machine is a good example of abstraction.
You need to know how to use your coffee machine to make coffee. You need to provide water and coffee beans, switch it on and select the kind of coffee you want to get.
The thing you don’t need to know is how the coffee machine is working internally to brew a fresh cup of delicious coffee. You don’t need to know the ideal temperature of the water or the amount of ground coffee you need to use.
Someone else worried about that and created a coffee machine that now acts as an abstraction and hides all these details. You just interact with a simple interface that doesn’t require any knowledge about the internal implementation.
You can use the same concept in object-oriented programming languages like Java.
Objects in an OOP language provide an abstraction that hides the internal implementation details. Similar to the coffee machine in your kitchen, you just need to know which methods of the object are available to call and which input parameters are needed to trigger a specific operation. But you don’t need to understand how this method is implemented and which kinds of actions it has to perform to create the expected result.
There are primarily two types of abstraction implemented in OOPs. One is data abstraction which pertains to abstracting data entities. The second one is process abstraction which hides the underlying implementation of a process. Let’s take a quick peek into both of these.
Data abstraction is the simplest form of abstraction. When working with OOPS, you primarily work on manipulating and dealing with complex objects. This object represents some data but the underlying characteristics or structure of that data is actually hidden from you. Let’s go back to our example of making coffee.
Let’s say that I need a special hazelnut coffee this time. Luckily, there’s a new type of coffee powder or processed coffee beans that already have hazelnut in it. So I can directly add the hazelnut coffee beans and the coffee machine treats it as just any other regular coffee bean. In this case, the hazelnut coffee bean itself is an abstraction of the original data, the raw coffee beans. I can use the hazelnut coffee beans directly without worrying about how the original coffee beans were made to add the hazelnut flavour to it.
Therefore, data abstraction refers to hiding the original data entity via a data structure that can internally work through the hidden data entities. As programmers, we don’t need to know what the underlying entity is, how it looks etc.
Where data abstraction works with data, process abstraction does the same job but with processes. In process abstraction, the underlying implementation details of a process are hidden. We work with abstracted processes that under the hood use hidden processes to execute an action.
Circling back to our coffee example, let’s say our coffee machine has a function to internally clean the entire empty machine for us. This is a process that we may want to do every once a week or two so that our coffee machine stays clean. We press a button on the machine which sends it a command to internally clean it. Under the hood, there is a lot that will happen now. The coffee machine will need to clean the piston, the outlets or nozzles from which it pours the coffee, and the container for the beans, and then finally rinse out the water and dry out the system.
A single process of cleaning the coffee machine was known to us, but internally it implements multiple other processes that were actually abstracted from us. This is process abstraction in a nutshell.
Well, this process abstraction example really got me thinking of a very futuristic coffee machine!
Now that we understand abstraction well, let’s see how we can implement it. Since I’ve spun my coffee stories so much already, let’s actually go ahead and implement the coffee machine example in Java. You do the same in any other object-oriented programming language. The syntax might be a little bit different, but the general concept is the same.
Modern coffee machines have become pretty complex. Depending on your choice of coffee, they decide which of the available coffee beans to use and how to grind them. They also use the right amount of water and heat it to the required temperature to brew a huge cup of filter coffee or a small and strong espresso.
Using the concept of abstraction, you can hide all these decisions and processing steps within your CoffeeMachine class. If you want to keep it as simple as possible, you just need a constructor method that takes a Map of CoffeeBean objects to create a new CoffeeMachine object and a brewCoffee method that expects your CoffeeSelection and returns a Coffee object.
You can clone the source of the example project at https://github.com/thjanssen/Stackify-OopAbstraction.
import org.thoughts.on.java.coffee.CoffeeException; import java.utils.Map; public class CoffeeMachine { private Map<CoffeeSelection, CoffeeBean> beans; public CoffeeMachine(Map<CoffeeSelection, CoffeeBean> beans) { this.beans = beans } public Coffee brewCoffee(CoffeeSelection selection) throws CoffeeException { Coffee coffee = new Coffee(); System.out.println(“Making coffee ...”); return coffee; } }
CoffeeSelection is a simple enum providing a set of predefined values for the different kinds of coffees.
public enum CoffeeSelection { FILTER_COFFEE, ESPRESSO, CAPPUCCINO; }
And the classes CoffeeBean and Coffee are simple POJOs (plain old Java objects) that only store a set of attributes without providing any logic.
public class CoffeeBean { private String name; private double quantity; public CoffeeBean(String name, double quantity) { this.name = name; this.quantity; } }
public class Coffee { private CoffeeSelection selection; private double quantity; public Coffee(CoffeeSelection, double quantity) { this.selection = selection; this. quantity = quantity; } }
Using the CoffeeMachine class is almost as easy as making your morning coffee. You just need to prepare a Map of the available CoffeeBeans. After that, instantiate a new CoffeeMachine object. Finally, call the brewCoffee method with your preferred CoffeeSelection.
import org.thoughts.on.java.coffee.CoffeeException; import java.util.HashMap; import java.util.Map; public class CoffeeApp { public static void main(String[] args) { // create a Map of available coffee beans Map<CoffeeSelection, CoffeeBean> beans = new HashMap<CoffeeSelection, CoffeeBean>(); beans.put(CoffeeSelection.ESPRESSO, new CoffeeBean("My favorite espresso bean", 1000)); beans.put(CoffeeSelection.FILTER_COFFEE, new CoffeeBean("My favorite filter coffee bean", 1000)); // get a new CoffeeMachine object CoffeeMachine machine = new CoffeeMachine(beans); // brew a fresh coffee try { Coffee espresso = machine.brewCoffee(CoffeeSelection.ESPRESSO); } catch(CoffeeException e) { e.printStackTrace(); } } // end main } // end CoffeeApp
You can see in this example that the abstraction provided by the CoffeeMachine class hides all the details of the brewing process. That makes it easy to use and allows each developer to focus on a specific class.
If you implement the CoffeeMachine, you don’t need to worry about any external tasks, like providing cups, accepting orders or serving the coffee. Someone else will work on that. Your job is to create a CoffeeMachine that makes good coffee.
And if you implement a client that uses the CoffeeMachine, you don’t need to know anything about its internal processes. Someone else already implemented it so that you can rely on its abstraction to use it within your application or system.
That makes the implementation of a complex application a lot easier. And this concept is not limited to the public methods of your class. Each system, component, class, and method provides a different level of abstraction. You can use that on all levels of your system to implement software that’s highly reusable and easy to understand.
Let’s dive a little bit deeper into the coffee machine project and take a look at the constructor method of the CoffeeMachine class.
import java.util.Map; public class CoffeeMachine { private Map<CoffeeSelection, Configuration> configMap; private Map<CoffeeSelection, CoffeeBean> beans; private Grinder grinder; private BrewingUnit brewingUnit; public CoffeeMachine(Map<CoffeeSelection, CoffeeBean> beans) { this.beans = beans; this.grinder = new Grinder(); this.brewingUnit = new BrewingUnit(); // create coffee configuration this.configMap = new HashMap<CoffeeSelection, Configuration>(); this.configMap.put(CoffeeSelection.ESPRESSO, new Configuration(8, 28)); this.configMap.put(CoffeeSelection.FILTER_COFFEE, new Configuration(30, 480)); } }
As you can see in the code snippet, the constructor not only stores the provided Map of available CoffeeBeans in an internal property, it also initializes an internal Map that stores the configuration required to brew the different kinds of coffees and instantiates a Grinder and a BrewingUnit object.
All these steps are not visible to the caller of the constructor method. The developer most likely doesn’t even know that the Grinder or BrewingUnit class exists. That’s another example of the abstraction that the CoffeeMachine class provides.
The classes Grinder and BrewingUnit provide abstractions on their own. The Grinder abstracts the complexity of grinding the coffee and BrewingUnit hides the details of the brewing process.
public class Grinder { public GroundCoffee grind(CoffeeBean coffeeBean, double quantityCoffee) { // ... } }
public class BrewingUnit { public Coffee brew(CoffeeSelection selection, GroundCoffee groundCoffee, double quantity) { // ... } }
That makes the implementation of the CoffeeMachine class a lot easier. You can implement the brewCoffee method without knowing any details about the grinding or brewing process. You just need to know how to instantiate the 2 classes and call the grind and brew methods.
In this example, I took the abstraction one step further and implemented 3 methods to brew the different kinds of coffee. The brewCoffee method, which gets called by the client, just evaluates the provided CoffeeSelection and calls another method that brews the specified kind of coffee.
The brewFilterCoffee and brewEspresso methods abstract the specific operations required to brew the coffee.
private Coffee brewFilterCoffee() { Configuration config = configMap.get(CoffeeSelection.FILTER_COFFEE); // grind the coffee beans GroundCoffee groundCoffee = this.grinder.grind( this.beans.get(CoffeeSelection.FILTER_COFFEE), config.getQuantityCoffee()); // brew a filter coffee return this.brewingUnit.brew( CoffeeSelection.FILTER_COFFEE, groundCoffee, config.getQuantityWater()); }
private Coffee brewEspresso() { Configuration config = configMap.get(CoffeeSelection.ESPRESSO); // grind the coffee beans GroundCoffee groundCoffee = this.grinder.grind( this.beans.get(CoffeeSelection.ESPRESSO), config.getQuantityCoffee()); // brew an espresso return this.brewingUnit.brew( CoffeeSelection.ESPRESSO, groundCoffee, config.getQuantityWater()); }
I defined both methods as private because I just want to provide an additional, internal level of abstraction. That not only makes the implementation of the brewCoffee method a lot easier, it also improves the reusability of the code.
You could, for example, reuse the brewEspresso method when you want to support the CoffeeSelection.CAPPUCCINO. You would then just need to implement the required operations to heat the milk, call the brewEspresso method to get an espresso, and add it to the milk.
A lot of times programmers often confuse abstraction with encapsulation because in reality the two concepts are quite intertwined and share a relationship between them. Abstraction, as we’ve seen pertains to hiding underlying details and implementation in a program. Encapsulation, on the other hand, describes how abstraction occurs in a program.
Abstraction is a design-level process but encapsulation is an implementation process. Encapsulation tells us how exactly you can implement abstraction in the program. Abstraction pertains to only displaying the essential details to the user whereas encapsulation pertains to typing up all the data members and associated member functions into a single abstracted unit.
Abstraction is a general concept which you can find in the real world as well as in OOP languages. Any objects in the real world that hide internal details provide an abstraction. The objects may be your coffee machine or classes in your current software project,
These abstractions make it a lot easier to handle complexity by splitting them into smaller parts. In the best case, you can use them without understanding how they provide the functionality. And that helps you to split the complexity of your next software project into manageable parts. It also enables you every morning to brew a fresh cup of amazing coffee while you’re still half asleep.
Looking to continually improve your applications? Most OOP languages are supported by Stackify’s free dynamic code profiler, Prefix, and Stackify’s full lifecycle APM, Retrace. Try both for free.
If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]