Table of contents
    blog cover

    One Design Pattern a Week: Week 2

    Software Engineer
    Software Engineer
    Design Pattern
    Design Pattern
    Welcome back to my "One Design Pattern a Week" series!

    Try to solve this real problem: Object Creation

    Imagine you're developing a large web application that needs to support multiple types of user accounts such as Admin, Guest, and Member. Each type of user requires different initializations, configurations, and permissions.
    If you used a factory library like factory-bot, you may be familiar with this syntax:
    // language: ruby
    create :user, name: "John"
    create :admin_user, name: "Jane"

    Here are some potential issues:
    • Complex Creation Logic: The creation logic for different user types can become complex and difficult to manage if implemented in a single place.
    • Code Duplication: Without a proper structure, you might end up duplicating code when creating different types of user accounts.

    Take a moment to think about how you would solve this problem. How can you create different types of user accounts in a systematic and manageable way?

    An example of bad design

    // language: ruby
    class User
      attr_accessor :name, :role
    
      def initialize(name, role)
        @name = name
        @role = role
      end
    end
    
    def create_user(type, name)
      case type
      when :admin
        user = User.new(name, 'Admin')
        # Additional admin-specific initialization
      when :guest
        user = User.new(name, 'Guest')
        # Additional guest-specific initialization
      when :member
        user = User.new(name, 'Member')
        # Additional member-specific initialization
      else
        raise "Unknown user type: #{type}"
      end
      user
    end
    
    # Usage
    admin = create_user(:admin, 'Alice')
    guest = create_user(:guest, 'Bob')
    member = create_user(:member, 'Charlie')
    
    puts admin.role  # Output: Admin
    puts guest.role  # Output: Guest
    puts member.role # Output: Member

    Issues with this implementation:
    1. Poor Maintainability: The create_user method becomes hard to maintain as the logic for creating different types of users grows. Any change in the initialization logic must be manually updated in multiple places.
    2. Code Duplication: Common initialization logic for different user types may be duplicated, leading to code bloat.
    3. Violation of Single Responsibility Principle: The client code is responsible for both the creation and the business logic, making it harder to manage.
    4. Scalability: Adding a new user type requires modifying the create_user method, which increases the risk of introducing bugs.

    Factory Pattern

    The Factory pattern is a creational design pattern that provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. This is particularly useful when the exact type of the object to be created isn't known until runtime.

    Key Characteristics:
    • Encapsulation of Object Creation: Encapsulates the object creation process to separate it from the code that uses the objects.
    • Flexibility: Makes it easy to introduce new types of objects without changing the existing code.
    • Single Responsibility Principle: Adheres to the Single Responsibility Principle by separating the object creation logic from the business logic.

    Implementing Factory Pattern in Ruby

    Let's implement a simple factory for creating different types of user accounts.
    // language: ruby
    class User
      attr_accessor :name, :role
    
      def initialize(name, role)
        @name = name
        @role = role
      end
    end
    
    class Admin < User
      def initialize(name)
        super(name, 'Admin')
        # Additional admin-specific initialization
      end
    end
    
    class Guest < User
      def initialize(name)
        super(name, 'Guest')
        # Additional guest-specific initialization
      end
    end
    
    class Member < User
      def initialize(name)
        super(name, 'Member')
        # Additional member-specific initialization
      end
    end
    
    class UserFactory
      def self.create_user(type, name)
        case type
        when :admin
          Admin.new(name)
        when :guest
          Guest.new(name)
        when :member
          Member.new(name)
        else
          raise "Unknown user type: #{type}"
        end
      end
    end
    
    # Usage
    admin = UserFactory.create_user(:admin, 'Alice')
    guest = UserFactory.create_user(:guest, 'Bob')
    member = UserFactory.create_user(:member, 'Charlie')
    
    puts admin.role  # Output: Admin
    puts guest.role  # Output: Guest
    puts member.role # Output: Member

    More Practical Examples

    1. Shape Creation
    A shape creation system can use the Factory pattern to create different types of shapes such as Circle, Square, and Triangle.
    // language: ruby
    class Shape
      def draw
        raise 'Abstract method called'
      end
    end
    
    class Circle < Shape
      def draw
        puts "Drawing a Circle"
      end
    end
    
    class Square < Shape
      def draw
        puts "Drawing a Square"
      end
    end
    
    class Triangle < Shape
      def draw
        puts "Drawing a Triangle"
      end
    end
    
    class ShapeFactory
      def self.create_shape(type)
        case type
        when :circle
          Circle.new
        when :square
          Square.new
        when :triangle
          Triangle.new
        else
          raise "Unknown shape type: #{type}"
        end
      end
    end
    
    # Usage
    circle = ShapeFactory.create_shape(:circle)
    circle.draw  # Output: Drawing a Circle
    
    square = ShapeFactory.create_shape(:square)
    square.draw  # Output: Drawing a Square
    
    triangle = ShapeFactory.create_shape(:triangle)
    triangle.draw  # Output: Drawing a Triangle

    2. Notification System
    A notification system can use the Factory pattern to create different types of notifications such as Email, SMS, and Push.
    // language: ruby
    class Notification
      def send_message(message)
        raise 'Abstract method called'
      end
    end
    
    class EmailNotification < Notification
      def send_message(message)
        puts "Sending Email: #{message}"
      end
    end
    
    class SMSNotification < Notification
      def send_message(message)
        puts "Sending SMS: #{message}"
      end
    end
    
    class PushNotification < Notification
      def send_message(message)
        puts "Sending Push Notification: #{message}"
      end
    end
    
    class NotificationFactory
      def self.create_notification(type)
        case type
        when :email
          EmailNotification.new
        when :sms
          SMSNotification.new
        when :push
          PushNotification.new
        else
          raise "Unknown notification type: #{type}"
        end
      end
    end
    
    # Usage
    email = NotificationFactory.create_notification(:email)
    email.send_message("Hello via Email")  # Output: Sending Email: Hello via Email
    
    sms = NotificationFactory.create_notification(:sms)
    sms.send_message("Hello via SMS")  # Output: Sending SMS: Hello via SMS
    
    push = NotificationFactory.create_notification(:push)
    push.send_message("Hello via Push Notification")  # Output: Sending Push Notification: Hello via Push Notification

    Conclusion

    The Factory pattern is a powerful tool for managing object creation, ensuring that the creation logic is encapsulated and making it easy to introduce new types of objects without modifying existing code.

    Happy coding!

    Created at 2024-09-21 15:38:03 +0700

    Related blogs