Skip to content

Composition Over Inheritance: Building Flexible and Extensible System Architecture

In software design, while inheritance is powerful, overusing it often leads to code structures that become rigid and fragile. The "Composition Over Inheritance" principle advocates building complex objects by composing small, independent functional modules together, rather than through deep class inheritance hierarchies. This approach makes systems more flexible and easier to adapt to changing requirements.

"Composition Over Inheritance" is an important principle in object-oriented design. It emphasizes building complex functionality by composing multiple small, specialized objects, rather than through deep inheritance hierarchies. This design philosophy makes systems more flexible, maintainable, and better able to adapt to change.

Limitations of Inheritance

Before diving deep into the composition pattern, let's first look at the problems that exist in traditional inheritance approaches:

1. Fragile Base Class Problem

javascript
// Fragile base class example
class Animal {
  makeSound() {
    console.log("Animal makes a sound");
  }

  move() {
    console.log("Animal is moving");
    // Someday, a base class developer adds new functionality
    this.checkEnergyLevel();
  }

  // Newly added method
  checkEnergyLevel() {
    console.log("Check energy level");
  }
}

class Bird extends Animal {
  makeSound() {
    console.log("Bird chirps");
  }

  // Override move method
  move() {
    console.log("Bird is flying");
    // Doesn't call checkEnergyLevel, potentially causing base class functionality to fail
  }

  fly() {
    console.log("Bird flies high");
  }
}

class Penguin extends Bird {
  makeSound() {
    console.log("Penguin squawks");
  }

  // Penguins can't fly, override method
  move() {
    console.log("Penguin is swimming");
  }

  fly() {
    console.log("Penguin can't fly");
    // This implementation violates the Liskov Substitution Principle
    throw new Error("Penguins can't fly!");
  }
}

// Problem: Base class changes can break all subclasses
const bird = new Bird();
bird.move(); // "Bird is flying" - doesn't check energy level

const penguin = new Penguin();
try {
  penguin.fly(); // Throws exception - breaks polymorphism
} catch (error) {
  console.log(error.message);
}

2. Over-Engineering Problems

javascript
// Over-engineered inheritance hierarchy
class Vehicle {
  start() {
    console.log("Start vehicle");
  }
}

class MotorVehicle extends Vehicle {
  start() {
    super.start();
    console.log("Start engine");
  }
}

class Car extends MotorVehicle {
  start() {
    super.start();
    console.log("Start car system");
  }
}

class ElectricCar extends Car {
  start() {
    super.start();
    console.log("Start electric system");
  }
}

class SelfDrivingCar extends ElectricCar {
  start() {
    super.start();
    console.log("Start autonomous driving system");
  }
}

// Problem: Every new feature requires a new subclass
const car = new SelfDrivingCar();
car.start();
// Output multiple nested start messages

3. Violation of Open-Closed Principle

javascript
class Shape {
  draw() {
    throw new Error("Subclass must implement draw method");
  }
}

class Circle extends Shape {
  constructor(radius) {
    super();
    this.radius = radius;
  }

  draw() {
    console.log(`Draw circle with radius ${this.radius}`);
  }
}

class Square extends Shape {
  constructor(side) {
    super();
    this.side = side;
  }

  draw() {
    console.log(`Draw square with side ${this.side}`);
  }
}

// Now need to add color functionality - must modify base class or create many subclasses
class ColoredCircle extends Circle {
  constructor(radius, color) {
    super(radius);
    this.color = color;
  }

  draw() {
    console.log(`Draw ${this.color} circle`);
  }
}

class ColoredSquare extends Square {
  constructor(side, color) {
    super(side);
    this.color = color;
  }

  draw() {
    console.log(`Draw ${this.color} square`);
  }
}

// If transparency, borders, and other attributes are also needed, combination explosion!

Composition Pattern Solutions

The composition pattern solves inheritance problems by breaking functionality into independent, reusable components.

1. Basic Composition Example

javascript
// Functional components
class Engine {
  constructor(type, power) {
    this.type = type;
    this.power = power;
  }

  start() {
    console.log(`${this.type} engine started, power ${this.power} hp`);
  }

  stop() {
    console.log(`${this.type} engine stopped`);
  }
}

class GPS {
  constructor(version) {
    this.version = version;
  }

  navigate(destination) {
    console.log(`GPS ${this.version} navigating to ${destination}`);
  }
}

class AudioSystem {
  constructor(brand) {
    this.brand = brand;
    this.isPlaying = false;
  }

  play() {
    this.isPlaying = true;
    console.log(`${this.brand} audio system started playing music`);
  }

  stop() {
    this.isPlaying = false;
    console.log(`${this.brand} audio system stopped playing`);
  }
}

class Battery {
  constructor(capacity) {
    this.capacity = capacity; // kWh
    this.charge = capacity; // Current charge
  }

  charge(amount) {
    this.charge = Math.min(this.capacity, this.charge + amount);
    console.log(`Charged ${amount} kWh, current charge ${this.charge}/${this.capacity}`);
  }

  consume(amount) {
    if (this.charge >= amount) {
      this.charge -= amount;
      console.log(
        `Consumed ${amount} kWh, remaining charge ${this.charge}/${this.capacity}`
      );
      return true;
    }
    console.log(`Insufficient charge! Need ${amount} kWh, only have ${this.charge} kWh`);
    return false;
  }
}

// Create car through composition
class Car {
  constructor(config = {}) {
    this.engine = new Engine(
      config.engineType || "gasoline",
      config.enginePower || 150
    );
    this.gps = config.hasGPS ? new GPS(config.gpsVersion || "2.0") : null;
    this.audio = config.hasAudio
      ? new AudioSystem(config.audioBrand || "Sony")
      : null;
    this.battery = config.hasBattery
      ? new Battery(config.batteryCapacity || 50)
      : null;
  }

  start() {
    console.log("Car startup process begins");

    if (this.battery && !this.battery.consume(1)) {
      console.log("Startup failed: insufficient battery");
      return false;
    }

    this.engine.start();

    if (this.gps) {
      this.gps.navigate("Default destination");
    }

    console.log("Car startup complete");
    return true;
  }

  stop() {
    this.engine.stop();
    this.audio?.stop();
    console.log("Car stopped");
  }

  playMusic() {
    if (this.audio) {
      this.audio.play();
    } else {
      console.log("This car doesn't have an audio system");
    }
  }

  upgradeGPS(newVersion) {
    if (this.gps) {
      this.gps = new GPS(newVersion);
      console.log(`GPS upgraded to version ${newVersion}`);
    } else {
      this.gps = new GPS(newVersion);
      console.log(`GPS version ${newVersion} installed`);
    }
  }
}

// Create different car configurations
const basicCar = new Car({
  engineType: "gasoline",
  enginePower: 120,
});

const luxuryCar = new Car({
  engineType: "V8 gasoline",
  enginePower: 400,
  hasGPS: true,
  gpsVersion: "3.0",
  hasAudio: true,
  audioBrand: "Bose",
});

const electricCar = new Car({
  engineType: "electric",
  enginePower: 300,
  hasGPS: true,
  gpsVersion: "4.0",
  hasAudio: true,
  audioBrand: "B&O",
  hasBattery: true,
  batteryCapacity: 100,
});

// Usage examples
basicCar.start(); // Basic car
luxuryCar.start(); // Luxury car
luxuryCar.playMusic(); // Luxury car plays music
electricCar.start(); // Electric car

2. Behavioral Composition Pattern

javascript
// Behavioral components
class Drawable {
  constructor(shape, style = {}) {
    this.shape = shape;
    this.style = {
      color: "black",
      fillColor: "transparent",
      lineWidth: 1,
      ...style,
    };
  }

  draw(ctx, x, y) {
    ctx.save();
    ctx.strokeStyle = this.style.color;
    ctx.fillStyle = this.style.fillColor;
    ctx.lineWidth = this.style.lineWidth;

    switch (this.shape) {
      case "circle":
        this.drawCircle(ctx, x, y);
        break;
      case "rectangle":
        this.drawRectangle(ctx, x, y);
        break;
      case "triangle":
        this.drawTriangle(ctx, x, y);
        break;
    }

    ctx.restore();
  }

  drawCircle(ctx, x, y) {
    ctx.beginPath();
    ctx.arc(x, y, this.style.radius || 20, 0, Math.PI * 2);
    ctx.fill();
    ctx.stroke();
  }

  drawRectangle(ctx, x, y) {
    const width = this.style.width || 40;
    const height = this.style.height || 30;
    ctx.fillRect(x - width / 2, y - height / 2, width, height);
    ctx.strokeRect(x - width / 2, y - height / 2, width, height);
  }

  drawTriangle(ctx, x, y) {
    const size = this.style.size || 25;
    ctx.beginPath();
    ctx.moveTo(x, y - size);
    ctx.lineTo(x - size, y + size);
    ctx.lineTo(x + size, y + size);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();
  }
}

class Movable {
  constructor(speed = 5) {
    this.speed = speed;
    this.x = 0;
    this.y = 0;
  }

  moveTo(x, y) {
    this.x = x;
    this.y = y;
  }

  moveBy(dx, dy) {
    this.x += dx * this.speed;
    this.y += dy * this.speed;
  }

  getPosition() {
    return { x: this.x, y: this.y };
  }
}

class Interactive {
  constructor() {
    this.onClick = null;
    this.onHover = null;
    this.isEnabled = true;
  }

  click() {
    if (this.isEnabled && this.onClick) {
      this.onClick();
    }
  }

  hover() {
    if (this.isEnabled && this.onHover) {
      this.onHover();
    }
  }

  enable() {
    this.isEnabled = true;
  }

  disable() {
    this.isEnabled = false;
  }
}

class Animated {
  constructor() {
    this.animations = [];
    this.isAnimating = false;
  }

  addAnimation(animation) {
    this.animations.push(animation);
  }

  start() {
    this.isAnimating = true;
    this.animate();
  }

  stop() {
    this.isAnimating = false;
  }

  animate() {
    if (!this.isAnimating) return;

    this.animations.forEach((animation) => {
      if (animation.update()) {
        animation.draw();
      }
    });

    requestAnimationFrame(() => this.animate());
  }
}

// Create game objects through composition
class GameObject {
  constructor(x, y, config = {}) {
    this.drawable = config.drawable || null;
    this.movable = config.movable || null;
    this.interactive = config.interactive || null;
    this.animated = config.animated || null;

    if (this.movable) {
      this.movable.moveTo(x, y);
    }
  }

  update(deltaTime) {
    // Update all components
  }

  render(ctx) {
    if (this.drawable && this.movable) {
      const pos = this.movable.getPosition();
      this.drawable.draw(ctx, pos.x, pos.y);
    }
  }

  handleClick() {
    this.interactive?.click();
  }

  handleHover() {
    this.interactive?.hover();
  }
}

// Usage example
const canvas = document.createElement("canvas");
canvas.width = 800;
canvas.height = 600;
document.body.appendChild(canvas);

const ctx = canvas.getContext("2d");

// Create a clickable circle
const clickableCircle = new GameObject(100, 100, {
  drawable: new Drawable("circle", {
    radius: 30,
    fillColor: "blue",
    color: "darkblue",
  }),
  movable: new Movable(3),
  interactive: new Interactive(),
});

clickableCircle.interactive.onClick = () => {
  console.log("Circle was clicked!");
  clickableCircle.drawable.style.fillColor =
    clickableCircle.drawable.style.fillColor === "blue" ? "red" : "blue";
};

// Create a movable rectangle
const movableRect = new GameObject(200, 200, {
  drawable: new Drawable("rectangle", {
    width: 60,
    height: 40,
    fillColor: "green",
  }),
  movable: new Movable(2),
});

// Animation loop
function gameLoop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  clickableCircle.update();
  movableRect.update();

  clickableCircle.render(ctx);
  movableRect.render(ctx);

  requestAnimationFrame(gameLoop);
}

gameLoop();

// Add interaction
canvas.addEventListener("click", (e) => {
  const rect = canvas.getBoundingClientRect();
  const x = e.clientX - rect.left;
  const y = e.clientY - rect.top;

  const circlePos = clickableCircle.movable.getPosition();
  const distance = Math.sqrt(
    Math.pow(x - circlePos.x, 2) + Math.pow(y - circlePos.y, 2)
  );

  if (distance <= 30) {
    clickableCircle.handleClick();
  }
});

// Keyboard movement control
document.addEventListener("keydown", (e) => {
  switch (e.key) {
    case "ArrowUp":
      movableRect.movable.moveBy(0, -1);
      break;
    case "ArrowDown":
      movableRect.movable.moveBy(0, 1);
      break;
    case "ArrowLeft":
      movableRect.movable.moveBy(-1, 0);
      break;
    case "ArrowRight":
      movableRect.movable.moveBy(1, 0);
      break;
  }
});

Practical Applications of Composition Pattern

1. Web Component System

javascript
// Component functional modules
class EventManager {
  constructor() {
    this.listeners = new Map();
  }

  on(event, handler) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    this.listeners.get(event).push(handler);
  }

  off(event, handler) {
    if (this.listeners.has(event)) {
      const handlers = this.listeners.get(event);
      const index = handlers.indexOf(handler);
      if (index > -1) {
        handlers.splice(index, 1);
      }
    }
  }

  emit(event, ...args) {
    if (this.listeners.has(event)) {
      this.listeners.get(event).forEach((handler) => {
        handler(...args);
      });
    }
  }
}

class Validator {
  constructor() {
    this.rules = [];
    this.errors = [];
  }

  addRule(field, validator, message) {
    this.rules.push({ field, validator, message });
  }

  validate(data) {
    this.errors = [];
    let isValid = true;

    for (const { field, validator, message } of this.rules) {
      if (!validator(data[field])) {
        this.errors.push({ field, message, value: data[field] });
        isValid = false;
      }
    }

    return isValid;
  }

  getErrors() {
    return [...this.errors];
  }
}

class Renderer {
  constructor(template) {
    this.template = template;
  }

  render(data) {
    return this.template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
      return data[key] !== undefined ? data[key] : "";
    });
  }
}

class StateManager {
  constructor(initialState = {}) {
    this.state = { ...initialState };
    this.subscribers = [];
  }

  setState(newState) {
    const oldState = { ...this.state };
    this.state = { ...this.state, ...newState };
    this.notify(oldState, this.state);
  }

  getState() {
    return { ...this.state };
  }

  subscribe(callback) {
    this.subscribers.push(callback);
    return () => {
      const index = this.subscribers.indexOf(callback);
      if (index > -1) {
        this.subscribers.splice(index, 1);
      }
    };
  }

  notify(oldState, newState) {
    this.subscribers.forEach((callback) => callback(newState, oldState));
  }
}

// Compose to create form component
class FormComponent {
  constructor(element, config = {}) {
    this.element = element;
    this.events = new EventManager();
    this.validator = config.hasValidation ? new Validator() : null;
    this.renderer = new Renderer(config.template || "<div>{{content}}</div>");
    this.state = new StateManager(config.initialState || {});

    this.setupValidation();
    this.bindEvents();
  }

  setupValidation() {
    if (!this.validator) return;

    // Add common validation rules
    this.validator.addRule(
      "email",
      (val) => typeof val === "string" && val.includes("@"),
      "Please enter a valid email address"
    );

    this.validator.addRule(
      "required",
      (val) => val !== null && val !== undefined && val !== "",
      "This field is required"
    );
  }

  bindEvents() {
    this.state.subscribe((newState, oldState) => {
      this.render(newState);
      this.events.emit("stateChange", newState, oldState);
    });

    this.element.addEventListener("submit", (e) => {
      e.preventDefault();
      this.handleSubmit();
    });
  }

  handleSubmit() {
    const formData = this.getFormData();

    if (this.validator && !this.validator.validate(formData)) {
      this.events.emit("validationError", this.validator.getErrors());
      return;
    }

    this.events.emit("submit", formData);
  }

  getFormData() {
    const formData = {};
    const inputs = this.element.querySelectorAll("input, select, textarea");

    inputs.forEach((input) => {
      formData[input.name] = input.value;
    });

    return formData;
  }

  render(data) {
    this.element.innerHTML = this.renderer.render(data);
  }

  validate() {
    if (!this.validator) return true;

    const formData = this.getFormData();
    return this.validator.validate(formData);
  }

  setState(newState) {
    this.state.setState(newState);
  }

  getState() {
    return this.state.getState();
  }
}

// Usage example
const loginForm = new FormComponent(document.getElementById("loginForm"), {
  hasValidation: true,
  template: `
    <form>
      <div>
        <label>Email:</label>
        <input type="email" name="email" value="{{email}}">
      </div>
      <div>
        <label>Password:</label>
        <input type="password" name="password" value="{{password}}">
      </div>
      <button type="submit">Login</button>
      <div class="errors">{{errors}}</div>
    </form>
  `,
  initialState: {
    email: "",
    password: "",
    errors: "",
  },
});

// Add custom validation
loginForm.validator.addRule(
  "password",
  (val) => typeof val === "string" && val.length >= 6,
  "Password must be at least 6 characters long"
);

// Listen for events
loginForm.events.on("submit", (formData) => {
  console.log("Form submitted:", formData);
  // Send login request
});

loginForm.events.on("validationError", (errors) => {
  const errorMessages = errors
    .map((err) => `${err.field}: ${err.message}`)
    .join(", ");
  loginForm.setState({ errors: errorMessages });
});

2. Service Layer Composition

javascript
// Service components
class CacheService {
  constructor() {
    this.cache = new Map();
    this.ttl = new Map();
  }

  set(key, value, expireTime = 5 * 60 * 1000) {
    this.cache.set(key, value);
    this.ttl.set(key, Date.now() + expireTime);
  }

  get(key) {
    if (!this.cache.has(key)) return null;

    if (Date.now() > this.ttl.get(key)) {
      this.delete(key);
      return null;
    }

    return this.cache.get(key);
  }

  delete(key) {
    this.cache.delete(key);
    this.ttl.delete(key);
  }

  clear() {
    this.cache.clear();
    this.ttl.clear();
  }
}

class LoggerService {
  constructor(level = "info") {
    this.level = level;
  }

  log(message, level = "info") {
    const timestamp = new Date().toISOString();
    console.log(`[${timestamp}] [${level.toUpperCase()}] ${message}`);
  }

  error(message) {
    this.log(message, "error");
  }

  warn(message) {
    this.log(message, "warn");
  }

  info(message) {
    this.log(message, "info");
  }
}

class HttpService {
  constructor(baseURL = "") {
    this.baseURL = baseURL;
  }

  async get(url, options = {}) {
    try {
      const response = await fetch(`${this.baseURL}${url}`, {
        method: "GET",
        ...options,
      });

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      return await response.json();
    } catch (error) {
      throw new Error(`Request failed: ${error.message}`);
    }
  }

  async post(url, data, options = {}) {
    try {
      const response = await fetch(`${this.baseURL}${url}`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          ...options.headers,
        },
        body: JSON.stringify(data),
        ...options,
      });

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      return await response.json();
    } catch (error) {
      throw new Error(`Request failed: ${error.message}`);
    }
  }
}

class AuthService {
  constructor() {
    this.http = new HttpService("https://api.example.com");
    this.cache = new CacheService();
    this.logger = new LoggerService("auth");
  }

  async login(email, password) {
    this.logger.info(`User login attempt: ${email}`);

    try {
      const response = await this.http.post("/auth/login", {
        email,
        password,
      });

      // Cache user info
      this.cache.set("user", response.user, 30 * 60 * 1000); // 30 minutes
      this.cache.set("token", response.token, 30 * 60 * 1000);

      this.logger.info(`User login successful: ${email}`);
      return response;
    } catch (error) {
      this.logger.error(`Login failed: ${error.message}`);
      throw error;
    }
  }

  async logout() {
    try {
      const token = this.cache.get("token");
      if (token) {
        await this.http.post("/auth/logout", { token });
      }

      this.cache.delete("user");
      this.cache.delete("token");

      this.logger.info("User logout successful");
    } catch (error) {
      this.logger.error(`Logout failed: ${error.message}`);
    }
  }

  getCurrentUser() {
    return this.cache.get("user");
  }

  isAuthenticated() {
    return this.cache.get("token") !== null;
  }
}

class UserService {
  constructor() {
    this.http = new HttpService("https://api.example.com");
    this.cache = new CacheService();
    this.logger = new LoggerService("user");
    this.authService = new AuthService();
  }

  async getUserProfile(userId) {
    const cacheKey = `profile:${userId}`;

    // Check cache
    let profile = this.cache.get(cacheKey);
    if (profile) {
      this.logger.info(`Get user profile from cache: ${userId}`);
      return profile;
    }

    try {
      profile = await this.http.get(`/users/${userId}`);

      // Cache result
      this.cache.set(cacheKey, profile, 10 * 60 * 1000); // 10 minutes

      this.logger.info(`Get user profile successful: ${userId}`);
      return profile;
    } catch (error) {
      this.logger.error(`Get user profile failed: ${error.message}`);
      throw error;
    }
  }

  async updateProfile(userId, updates) {
    this.logger.info(`Update user profile: ${userId}`);

    try {
      const updatedProfile = await this.http.put(`/users/${userId}`, updates);

      // Update cache
      const cacheKey = `profile:${userId}`;
      this.cache.set(cacheKey, updatedProfile, 10 * 60 * 1000);

      this.logger.info(`User profile updated successfully: ${userId}`);
      return updatedProfile;
    } catch (error) {
      this.logger.error(`Update user profile failed: ${error.message}`);
      throw error;
    }
  }
}

// Usage example
const authService = new AuthService();
const userService = new UserService();

// User login
authService
  .login("[email protected]", "password123")
  .then(() => {
    console.log("Login successful");

    // Get user profile
    const user = authService.getCurrentUser();
    return userService.getUserProfile(user.id);
  })
  .then((profile) => {
    console.log("User profile:", profile);
  })
  .catch((error) => {
    console.error("Operation failed:", error);
  });

Advantages of Composition Pattern

1. Flexibility

javascript
// Dynamic composition of functionality
class TaskManager {
  constructor() {
    this.features = {
      logging: null,
      priority: null,
      deadline: null,
      notification: null,
      persistence: null,
    };
  }

  addFeature(name, feature) {
    if (this.features.hasOwnProperty(name)) {
      this.features[name] = feature;
      console.log(`Feature ${name} added`);
    }
  }

  removeFeature(name) {
    if (this.features[name]) {
      this.features[name] = null;
      console.log(`Feature ${name} removed`);
    }
  }

  executeTask(task) {
    console.log(`Execute task: ${task.name}`);

    // Execute features in sequence
    this.features.logging?.log(task);
    this.features.priority?.process(task);
    this.features.deadline?.check(task);

    const result = this.performTask(task);

    this.features.notification?.send(result);
    this.features.persistence?.save(result);

    return result;
  }

  performTask(task) {
    console.log(`Task ${task.name} execution complete`);
    return { task, success: true, timestamp: Date.now() };
  }
}

// Feature components
class LoggingFeature {
  log(task) {
    console.log(`[LOG] Start executing task: ${task.name}`);
  }
}

class PriorityFeature {
  process(task) {
    if (task.priority === "high") {
      console.log(`[PRIORITY] High priority task, execute immediately`);
    }
  }
}

class DeadlineFeature {
  check(task) {
    if (task.deadline && Date.now() > task.deadline) {
      console.log(`[DEADLINE] Warning: Task expired`);
    }
  }
}

class NotificationFeature {
  send(result) {
    console.log(`[NOTIFY] Task completion notification: ${result.task.name}`);
  }
}

class PersistenceFeature {
  save(result) {
    console.log(`[PERSIST] Save task result to database`);
  }
}

// Dynamically configure task manager
const taskManager = new TaskManager();

// Add features based on requirements
taskManager.addFeature("logging", new LoggingFeature());
taskManager.addFeature("priority", new PriorityFeature());
taskManager.addFeature("deadline", new DeadlineFeature());

// Add or remove features at runtime
taskManager.addFeature("notification", new NotificationFeature());

const task1 = {
  name: "Send email",
  priority: "high",
  deadline: Date.now() + 3600000, // Expires in 1 hour
};

taskManager.executeTask(task1);

// Remove notification feature
taskManager.removeFeature("notification");

const task2 = {
  name: "Generate report",
  priority: "normal",
};

taskManager.executeTask(task2);

2. Testing Friendliness

javascript
// Testable composition design
class EmailService {
  constructor(emailProvider, logger, validator) {
    this.emailProvider = emailProvider;
    this.logger = logger;
    this.validator = validator;
  }

  async sendEmail(to, subject, body) {
    this.logger.info(`Prepare to send email to: ${to}`);

    if (!this.validator.isValidEmail(to)) {
      throw new Error("Invalid email address");
    }

    try {
      const result = await this.emailProvider.send(to, subject, body);
      this.logger.info(`Email sent successfully: ${result.messageId}`);
      return result;
    } catch (error) {
      this.logger.error(`Email send failed: ${error.message}`);
      throw error;
    }
  }
}

// Mock components for testing
class MockEmailProvider {
  async send(to, subject, body) {
    console.log(`[MOCK] Send email: ${to} - ${subject}`);
    return { messageId: "mock-id-123", status: "sent" };
  }
}

class MockLogger {
  info(message) {
    console.log(`[MOCK INFO] ${message}`);
  }

  error(message) {
    console.log(`[MOCK ERROR] ${message}`);
  }
}

class MockValidator {
  isValidEmail(email) {
    return email.includes("@");
  }
}

// Test
async function testEmailService() {
  const mockProvider = new MockEmailProvider();
  const mockLogger = new MockLogger();
  const mockValidator = new MockValidator();

  const emailService = new EmailService(
    mockProvider,
    mockLogger,
    mockValidator
  );

  try {
    await emailService.sendEmail(
      "[email protected]",
      "Test Email",
      "This is a test email"
    );
    console.log("Test passed");
  } catch (error) {
    console.error("Test failed:", error.message);
  }
}

testEmailService();

Best Practices of Composition Pattern

1. Interface Design

javascript
// Define clear interfaces
class IRenderable {
  render(context) {
    throw new Error("Subclass must implement render method");
  }
}

class IUpdatable {
  update(deltaTime) {
    throw new Error("Subclass must implement update method");
  }
}

class IDestroyable {
  destroy() {
    throw new Error("Subclass must implement destroy method");
  }
}

// Components implementing interfaces
class SpriteRenderer extends IRenderable {
  constructor(image, x, y) {
    super();
    this.image = image;
    this.x = x;
    this.y = y;
  }

  render(context) {
    context.drawImage(this.image, this.x, this.y);
  }
}

class PhysicsComponent extends IUpdatable {
  constructor(gravity = 9.8) {
    super();
    this.velocity = { x: 0, y: 0 };
    this.gravity = gravity;
  }

  update(deltaTime) {
    this.velocity.y += this.gravity * deltaTime;
    return this.velocity;
  }
}

class LifecycleManager extends IDestroyable {
  constructor() {
    super();
    this.resources = [];
  }

  addResource(resource) {
    this.resources.push(resource);
  }

  destroy() {
    this.resources.forEach((resource) => {
      if (resource.destroy) {
        resource.destroy();
      }
    });
    this.resources = [];
  }
}

2. Dependency Injection

javascript
// Dependency injection container
class DIContainer {
  constructor() {
    this.services = new Map();
    this.singletons = new Map();
  }

  register(name, factory, options = {}) {
    this.services.set(name, { factory, options });
  }

  resolve(name) {
    const service = this.services.get(name);
    if (!service) {
      throw new Error(`Service ${name} not registered`);
    }

    if (service.options.singleton) {
      if (!this.singletons.has(name)) {
        this.singletons.set(name, service.factory(this));
      }
      return this.singletons.get(name);
    }

    return service.factory(this);
  }
}

// Use dependency injection
const container = new DIContainer();

container.register("logger", () => new LoggerService(), { singleton: true });
container.register("cache", () => new CacheService(), { singleton: true });
container.register("http", () => new HttpService(), { singleton: true });

container.register("authService", (container) => {
  return new AuthService(
    container.resolve("http"),
    container.resolve("cache"),
    container.resolve("logger")
  );
});

// Use services
const authService = container.resolve("authService");

Summary

"Composition Over Inheritance" is one of the core principles of modern software design. Through the composition pattern, you can:

  1. Build flexible systems: Create diverse objects by combining different functional modules
  2. Improve code reusability: Create independent functional components that can be reused in multiple places
  3. Enhance maintainability: Modifying one component won't affect other components
  4. Improve testability: Each component can be tested independently
  5. Support runtime changes: Dynamically add, remove, or replace functionality at runtime

The core idea of the composition pattern is to break complex systems down into small, specialized, composable components. This design philosophy applies not only to object-oriented programming but also to functional programming and other programming paradigms.

Remember, good design isn't about how deep inheritance hierarchies are, but about how components collaborate elegantly. Through composition, you can build more flexible, maintainable, and extensible software systems.