A lot of important aspects in object-oriented programming are often ignored when integrating new functionality into a system. It all comes from a fear of overcomplicating and over-engineering. When given a task to extend the functionality of Kentico by writing a custom module or component, developers often jump onto writing code and getting things "to work". Problems arise when that functionality needs to be changed, extended or tested.
Interfaces provide a great way of "detaching" modules from the application.
They are increasingly more common within Kentico, as they provide a neat way to do customisations. Customised providers are easily registered in the system with assembly attributes . Implementing interfaces allows modifications such as changing the way a preferred payment method is determined in the shopping cart checkout process.
The most powerful aspect is that the functionality is publicly exposed in the system, and it can be used for custom features that are not necessarily related to built-in Kentico features.
Let's assume that your application needs to integrate an SMS gateway for sending notifications. Here's a rough representation of a class that does just that:
public class SmsGatewayConnector
{
public void SendSms(string phoneNumber, string message)
{
// Connect to the SMS gateway provider
// ... and send the message!
}
}
When the time comes, you need to send an SMS message:
public class SomethingHappenedNotifier
{
public void SomethingHappened(string phoneNumber)
{
var smsGateway = new SmsGatewayConnector();
smsGateway.SendSms(phoneNumber, "Something happened!");
}
}
This is fine, however:
If we implement an interface, swapping out the SMS gateway implementation becomes much easier.
public interface ISmsGateway
{
void SendSms(string phoneNumber, string message);
}
public class SmsGatewayConnector : ISmsGateway
{
public void SendSms(string phoneNumber, string message)
{
// Connect to the SMS gateway provider
// ... and send the message!
}
}
Since our goal is to dynamically obtain an implementation of the SMS gateway class, we can use the CMS.Core.Service
class, in combination with the RegisterImplementation
attribute.
The RegisterImplementation
attribute will ensure that when we ask for an instance of a class that implements the ISmsGateway
interface, we get the SmsGatewayConnector
class.
[assembly: RegisterImplementation(typeof(ISmsGateway), typeof(SmsGatewayConnector))]
public class SmsGatewayConnector : ISmsGateway
{
public void SendSms(string phoneNumber, string message)
{
// Connect to the SMS gateway provider
// ... and send the message!
}
}
Now, the class that 'consumes' the SMS gateway can use the CMS.Core.Service
class to ask for an SMS gateway implementation.
public class SomethingHappenedNotifier
{
public void SomethingHappened(string phoneNumber)
{
var smsGateway = Service<ISmsGateway>.Entry();
smsGateway.SendSms(phoneNumber, "Something happened!");
}
}
The Service<>.Entry()
method returns a static singleton instance of an ISmsGateway
- in this case, an SmsGatewayConnector
, since that is the class we registered an implementation for.
Another alternative to using the RegisterImplementation
assembly attribute is to use the Service<>.Use<>()
method, to manually "register" an implementation.
For example, this could be done during the initialisation of a custom module:
public class MyModule : Module
{
public MyModule() : base("MyModule") { }
protected override void OnInit()
{
Service<ISmsGateway>.Use<SmsGatewayConnector>();
}
}
This gives us the flexibility to register different implementations for different cases. One of the possibilities is to register a 'dummy' implementation for test purposes, if a particular web.config
key is set.
public class DummySmsGateway : ISmsGateway
{
public void SendSms(string phoneNumber, string message)
{
EventLogProvider.LogInformation("SMSGateway", "Test", message);
}
}
public class MyModule : Module
{
public MyModule() : base("MyModule") { }
protected override void OnInit()
{
var testMode = SettingsHelper.AppSettings["SmsGatewayTestMode"];
if (testMode == "true")
{
Service<ISmsGateway>.Use<DummySmsGateway>();
}
else
{
Service<ISmsGateway>.Use<SmsGatewayConnector>();
}
}
}
The Service
class returns a singleton when the Entry()
method is invoked. Every time you call Entry()
, you get the same instance.
The whole concept explained for the Service
class can be applied to the CMS.Core.ObjectFactory
class in a similar fashion.
The ObjectFactory
class can provide you with a non-singleton instance.
// Register an implementation
ObjectFactory<ISmsGateway>.SetObjectTypeTo<SmsGatewayConnector>();
// Get a new instance
var smsGateway = ObjectFactory<ISmsGateway>.New();
Some benefits of this approach are:
The recommended way of customising providers in Kentico mentions the use of the RegisterCustomProvider
assembly attribute, to register the provider as a replacement for the built-in providers.
Using the ObjectFactory
class, you can have more control over the registration of custom providers. This opens up possibilities such as enabling and disabling custom providers by changing web.config
settings. Of course, this is something that you still have to perform during the initialisation phase of the application.
public class MyModule : Module
{
public MyModule() : base("MyModule") { }
protected override void OnInit()
{
// If the 'LoadCustomProvider' web.config setting is set to 'true', override the provider
if (SettingsHelper.AppSettings["LoadCustomProvider"] == "true")
{
var provider = (ICustomizableProvider)ObjectFactory.New(typeof(CustomShippingOptionInfoProvider));
provider.SetAsDefaultProvider();
}
else
{
// Custom provider is not loaded - default provided is used
}
}
}
Probably, yes. However, if you're not already doing that, a great start would be to utilise Kentico's built-in features, to improve the architecture of your application.