Intro
So this is the second part of the blog series Design Patterns. In the first part, we took a look at the five popular design patterns which are Factory, Builder, Singleton, Adapter and Bridge. Let's take a look at the next five. First, we shall start with the Decorator pattern.
#6 Decorator
A decorator is a very simple pattern because all it does is change the behaviour of an object. It is also very useful when we want to obey SRP (Single Responsibility Pattern) as it allows the division of functionalities between classes. A real-world example would be a car trip. There is a regular car when we are driving around the city, but when we go on a trip and more space is needed, often roof boxes are placed on cars and then they are ready for a trip. What is neat about those roof boxes is that they can be placed or removed when needed. The same thing is with the decorator. It can modify some behaviour at the runtime, so when it is needed. Let's code the example.
abstract class Car {
void drive();
void packStuff();
void unpackStuff();
}
class CarWagon implements Car {
@override
void drive() => print('Driving freely...');
@override
void packStuff() => print('Opening trunk, putting stuff...');
@override
void unpackStuff() => print('Opening trunk, taking stuff...');
}
Here above are Car interface and the implementation of that interface is CarWagon
. Now what if we want to go to the seaside, we don't want a new car for that. We want to equip our car with a roof box and we will do it this way:
class CarWagonRoofBox implements Car {
final CarWagon carWagon;
CarWagonRoofBox(this.carWagon);
@override
void drive() => carWagon.drive();
@override
void packStuff() {
carWagon.packStuff();
print('Opening roof box too, putting stuff...');
}
@override
void unpackStuff() {
carWagon.unpackStuff();
print('Opening roof box too, taking stuff...');
}
}
Using DI (Dependency Injection) we equip our car with a roof box. That is it, the decorator pattern is finished. The final result:
final CarWagon carWagon = CarWagon();
carWagon.packStuff();
carWagon.unpackStuff();
print('Going to seaside...');
final CarWagonRoofBox carWagonWithRoofBox = CarWagonRoofBox(carWagon);
carWagonWithRoofBox.packStuff();
carWagonWithRoofBox.unpackStuff();
And this is the output:
Opening trunk, putting stuff...
Opening trunk, taking stuff...
Going to seaside...
Opening trunk, putting stuff...
Opening roof box too, putting stuff...
Opening trunk, taking stuff...
Opening roof box too, taking stuff...
This was an easy one, agree? The next pattern we will examine is the Facade pattern.
#7 Facade
The facade pattern is by the book the following: "Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use." What it means by that is the facade is something like a higher-order interface. Here is an explanation with a real-world example. When you are shopping for a new car, a salesperson is like a facade person to you because you are not aware of the complex processes needed to sell you a car, all you have to do is pick a model, customize it and after some time it will arrive, waiting for you. How cars are ordered and produced is not your concern. Some material inputs are processed on the manufacturing line, the engine is connected to a chassis, interior parts are stitched, the onboard computer is programmed and at the end, there is a quality check. That is how the car is produced, but the whole process is hidden from you as a user, that is what facade pattern is. Let's code the example straightforward.
class ChassisWorker {
void processInputMaterial() => print('Welding the chassis...');
void paint() => print('Painting the chassis...');
void finish() => print('Finishing the final details...');
}
class EngineWorker {
void makeCylinders() => print('Making the cylinders...');
void assembleEngine() => print('Assembling the engine...');
void testEngine() => print('Testing the engine...');
}
class InteriorWorker {
void cleanLeatherPelts() => print('Cleaning leather pelts...');
void stitchLeather() => print('Stitching the leather...');
void coatLeather() => print('Coating the leather...');
}
For a clearer explanation, I used concrete classes rather than interfaces. The classes above describe the smaller, more complex process of car manufacturing. The class below wraps those processes behind the high-level class, CarFactoryFacade
.
class CarFactoryFacade {
final ChassisWorker chassisWorker = ChassisWorker();
final EngineWorker engineWorker = EngineWorker();
final InteriorWorker interiorWorker = InteriorWorker();
void produceCar() {
_startManufacturingProcess();
_assembleComponents();
_qualityCheck();
}
void _startManufacturingProcess() {
chassisWorker.processInputMaterial();
engineWorker.makeCylinders();
interiorWorker.cleanLeatherPelts();
}
void _assembleComponents() {
chassisWorker.paint();
engineWorker.assembleEngine();
interiorWorker.stitchLeather();
}
void _qualityCheck() {
chassisWorker.finish();
engineWorker.testEngine();
interiorWorker.coatLeather();
}
}
Now, when the client or someone else needs to use or describe the whole process, all it needs is just this one high-level method. The final step is to call that facade in the app.
final CarFactoryFacade facade = CarFactoryFacade();
facade.produceCar();
The output is the following:
Welding the chassis...
Making the cylinders...
Cleaning leather pelts...
Painting the chassis...
Assembling the engine...
Stitching the leather...
Finishing the final details...
Testing the engine...
Coating the leather...
There you go, this is the facade design pattern. The next pattern we will cover is the Service locator, Singleton on steroids.
#8 Service locator (Singleton on steroids)
Service Locator is used to decouple client and interface implementation. It is also a Singleton, but instead of returning itself as an instance, it returns something we are looking for, a service for example. An example from real life would be the DNS server. We as a client want to get the remote website. What DNS gets is a readable web address, then it looks up the IP address in its pool of addresses, if it is found, it is returned to us and we do the rest that we want. We as a client don't know where the wanted website is located, we ask a DNS server and it gets it for us, the same is with the service locator. Here is an example:
abstract class Website {
String getUrl();
void render();
}
class PageOne implements Website {
final String url = 'www.page-one.com';
@override
String getUrl() => url;
@override
void render() => print('Rendering page one... ');
}
class PageTwo implements Website {
final String url = 'www.page-two.com';
@override
String getUrl() => url;
@override
void render() => print('Rendering page two...');
}
We have a website interface that can return its URL and render itself. Beneath we create only two websites.
class WebsiteServiceLocator {
static final WebsiteServiceLocator _instance = WebsiteServiceLocator._privateConstructor();
WebsiteServiceLocator._privateConstructor();
static WebsiteServiceLocator get instance => _instance;
final HashSet _websites = HashSet<Website>.from([PageOne(), PageTwo()]);
void registerWebsite(Website websiteToRegister) => _websites.add(websiteToRegister);
Website? findWebsite(String url) {
try {
return _websites.firstWhere((website) => website.getUrl() == url);
} catch (e) {
return null;
}
}
}
In the code above, there is a service locator which is a singleton and it holds the HashSet of all known pages. There is also an option to register a new one when needed. The most important method is findWebsite
which returns the wanted website. This way, a client only needs to communicate with a WebServiceLocator
without any worries about how many websites are there, the service locator will do the job for it. And there it is, you can memorize the service locator as a DNS server. However, some issues appear when you are using this pattern, so if it isn't necessary, do not use this pattern. Firstly, it makes unit testing harder because it is a static singleton class, so there are no mocked objects. It also creates hidden dependencies which can cause runtime client breaks. A solution to these problems is using the Dependency Injection pattern. If you are a Flutter developer, then you probably heard of the well-known get_it
package. It is the implementation of the Service Locator design pattern. Alternatives for get_it
are for example riverpod
and provider
packages. I personally prefer the riverpod
.
#9 Observer
For this design pattern, I'll give an example for the Flutter framework, but first the real-world example. In the restaurant, a waiter observes all the tables and if there is a new guest, he/she welcomes the guest and takes an order. While the guest is eating, the waiter still observes all tables and when another guest is finished, he/she charges the guest and cleans the table, then all over again. So, the waiter observes and when there is a new event, he/she reacts to it. This will be a classic OOP example, let's code it.
In Flutter, this pattern is used in almost any application. Any interactive widget has a listener, for example, GestureDetector has an onTap listener. If you have to show data changes from a local database or a WebSocket as an input, the observer is also implemented as a Stream. The most important example is state management. Let's see the provider
since it is the most basic state management solution. The following code snippet notifies the UI when a fuel tank level changes.
class FuelTankModel extends ChangeNotifier {
int tankLevelPercentage = 100;
void decreaseLevel() {
tankLevelPercentage--;
notifyListeners();
}
void increaseLevel() {
tankLevelPercentage++;
notifyListeners();
}
}
Now we have to rebuild UI every time it is notified about the change.
...
child: Center(
child: Consumer<FuelTankModel>(builder: ((context, tankLevel, child) {
return Text('Current fuel level: $tankLevel%');
}),
),
),
...
This was the observer pattern. It can be implemented in various use cases and it is easy to implement. The next pattern is the State pattern. At first, it will seem very similar to the Observer pattern, but there are some differences that you'll see in the next chapter.
#10 State
State is a behavioral design pattern, its observers change their behavior when the state of that object changes, so let's make an example of human behavior. In the morning we are in our pajamas and our actions are focused on preparing for the day ahead of us. Next, before work, in the gym, we are in sports clothes and focused on exercises and how many reps/series we do. Later in the office, we are in our formal clothes and front of the clients trying to behave as professionally as possible. After work, hanging out with our friends, we are relaxed and we behave appropriately. So our behavior changes based on the events and environments that surround us, the same is with the state design pattern. Let's code the given example above.
abstract class State {
void change();
void behave();
}
class MorningState implements State {
final Person person;
MorningState(this.person) {
change();
}
@override
void change() {
person.clothes = 'pajamas';
}
@override
void behave() {
print('Wearing ${person.clothes}, preparing for the day...');
}
}
class GymState implements State {
final Person person;
GymState(this.person) {
change();
}
@override
void change() {
person.clothes = 'sports clothes';
}
@override
void behave() {
print('Wearing ${person.clothes}, working out...');
}
}
class BusinessState implements State {
final Person person;
BusinessState(this.person) {
change();
}
@override
void change() {
person.clothes = 'suit';
}
@override
void behave() {
print('Wearing ${person.clothes}, focusing on productivity...');
}
}
In the code above, we created three states in which a person will be throughout the day, now let's create that person.
class Person {
late String clothes;
late State behaviour;
Person() {
clothes = 'pajamas';
behaviour = MorningState(this);
}
void doDailyTask(State state) {
behaviour = state;
state.behave();
}
}
The person wears clothes and has some behavior based on the state it is. Every person starts the day in the morning, therefore the MorningState
in the constructor. There is also a method doDailyTask
which takes a new state as a parameter and puts that person in the passed state. This is the main part of the code:
void main() {
final person = Person();
person.doDailyTask(MorningState(person));
person.doDailyTask(GymState(person));
person.doDailyTask(BusinessState(person));
}
Here is the output:
Wearing pajamas, preparing for the day...
Wearing sports clothes, working out...
Wearing suit, focusing on productivity...
This was the final design pattern to cover. The state design pattern should be used when our object has to change its behavior if its internal state changes. One of the examples would be UI rendering. If the UI state is loading, the loading indicator should be displayed on the UI, if the state is an error, the error message should be displayed and finally if everything was fine, the result should be displayed to the user.
Conclusion
Here is the end of the blog. This was an opportunity to learn new things and better understand already known. If you like the content of this blog, I recommend you to read the book "Design Patterns: Elements of Reusable Object-Oriented Software". It covers all this, but with greater depth. Thank you for your time and happy coding! :D
Top comments (0)