Introduction to Fundamental Design Patterns - Part 1

Introduction to Fundamental Design Patterns - Part 1

Discover how design patterns can transform your code into elegant and flexible software.

Introduction

Design patterns are like blueprints for building software. They’re proven solutions to common programming problems. By learning these patterns, you can write cleaner, more efficient, and maintainable code. In this post, we’ll explore some of the most important design patterns and how to use them.

Prerequisites to Understand Design Patterns

Object-Oriented Programming (OOP)

  • Encapsulation

  • Inheritance

  • Polymorphism

  • Abstraction

Software Design Principles

  • SOLID Principles

  • DRY (Don't Repeat 1Yourself) [ Identifying and eliminating redundant code ]

  • KISS (Keep It Simple, Stupid)

What's a design pattern?

Design patterns are typical solutions to commonly occurring problems in software design. They are like pre-made blueprints that you can customize to solve a recurring design problem in your code.

This sounds more theoretical right. we can see these definitions all books. Let me explain in simple terms.

Let's say you're building a game. You need to create different types of characters: a hero & villain. How we implement this on our application.

   <?php

class Hero
{
    public function whoIam(): string
    {
        return "Hero";
    }
}

class Villain
{
    public function whoIam(): string
    {
        return "Villain";
    }
}

function createCharacter(string $character): Hero | Villain | null
{
    if ($character === "hero") {
        return new Hero;
    }

    if ($character === "villan") {
        return new Villain;
    }
    return null;
}

echo (createCharacter("hero"))->whoIam()."\n";
echo (createCharacter("villan"))->whoIam()."\n";

We could create a function createCharacter to get a new object based on the character we need. This looks good & you may think code works what is wring with it?. Let think now you need to add a new character monster & the each character may have few more function like what is my power function which returns the power of some characters. Some may not have any powers too. Sounds easy let’s code it.

<?php

class Hero
{
    public function whoIam(): string
    {
        return "Hero";
    }

    public function myPower(): string
    {
        return "I can jump high & run fast";
    }
}

class Monster
{
    public function whoIam(): string
    {
        return "Monster";
    }

    public function myPower(): string
    {
        return "I can eat people";
    }
}

class Villain
{
    public function whoIam(): string
    {
        return "Villain";
    }
}

function createCharacter(string $character): Hero | Villain | Monster |null
{
    if ($character === "hero") {
        return new Hero;
    }

    if ($character === "villan") {
        return new Villain;
    }

    if ($character === "monstor") {
        return new Monster;
    }

    return null;
}

echo (createCharacter("hero"))->whoIam() . "\n";
echo (createCharacter("villan"))->whoIam() . "\n";
echo (createCharacter("monster"))->whoIam() . "\n";

Now you could see our createCharacter function is getting messy with bunch of if conditions & Return type hinting was growing. Think you may end up on N number of character on your game in future. The code will start get messy ,unmaintainable & hard for developer to read the code**.**

Instead of writing separate code for each character, you can use a design pattern called Factory Pattern. With the Factory Pattern, you can create a "character factory" that knows how to create different types of characters. This way, you can easily add new character types without changing existing code.

So, there are different types of design patterns which are widely used to solve these kind of code structuring problems. I hope now it make sense & you understood what is design pattern.

Why use design patterns?

  • Improve code quality: Design patterns help you write better, more organized code.

  • Make your code easier to understand: Other developers can quickly grasp your code if it follows well-known patterns.

  • Maintainable: It's easy to understand, modify, and update.

💡
Remember: Design patterns are tools, not rules. Use them wisely to solve specific problems. Don't overuse them, as it can make your code more complex.

In the first part of this article, we will delve into two of the most fundamental creational design patterns.

Creational Design Patterns

Creational design patterns are a set of design patterns that deal with object creation mechanisms, trying to create objects in a manner suitable to the situation. These patterns abstract the instantiation process. This makes the software independent of how objects are created, composed, and represented.

Factory Pattern

This pattern defines an interface for creating an object, but lets subclasses decide which class to instantiate. This promotes loose coupling between the client and the concrete classes.

Builder Pattern

This pattern separates the construction of a complex object from its representation. It allows the same construction process to create different representations of the object.

Factory Pattern

The Factory Pattern is a type of Creational Design Pattern that provides a way to create objects without specifying the exact class of object that will be created. The Factory Pattern centralizes object creation, allowing for more flexible and maintainable code.

In simpler terms, the Factory Pattern abstracts the instantiation process of objects, so the client code doesn't need to know the details of how or which object is created. Instead, it relies on a factory method to handle the object creation.

Advantages of Factory Pattern

Encapsulation:

The creation logic is encapsulated within the factory class, reducing the dependency between the client code and the concrete classes.

Flexibility:

It provides flexibility to add new classes without changing the client code.

Maintainability:

Centralized object creation simplifies maintaining and extending the code, especially when adding new types of objects.

Applying the Factory Pattern to the Example

Let’s take a look at the example code you’ve provided. The goal is to refactor the code to implement the Factory Pattern. In the existing code, the createCharacter() function returns different types of characters (Hero, Villain, Monster) based on the string input. However, this method could be enhanced to follow the Factory Pattern more strictly.

Current Code Breakdown

In the current approach:

  • We have separate classes (Hero, Monster, Villain), each with its own methods like whoIam() and myPower().

  • A function createCharacter() creates instances based on the input string.

  • The client code calls createCharacter() to get an object and invoke methods.

Refactored Code Using Factory Pattern

Since we have different characters (Hero, Villain, and Monster), each with a whoIam() method, we can define a common interface Character that all classes will implement.

// Define the Character interface
interface Character
{
    public function whoIam(): string;
}
// Define the concrete classes implementing the Character interface
class Hero implements Character
{
    public function whoIam(): string
    {
        return "Hero";
    }

    public function myPower(): string
    {
        return "I can jump high & run fast";
    }
}

class Monster implements Character
{
    public function whoIam(): string
    {
        return "Monster";
    }

    public function myPower(): string
    {
        return "I can eat people";
    }
}

class Villain implements Character
{
    public function whoIam(): string
    {
        return "Villain";
    }
}

We will create a CharacterFactory class that will be responsible for creating instances of characters.

<?php
// Create a Factory class
class CharacterFactory
{
    public static function createCharacter(string $characterType): ?Character
    {
        switch (strtolower($characterType)) {
            case 'hero':
                return new Hero();
            case 'monster':
                return new Monster();
            case 'villain':
                return new Villain();
            default:
                return null;
        }
    }
}

Example usage

<?php
// Example usage:
$hero = CharacterFactory::createCharacter("hero");
echo $hero ? $hero->whoIam() . "\n" : "Character not found\n";

Refactored Code

Character Interface

We define an interface Character that includes the method whoIam(), which is common to all character classes (Hero, Monster, Villain). This allows all the character classes to be treated polymorphically, meaning they can all be referenced using the Character type.

Character Factory Class

The CharacterFactory class contains a static method createCharacter(), which abstracts the object creation process. Based on the input string (e.g., "hero", "monster", "villain"), it decides which concrete character class to instantiate and return.

Client Code

The client code now simply calls CharacterFactory::createCharacter() and gets the correct object. It does not need to worry about how the object is created or which class is being used.

Error Handling

If an invalid string is passed to createCharacter(), it will return null, and the client code handles that case by checking if the returned object is valid.

Benefits of Refactoring

  • Encapsulation: The object creation logic is centralized in the CharacterFactory class, which encapsulates the instantiation details.

  • Extensibility: If we want to add a new character type (e.g., "Sidekick"), we can simply add a new class that implements the Character interface and update the CharacterFactory without changing the client code.

  • Maintainability: As the system grows and more character types are added, the changes are isolated within the factory class and concrete classes, making it easier to maintain.

Conclusion

In this section, we explored the Factory Pattern, a powerful creational design pattern that helps centralize object creation and decouple the client code from the specifics of object instantiation. By applying this pattern, we refactored the initial example, making it more flexible, maintainable, and scalable.

As we conclude Part 1, I hope you gained a clearer understanding of the Factory Pattern and how it can be applied in real-world scenarios. Stay tuned for Part 2, where we'll dive deeper into other design patterns and further enhance your development skills.

🔗 Like & Share this article with your engineering friends to spread the knowledge! Your support motivates me to create more content like this and help developers grow. Keep learning, keep coding, and stay tuned for Part 2! 🚀

Did you find this article valuable?

Support Saravana sai blog by becoming a sponsor. Any amount is appreciated!