Design pattern principles:
- The code should be closed to change and open to extension, should be easily maintainable for future change requests.
- Prefer composition (change behavior in runtime, encapsulate family of algorithms) over inheritance (your behavior is stuck). (why?)
- All design patterns have pros and cons. The decision of whether I should use a design pattern in a situation should be taken after analyzing whether the product you are building can afford to have cons offered by the pattern.
Design pattern solution (what): Class diagram
Pros and Cons (why):
Decorator pattern (follows open-closed principle):
When there are a lot of combinations to deal with different categories.
Normal solution (stupid way): Enumerate all the combinations possible and write a method in subclass specific to each combination.
Maintenance problems with this solution:
- If the price of milk goes up, they have to change the code in all the places.
- If they add a new item in one category, the whole thing explodes even more combinatorically.
One more solution:
Take Boolean instance variable for the presence of each specific condiment and deduce cost of the all condiments based on that in the parent class and you can add the cost of that specific beverage alone in the subclass and call super.cost().
Some problems with this solution:
- We have to alter existing code if there is a change in the price of any condiment.
- New condiments in future means, new instance variables, new setters and getters and we also need to modify the cost method in the superclass to account for this new condiment as well.
- Some of the beverage and condiment combinations may not be appropriate and we have no way of restricting them.
- What if the customer wants a double mocha?
Time for One more solution:
When inheriting behavior by sub-classing, the behavior is set statically at compile time.
Design pattern solution:
Inheriting behavior at runtime through composition and delegation. More flexibility.
If u want a Dark roast with condiments of mocha and whip, Take a dark roast object, decorate it with a mocha object and decorate it with the whip and call the cost function and rely on delegation.
Decorator objects are kind of wrappers.
The concrete decorator has an instance variable for the thing it decorates.
We can implement new decorators any time to add new behavior instead of changing existing code which is thoroughly tested.
Decorators are usually created by using Factory and Builder patterns.
Alternative to sub-classing for extending functionality.
Enables a lot of combinations with a minimal number of classes possible.
- Introduces complexity to the code and reduces readability.
- If you have some logic that is specific to a component type like the discount in this case, then this would not be applicable.
- You end up adding a lot of small classes.
- To simplify the interface of a group of classes.
This along with the composite pattern helps you deal with a collection of Objects better.
- If you want to iterate across collectibles of varied types like ArrayList, Hashmap, etc, provided the component type remains same (check).
- To provide a way to access elements of an aggregate object sequentially without worrying about its underlying implementation.
- If you want to treat tree-like structures, leaf nodes, and composites uniformly.
- If you want your sub-categories to behave in the same way as your categories.
- The difference from iterator pattern is, here the component type itself varies.
Looks familiar to Depth-first search.
How to use it:
Pros and cons:
- The abstract Menu component class acts as an interface for both leaf node and composite node, thus breaking single responsibility principle.
Before closing out discussion on the collection of objects, touch upon Bounded generics.
Force not to add improper types into your collectibles in the first place. Archiver case.
Below three patterns change the behavior at runtime using composition.
The State Pattern:
Encapsulate state-based behavior and delegate behavior to the current state.
Pros and cons:
- A lot of classes, one for each state.
The Strategy Pattern:
Encapsulate interchangeable behaviors and use delegation to decide which behavior to use.
Configures context classes with a behavior.
Template Method Pattern:
Subclasses decide how to implement steps in an algorithm.
- Tying your class to a concrete implementation (new) is very fragile and less flexible.
Situation: you have different closely related concrete classes and you choose to instantiate one of these based some conditional logic.
Put if, else blocks and use the new operator to instantiate an appropriate object in each block.
If there is a new object (new duck type), that has to be added to this conditional logic and you have to figure out and add it in all places wherever it is applicable. This is error-prone.
Code to an interface.
Here, we have to figure out which implementation to instantiate for what type. This requires a piece of conditional logic code. This has to change if new implementations are added or any of the existing implementations are to be removed. So, this is not closed for modification.
Factory method lets subclasses decide which class to instantiate.
- When a group of objects needs to be notified of some state changes.
- Code to an interface, not to an implementation.
- Use bounded generics instead of Object collectibles for better validation and type casting issues.
- Usually, an anti-pattern to modify objects coming in as arguments. Use final modifiers.
You may or may not use any of these patterns in your daily life, but understanding these and going through pros and cons of them will significantly impact your thought process and help you make better decisions.