Almost every mobile phone and table sold today uses a USB cable to power and charge it. I can find a variety of adapters to connect that USB cable to different styles power outlets in the United States or other countries, or to connect it to a 12-volt automotive system. Of course some of these adapters work better than others, so getting one from a questionable source may put my device at risk. But being able to substitute any adapter that provides me with a USB port is what the adapter pattern is all about.
A software example
So let us take a peak at a point of sale system. I have an object which represents a sale taking place, and it contains the items being purchased. I also have a sale controller that has the responsibility of adding or removing items from that collection, and handling the checkout when the customer is ready. That controller interfaces to a credit-card service provider to process a credit-card charge:
I can guarantee that in the future the credit card service will change; providers come and go and change for reasons as non-technical as a management decision. But the SaleController class is tightly coupled to the IntuitCardService class, so to replace it we have to change the SaleController to create an instance of something else. That creates all types of problems.
The design principles involved
The interface the new provider uses is sure to be different, so that will require some refactoring of SaleController to accommodate it. That violates Robert Martin's open for extension, closed for modification principle: the O in SOLID (Martin). Opening up the class to change the provider risks everything else the class does.
Open/closed is the more obvious principle violated, but the even more important principle of single responsibility seems not to be met, the S in SOLID. It appears in this design that the controller is responsible for building and completing the sale, and also for choosing the service provider. That makes two responsibilities for the controller.
Reducing Dependency
Earlier I mentioned designing to the abstraction, and that is the key to reducing the dependency between the classes. The adapter pattern fixes the problem of changing providers by inserting an an interface, an abstraction, between the controller and the service providers. Any class that provides that interface can be plugged in where the controller expects the provider:
I cannot stress enough the importance of the interface: design to the abstraction (or code to the interface) is where the client only knows the object it uses implements the interface. It does not really care about what the object really is, only that it can use it in a particular way. So the client can be handed an instance of any object that provides this interface: an IntuitServiceAadapter, a PaypalServiceAdapter, or in the future a ChaseServiceAdapater.
Of course the service providers all have different interfaces. The adapter classes are created to translate from the service provider classes to the interface that the controller expects. New adapters can be created for new service providers at any time. It is these adapters that are used by the controller.
The interface belongs to the client. In this case the controller is the client, so the interface is what it needs and belongs to it. That idea sets up the design so that new adapters can be created at any time to meet the interface the client expects. The interface definition belongs in the application with the client; the adapter implementations belong in their own libraries so they can be loaded dynamically.
So this achieves the open/closed principle: the controller is closed, once written it does not have to be opened up again to refactor it for a new service provider. The interface the controller expects can be extended to new adapters for additional service providers as they are required.
Creating the Resource
The adapter pattern does not solve a problem that we identified earlier: the decision on what adapter to create and use is still left up to the controller in this example. That still violates the principle single responsibility, which is even more important than open/closed. And yet it still violates open/closed to, because we may have to refactor the controller to choose a new provider.
One way to fix that problem is to look at the factory method pattern, which I always introduce immediately after the adapter pattern. It is just a good fit here. The factory method delegates the responsibility of choosing the adapter to a class other than the controller, when the controller asks for it.
To be fair there are two other solutions that also need to be considered. The first is to externalize the choice of adapter from the controller; have the controller read the class to be instantiated from a properties file, so changing the adapter class only requires a change to the properties file and no changes to the controller.
The other possibility is dependency injection: someone else creates the adapter and injects it into the controller. A simple implementation would be to inject the adapter as a parameter to the controller constructor, or to call a setter with the adapter after the controller has been instantiated. The difference here is timing: the controller chooses the time the adapter is created when it calls a factory method, while dependency injection places the choice of when the adapter is created outside of the controller.
Intent
On a final note, moving forward we will find many patterns that rely on designing to the abstraction and structurally the diagrams are almost identical to the adapter pattern. The key is the intent of the patterns; the adapter pattern solves the problem of plugging in new providers. The strategy pattern looks almost the same, but the intent is to plug in different algorithms. This will become more evident as you proceed.
No comments:
Post a Comment