Applying Singleton, Builder, Composite, and Abstract Factory Patterns for Smart Home Devices

In this lesson, we will integrate design patterns into a practical project: building a smart home system. You'll learn how to create and adapt various smart home devices using the Singleton, Builder, Composite, and Abstract Factory patterns in JavaScript. By the end, you will have a solid understanding of how these design patterns can make your smart home system more efficient, modular, and easier to maintain.

Quick Summary
  1. Singleton Pattern:

    • Purpose: Ensures a class has only one instance and provides a global point of access to it.
    • Steps:
      • Define a class (Singleton) with a static method to hold the single instance.
      • Implement the getInstance method to manage instance creation and access.
  2. Builder Pattern:

    • Purpose: Constructs complex objects step by step, providing a flexible solution for object creation.
    • Steps:
      • Define a class (SmartHomeDevice) for the object.
      • Create a builder class (SmartHomeDeviceBuilder) with methods to set object properties and a method to return the final object.
  3. Composite Pattern:

    • Purpose: Treats individual objects and compositions of objects uniformly.
    • Steps:
      • Define a base class (DeviceComponent) for the composite structure.
      • Implement LeafDevice for individual objects and CompositeDevice for composite objects.
  4. Abstract Factory Pattern:

    • Purpose: Provides an interface for creating families of related or dependent objects without specifying their concrete classes.
    • Steps:
      • Define an abstract factory class (SmartDeviceFactory).
      • Implement concrete factories (LightFactory, FanFactory) to create specific sensor and actuator objects.
Implementing the Singleton Pattern

To start, we implement the Singleton pattern in JavaScript to ensure that a class has only one instance and provides a global point of access to it.

class Singleton {
    constructor() {
        if (Singleton.instance) {
            return Singleton.instance;
        }
        Singleton.instance = this;
        // Additional initialization code can go here
    }

    static getInstance() {
        if (!Singleton.instance) {
            Singleton.instance = new Singleton();
        }
        return Singleton.instance;
    }
}

// Usage
const singleton1 = Singleton.getInstance();
const singleton2 = Singleton.getInstance();
console.log(singleton1 === singleton2);  // Output: true

The getInstance method checks if an instance exists and creates one if it does not.

Constructing Devices with the Builder Pattern

Next, we use the Builder pattern to construct complex SmartHomeDevice objects step by step.

class SmartHomeDevice {
    constructor() {
        this.sensors = [];
        this.actuators = [];
        this.name = "";
    }
}

class SmartHomeDeviceBuilder {
    constructor() {
        this.device = new SmartHomeDevice();
    }

    addSensors(sensors) {
        this.device.sensors = sensors;
        return this;
    }

    addActuators(actuators) {
        this.device.actuators = actuators;
        return this;
    }

    setName(name) {
        this.device.name = name;
        return this;
    }

    build() {
        return this.device;
    }
}

// Usage
const builder = new SmartHomeDeviceBuilder();
const device = builder
    .setName('Smart Light')
    .addSensors(['Light Sensor'])
    .addActuators(['LED'])
    .build();

console.log(device.name);  // Output: Smart Light

The SmartHomeDeviceBuilder class provides methods to set device properties and construct the final SmartHomeDevice.

Managing Composition of Devices with Composite Pattern

Now, we implement the Composite pattern to manage individual and composite objects uniformly.

class DeviceComponent {
    constructor(name) {
        this.name = name;
    }

    operation() {
        throw new Error('Method "operation()" must be implemented.');
    }
}

class LeafDevice extends DeviceComponent {
    operation() {
        console.log(`Leaf ${this.name} operation`);
    }
}

class CompositeDevice extends DeviceComponent {
    constructor(name) {
        super(name);
        this.children = [];
    }

    add(component) {
        this.children.push(component);
    }

    operation() {
        console.log(`Composite ${this.name} operation`);
        this.children.forEach(child => child.operation());
    }
}

// Usage
const leaf1 = new LeafDevice('Light');
const leaf2 = new LeafDevice('Fan');
const composite = new CompositeDevice('Room');
composite.add(leaf1);
composite.add(leaf2);
composite.operation();
// Output:
// Composite Room operation
// Leaf Light operation
// Leaf Fan operation

The CompositeDevice class allows us to treat individual DeviceComponent objects uniformly as part of a composite structure.

Creating Device Families with Abstract Factory Pattern

Finally, we implement the Abstract Factory pattern to create families of related objects without specifying their concrete classes.

class SmartDeviceFactory {
    createSensor() {
        throw new Error('Method "createSensor()" must be implemented.');
    }

    createActuator() {
        throw new Error('Method "createActuator()" must be implemented.');
    }
}

class LightFactory extends SmartDeviceFactory {
    createSensor() {
        return new LightSensor();
    }

    createActuator() {
        return new LEDActuator();
    }
}

class FanFactory extends SmartDeviceFactory {
    createSensor() {
        return new TemperatureSensor();
    }

    createActuator() {
        return new MotorActuator();
    }
}

class LightSensor {
    constructor() {
        console.log('Light Sensor created');
    }
}

class LEDActuator {
    constructor() {
        console.log('LED Actuator created');
    }
}

class TemperatureSensor {
    constructor() {
        console.log('Temperature Sensor created');
    }
}

class MotorActuator {
    constructor() {
        console.log('Motor Actuator created');
    }
}

// Usage for LightFactory
const lightFactory = new LightFactory();
const lightSensor = lightFactory.createSensor();  // Output: Light Sensor created
const lightActuator = lightFactory.createActuator();  // Output: LED Actuator created

// Usage for FanFactory
const fanFactory = new FanFactory();
const fanSensor = fanFactory.createSensor();  // Output: Temperature Sensor created
const fanActuator = fanFactory.createActuator();  // Output: Motor Actuator created

The LightFactory and FanFactory classes implement methods to create specific sensor and actuator objects.

Conclusion

Implementing Singleton, Builder, Composite, and Abstract Factory patterns in our smart home system allows us to create, organize, and manage devices in a structured and reusable manner. The Singleton pattern ensures a single instance of a class, the Builder pattern provides a flexible solution for object creation, the Composite pattern handles both individual and composite objects uniformly, and the Abstract Factory pattern creates families of related objects. This approach makes our smart home system more modular, flexible, and easier to maintain and extend.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal