Customizing Elastic Path is usually simple. Customizing Elastic Path the right way takes some more thought. One of the best ways to customize EP is by using the decorator design pattern. Decorators, aka. "wrappers", are a great way to add functionality (i.e. customizations) to an object without modifying the object itself.
First, why decorate a class? Isn't it easier to just modify the class itself?
There are a number of advantages to using decorators:
- You may not have access to the source code; wrapping the class you wish to customize may be your only option.
- In addition to the customized behavior of a class, you may wish to retain the original OOTB behavior.
- The less EP code you modify, the smoother future EP upgrades will be!
- Wrapping a class ensures that the unit/integration tests for that class will not break.
- Decorators are easy to understand and are a great example of object-oriented programming.
I recently developed a customization for a Professional Services project involving the new Settings Framework (available in Elastic Path Commerce 6.1). You can read more about the Settings Framework here, but what you need to know for now is that settings are strings stored in the database and identified by a PATH. All settings have a default value. In addition to the default value, a setting can have multiple context values. For example, consider a setting defined by the path CONFIG/siteAddress that has the default value ep.com. This setting also has two contexts: one for SnapItUp where the value is snapitup.ep.com and one for SLRWorld where the value is slrworld.ep.com.
The two context-specific values as well as the default context-less value for the CONFIG/siteAddress setting is conveyed in the following table:
| Context | Value |
|---|---|
| <no context> | ep.com |
| SnapItUp | snapitup.ep.com |
| SLRWorld | slrworld.ep.com |
The SettingsService provides a means to retrieve settings from the database.
Based on our data set above, the following service calls return different values:
- SettingsService.getSettingValue("CONFIG/siteAddress").getValue() returns "ep.com"
- SettingsService.getSettingValue("CONFIG/siteAddress", "SnapItUp").getValue() returns "snapitup.ep.com"
- SettingsService.getSettingValue("CONFIG/siteAddress", "SLRWorld").getValue() returns "slrworld.ep.com"
Very useful, but our PS project requires environment-specific settings. Not only do we have multiple stores (i.e. contexts), each store also has multiple environments. For instance, the Snap It Up and SLR World stores each have 2 environments: a test environment and a production environment; each of these environments have a different CONFIG/siteAddress setting value.
Here is our dataset:
| Context | Test environment value | Production environment value |
|---|---|---|
| <no context> | ep.com | ep.com |
| SnapItUp | snapitup.test.ep.com | snapitup.ep.com |
| SLRWorld | slrworld.test.ep.com | slrworld.ep.com |
So, we actually want SettingsService.getSettingValue("CONFIG/siteAddress", "SnapItUp").getValue() to return different values depending on which environment it's being called in; it should return snapitup.test.ep.com when called from the test environment and snapitup.ep.com when called from the production environment. This feature is not supported OOTB.
You could implement this by modifying the SettingsService class. Here is the SettingsService.getSettingValue() method in pseudocode form:
String getSettingValue(String path, String context) {
// Check the database for a setting value that has the given path and the given context.
String settingValue = database.getSettingValue(path, context);
if (settingValue != null)
return settingValue;
// If a setting value with the given context does not exist, then return the default context-less value.
return database.getSettingValue(path);
}
Modifying this method and making it environment-specific is easy:
String getSettingValue(String path, String context) {
// The environment itself is stored in a setting.
String environment = this.getSettingValue("SYSTEM/ENVIRONMENT");
String envSpecificContext = environment + "/" + context;
// Check the database for a setting value that has the given path and the given context.
String settingValue = database.getSettingValue(path, envSpecificContext);
if (settingValue != null)
return settingValue;
// If a setting value with the given context does not exist, then return the default context-less value.
return this.getSettingValue(path);
}
What did we do? We first grabbed the environment value from another setting (SYSTEM/ENVIRONMENT). We then prepended the environment onto the given context. Next, we looked in the database for a setting value with a context in the form <environment>/<context>. This code will work as long as our database contains the following:
| Context | Value |
|---|---|
| <no context> | ep.com |
| test/SnapItUp | snapitup.test.ep.com |
| test/SLRWorld | slrworld.test.ep.com |
| production/SnapItUp | snapitup.ep.com |
| production/SLRWorld | slrworld.ep.com |
But there are problems with modifying the SettingsService class like this:
- There's no way to retrieve non-environment-specific settings now.
- Any existing SettingsService test-cases are probably broken and will need to be fixed.
- If the next version of EP has modifications to the SettingsService class, there will be merge conflicts when this code is upgraded.
Here's how we can implement this customization by decorating the SettingsService instead:
class SettingsServiceDecorator implements SettingsService {
private SettingService settingsServiceComponent;
// Spring injector method.
void setSettingsServiceComponent(SettingsService settingsService) {
this.setttingsServiceComponent = settingsService;
}
// This method has the same behavior of the component class; thus, just delegate to the SettingsService.
String getSettingValue(String path) {
return this.settingsServiceComponent.getSettingValue(path);
}
...
String getSettingValue(String path, String context) {
String environment = this.getSettingValue("SYSTEM/ENVIRONMENT");
String envSpecificContext = environment + "/" + context;
return this.settingsServiceComponent.getSettingValue(path, envSpecificContext);
}
}
Here is a high-level diagram of what we've done:
Things to note here:
- The new SettingsServiceDecoratorImpl class implements the same SettingsService interface of the class that it's decorating, SettingsServiceImpl.
- This class has a reference to the class it is decorating; it's "wrapping" the SettingsServiceImpl class.
- Methods like getSettingValue(String) that have the same behavior as the component class simply delegate to the component class's method.
We now have an environment-specific settings service in addition to the OOTB settings service. When an environment-specific setting needs to be retrieved, we'll use the decorated class and when a non-environment-specific setting needs to be retrieved, we'll just use the OOTB class.
// Retrieve an environment-specific setting value.
SettingsService settingsServiceDecorator = new SettingsServiceDecoratorImpl();
String envSpecificSetting = settingsServiceDecorator.getSettingValue(path, context);
// Retrieve a normal non-environment-specific setting value.
SettingsService settingsService = new SettingsServiceImpl();
String nonEnvSpecificSetting = settingsService.getSettingValue(path, context);
Pretty nice. Let's go one step further. EP classes that need to retrieve settings have a SettingsService injected into them via Spring Dependency Injection. Remember, the new SettingsServiceDecoratorImpl class implements the same interface as the original SettingsServiceImpl class. So if the ProductService, for example, requires the new environment-specific settings service, we do not even have to modify the ProductService class. We need only modify Spring's ApplicationContext.xml file (or service.xml in Elastic Path's case).
Before:
<bean id="productService" parent="txProxyTemplate">
<property name="target">
<bean class="com.elasticpath.service.catalog.impl.ProductServiceImpl">
...
<property name="settingsService">
<ref bean="settingsService"/>
</property>
</bean>
</property>
</bean>
<bean id="settingsService" parent="txProxyTemplate">
<property name="target">
<bean class="com.elasticpath.service.settings.impl.SettingsServiceImpl">
<property name="settingsDao" ref="settingsDao"/>
<property name="settingValueFactory" ref="settingValueFactory"/>
</bean>
</property>
</bean>
After:
<bean id="productService" parent="txProxyTemplate">
<property name="target">
<bean class="com.elasticpath.service.catalog.impl.ProductServiceImpl">
...
<property name="settingsService">
<ref bean="settingsServiceDecorator"/>
</property>
</bean>
</property>
</bean>
<bean id="settingsService" parent="txProxyTemplate">
<property name="target">
<bean class="com.elasticpath.service.settings.impl.SettingsServiceImpl">
<property name="settingsDao" ref="settingsDao"/>
<property name="settingValueFactory" ref="settingValueFactory"/>
</bean>
</property>
</bean>
<bean id="settingsServiceDecorator" parent="txProxyTemplate">
<property name="target">
<bean class="com.elasticpath.service.settings.impl.SettingsServiceDecoratorImpl">
<property name="settingsServiceComponent" ref="settingsService"/>
</bean>
</property>
</bean>
What did we do?
- The ProductService is injected with a SettingsService in both cases. In the before, that SettingsService is an instance of SettingsServiceImpl. In the after, that SettingsService is an instance of SettingsServiceDecoratorImpl. The ProductService itself doesn't care as long as the instance implements the SettingsService interface!
- The new SettingsServiceDecoratorImpl has an instance of SettingsServiceImpl injected into it; this is the class that it's wrapping.
As you can see, using the decorator design pattern is a great way to customize Elastic Path without breaking existing behavior. And by not directly modifying core application code, you're helping to ensure smooth upgrades in the future.




