Skip to content

Instantly share code, notes, and snippets.

@mattkenefick
Created July 15, 2023 19:45
Show Gist options
  • Save mattkenefick/97bf4df64864b63d015850e6bde948bb to your computer and use it in GitHub Desktop.
Save mattkenefick/97bf4df64864b63d015850e6bde948bb to your computer and use it in GitHub Desktop.

Composition and inheritance are both fundamental concepts in object-oriented programming (OOP) that help in organizing and structuring code. While they achieve similar goals, they differ in their approach and usage. Here's a comparison between composition and inheritance:

Composition:

  1. Composition is a design principle that allows objects to be composed of other objects or components.
  2. It emphasizes building complex functionality by combining simpler, reusable components.
  3. It follows the "has-a" relationship, where an object contains or is composed of other objects.
  4. Components can be dynamically composed or changed at runtime, providing flexibility and modularity.
  5. Composition promotes code reusability, as components can be shared across different objects.
  6. It encourages loose coupling between objects, making the system more maintainable and adaptable.
  7. Composition can lead to a more modular and flexible code structure, as components can be easily replaced or modified without affecting the entire system.

Inheritance:

  1. Inheritance is a mechanism in which a class inherits properties and behaviors from another class, called the superclass or base class.
  2. It follows the "is-a" relationship, where a derived class is a specialized version of the base class.
  3. Inheritance establishes a hierarchical relationship between classes, forming an "inheritance tree" or "class hierarchy."
  4. Derived classes can inherit and extend the functionality of the base class, adding new features or modifying existing ones.
  5. Inheritance promotes code reuse by inheriting common functionality from the base class.
  6. It allows polymorphism, where objects of derived classes can be treated as objects of the base class, enabling flexibility and extensibility.
  7. Changes made in the base class can propagate to derived classes, which can be advantageous or problematic depending on the situation.

Choosing between Composition and Inheritance:

  1. Use composition when you want to build objects by combining simpler components, promote code reusability, and achieve greater flexibility and modularity.
  2. Use inheritance when you want to establish a hierarchical relationship between classes, reuse common functionality, and leverage polymorphism.

In practice, the choice between composition and inheritance depends on the specific requirements and design goals of the system. Sometimes, a combination of both approaches is used, where composition is used to build complex objects and inheritance is used to create specialized versions of those objects.


Here's an example in TypeScript that demonstrates the use of both composition and inheritance:

// Composition: Creating a simple Logger component
class Logger {
	log(message: string): void {
		console.log(`[Logger]: ${message}`);
	}
}

// Inheritance: Creating a class that extends Logger
class FileLogger extends Logger {
	logToFile(message: string): void {
		console.log(`[FileLogger]: Writing to file: ${message}`);
		// Additional file logging implementation...
	}
}

// Composition: Creating a class that uses Logger component
class UserService {
	private logger: Logger;

	constructor(logger: Logger) {
		this.logger = logger;
	}

	createUser(name: string): void {
		// Business logic to create a user...
		this.logger.log(`User created: ${name}`);
	}
}

// Usage example
const logger = new Logger();
const fileLogger = new FileLogger();

const userService = new UserService(logger);
userService.createUser('John Doe');

const fileUserService = new UserService(fileLogger);
fileUserService.createUser('Jane Smith');
fileUserService.createUser('Alice Johnson');

In this example, we have a Logger class that represents a simple logging component. It has a log method that logs a message to the console. This demonstrates composition.

Next, we create a FileLogger class that extends the Logger class. It inherits the log method from the base class and adds a new method logToFile that logs a message to a file. This showcases inheritance.

Then, we define a UserService class that uses the Logger component through composition. It takes an instance of Logger as a constructor parameter and uses it to log messages during the createUser method.

Finally, we create instances of Logger and FileLogger and pass them to UserService to demonstrate the usage of both composition and inheritance together. The userService logs messages to the console, while the fileUserService logs messages both to the console and a file using the inherited method from FileLogger.

Note that this is a simplified example to illustrate the concept. In real-world scenarios, composition and inheritance can be used in more complex ways to achieve specific design goals.


The example provided demonstrates a form of dependency injection and relates to the concept of Inversion of Control (IoC).

Dependency injection is a design pattern that allows the dependencies of a class to be provided from external sources rather than being created internally. In the example, the UserService class relies on the Logger component for logging. Instead of creating an instance of Logger internally within the UserService class, the dependency is injected through the constructor.

By accepting a Logger instance as a constructor parameter, the UserService class becomes independent of the specific implementation of the logger. It can accept any object that conforms to the Logger interface or extends the Logger class. This promotes loose coupling and makes it easier to substitute different logger implementations or mock the logger for testing purposes.

This practice of injecting dependencies from the outside is a key aspect of dependency injection. It helps separate the concerns of creating objects and defining their dependencies, making the code more flexible, maintainable, and testable.

In terms of Inversion of Control (IoC), it is a broader principle that encompasses the concept of dependency injection. IoC refers to the inversion of control flow in a software system. Instead of a class being responsible for creating or managing its dependencies, control is inverted to an external entity, often referred to as an IoC container or framework.

In the given example, the responsibility of creating the Logger instance is externalized from the UserService class. The client code or a higher-level component is responsible for creating the Logger instance and injecting it into the UserService class. This adheres to the IoC principle, as the control of creating and managing dependencies is inverted to the external code.

IoC containers or frameworks help automate the process of dependency injection by managing the creation and injection of dependencies. They provide a way to configure and wire up the dependencies at runtime. These containers effectively control the lifecycle of objects and manage the injection of dependencies based on the configuration or annotations provided.

By adopting dependency injection and following the IoC principle, the code becomes more decoupled, modular, and easier to maintain and test. It promotes code reusability, flexibility, and scalability by allowing components to be easily replaced or extended with different implementations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment