Hi there, it’s a great pleasure having you here. I’m guessing you are a software engineer and you know about software design patterns, cool! However, if you are like me, you may have asked yourself at some point how to know when to use software design patterns and what kind of problems are best solved using software design patterns; I’ll attempt to answer this by sharing my experience.
Recently, I was working on a grocery shopping app and one of the UI screens had the requirement to build an activity showing a list of products such that when a user clicked to add a product to cart, the count of that product would be updated and all other occurrences of that same product elsewhere in the app would also be updated.
Since an image is worth more than a thousand words, let me spare you many words — the image below describes the expected behavior.
Thinking through this problem and the interacting pieces, I felt this would be a good case to apply a software design pattern.
Introducing the Observer Design Pattern
According to Wikipedia, the observer design pattern is a software design pattern in which an object (called the subject) maintains a list of its dependents (called observers) and notifies them automatically of any state changes, usually by calling one of their methods
From the definition, the Observer pattern seemed ideal for this problem. This doesn’t mean other patterns wouldn’t work — at least the Publish-Subscribe Design pattern, which is subtly different from the Observer pattern, could also work. If you want to know more on the differences between the two patterns, here is an article by Ahmed shamim hassan, who did a great job in explaining the difference between both patterns.
Now to the more interesting part of this — the application & implementation of this pattern to solve this interesting problem.
Identifying the key components
When implementing a software system, it’s important to identify the key components that interact in the system and what roles they play. The key components in an observer pattern are the subject and the observers.
For this specific requirement, the subject is the Cart
model and the observers are…? (You want to take a guess?) To appreciate what components constitute the observers, it’s important to understand the expected functionality of this activity screen.
The requirement is such that when a user adds a product to a cart, the count of that product is updated in that current view and the count of every other occurrence of that same product in a view is also updated. So if Product A was listed under the Featured Products section and also listed under Fresh Foods Department, the count of both instances in their respective views would be updated if Product A was added to the cart. This suggests a 1-to-N relationship between product and view as illustrated below.
I’m guessing by now you’ve been able to figure out what the observers are? That’s right, the product views
are the observers.
Implementing the Observer Pattern
To implement this pattern, first there had to be a way for the observers to subscribe to changes in the subject (Cart
) state. To do this, I created an interface in the Cart
class that would be implemented each product view
.
public class Cart {
/**
* Listens for cart state change events
*/
public interface OnStateChangeListener {
void onStateChange(View v);
}
}
Each product view simply had to create an instance of this interface (with its implementation) — one listener per product view.
So now we have instantiated these view listeners but they are not listening for changes to the Cart
model. To attach listeners to the subject, the Cart
class exposes a static setOnStateChangeListener
method that takes a reference to the product, a reference to the product view and an instance of the OnStateChangeListener
interface as arguments as shown below. This process of attaching listeners to the Cart
is how the view
subscribes to the subject.
//view subscribes by attaching listener to Cart state
Cart.setOnStateChangeListener(product, view, mCartListener);
The more interesting part is what goes on in the Cart
class when a call is made to the setOnStateChangeListener
method. Internally, the Cart
class maintains two HashMaps -one for Product listeners and the other for View listeners.
private static HashMap<OnStateChangeListener, View> viewListeners = new LinkedHashMap<>();
private static HashMap<Product, ArrayList<OnStateChangeListener>> productListeners = new LinkedHashMap<>();
Hang on, hang on! I am guessing you’re wondering why I had to maintain two HashMaps, I’ll explain shortly.
From earlier explanations, we can agree that there is a 1-to-1 relationship between listener and view because each view has its own instance of CartStateListener
. The viewListeners
HashMap as shown in Fig. 3 below, is a data representation of that listener-view mapping
The productListeners
hash on the other hand maintains a bucket of listeners for each specific product because a single product can be listed in many places (views) in the app and each view has its own listener.
For example, from the illustration below, Product A is listed in three places: ProductView #1, ProductView #2 and ProductView N having listeners viewOneListener, viewThreeListener and viewNListener respectively. These means all three listeners have to be aware of changes to the Cart
state that affects Product A.
So it’s safe to say the listeners act as some bridge (in the sense of bridge table) between the two maps, linking the product
model and the product view
.
Every time there is a change to Cart
state either because a product is added to the cart or removed, the Cart model publishes these changes by making a call to some notifyAllListeners(product)
method passing the specific product as an argument. This is what happens when that call is made:
//Get all the listeners for this specific product
ArrayList<OnStateChangeListener> x = productListeners.get(product);
//For each listener
for (OnStateChangeListener listener: x){
//Get the view that owns this listener
View v = viewListeners.get(listener);
//Trigger update on the subscribed view
listener.onStateChange(v);
}
The code below shows what happens in the view
mCartListener = new Cart.OnStateChangeListener() {
@Override
public void onStateChange(View productView) {
//productView gets updated here
}
};
And voilà, it works!
Possible Optimizations
Since optimize is the name of the game, we always want to look out for ways we could optimize. One I thought of was adding logic to remove listeners from the HashMaps for products not in the user’s view; that way ensuring that the size of the HashMaps don’t grow considerably.
Summary
Knowing when to apply a software design pattern to a specific problem first requires a good understanding of the workings of the specific pattern you want to use and an equally good understanding of the problem you are trying to solve but most importantly, understanding if the software pattern is a good fit for the problem.
I hope that wasn’t too long a read (I bet it was…) but I hope even more that it was worth your time. If there’s a better way you would have implemented this and would like to chat about it or you would love to chat anything else over coffee I hope, feel free to reach out on Twitter @meekg33k.
E go be! ✌️✊✌️