We recently ran into a challenging design issue at work. We are working on a web application that must support internationalization since we have customers in many different countries. The profile for each customer has different settings and configurations based upon the customer’s country.
These settings/configurations overlap and differ across countries. The goal, of course, is to design/implement the code in a way that enables us to reuse code that is common across countries. Below is the scenario that we are dealing with:
USCustomer.FeatureA()
is the same as CACustomer.FeatureA()
USCustomer.FeatureB()
is the same as KRCustomer.FeatureB()
CACustomer.FeatureC()
is the same as KRCustomer.FeatureC()
KRCustomer.FeatureA()
differs from USCustomer.FeatureA()
CACustomer.FeatureB()
differs from USCustomer.FeatureB()
USCustomer.FeatureC()
differs from CACustomer.FeatureC()
The first attempt was of course to try and solve this via sub-classing. Let’s look at the different options that we have for sub-classing:
- Make CA & KR sub-classes of US. This allows us to re-use
USCustomer.FeatureA()
for CA and USCustomer.FeatureB()
for KR. But it doesn't allow us to re-use CACustomer.FeatureC()
for KR. - Okay, no problem, you say. We'll just make KR a sub-class of CA. So, CA is a sub-class of US and KR is a sub-class of CA. But then we run into the issue that
USCustomer.FeatureB()
can no longer to be re-used for KRCustomer.FeatureB()
.
Now imagine this kind of situation spread across 15 different countries. Ouch! So sub-classing clearly is not the answer.
It sounds like we need some sort of “strategy” that'll let us swap/differ settings and configurations across countries without all this coupling. Well the strategy is to apply the Strategy design pattern. Below is the overall strategy:
- Encapsulate the features: Each feature gets its own class. So we end up with the following classes:
Customer
CACustomer
KRCustomer
FeatureA : IFeatureA
FeatureB : IFeatureB
FeatureC : IFeatureC
FeatureA1 : IFeatureA
FeatureB1 : IFeatureB
FeatureC1 : IFeatureC
- Setup the constructor of the
Customer
class to take in the concrete class that is specific to the feature that they require as a parameter. For instance:
public class Customer
{
public Customer() :
this(new FeatureA(), new FeatureB(), new FeatureC())
{ }
public Customer(IFeatureA a, IFeatureB b, IFeatureC c)
{
featureA = a;
featureB = b;
featureC = c;
}
public void DoInitialSetup()
{
SetupFeatureA(featureA);
SetupFeatureB(featureB);
SetupFeatureC(featureC);
}
private IFeatureA featureA;
private IFeatureB featureB;
private IFeatureC featureC;
}
public class CACustomer : Customer
{
public CACustomer() :
base(new FeatureA(), new FeatureB1(), new FeatureC1())
{ }
}
public class KRCustomer : Customer
{
public KRCustomer() :
base(new FeatureA1(), new FeatureB(), new FeatureC1())
{ }
}
And voila! We can now swap implementations across countries and add different implementations for different countries as needed.
The above example was simplified to effectively demonstrate the Strategy pattern without confusing the reader. In particular, in the above example, the customer classes directly instantiate the features that they need. This is generally a bad idea in practice for at least two reasons:
- It makes it difficult to unit test the
Customer
class. - Each type of customer is still coupled to the specific feature-set that it is using. In other words, the features cannot be changed dynamically.
Instead of directly instantiating the classes, we should be using an IOC container to handle the object instantiations and configurations.
For instance, for our application, we are using an XML file to tie concrete implementations with their respective countries. Below is an example:
DefaultLocalization.xml
<interface name="IFeatureA">
<class name="FeatureA">
</interface>
<interface name="IFeatureB">
<class name="FeatureB">
</interface>
<interface name="IFeatureC">
<class name="FeatureC">
</interface>
CALocalization.xml
<interface name="IFeatureB">
<class name="FeatureB1">
</interface>
<interface name="IFeatureC">
<class name="FeatureC1">
</interface>
KRLocalization.xml
<interface name="IFeatureA">
<class name="FeatureA1">
</interface>
<interface name="IFeatureC">
<class name="FeatureC1">
</interface>
Based on the current country, the right classes get instantiated. If we ever need to change KR to use FeatureC
instead of FeatureC1
, all we do is make the change in the XML file and we’re done.
So, in conclusion, the key to the strategy pattern is encapsulation: Remove the logic that differs from the logic that stays the same via encapsulation.
Reddit