In my previous article I talked about the library system utilizing the Unit of Work & Repository pattern to persist the data and we can see that through the HTTP Post to the API which added a book to the database.
In this article we want to extend the capability when adding a book. Instead of a generic book we want to be able to add different type of books such as paperback book and audio book.
Despite under the category Book, audio book and paperback book is different in term of their properties. In our example we will differentiate that paperback book has the total number of pages but audio book has the duration instead.
In an antipattern sense, the service or the control has to has knowledge about what type of book we want to create and set this property accordingly in a if else manner which can get complicated fast if we have many other type of books. This problem is a perfect candidate to use Factory Method pattern in order to make it more elegant and easy to maintain.
The Factory Method pattern is a creational design pattern that provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. It offers several benefits when creating different types of books, such as AudioBook
or PaperbackBook
, especially in a system where books might share common interfaces but have different properties or behaviors.
First we implement the IBookFactory. This interface expose the method CreateBook so that any caller will not have to worry about the details
public interface IBookFactory
{
Book AddBook(string title, string author, BookType type);
}
In the concrete factory class, we can see that adding an audio book is not the same as a paperback book.
public class AudioBookFactory : IBookFactory
{
public Book AddBook(string title, string author, BookType type)
{
return new Book
{
Title = title,
Author = author,
Type = type,
//For simplicity we use random, but technically this can be an API call
//to retrieve the total duration for this book.
DurationInMinutes = Random.Shared.Next(0, 1000),
};
}
}
public class PaperBackBookFactory : IBookFactory
{
public Book AddBook(string title, string author, BookType type)
{
return new Book
{
Title = title,
Author = author,
Type = type,
//For simplicity we use random, but technically this can be an API call
//to retrieve the total page for this book.
Pages = Random.Shared.Next(0, 1000),
};
}
}
In Program.cs we must register these with the service container so we can use them for Dependency Injection when we need them.
builder.Services.AddTransient<IBookFactory, AudioBookFactory>();
builder.Services.AddTransient<IBookFactory, PaperBackBookFactory>();
builder.Services.AddTransient<BookFactoryResolver>();
However, since we have many type of book factories, we need a factory resolver class that keeps all the different factories available so that upon receiving the request to add book of specific type we can retrieve the correct factory to produce it.
While the term "Factory Resolver" isn't a standard design pattern name recognized in classical design pattern literature, it is a combination of the Factory pattern with a mechanism to dynamically determine which concrete factory to use at runtime based on some criteria. This approach is useful in scenarios where the exact subclass of an object that needs to be instantiated cannot be determined at compile time and may depend on runtime conditions or configurations.
The class BookFactoryResolver
determined which concrete factory to use when creating book objects. This is based on some criteria, typically the type of book requested by the client code. For instance, if the system supports different types of books like AudioBook
, EBook
, or PrintedBook
, the BookFactoryResolver
decides which factory class (AudioBookFactory
, EBookFactory
, PrintedBookFactory
) to instantiate and use for creating the book object.
public class BookFactoryResolver
{
private readonly IServiceProvider _serviceProvider;
public BookFactoryResolver(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IBookFactory Resolve(BookType bookType)
{
var factories = _serviceProvider.GetServices<IBookFactory>();
return bookType switch
{
BookType.AudioBook => factories.OfType<AudioBookFactory>().FirstOrDefault(),
BookType.PaperBack => factories.OfType<PaperBackBookFactory>().FirstOrDefault(),
_ => throw new KeyNotFoundException("Factory not found for the given book type.")
};
}
}
The proper book factory is determine by the type of book in BookService.cs. It will then be used to add the book accordingly.
public async Task<bool> AddAsync(Book bookToAdd)
{
var bookFactory = _bookFactoryResolver.Resolve(bookToAdd.Type);
var createdBook = bookFactory.AddBook(bookToAdd.Title,bookToAdd.Author,bookToAdd.Type);
_unitOfWork.Books.Add(createdBook);
var result = await _unitOfWork.CompleteAsync();
return result > 0;
}
In the Factory pattern, the primary focus is on the creation of objects, not on their lifecycle management (like CRUD operations). The Factory pattern is used to encapsulate the creation logic of objects, making the system more modular, extensible, and easy to maintain. It separates the construction of objects from their representation and allows the instantiation of objects at runtime based on some conditions.
Given this, having only the AddBook
method in ourIBookFactory
interface aligns with the intent of the Factory pattern, which is to abstract and manage the creation of objects.
However, if you find yourself needing to perform CRUD operations on Book
instances, this typically falls outside the scope what a Factory is meant to handle. Instead, these operations should be managed by a different part of your application architecture, such in our case it reside within the unit of work and repository layers.
Here are the key benefits of the Factory Pattern:
Encapsulation of Creation Logic: By encapsulating the creation logic within specific factories (
AudioBook
Factory
,PaperbackBookFactor
y
), the complexity of instantiation is hidden from the client. This means changes to the creation process of a book type don't affect the client code.Single Responsibility Principle (SRP): Each factory has one responsibility—creating a specific type of book. This adherence to SRP makes the system easier to understand, maintain, and extend.
Open/Closed Principle: The system is open for extension but closed for modification. Adding a new type of book requires adding a new factory and possibly extending the factory interface, but it doesn't require modifying existing factories or the client code that uses them.
Flexibility and Scalability: The Factory Method pattern allows for more flexibility in adding new types of books or changing the instantiation logic for existing ones without affecting the rest of the system. It's particularly useful in systems expected to grow or change over time.
Type Safety: Clients work with abstract interfaces or base classes, not concrete implementations. This promotes type safety and reduces the risk of errors related to direct instantiation of classes.
Decoupling: The pattern decouples the creation of objects from their usage, leading to a system where object creation and business logic are not tightly intertwined. This decoupling makes the system more modular and easier to test.
For example, in our library system, if you want to add a new type of book, say EBook
, you would simply create an EBookFactory
implementing the IBookFactory
interface without needing to alter the existing client code or the other factories. This approach significantly simplifies the management of object creation as the system evolves.
In conclusion, in this article, we implemented the Factory Method pattern to handle different types of books, such as paperbacks and audiobooks, each with unique properties. We described how to implement the interface and concrete factory classes using a factory resolver mechanism to determine the appropriate factory at runtime (AudioBookFactory
and PaperBackBookFactory
). This ensures a more modular, extensible, and maintainable system.
Repo code:
https://github.com/amphan613/library-managment-system/tree/1856969b2401eb571380ee077df372a9c03977e1