Building Blocks of Code: Fun with Creational Design Patterns

Building Blocks of Code: Fun with Creational Design Patterns

All the images are from refactoring.guru/design-patterns. Please visit the site for a more detailed explanation.

What Are Creational Patterns?

  1. Creational patterns are like blueprints for building objects in software design. They focus on how objects are created, ensuring that the process is both flexible and reusable. Think of it like building different toy sets: instead of constructing each toy from scratch every time, you use a clever system to assemble them efficiently.

    These patterns are used to address issues where the process of creating objects can become repetitive, complex, or error-prone. They simplify object creation, make your code easier to manage, and allow you to reuse components across your projects. In other words, creational patterns save you from the headaches of writing repetitive code while ensuring everything fits together nicely.

    Why Use Creational Patterns?

    1. Flexibility: They make it easy to adjust the way objects are created without modifying the core code.

    2. Efficiency: By reusing creation logic, they reduce repetitive coding tasks.

    3. Scalability: As your project grows, these patterns ensure that adding new object types is simple and hassle-free.

    4. Readability: They organize your code, making it more understandable and less error-prone.

Some of the most popular creational patterns include:

  1. Singleton: Ensures only one instance of a class is created.

  2. Prototype: Creates new objects by copying existing ones, making it quick to generate similar items.

  3. Builder: Simplifies the creation of complex objects by breaking it into smaller, manageable steps.

  4. Factory: Provides a way to create objects based on specific conditions, avoiding repetitive logic.

Singleton

Singleton pattern

Having only one instance of the class.

  1. Why does removing the top LaundryWorker._(); cause an error below?

    By creating the instance with _.(), Dart ensures that only one instance of the class is created, and it must be created before it can be referenced.

  2. Why does removing the static keyword cause "Instance members can't be accessed from a factory constructor" error?

    In the singleton pattern, we try to access the single instance of the class. If we remove the static keyword, we need to create an instance (object) of the class like LaundryWorker().instance. This means we have already created a new instance, which doesn't make sense. That's why the static keyword is necessary.

class LaundryWorker {

    LaundryWorker._();
    static final LaundryWorker instance = LaundryWorker._();

    factory LaundryWorker(){
      return instance;
    }
    void doWork() {
        doLaundry();
    }

    void doLaundry() {
        // does laundry
    }
}

  1. Prototyping

    Prototype Design Pattern

    Prototyping involves adding a function directly to the class, allowing other objects to easily replicate the class, including its private methods. This is typically achieved by creating a method called copyWith. This method accepts new parameters, assigns these parameters to a new instance of the class, and then returns this newly created instance. By doing so, it enables the creation of modified copies of the original object while preserving the original class structure and its private methods. This approach is particularly useful when you want to create variations of an object without altering the original instance.

class LaundryWorker {
    final salary ;

    LaundryWorker._(this.salary);
    static final LaundryWorker instance = LaundryWorker._(0);

    factory LaundryWorker(){
      return instance;
    }

    LaundryWorker copyWith({int? salary}) => LaundryWorker._(salary ?? this.salary);

    void doWork() {
        doLaundry();
    }

    void doLaundry() {
        // does laundry
    }
}

  1. Builder

    Builder design pattern

When you have many parameters, most of them will be null. This can be very annoying because you have to pass the parameters each time you create an instance of the class. A better approach is to create a function that sets the values and returns the instance of the class. This ensures you don't need to pass parameters to the function every time; you just call the function to set the parameters. It requires more code, but it reduces errors and improves the readability of the code. You don't usually need this in smaller projects, but for larger projects, it's important to make the code readable and understandable to a wider audience.

class LaundryWorker {
    int? salary;
    bool? isAvailable; 

    LaundryWorker._(this.salary );
    static final LaundryWorker instance = LaundryWorker._(0 );
    factory LaundryWorker(){
      return instance;
    }

    LaundryWorker buildisAvailable(bool isAvailable){
      this.isAvailable = isAvailable;
      return this;
    }
    LaundryWorker copyWith({int? salary , bool? isAvailable}) => LaundryWorker._(salary ?? this.salary );

    void doWork() {
        doLaundry();
    }

    void doLaundry() {
        // does laundry
    }
}

 void main() {
    LaundryWorker laundaryWorker = LaundryWorker.instance
                  .buildisAvailable(true);
    var manager = Manager(CarWasher(), laundaryWorker , SpecialGrassCutter());
    manager.manage();

}

  1. Factory

    Factory Method pattern

    This is a very simple and straightforward method to use. If you need to create objects based on logic that is not readable and is time-consuming because you have to write it multiple times, what do you do when you have to write the same thing repeatedly? You create a function. So, we create something called a factory that handles the checking and returns the object for us. It's as simple as that.

  2. ```dart class LaundryWorker { int? salary; bool? isAvailable;

    LaundryWorker.({this.salary} ); static final LaundryWorker instance = LaundryWorker.(salary: 0 ); factory LaundryWorker(){ return instance; }

    LaundryWorker factory(String type){ if(type == "Premium"){ return LaundryWorker.( salary : 1000 ); }else{ return LaundryWorker.( salary : 100 ); } }

    LaundryWorker builisAvailable(bool isAvailable){ this.isAvailable = isAvailable; return this; } LaundryWorker copyWith({int? salary , bool? isAvailable}) => LaundryWorker._(salary ?? this.salary );

    void doWork() { doLaundry(); }

    void doLaundry() { // does laundry } }

void main() { LaundryWorker laundary = LaundryWorker.instance .builisAvailable(true).factory("Premium");

var manager = Manager(CarWasher(), laundary , SpecialGrassCutter()); manager.manage();

} ```