DEV Community

Filip Kisić
Filip Kisić

Posted on

Design patterns - Part II

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...');
}
Enter fullscreen mode Exit fullscreen mode

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...');
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

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...');
}
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

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...');
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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%');
    }),
  ),
),
...
Enter fullscreen mode Exit fullscreen mode

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...');
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

Here is the output:

Wearing pajamas, preparing for the day...
Wearing sports clothes, working out...
Wearing suit, focusing on productivity...
Enter fullscreen mode Exit fullscreen mode

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)