Design Patterns: Learning Abstract Factory Method through real life examples

Kousik Nath
codeburst
Published in
8 min readDec 20, 2019

--

Consider the following scenario.

You have to design a generic vending machine which can provide you different variants of Coffee, Hot Water, Hot Milk, Coke, Lemonade & what not. How will you design that vending machine software?

This post is not about designing the complete vending machine, rather it discusses only one portion of the machine — how to support different variants of beverages, customers will browse all product & choose any one as per their choice. Remember, going forward you may be asked to add support for say fruit juice, tea or some variety of milk shake, chocolate shake etc. So the design should be extensible.

Let’s think bottom up.

Say the problem statement only asks you to design a coffee vending machine which can provide you with different variants of coffee like — Espresso, Latte, Cappuccino, Black Coffee etc. How would you design that?

Probably you would start with defining some coffee type enums & creating coffee objects in a if-else or switch statements or wrap this whole object creation logic in a factory, if you don’t know what a factory pattern is, please look at this post & understand that first.

Sample Code:

The above approach works. Cool! But notice one thing — even if you are supporting Coffee only, different variants of coffee may have different ingredients. Example: Black Coffee might not require sugar, it only requires coffee powder, water or milk. Similarly other variants of coffee might differ in terms of percentage of ingredients like say the same quantity of Cappuccino may need more coffee powder than the same quantity of Latte. How will you support those scenarios in your code?

In the above code snippet, the caller of the above getCoffee() method may set different quantity of milk, sugar, water etc after getting an instance of the coffee. But is it right to do so? Should the caller care about instantiation logic of the coffee objects? Instead, inside the switch-case block itself, you can set those parameters while creating an instance. Ok! it’s a little better approach. But the code quickly becomes cumbersome if I ask you to add support for more beverages. Example — add support for coke, coke can be mixed with water or soda, but not with milk or coffee powder. So coke has completely different set of ingredients. And your switch case block has to be modified again. What if I say that a customer can customize the beverage — s/he might choose whether s/he needs sugar or not — how much quantity of sugar, whether s/he needs mix up of water & milk in the coffee or not, or whether s/he needs soda in coke or not. How will you support that?

At this point in time, you are probably considering to create an object of type Customization which contains different fields like — milk, water, soda, sugar etc and pass it to your factory or switch-case block. So now your coffee creation code looks something like below:

See the above code block. It does multiple things — it knows how to create objects & how to set their respective parameters. Basically you have put all the object creation logic related code in this factory & it does more than one thing. Moreover, if you need to add support for say tea, the factory has to know how to create tea & how to set its ingredients as well.

This is just a plain violation of Single Responsibility Principle. And the code is not maintainable either. More beverages you add, more code you add or modify in the above code segment.

Consider another scenario, what if I say the DEFAULT values that are defined above should come from different configuration file or data source or different service? Say default values or formula of coffee preparation comes from service 1 & formula of coke comes from service 2. How will you accommodate those changes in the above code? Currently you have assumed all formula to be constant but the owner of the vending machine does not want to set any constant formula in the machine, rather he has his own server where formula are hosted & he wants to fetch from there probably because the formula is a secret formula & he does not want anyone to view those formula. Hypothetical scenario but quite possible in some form or the other.

Your current design does not support that. You need to modify the same code again & again. This is just unmaintainable.

How do you write your code in such a way that your changes are always minimum. Change is constant & inevitable, you have to always change something in your code, but how to minimize that change?

In our scenario, we are supporting actually different products but all are broadly defined as beverage products at least for the customer. It should be easy to change formula of any of the beverages. So we are dealing with runtime polymorphism for sure but at the same time the instantiation logic or parameter setting logic are completely different from each other. We are broadly dealing with beverage products, but their nature & ingredients are fundamentally different. Under beverage umbrella, they belong to different kind of categories like Black Coffee belongs to Coffee category, CocaCola belongs to Coke category, Orange / Mango juice belongs to juice category etc. Such different category objects creation under the same umbrella is supported by Abstract Factory Method.

Typically, when the underlying implementations or mechanisms of object creation vary a lot but those implementations can be placed under the same top level umbrella & you need same public API to access them, there is a good chance that you need Abstract Factory Method design pattern.

Let’s remodel the vending machine as shown in the image below. Let’s see what this modelling offers to us.

We have a Product interface which is the super type of the end product that we are expecting from the system.

Coffee products Cappuccino, BlackCoffee, juice product like Lemonade, Coke product like CocaCola, other products like HotMilk — all implements the Product interface because as per the assumption of our system, we are creating different products with the same interface for the end user.

All of the mentioned products implement make() method which will be invoked by the caller to finally prepare the product.

Each of the objects additionally stores what preparation & customization parameters were passed while creating those objects in prep & cust instance members respectively. Storing these parameters might not be required at all, depends on your use case.

Notice how the setter methods of all the product vary: Cappuccino & BlackCoffee have setMilk() method while Lemonade & CocaCola don’t have them. Similarly CocaCola has setCoke() while others don’t have that. These methods are very product specific method which are needed for setting the right amount of ingredients.

Each of the products are instantiated inside its own factory. Example: Lemonade is instantiated by LemonadeFactory. Only the factory knows how to instantiate the product, what quantity of ingredients to set, this abstracts out the product creation & parameter setting logic from the caller — caller does not need to care about how to create products, caller delegates the task of object creation to the factory & the factory does all the hard work. What ingredients to use, how to fetch the ingredients formula, how to calculate their right quantity — all the things are taken care of by the factory. The advantage of the approach is:

  • As mentioned caller does not care how the products are created.
  • The instantiation logic is concentrated in the factory, so if you need to remove some existing implementation, remove the product & the factory.
  • If you need to add a new product first check if any of the existing product & factory can be used to support that new product without much change, otherwise add new product & factory — no change is required in much of the existing code, only probably you need to add a switch case in the ProductFactory in order to retrieve the new factory. If you don’t want to make this change as well, check my previous post in order to understand how to dynamically register classes in factory.
  • Since all the products implement Product interface, even if new product is added, the caller does not need to change anything at its side to support it.

Note: It’s not mandatory to create a factory for each product, if you have multiple products which can be created with same setter methods (same preparation materials), which only vary in terms of quantity of ingredients, they can be created through a single factory.

ProductFactory is an abstract class that defines the contract all factories which are responsible for product creation should implement. Also there is a static method getProductFactory() which facilitates the discovery of factories to the client.

Customization & Preparation are sample generic objects showing what can be the possible customization parameters & overall ingredients in the system across products.

In order to better understand, look at the hierarchy above, as explained try to figure out in your mind why different factories were created, how different setter methods are not used outside the factory, how the full abstraction is created & in which scenario this abstraction is applicable. Try to see if you already have same sort of use cases & how to map them to this design approach.

The following is another very good use case of Abstract Factory Method borrowed from a Oracle documentation which shows how to model data access strategy if your client is only exposed to very specific DAO interfaces, but you have multiple data storage implementation available which can offer data access through the same interface.

Courtesy: Oracle

Here, say DAO1 exposes some customer entity related operations like find, create etc, DAO2 exposes their bank account related operations. You have different data storage mechanisms like relational database, Xml format file storage & Object storage which can store customer & account data in different format but exposes them in the same way to the client. Each dedicated factory like RdbDAOFactory can create both DAO implementations like RdbDAO1 & RdbDAO2 etc. All the factories extends DAOFactory abstract super class which is used by the client / caller to get a very specific factory instance & then access their corresponding DAO objects.

Note that only the respective factories know how to create connection to the underlying data storage & how to communicate with them. The client / caller does not need to pay any heed to such internals. This is how abstraction for heterogenous objects are created by Abstract Factory Method.

In this example also you have multiple implementations which are instantiated by their corresponding factories but they all serve the same purpose to the client — exposing DAO1 & DAO2 operations.

Hope both the examples clear your concept about Abstract Factory Method pattern & most importantly when to use them. I did not use much theoretical terms in the explanation, rather kept it as much as practical as possible. Whenever you need to write some object creation logic, try to see if this pattern fits your use case, don’t try to fit forcefully, that will break the whole purpose of using design patterns.

Any thoughts, let me know in the comments please.

If you have reached this much, please share this article so that other people also benefit.

--

--

Deep discussions on problem solving, distributed systems, computing concepts, real life systems designing. Developer @Uber. https://in.linkedin.com/in/kousikn