DEV Community

Jasper Oh/YJ Oh
Jasper Oh/YJ Oh

Posted on

🧑‍🎨 Creational Patterns - 2️⃣

Creational Patterns (🛠 Second) - draft

Patterns that deal with object creation


1. Singleton

What is Singleton Pattern?

Start from I need a system where there is a single object that is accessible from anywhere in the code - how do I do this without global variables? - In other word, Singleton is a globally accessible class where we guarantee only a single instance is created.

Let's see the code

class singleton
{
public:
    static singleton& get_instance()
    {
        static singleton instance; // Lazy Initialization is used in Singleton
        return instance; // Instantiated on first use.
    }
private:
    int value;
    singleton() = default; //private constructor

public:
    singleton(singleton const&)= delete; //deleting copy constructor
    void operator=(singleton const&)= delete; //deleting assignment operator

    int get_value() {
        return value++;
    }
};

Enter fullscreen mode Exit fullscreen mode

So, As you can see in this pattern,

  1. Instantiates the object on its first use
  2. Ideally hides a private constructor
  3. Reveals a public get_instance function that returns a reference to a static instance of the class
  4. Provides "global" access to a single object

2. Factory / Factory Method

What is the Factory / Factory Method Pattern?

In some articles & stackoverflows, mention that Factory and Factory Method is slightly different, but trying to explain in the same category.

When you want to separate creation code from client code.

When you want a client or service to only depend on a base class for object creation and not each concrete class

Let's check the UML of Factory Pattern

Image description

Just like above UML, We can create the "ConcreteProductA and B" with the AbstractFactory.

Base on this, lets see the code actually trying to solve the real world problem.

Image description

/*
Forum class - ForumWebsite
*/

struct Forum {
    string name;
    vector<User*> users;
    Forum(string name) : name(name) {}

    UserFactory* register_user() {
        cout << "Welcome! Do you want to make an account or continue as a guest?" << endl;
        cout << "1. Make Account" << endl;
        cout << "2. Continue as Guest" << endl;
        int choice = 0;
        cout << "Enter your choice (1-2)" << endl;
        cin >> choice;

        UserFactory *factory_class;
        if(choice == 1)
            factory_class = new MemberUserFactory;
        else
            factory_class = new GuestUserFactory;

        return factory_class;
    }

    void add_user() {
        UserFactory *factory = register_user(); //get a member or guest factory
        users.push_back(factory->create_user());
        delete factory;
    }

    ~Forum() {
        for (User *u : users)
        {
            delete u;
        }
    }
};
Enter fullscreen mode Exit fullscreen mode

UserFactory, GuestFactory, MemberFactory


struct UserFactory {
    virtual User* create_user() = 0;
};


struct GuestUserFactory : UserFactory {
    User* create_user() override {
        return new Guest("","");
    }
};

struct MemberUserFactory : UserFactory {
    User* create_user() override {
        string username;
        string password;
        cout << "Enter username: " << endl;
        cin >> username;
        cout << "Enter password: " << endl;
        cin >> password;
        return new Member(username, password);
    }
};
Enter fullscreen mode Exit fullscreen mode

User Abstract class, Member, Guest

enum Permission {
    NONE,
    READ,
    LIKE,
    SHARE,
    FLAG,
    WRITE
};

struct User {
    string username;
    string password;
    vector<Permission> permissions;
    string date_joined;

    User(string username, string password) : username(username), password(password) {
        // declaring argument of time()
        time_t my_time = time(NULL);
        date_joined = ctime(&my_time);
    }

    bool authenticate(const string& password_input) {
        return password_input == password;
    }

    pair<string, string> get_profile() {
        return make_pair(username, date_joined);
    }

    virtual string getString() const {
        return "Username: ";
    }
    friend ostream& operator<<(ostream& os, const User& u)
    {
        os << u.getString() << u.username << " Date joined: " << u.date_joined << endl;
        return os;
    }
};

struct Guest : User {
    Guest(string username, string password) : User(username, password) {
        permissions.push_back(Permission::READ);
        permissions.push_back(Permission::LIKE);
        permissions.push_back(Permission::SHARE);
        permissions.push_back(Permission::FLAG);
    }

    string getString() const override {
        return "Guest User: ";
    }

};

struct Member : User {
    Member(string username, string password) : User(username, password) {
        permissions.push_back(Permission::READ);
        permissions.push_back(Permission::LIKE);
        permissions.push_back(Permission::SHARE);
        permissions.push_back(Permission::FLAG);
        permissions.push_back(Permission::WRITE);
    }

    string getString() const override {
        return "Member User: ";
    }
};

Enter fullscreen mode Exit fullscreen mode

As we can see, these codes provide a separate interface for object creation where each subclass can alter the type of object created.

If we want to add a new product, just create a new Factory and override the creation method in it.


3. Abstract Factory

What is Abstract Factory Design Pattern?

Creational Design Pattern that lets you produce families of related objects without specifying their concrete classes

Let's check the UML of Abstract Factory Pattern

Image description

Looks very similar with Factory Pattern (Obviously) - and as we can see this pattern offers the interface for creating a family of related objects, without explicitly specifying their classes - factory for factories

Base on the UML, lets check code

AbstractProductA, B

/**
 * AbstractProductA
 */
class AbstractProductA {
public:
    virtual ~AbstractProductA() {}
    virtual string UsefulFunctionA() const = 0;
};

/**
 * AbstractProductB
 */
class AbstractProductB {
public:
    virtual ~AbstractProductB() {}
    virtual string UsefulFunctionB() const = 0;

    virtual string AnotherUsefulFunctionB(const AbstractProductA& collaborator) const = 0;
};

class ConcreteProductB1 : public AbstractProductB {
public:
    string UsefulFunctionB() const override {
        return "ConcreteProductB1";
    }

    string AnotherUsefulFunctionB(const AbstractProductA& collaborator) const override {
        const string result = collaborator.UsefulFunctionA();
        return "The result of the B1 collaborating with ( " + result + " )";
    }
};

Enter fullscreen mode Exit fullscreen mode

ConcreteA1, A2, B1, B2

class ConcreteProductA1 : public AbstractProductA {
public:
    string UsefulFunctionA() const override {
        return "ConcreteProductA1";
    };
};

class ConcreteProductA2 : public AbstractProductA {
public:
    string UsefulFunctionA() const override {
        return "ConcreteProductA2";
    }
};

class ConcreteProductB1 : public AbstractProductB {
public:
    string UsefulFunctionB() const override {
        return "ConcreteProductB1";
    }

    string AnotherUsefulFunctionB(const AbstractProductA& collaborator) const override {
        const string result = collaborator.UsefulFunctionA();
        return "The result of the B1 collaborating with ( " + result + " )";
    }
};

class ConcreteProductB2 :public AbstractProductB {
public:
    string UsefulFunctionB() const override {
        return "ConcreteProductB2";
    }
    string AnotherUsefulFunctionB(const AbstractProductA& collaborator) const override {
        const string result = collaborator.UsefulFunctionA();
        return "The result of the B2 collaborating with ( " + result + " )";
    }
};
Enter fullscreen mode Exit fullscreen mode

Abstract Factory (interface)

class AbstractFactory {
public:
    virtual AbstractProductA* CreateProductA() const = 0;
    virtual AbstractProductB* CreateProductB() const = 0;
};
Enter fullscreen mode Exit fullscreen mode

ConcreteFactory1, 2

class ConcreteFactory1 : public AbstractFactory {
public:
    AbstractProductA* CreateProductA() const override {
        return new ConcreteProductA1();
    }

    AbstractProductB* CreateProductB() const override {
        return new ConcreteProductB1();
    }
};

class ConcreteFactory2 : public AbstractFactory {
public:
    AbstractProductA* CreateProductA() const override {
        return new ConcreteProductA2();
    }

    AbstractProductB* CreateProductB() const override {
        return new ConcreteProductB2();
    }
};
Enter fullscreen mode Exit fullscreen mode

Client code

void ClientCode(const AbstractFactory& factory) {
    AbstractProductA* productA = factory.CreateProductA();
    AbstractProductB* productB = factory.CreateProductB();

    cout << productA -> UsefulFunctionA() << endl;
    cout << productB -> AnotherUsefulFunctionB(*productA) << endl;

    delete productA;
    delete productB;
}
Enter fullscreen mode Exit fullscreen mode

At last, in main

    ConcreteFactory1* factory1 = new ConcreteFactory1();
    ClientCode(*factory1);

    ConcreteFactory2* factory2 = new ConcreteFactory2();
    ClientCode(*factory2);

    delete factory1;
    delete factory2;
Enter fullscreen mode Exit fullscreen mode

Just like we saw, if the exact number of product families are unknown and you have to add new ones in the future, we can use this pattern.


4. Builder

What is the builder design pattern?

In easy words, I hate my constructor looks ugly and long.. like new House( ... x 100); and we can customize the Builder class to take in its own parameters and options. And plus, We can optionally implement a manager class called Director that can be responsible for different subroutines that call different methods in a builder class.

Lets check a UML

Image description

And base on the UML, lets dive in to the code

Builder interface

class Builder{
    public:
    virtual ~Builder(){}
    virtual void AddTopping(string topping) const =0;
    virtual void AddBase(string base) const =0;
    virtual void AddSauce(string sauce) const =0;
};
Enter fullscreen mode Exit fullscreen mode

ConcreteBuilder1

class ConcreteBuilder1 : public Builder{
    private:

    Product1* product;

    public:
    ConcreteBuilder1(){
        this->Reset(); //builder has an instance of Product ready
    }

    ~ConcreteBuilder1(){
        delete product;
    }

    void Reset(){
        this->product= new Product1();
    }
    /**
     * All production steps work with the same product instance.
     */

    void AddTopping(string topping)const override{
        this->product->toppings.push_back(topping);
    }

    void AddBase(string base)const override{
        this->product->base = base;
    }

    void AddSauce(string sauce)const override{
        this->product->sauce = sauce;
    }

    /**
     * Please be careful here with the memory ownership. Once you call
     * GetProduct the user of this function is responsable to release this
     * memory. Here could be a better option to use smart pointers to avoid
     * memory leaks
     */

    Product1* GetProduct() {
        Product1* result= this->product;
        this->Reset(); //resets product pointer to be ready for next pizza to build
        return result;
    }
};

Enter fullscreen mode Exit fullscreen mode

Product1

class Product1{
    public:
    std::vector<std::string> toppings;
    string base = "";
    string sauce = "";

    friend ostream& operator<<(ostream& os, const Product1& p) {
        stringstream toppings_str;
        os << "Product parts: ";
        for (size_t i=0;i<p.toppings.size();i++){
            if(p.toppings[i]== p.toppings.back()){
                toppings_str << p.toppings[i];
            }else{
                toppings_str << p.toppings[i] << ", ";
            }
        }
        os << p.base << ", ";
        os << p.sauce << ", ";
        os << toppings_str.str();
        os << "\n\n";
        return os;
    }
};
Enter fullscreen mode Exit fullscreen mode

Director (Completely Optional)

class Director{
    /**
     * @var Builder
     */
    private:
    Builder* builder;
    /**
     * The Director works with any builder instance that the client code passes
     * to it. This way, the client code may alter the final type of the newly
     * assembled product.
     */

    public:

    void set_builder(Builder* builder){
        this->builder=builder;
    }

    /**
     * The Director can construct several product variations using the same
     * building steps.
     */

    void BuildMinimalViableProduct(){
        this->builder->AddBase("Wheat Pizza");
    }

    void BuildFullFeaturedProduct(){
        this->builder->AddBase("Wheat Pizza");
        this->builder->AddSauce("Tomato");
        this->builder->AddTopping("Pepperoni");
        this->builder->AddTopping("Mozzarella");
        this->builder->AddTopping("Olives");
    }
};
Enter fullscreen mode Exit fullscreen mode

Client Code

void ClientCode(Director& director)
{
    ConcreteBuilder1* builder = new ConcreteBuilder1();
    director.set_builder(builder);

    //Use director to call builder to create pre-made products for us
    std::cout << "Standard basic product:\n";
    director.BuildMinimalViableProduct();

    Product1* p= builder->GetProduct();
    cout << *p;
    delete p;

    std::cout << "Standard full featured product:\n";
    director.BuildFullFeaturedProduct();

    p = builder->GetProduct();
    cout << *p;
    delete p;

    // Remember, the Builder pattern can be used without a Director class.
    std::cout << "Custom product:\n";
    builder->AddBase("Sour dough Pizza");
    builder->AddTopping("Mushrooms");
    builder->AddSauce("Sour cream");
    p=builder->GetProduct();
    cout << *p;
    delete p;

    delete builder;
}
Enter fullscreen mode Exit fullscreen mode

At last in main

    Director* director= new Director();
    ClientCode(*director);
    delete director;
    return 0;
Enter fullscreen mode Exit fullscreen mode

As you can see we can get rid of numerous constructors. And When a product is built out of many parts that can be mixed and matched.

This allows you to build variations while avoiding inheritance hierarchies


Top comments (0)