In software engineering, composition plays an important role in object-oriented programming. If you’ve come across other articles or videos I’ve put out about object-oriented programming, you’ll know that I try to push composition over inheritance as much as I can. This is strictly because I have seen overuse of inheritance throughout my entire career but I’ve never seen overuse of composition! But in this article, I’ll be walking you through examples of composition in C# so you can see what it means to see objects modeled with an “is made up of” relationship.
What is Composition in Object-Oriented Programming?
In object-oriented programming, composition refers to the concept of building complex functionality by combining smaller objects together. When we talk about composition, we mean that objects can be composed of other objects, known as “components” or “parts”, to achieve a desired behavior or functionality. I like saying that composition represents an “is made up of” relationship, whereas when we talk about inheritance we mean that things have an “is a” relationship.
Let’s see if we can get this idea to be a little bit more clear with an example. We’ll stick to something that isn’t C# specific or even code-related so that you can rationalize the concept of an “is made up of” relationship.
Let’s consider a music band as a combination of musicians:
a guitarist
a drummer
a singer
Each member plays their own instrument or role and there is no relationship between each of these members aside from being in the same band. When we describe the band itself, we would say the band “has-a” guitarist, “has-a” drummer, and “has-a” singer. We could also then say that the band “is made up of” these musicians!
This illustrates composition, where we assemble different elements (musicians) each with unique capabilities, to create a complete unit (the band) that performs music together. Each member contributes their part to the overall performance, showcasing how individual components are combined to achieve a larger, coordinated function. I might even slip in another example here where we say the complexity of the musicians working together is “encapsulated” when are on the outside of the band, listening to the music.
How Composition Works in CSharp
So, how does composition work in C#, and how can you leverage this technique in your software development? Hint: it doesn’t start with inheriting from a base class!
To create composition relationships between objects in C#, we can use classes and interfaces. A class represents a blueprint for creating objects, while an interface defines a contract that a class must follow. By using composition, we can bring together various classes and interfaces to form a cohesive and flexible system.
It’s also important to note that we can compose objects of other objects and NOT use interfaces at all, but I thought excluding the usage of interfaces entirely might be misleading. So you can use interfaces as the things you will compose other objects of, but you do not need to for it to be a composition relationship.
Examples of Composition in CSharp
In object-oriented programming, composition is a powerful concept that allows us to model relationships between real-world entities. By leveraging composition, we can create complex systems by combining smaller, more manageable components. Let’s explore some examples of composition in different scenarios using C# code snippets.
Music Player Composition Example in CSharp
Let’s see composition in action! Consider this simple example of a music player application. We might have a MusicPlayer
class that represents the core functionality of the application, such as playing audio files. However, the music player also needs additional capabilities, such as a user interface and audio codecs. That is, we can’t have something that plays music without it being made of these two dependencies! Here’s some example code:
public class MusicPlayer
{
private readonly IUserInterface _userInterface;
private readonly IAudioCodec _audioCodec;
public MusicPlayer(
IUserInterface userInterface,
IAudioCodec audioCodec)
{
_userInterface = userInterface;
_audioCodec = audioCodec;
}
// Do stuff using the user interface
// and the audio codec
// Rest of the implementation...
}
In the example above, the MusicPlayer
class depends on an IUserInterface
and an IAudioCodec
interface. Instead of implementing these dependencies directly in the MusicPlayer
class, we use composition to inject them as constructor parameters. This allows us to easily swap out different implementations of the user interface and audio codecs, providing flexibility and extensibility to our music player application.
Now, let’s see how we can compose the music player with concrete implementations of the interfaces:
public class ConsoleUserInterface : IUserInterface
{
// Implementation of IUserInterface...
}
public class Mp3AudioCodec : IAudioCodec
{
// Implementation of IAudioCodec...
}
// Usage
var userInterface = new ConsoleUserInterface();
var audioCodec = new Mp3AudioCodec();
var musicPlayer = new MusicPlayer(
userInterface,
audioCodec);
In this example, we have two classes, ConsoleUserInterface
and Mp3AudioCodec
, that implement the IUserInterface
and IAudioCodec
interfaces, respectively. We then create instances of these concrete classes and pass them into the MusicPlayer
constructor to compose the music player object.
By using composition, we have decoupled the specific implementations from the MusicPlayer
class, making it more flexible and maintainable. Furthermore, if we want to add new user interfaces or audio codecs in the future, we can simply create new classes that implement the respective interfaces and inject them into the MusicPlayer
class, without needing to modify its code.
Example of Composition Between a Car and its Engine
I often fall back to this example when explaining composition — and in fact, you’ll find something like this in my Dometrain courses. In this composition scenario, a car contains an engine, but the car does not inherit from the engine class. Instead, the car class has a reference to an engine object, which it uses to perform actions such as starting the engine or accelerating. If you start to consider how many different parts go into the car — or even how many parts go into an engine — you can start to see how we can compose objects of other objects.
Let’s have a quick look at some simple code that shows how this is modeled:
public class Engine
{
// Engine implementation
}
public class Car
{
private readonly Engine _engine;
public Car(Engine engine)
{
_engine = engine;
}
// Car methods and properties
}
By using composition, we can easily swap out different types of engines without modifying the car class. This flexibility allows us to handle various engine configurations, such as gasoline or electric, without changing the car’s implementation.
Composition Example of a House and its Rooms
A house is composed of multiple rooms, each serving a different purpose. Composition can be used to represent this relationship in an object-oriented manner.
public interfac IRoom
{
// Room properties and methods
}
public class House
{
private readonly IReadOnlyList<IRoom> _rooms;
public House(List<Room> rooms)
{
_rooms = rooms;
}
// House methods and properties
}
By using composition, we can easily create a house of different rooms. The way that this particular House
type is set up, we can pass in a collection of IRoom
instances and we’ll track them in a field. But because we’re using an IRoom
interface, this could allow us to create a house with multiple rooms where we don’t care about the implementation of the room! So if you wanted your dream house to be 50 kitchens, you could absolutely create a Kitchen
type that inherits from IRoom and pass in 50 instances of them when you create your house!
Multiple Levels of Composition in CSharp
To conclude the code examples, let’s go one level deeper. We’ve seen several examples of having one object being composed of one or more other objects, but what if we wanted to illustrate the extension of this?
In this code example, we’ll look at a library:
// public getters and setters just for
// ease-of-use when showing this example
public class Library
{
public string Name { get; set; }
public List<BookShelf> BookShelves { get; set; } = new List<BookShelf>();
}
public class BookShelf
{
public int Number { get; set; }
public List<Book> Books { get; set; } = new List<Book>();
}
public class Book
{
public string Title { get; set; }
public string Author { get; set; }
public int YearPublished { get; set; }
}
// Example Usage
var myLibrary = new Library
{
Name = "Central Library",
BookShelves = new List<BookShelf>
{
new BookShelf
{
Number = 1,
Books = new List<Book>
{
new Book
{
Title = "C# in Depth",
Author = "Jon Skeet",
YearPublished = 2019
},
new Book
{
Title = "The Pragmatic Programmer",
Author = "Andy Hunt & Dave Thomas",
YearPublished = 1999
}
}
}
}
};
In this example, we can see that libraries are made of bookshelves. But we can also see that bookshelves are composed of books that need to go onto the shelves! This shows multiple levels of composition taking place.
Benefits and Considerations of Composition
By breaking down complex systems into smaller, reusable components, composition allows for easier code management and promotes code reuse. Now that we’ve seen some code examples showing how this is done, let’s walk through the advantages and considerations for composition.
Advantages of Using Composition
Code Reuse: Composition allows us to create smaller, reusable components that can be used in different parts of our codebase. By composing objects together, we can build complex systems by leveraging the functionality and behavior already implemented in those smaller components.
Flexibility: One of the key benefits of composition is the flexibility it provides. We can easily modify the behavior of a system by adding or removing composed objects without impacting the rest of the codebase. This flexibility makes it easier to adapt and extend our software to meet changing requirements.
Maintainability: Composition promotes modular and decoupled code, making it easier to maintain and update. By breaking down a system into smaller components, each responsible for a specific task, we can isolate changes and minimize the impact on other parts of the codebase. This makes our codebase more maintainable and reduces the risk of introducing bugs during updates or modifications.
Testability: When functionality is buried deep in base classes and abstract classes because overuse of inheritance, testing becomes more difficult. Often we need to either implement what is called “fakes” for our tests or we need to get very creative with finding ways to exercise code closer to the base class. With composition often that logic doesn’t need to become as buried and therefore it’s easier to test.
Considerations when Using Composition
While composition offers numerous benefits, there are some considerations to keep in mind when using it in software design:
Managing the Lifecycle of Composed Objects: Properly managing the lifecycle of composed objects is important to avoid memory leaks or resource allocation issues. Ensuring that objects are appropriately instantiated and disposed of is important when working with composed systems. In more complex systems, it’s important to understand who owns which dependency — especially if it’s shared.
Interface Usage: I mentioned earlier that composition does not require interfaces, but a side effect can be that people can more easily lean into overusing interfaces. Everything gets an interface! While I do feel that can often help with unit testing — to a certain extent — it can start to become unwieldy when you’re adding them for no purpose just because you see the pattern. I am guilty of this.
“Newing” it all up: When you have to keep passing dependencies in via the constructor to compose objects, where do these all get made? Well, we often find ourselves adding instantiation code the the entry point and this grows out of control. However, you can use dependency injection — things like Autofac or the built-in IServiceCollection — to make this more streamlined.
Best Practices for Using Composition in CSharp
When it comes to using composition in C#, there are several best practices and guidelines that can help you design and organize your code in a clear and efficient manner. Here are some tips to help you make the most out of composition in C#:
Identify the components: Before you start implementing composition in your code, it’s important to identify the different components or objects that will interact with each other. This will help you determine how to structure your code and which objects should be composed together.
Encapsulate functionality: One of the key benefits of composition is that it allows you to encapsulate functionality within separate objects. Each component should have a well-defined and focused responsibility, making it easier to understand, test, and maintain your code. You heard me sneak in “encapsulation” earlier in the article, and here it is again.
Avoid tight coupling: Composition promotes loose coupling between objects, which helps increase the flexibility and maintainability of your code. Avoid directly instantiating objects within other objects, as this can create tight dependencies. Pass dependencies in via the constructor and consider using interfaces as required.
Favor composition over inheritance: Inheritance can lead to rigid and tightly coupled code. Instead, prefer composition as it allows for more flexible and modular designs. Seriously. It’s more often than not what you’d prefer.
Keep classes small and focused: As you compose objects together, strive to keep your classes small and focused on a single responsibility — enter the Single Responsibility Principle (SRP). This helps make your code more readable, testable, and maintainable. If a class becomes too large or complex, consider breaking it down into smaller, more manageable components. Also, avoid having so many tiny thin classes that you’ve gone to the other extreme.
Use dependency injection: Dependency injection is a powerful technique that complements composition. It allows you to inject dependencies into an object rather than creating them internally. This helps decouple components and makes it easier to swap out dependencies or mock them for testing. We have helpful frameworks to leverage for this!
That’s quite the list — but it’s relatively easy to keep the core idea in mind: You are making objects up from other objects and not inheriting them.
Wrapping Up Examples of Composition in CSharp
To recap, I walked through a handful of simple examples of composition in C# — each trying to use real-world objects to make the examples more understandable. I listed out the benefits, considerations, and even the best practices to follow when it comes to leveraging composition. Remember to use composition to model “is made up of” relationships!
If you found this useful and you’re looking for more learning opportunities, consider subscribing to my free weekly software engineering newsletter and check out my free videos on YouTube! Meet other like-minded software engineers and join my Discord community!
Want More Dev Leader Content?
- Follow along on this platform if you haven’t already!
- Subscribe to my free weekly software engineering and dotnet-focused newsletter. I include exclusive articles and early access to videos: SUBSCRIBE FOR FREE
- Looking for courses? Check out my offerings: VIEW COURSES
- E-Books & other resources: VIEW RESOURCES
- Watch hundreds of full-length videos on my YouTube channel: VISIT CHANNEL
- Visit my website for hundreds of articles on various software engineering topics (including code snippets): VISIT WEBSITE
- Check out the repository with many code examples from my articles and videos on GitHub: VIEW REPOSITORY
Top comments (0)