The Problem
The software application that I am upgrading needs to use different UI and business logic components depending on what service is being sold. Furthermore, these services will be expanded and changed in the future and the application needs to be designed to meet these future changes with minimal impact.
The Current Situation
It’s not pretty. Let’s face it. You look at the application and you can see that it was designed to sell a given type of service. Then, later, another service was added and the way of switching between the two was on the basis of “if” statements. The view and (unfortunately) the calculation logic is wrapped up inside web user controls on the same web page and there is a decision point in the code to decide which of them to display:
if (service.ServiceType == ServicesTypes.ServiceTypeA)
{
serviceTypeAControl.Visible = true;
}
else
{
serviceTypeBControl.Visible = true;
}
I’m sure you’ve seen this a million times. What’s wrong here? It is actually worth asking the question because it’s an important one to ask, and the answer is all about extensibility. If the code said:
maleUIComponent.Visible = (sex == sexTypes.Male);
femaleUIComponent.Visible = !( sex == sexTypes.Male);
This would be better, because we know the enumeration is static and we greatly simplify our code by not having to cope with future extensibility. However, in the first scenario above we know that we have to cope with new services. This leaves us with one of the first key rules of extensibility:
“If you correctly design an application to be extensible, it should not require any existing components to be changed in order to add new extensions.”
As you can see from the “if” statement above, if you use this pattern to extend the solution and add just one more service, you will require at the very least the following modifications:
· Additional web user control· Change to the web form to add the control
· Change to the code-behind to update the switching
In doing this there is every possibility that something could go wrong in the code and break functionality that is already in place. Even though in this case the code is quite simple it illustrates the point. If this was a highly complex UI such as Visual Studio it would be almost impossible to aggregate new components from different development teams because changes in the components would be happening all the time and you’d never arrive at a stable system.
The Solution
Abstraction. That’s what helps you here. Interfaces. Finding what is common in the uncommon. Let’s keep with our example application where we are producing quotes for a customer and look at the UI involved. We have a UI that needs to display a different component depending on the service type in order to view a quote. The first thing we do is define common base entities that describe our business model. In this case we are dealing with quotes for a network service, so we might have classes like these:
public class Customer
{
...
}
public class Service
{
public string ServiceName { get; set; }
public string ServiceCode { get; set; }
...
}
public class Quote
{
public Service SelectedService { get; set; }
public Customer SelectedCustomer { get; set; }
}
Having created the base entities, the language of our business data, we can then move on to the next stage and create interfaces that describe common functionality. For example, if we are looking at the creation and viewing of quotes we might have an interface such as this:
public class QuoteEventArgs : EventArgs
{
public Quote Quote { get; set; }
}
public interface IQuoteView
{
public Quote SelectedQuote { get; set; }
public event EventHandler
public event EventHandler
public event EventHandler
}
If we’re doing this well, we will probably want to create a base class for our quote view controls. Note that an interface defines the common behaviour or our components, and it is this that we should reference later, but interfaces cannot provide any common implementation. That is done by a base class. If you’re doing this already you might think this is really dull and obvious stuff, but I am posting this blog series precisely because applications are developed without design of this sort.
So, base class (in this case I am assuming that we will be using web server controls, but the same principles apply for user controls as well):
public QuoteViewBase : Control, IQuoteView
{
public Quote SelectedQuote
{
get { return this.ViewState[“quote”] as Quote; }
set { this.ViewState[“quote”] = value; }
}
// Events here .....
// Protected methods etc...
}
Not wanting to labour the point, but we have a base class that can store our quote in the view state and already has the events wired in as well. We’re good to go. Almost. Remember that we are trying to abstract our application, and so we need to make sure that a web page that is going to display these controls does not have to be affected by new functionality. This is done by using that most powerful of design patterns – the factory pattern.
The factory pattern allows us to create our controls whilst hiding the implementation of their creation from the rest of the application. Therefore, even if the internals of the factory are changed there is no knock-on impact elsewhere. Our factory might look like this:
public class QuoteViewerFactory
{
public IQuoteView CreateQuoteViewer(Service service)
{
// Implement the factory logic in here.....
}
}
Powerful stuff. I can almost sense you gasp as you absorb the implications of this. It’s like being set free. It’s like all those if statements and switch statements are ugly and belong to the bad old days. It’s a new dawn. In order to hook the controls in to our web page all we need to do is create a placeholder in the page, such as a panel, that we can create controls inside, and then we create a single control for our quote:
protected void Page_Init(object sender, EventArgs e)
{
try
{
Quote quote = Session[“CurrentQuote”] as Quote;
this.PlaceholderPanel.Controls.Clear();
IQuoteView control = new QuoteViewerFactory().CreateQuoteViewer(quote.SelectedService);
this.PlaceholderPanel.Controls.Add(control);
}
catch (Exception ex)
{
// Log error etc.
}
}
Now that we have made our service-agnostic base classes and interfaces, and we have modified our application to take advantage of them, we now need to create the specific implementations of the UI for the services we need to sell. In order to take full advantage of this pattern, make sure that these service-specific components are placed in separate libraries from the core components. So, moving on, first we create service-specific entities:
public class ServiceA : Service
{
// Add specific properties
}
After this we will need to create our viewer for ServiceTypeA:
public class ServiceTypeAQuoteView : QuoteViewBase
{
// Add UI components and any other internal logic
}
We’ve done it! We have created a web page that loads in the appropriate control based on the service we have selected in our quote. We can add new services and the existing web form is unchanged. If we design the contents of the factory correctly all we have to do is add configuration and there should be no changes here either. There are different ways of doing this, but they all center on the same concepts:
- Create a list of all of the available classes that conform to the correct interfaces. We need to have a key that we will look up on as well as the full type name of the class.
- Given the key, e.g. the ServiceCode above, we look up in our configuration and get the type information of the corresponding class.
- Using the type, we dynamically create an instance of the appropriate class, but outside of our factory the application is unaware of anything except its interface. Note that there is a slight difference in web forms as when we create a control we should use the Page.LoadControl() method as our control needs to maintain a reference to the page that has loaded it, whereas if we are creating pluggable business logic we only need to use the System.Reflection.Activator.CreateInstance() method to create an object of the correct type.
Deployment
The fact that our UI application does not know about anything other than our base entities and interfaces is useful, but it does create a deployment issue if we’re not careful. Our application will not reference the service-specific libraries for the very reason that they can be added, removed or replaced without breaking our application, but it also means that a standard compile of the solution will not place the appropriate assemblies into the bin folder of the application and so straight out of the box our code will fail.
In order to get round this, from a development perspective we need to add build events onto our service-specific libraries so that we copy them into our bin directory so we can run them during development. From a packaging and deployment perspective we need to make sure that all of the libraries get into our installer. We have various options for this, but one of them may be to add more build events to copy the assemblies into a common “binaries” folder and then have a single build step to package all of these up and deploy them into the bin.
Review
That’s been a bit of a journey to say the least. This has not been intended as a detailed tutorial of how to achieve pluggability – that may come later depending on whether I get requests for it - but I really wanted to get across the core concepts of using interfaces and factories to isolate extensible functionality and hence deliver what we were looking for at the start – a pluggable application that requires minimal intervention to extend the functionality. The steps we went through were these:
· Create the base entities to describe our business data· Create interfaces that describe our business behaviour
· Create base classes that implement our interfaces and contain common behaviour
· Create a factory to abstract the creation of the concrete classes from the application
· Create our concrete business-specific functionality and place it in separate packages so that it can be deployed independently
· Create a build process that allows us to add new business-specific extensions in a way that allows them to be packaged up with the rest of the application.
No comments:
Post a Comment