In this article by Anshul Verma and Jitendra Zaa, author of the book Apex Design Patterns, we will discuss some problems that can occur mainly during the creation of class instances and how we can write the code for the creation of objects in a more simple, easy to maintain, and scalable way.
(For more resources related to this topic, see here.)
In this article, we will discuss the the factory method creational design pattern.
Often, we find that some classes have common features (behavior) and can be considered classes of the same family. For example, multiple payment classes represent a family of payment services. Credit card, debit card, and net banking are some of the examples of payment classes that have common methods, such as makePayment, authorizePayment, and so on. Using the factory method pattern, we can develop controller classes, which can use these payment services, without knowing the actual payment type at design time.
The factory method pattern is a creational design pattern used to create objects of classes from the same family without knowing the exact class name at design time.
Using the factory method pattern, classes can be instantiated from the common factory method. The advantage of using this pattern is that it delegates the creation of an object to another class and provides a good level of abstraction.
Let's learn this pattern using the following example:
The Universal Call Center company is new in business and provides free admin support to customers to resolve issues related to their products. A call center agent can provide some information about the product support; for example, to get the Service Level Agreement (SLA) or information about the total number of tickets allowed to open per month.
A developer came up with the following class:
public class AdminBasicSupport{
/**
* return SLA in hours
*/
public Integer getSLA()
{
return 40;
}
/**
* Total allowed support tickets allowed every month
*/
public Integer allowedTickets()
{
// As this is basic support
return 9999;
}
}
Now, to get the SLA of AdminBasicSupport, we need to use the following code every time:
AdminBasicSupport support = new AdminBasicSupport();
System.debug('Support SLA is - '+support.getSLA());
Output - Support SLA is – 40
The "Universal Call Centre" company was doing very well, and in order to grow the business and increase the profit, they started the premium support for customers who were willing to pay for cases and get a quick support. To make them special from the basic support, they changed the SLA to 12 hours and maximum 50 cases could be opened in one month. A developer had many choices to make this happen in the existing code. However, instead of changing the existing code, they created a new class that would handle only the premium support-related functionalities. This was a good decision because of the single responsibility principle.
public class AdminPremiumSupport{
/**
* return SLA in hours
*/
public Integer getSLA()
{
return 12;
}
/**
* Total allowed support tickets allowed every month is 50
*/
public Integer allowedTickets()
{
return 50;
}
}
Now, every time any information regarding the SLA or allowed tickets per month is needed, the following Apex code can be used:
if(Account.supportType__c == 'AdminBasic')
{
AdminBasicSupport support = new AdminBasicSupport();
System.debug('Support SLA is - '+support.getSLA());
}else{
AdminPremiumSupport support = new AdminPremiumSupport();
System.debug('Support SLA is - '+support.getSLA());
}
As we can see in the preceding example, instead of adding some conditions to the existing class, the developer decided to go with a new class. Each class has its own responsibility, and they need to be changed for only one reason. If any change is needed in the basic support, then only one class needs to be changed. As we all know that this design principle is known as the Single Responsibility Principle.
Business was doing exceptionally well in the call center, and they planned to start the golden and platinum support as well. Developers started facing issues with the current approach. Currently, they have two classes for the basic and premium support and requests for two more classes were in the pipeline. There was no guarantee that the support type will not remain the same in future. Because of every new support type, a new class is needed; and therefore, the previous code needs to be updated to instantiate these classes. The following code will be needed to instantiate these classes:
if(Account.supportType__c == 'AdminBasic')
{
AdminBasicSupport support = new AdminBasicSupport();
System.debug('Support SLA is - '+support.getSLA());
}else if(Account.supportType__c == 'AdminPremier')
{
AdminPremiumSupport support = new AdminPremiumSupport();
System.debug('Support SLA is - '+support.getSLA());
}else if(Account.supportType__c == 'AdminGold')
{
AdminGoldSupport support = new AdminGoldSupport();
System.debug('Support SLA is - '+support.getSLA());
}else{
AdminPlatinumSupport support = new AdminPlatinumSupport();
System.debug('Support SLA is - '+support.getSLA());
}
We are only considering the getSLA() method, but in a real application, there can be other methods and scenarios as well. The preceding code snippet clearly depicts the code duplicity and maintenance nightmare.
The following image shows the overall complexity of the example that we are discussing:
Although they are using a separate class for each support type, an introduction to a new support class will lead to changes in the code in all existing code locations where these classes are being used. The development team started brainstorming to make sure that the code is capable to extend easily in future with the least impact on the existing code. One of the developers came up with a suggestion to use an interface for all support classes so that every class can have the same methods and they can be referred to using an interface.
The following interface was finalized to reduce the code duplicity:
public Interface IAdminSupport{
Integer getSLA() ;
Integer allowedTickets();
}
Methods defined within an interface have no access modifiers and just contain their signatures.
Once an interface was created, it was time to update existing classes. In our case, only one line needed to be changed and the remaining part of the code was the same because both the classes already have the getSLA() and allowedTickets() methods.
Let's take a look at the following line of code:
public class AdminPremiumSupport{
This will be changed to the following code:
public class AdminBasicSupportImpl implements IAdminSupport{
The following line of code is as follows:
public class AdminPremiumSupport{
This will be changed to the following code:
public class AdminPremiumSupportImpl implements IAdminSupport{
In the same way, the AdminGoldSupportImpl and AdminPlatinumSupportImpl classes are written.
A class diagram is a type of Unified Modeling Language (UML), which describes classes, methods, attributes, and their relationships, among other objects in a system. You can read more about class diagrams at https://en.wikipedia.org/wiki/Class_diagram.
The following image shows a class diagram of the code written by developers using an interface:
Now, the code to instantiate different classes of the support type can be rewritten as follows:
IAdminSupport support = null;
if(Account.supportType__c == 'AdminBasic')
{
support = new AdminBasicSupportImpl();
}else if(Account.supportType__c == 'AdminPremier')
{
support = new AdminPremiumSupportImpl();
}else if(Account.supportType__c == 'AdminGold')
{
support = new AdminGoldSupportImpl();
}else{
support = new AdminPlatinumSupportImpl();
}
System.debug('Support SLA is - '+support.getSLA());
There is no switch case statement in Apex, and that's why multiple if and else statements are written. As per the product team, a new compiler may be released in 2016 and it will be supported. You can vote for this idea at https://success.salesforce.com/ideaView?id=08730000000BrSIAA0.
As we can see, the preceding code is minimized to create a required instance of a concrete class, and then uses an interface to access methods. This concept is known as program to interface. This is one of the most recommended OOP principles suggested to be followed. As interfaces are kinds of contracts, we already know which methods will be implemented by concrete classes, and we can completely rely on the interface to call them, which hides their complex implementation and logic. It has a lot of advantages and a few of them are loose coupling and dependency injection.
A concrete class is a complete class that can be used to instantiate objects. Any class that is not abstract or an interface can be considered a concrete class.
We still have one problem in the previous approach. The code to instantiate concrete classes is still present at many locations and will still require changes if a new support type is added. If we can delegate the creation of concrete classes to some other class, then our code will be completely independent of the existing code and new support types.
This concept of delegating decisions and creation of similar types of classes is known as the factory method pattern.
The following class can be used to create concrete classes and will act as a factory:
/**
* This factory class is used to instantiate concrete class
* of respective support type
* */
public class AdminSupportFactory {
public static IAdminSupport getInstance(String supporttype){
IAdminSupport support = null;
if(supporttype == 'AdminBasic')
{
support = new AdminBasicSupportImpl();
}else if(supporttype == 'AdminPremier')
{
support = new AdminPremiumSupportImpl();
}else if(supporttype == 'AdminGold')
{
support = new AdminGoldSupportImpl();
}else if(supporttype == 'AdminPlatinum')
{
support = new AdminPlatinumSupportImpl();
}
return support ;
}
}
In the preceding code, we only need to call the getInstance(string) method, and this method will take a decision and return the actual implementation. As a return type is an interface, we already know the methods that are defined, and we can use the method without actually knowing its implementation. This is a very good example of abstraction.
The final class diagram of the factory method pattern that we discussed will look like this:
The following code snippet can be used repeatedly by any client code to instantiate a class of any support type:
IAdminSupport support = AdminSupportFactory.getInstance ('AdminBasic');
System.debug('Support SLA is - '+support.getSLA());
Output : Support SLA is – 40
The problem with the preceding design is that whenever a new support needs to be added, we need to add a condition to AdminSupportFactory.
We can store the mapping between a support type and its concrete class name in Custom setting. This way, whenever a new concrete class is added, we don't even need to change the factory class and a new entry needs to be added to custom setting.
Consider custom setting created by the Support_Type__c name with the Class_Name__c field name of the text type with the following records:
Name |
Class name |
AdminBasic |
AdminBasicSupportImpl |
AdminGolden |
AdminGoldSupportImpl |
AdminPlatinum |
AdminPlatinumSupportImpl |
AdminPremier |
AdminPremiumSupportImpl |
However, using reflection, the AdminSupportFactory class can also be rewritten to instantiate service types at runtime as follows:
/**
* This factory class is used to instantiate concrete class
* of respective support type
* */
public class AdminSupportFactory {
public static IAdminSupport getInstance(String supporttype)
{
//Read Custom setting to get actual class name on basis of
Support type
Support_Type__c supportTypeInfo =
Support_Type__c.getValues(supporttype);
//from custom setting get appropriate class name
Type t = Type.forName(supportTypeInfo.Class_Name__c);
IAdminSupport retVal = (IAdminSupport)t.newInstance();
return retVal;
}
}
In the preceding code, we are using the Type system class. This is a very powerful class used to instantiate a new class at runtime. It has the following two important methods:
Inspecting classes, methods, and variables at runtime without knowing a class name, or instantiating a new object and invoking methods at runtime is known as Reflection in computer science.
One more advantage of using the factory method, custom setting, and reflection together is that if in future one of the support types need to be replaced by another service type permanently, then we need to simply change the appropriate mapping in custom setting without any changes in the code.
In this article, we discussed how to deal with various situations while instantiating objects using design patterns, using the factory method.
Further resources on this subject: