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
// 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
// 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 messages3. Violation of Open-Closed Principle
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
// 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 car2. Behavioral Composition Pattern
// 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
// 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
// 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
// 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
// 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
// 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
// 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:
- Build flexible systems: Create diverse objects by combining different functional modules
- Improve code reusability: Create independent functional components that can be reused in multiple places
- Enhance maintainability: Modifying one component won't affect other components
- Improve testability: Each component can be tested independently
- 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.