Refactor using interfaces
In the previous lesson, you learned about the challenges of tightly coupled code and how it violates the Open/Closed Principle. Now, let’s refactor the Library
and BorrowableBook
example to make the system more flexible and easier to maintain.
Here’s the original code that demonstrates the problem:
public class BorrowableBook
{
public string Title { get; set; }
public bool IsAvailable { get; private set; } = true;
public BorrowableBook(string title)
{
Title = title;
}
public void Borrow()
{
if (IsAvailable)
{
IsAvailable = false;
Console.WriteLine($"You have borrowed \"{Title}\".");
}
else
{
Console.WriteLine($"\"{Title}\" is already borrowed.");
}
}
}
public class Library
{
private BorrowableBook _book;
public Library(BorrowableBook book)
{
_book = book;
}
public void BorrowBook()
{
if (_book.IsAvailable)
{
_book.Borrow();
}
else
{
Console.WriteLine("The book is not available.");
}
}
}
This design tightly couples the Library
class to the BorrowableBook
class, making it difficult to add new types of borrowable items (for example, DVDs) without modifying the Library
class. To fix the tight coupling, we introduce an interface to decouple the borrowing behavior.
Introducing interfaces
An interface sets a contract for behavior without detailing its implementation. Introducing an interface abstracts the borrowing functionality and increases the system's flexibility.
Here’s the IBorrowable
interface:
public interface IBorrowable
{
bool IsAvailable { get; }
void Borrow();
}
This interface introduces two members:
IsAvailable
: A property indicating if the item is available for borrowing.Borrow
: A method to borrow the item.
By defining this interface, we decouple the borrowing behavior from any specific implementation.
Refactoring the BorrowableBook class
Next, we update the BorrowableBook
class to implement the IBorrowable
interface:
public class BorrowableBook : IBorrowable
{
public string Title { get; set; }
public bool IsAvailable { get; private set; } = true;
public BorrowableBook(string title)
{
Title = title;
}
public void Borrow()
{
if (IsAvailable)
{
IsAvailable = false;
Console.WriteLine($"You have borrowed \"{Title}\".");
}
else
{
Console.WriteLine($"\"{Title}\" is already borrowed.");
}
}
}
Now, the BorrowableBook
class adheres to the IBorrowable
interface, making it interchangeable with other classes that implement the same interface.
Refactoring the Library class
We also update the Library
class to depend on the IBorrowable
interface instead of the concrete BorrowableBook
class:
public class Library
{
private IBorrowable _item;
public Library(IBorrowable item)
{
_item = item;
}
public void BorrowItem()
{
if (_item.IsAvailable)
{
_item.Borrow();
}
else
{
Console.WriteLine("The item is not available.");
}
}
}
Now, the Library
class can work with any object that implements the IBorrowable
interface, making it more flexible and easier to extend.
Using Dependency Injection
Imagine you’re setting up a home entertainment system. Instead of permanently attaching a specific brand of speakers to your stereo, you use speaker "jacks" that can accept multiple types of compatible speaker plugs. This design allows you to easily replace or upgrade the speakers without having to change the entire stereo system.
In software, Dependency Injection works similarly. It allows a class to depend on an abstract interface rather than a specific implementation. Dependency Injection makes the system more flexible and easier to maintain because you can "plug in" different implementations without modifying the class itself.
The constructor in programming is like the technician connecting the speaker jacks during the setup of your stereo system. For the Library
class, the constructor (public Library(IBorrowable item)
) is where the dependency is provided. The constructor allows the Library
class to work with any compatible implementation, such as BorrowableBook
or BorrowableDVD
, without needing to change its internal structure. Just as the speaker jack enables flexibility in choosing different speakers, the constructor facilitates flexibility in using various borrowable items.
Here’s how it works in code:
public class Library
{
private IBorrowable _item;
public Library(IBorrowable item) // Dependency is injected here
{
_item = item;
}
public void BorrowItem()
{
if (_item.IsAvailable)
{
_item.Borrow();
}
else
{
Console.WriteLine("The item is not available.");
}
}
}
In this example:
- The constructor is where the "connection" (the
IBorrowable
dependency) is made, similar to how the speaker plug connects to the "jack." - The
IBorrowable
interface acts like the "stereo jack," defining the standard connection point. - The
BorrowableBook
andBorrowableDVD
are like different types of stereo speakers with unique connectors that get connected to the "jack."
By using Dependency Injection, the Library
class can work with any implementation of IBorrowable
. This approach provides:
- Flexibility: You can easily switch or add new implementations without modifying the
Library
class. - Testability: You can "plug in" mock implementations for testing purposes.
- Maintainability: The
Library
class doesn’t need to know the details of the specific implementation, making it easier to extend and maintain.
This design ensures that the Library
class is no longer tightly coupled to specific implementations, making the system more modular and adaptable.
Adding new borrowable items
With the interface in place, we can easily add new types of borrowable items without modifying the Library
class. For example, here’s a BorrowableDVD
class:
public class BorrowableDVD : IBorrowable
{
public string Title { get; set; }
public bool IsAvailable { get; private set; } = true;
public BorrowableDVD(string title)
{
Title = title;
}
public void Borrow()
{
if (IsAvailable)
{
IsAvailable = false;
Console.WriteLine($"You have borrowed the DVD \"{Title}\".");
}
else
{
Console.WriteLine($"The DVD \"{Title}\" is already borrowed.");
}
}
}
The BorrowableDVD
class implements the same IBorrowable
interface, so it can be used seamlessly with the Library
class.
Testing the system
Here’s a program to demonstrate the refactored system:
using System;
class Program
{
static void Main()
{
// Create borrowable items
IBorrowable book = new BorrowableBook("Adventure Works Cycles");
IBorrowable dvd = new BorrowableDVD("Graphic Design Institute");
// Create libraries
Library bookLibrary = new Library(book);
Library dvdLibrary = new Library(dvd);
// Borrow items
bookLibrary.BorrowItem();
bookLibrary.BorrowItem(); // Try borrowing again
Console.WriteLine();
dvdLibrary.BorrowItem();
dvdLibrary.BorrowItem(); // Try borrowing again
}
}
The output demonstrates the flexibility of the refactored system:
You have borrowed "Adventure Works Cycles".
"Adventure Works Cycles" is already borrowed.
You have borrowed the DVD "Graphic Design Institute".
The DVD "Graphic Design Institute" is already borrowed.
This refactored example demonstrates how interfaces reduce dependencies and improve modularity:
- Separation of Concerns: The
IBorrowable
interface isolates borrowing behavior, ensuring theLibrary
class doesn't depend on specific implementations. - Improved Flexibility: You can add new types of borrowable items (for example, DVDs) without modifying the
Library
class. - Simplified Maintenance: The system is easier to understand, test, and extend because responsibilities are clearly divided.