Hi everybody and welcome back.
In my first and second article I have shown you how to refactor existing code to use Template, Strategy and Observer design patterns.
Today, I am going to show you how to refactor an existing application to use the Abstract Factory and the Singleton pattern.
In my first and second article I have shown you how to refactor existing code to use Template, Strategy and Observer design patterns.
Today, I am going to show you how to refactor an existing application to use the Abstract Factory and the Singleton pattern.
The application we are going to explore today, is a very simple application. It contains a Client class that uses several Product classes, ProdA, ProdB and ProdC.
The client class will instantiate some Products and call some methods on these products.
The client class will instantiate some Products and call some methods on these products.
class Client def initialize @prod_A =
ProductA.new
end def foo @prod_A.do_your_stuff my_prod_B =
ProductB.new
my_prod_B.do_it @prod_C =
ProductC.new
@prod_C.perform end end class ProductA def do_your_stuff puts "I'm a ProductA, doing my stuff" end end class ProductB def do_it puts "I'm a ProductB, doing it" end end class ProductC def perform puts "I'm a ProductC, performing" end end my_client = Client.new my_client.foo
As you can see, the binding between the Client and Product classes is very tight; The Client class specifically names and creates the Product classes it uses.
This is one of the scenarios where refactor to Factory Pattern removes the coupling between the Client and the various Product classes.
Also, we are going to extend this application to create several families of products, therefore the Abstract Design Pattern will better suit this use case.
Also, we are going to extend this application to create several families of products, therefore the Abstract Design Pattern will better suit this use case.
The abstract factory pattern provides a way to encapsulate a group of individual factories that have a common theme without specifying their concrete classes. In normal usage, the client software creates a concrete implementation of the abstract factory and then uses the generic interface of the factory to create the concrete objects that are part of the theme. The client doesn't know (or care) which concrete objects it gets from each of these internal factories, since it uses only the generic interfaces of their products. This pattern separates the details of implementation of a set of objects from their general usage and relies on object composition, as object creation is implemented in methods exposed in the factory interface.
Thus,
we create an Abstract factory class containing three “abstract”
methods, one for each product creation that the main application.
module AbstractProductFactory
def create_product_A
puts "You should implement this method in the concrete factory"
end
def create_product_B
puts "You should implement this method in the concrete factory
end
def create_product_C
puts "You should implement this method in the concrete factory"
end
end
The
implementation for these methods will raise an exception because it will be responsibility of the specific Factory extending this Abstract
factory to create an instance of a specific family of product.
In
this same class we defined a concrete ProductFactory extending the
AbstractFactory class and overriding the three creation methods for
creating products.
Each of these method will create a different product (productA, productB and ProductC) belonging to the same product family. We will call this family "CoolProduct" to distinguish from the other families.
Each of these method will create a different product (productA, productB and ProductC) belonging to the same product family. We will call this family "CoolProduct" to distinguish from the other families.
class CoolProductFactory include AbstractProductFactory def create_product_A
CoolProductA.new
end def create_product_B
CoolProcductB.new
end def create_product_C
CoolProductC.new
end end
The
Client will change to accept a factory in his constructor. It will never instantiate a product by
itself anymore. Whenever it will need a product of a certain family and type, it will ask
the factory to provide one.
In the following piece of code, the client now ask the factory to create three different products. Applying this simple pattern we have decoupled the Client from the different Product. The responsibility of creating different family of product has been moved in the correspondent factory.
In the following piece of code, the client now ask the factory to create three different products. Applying this simple pattern we have decoupled the Client from the different Product. The responsibility of creating different family of product has been moved in the correspondent factory.
require_relative 'abstract_product_factory.rb'
class Client
attr_accessor :productFactory
def initialize(productFactory)
@productFactory = productFactory
@prod_A = @productFactory.create_product_A
@prod_B = @productFactory.create_product_B
@prod_C = @productFactory.create_product_C
end
def foo
@prod_A.do_your_stuff
@prod_B.do_it
@prod_C.perform
end
end
coolClient = Client.new(CoolProductFactory.new)
coolClient.foo
We have seen that the only responsibility of the factory is to create Product objects on demand. The factory doesn't keep any state. That said, a simple question should come in your mind; Do we need to create a different factory for each client using this application ? The answer is no, we don't need to.
We need only one instance for each factory and we can reuse it in each clients that needs it.
We need only one instance for each factory and we can reuse it in each clients that needs it.
This is the classic scenario where the Singleton pattern comes in help.
In software engineering, the singleton pattern is a design pattern that restricts the instantiation of a class to one object. This is useful when exactly one object is needed to coordinate actions across the system. The concept is sometimes generalized to systems that operate more efficiently when only one object exists, or that restrict the instantiation to a certain number of objects.
There are different possible
implementation of the singleton pattern. We decided to use the one best suits this scenario. We defined a class variable
called @@instance in the factory itself and we assigned an instance of the factory class to it. We created a method self.instance
to return this specific instance (and this instance only) when need
by the client. We used the code private_class_method :new to prevent
any other classes from instantiate this class again.
This assured us that we will always have one and only one instance of this Factory. Exactly what we wanted.
This assured us that we will always have one and only one instance of this Factory. Exactly what we wanted.
class CoolProductFactory include AbstractProductFactory
@@instance = CoolProductFactory.new
private_class_method :new def self.instance
return @@instance
end def create_product_A ProductCoolA.new end def create_product_B ProductCoolB.new end def create_product_C ProductCoolC.new end end
Now we can construct the client object just retrieving a concrete factory instance calling the method "instance" on the FactoryClass we need, and pass it to the client:
coolClient = Client.new(
CoolProductFactory.instance
) coolClient.foo
What if a new family of products needs to be added to the application ?
Easy, if you used the Abstract Factory Pattern correctly!
Let's say a new family of products called UncoolProduct comes into play.
We need to add three classes, one for each product, of this family. These products had their own behavior. They all look very similar to this one:
We need to add three classes, one for each product, of this family. These products had their own behavior. They all look very similar to this one:
require_relative 'product_a.rb'
class UncoolProductA < ProductA
def do_your_stuff
puts "I'm a UncoolProductA, doing my stuff"
end
end
UncoolProductA extends a class called productA which is extended also by the class CoolProductA.
The same is valid for UncoolProductB and UncoolProductC.
We
added a new concrete factory implementing the Abstract Factory.
This new concrete factory will have the responsibility of creating products for this new product family.
Also in this case, this factory will be a singleton.
This new concrete factory will have the responsibility of creating products for this new product family.
Also in this case, this factory will be a singleton.
class UnCoolProductFactory
include AbstractProductFactory
@@instance = UnCoolProductFactory.new
private_class_method :new
def self.instance
return @@instance
end
def create_product_A
UncoolProductA.new
end
def create_product_B
ProductUncoolB.new
end
def create_product_C
UncoolProductC.new
end
end
A new Client instance, the UncoolClient, will be created requiring an instance of the UncoolProductFactory. The client will now ask its factory to produce three uncool products.
unCoolClient = Client.new(
UnCoolProductFactory.instance
) unCoolClient.foo
Also,
we observe that:
- There will be two concrete Factory classes, cool and uncool. They may or may not have a common superclass.
- Each Factory class will contain a certain number of methods for creating a certain number of products.
- For each product (A, B and C) there'll be two concrete subclasses (CoolProduct, UncoolProduct).
- The main application should create the factory instance that is used to instantiate the Client.
The
latest point is probably the most important. It shows that the client
can operate on different product families simply swapping in and out the
concrete factory used. Basically our client can be a CoolClient or an
UnCoolClient simply receiving the correspondent factory from the class that creates it.
Below there is a class diagram for this application:
HAPPY CODING !!
LUCA