September 1, 2014
Designing For Change
You must welcome change as the rule but not as your ruler.
Denis Waitley
Today’s era of software is all about 3rd-party integration and the sharing of data. It’s no longer optional for your app to be able to integrate or be integrated with other apps and services. It’s a necessity. Whether it’s a small library, an open source framework, or a robust 3rd-party service - software today is built by connecting a variety of tools and services.
When adding a major new feature to your app there’s likely a plethora of existing 3rd-party solutions that you could plug in. Each one of these solutions has its own pros and cons and custom integration points. Wouldn’t it be great if you could spend less time worrying about future changes to the custom integration process and instead easily swap in/out solutions now or in the future? Let’s take a look at several reasons this capability would be helpful and how you can implement this approach with the adapter design pattern.
Why Design For Change?
Your app is going to need to change as its industry and market players evolve over time. Designing for change can save you a lot of time and headache when these changes occur in the not-too-distant future. It also leads to many other opportunities and benefits. Here are a few real-world examples:
Leveraging the Adapter Pattern
The adapter design pattern can be used to allow two or more unrelated or incompatible types to communicate. The adapter defines a standard (a set of common requirements) and acts as a bridge or translator for the unique objects. A socket wrench is a simple example of an adapter that provides a common interface for sockets of varying sizes and lengths. In this case, the socket wrench is the “adapter” and the varying sockets are “drivers”.
Generally, when writing a software adapter, you should create a base, abstract class to represent the “adapter”. This adapter class defines the common requirements that will be applied across all driver child classes. Some programming languages support the concept of an “interface” class - which is another valid way to structure the required driver methods.
Let’s reference some code from Volcano as an example:
The Adapter Class
This base gateway adapter class contains several helper methods (set()
, reset()
and id()
) as well as definitions for the required find_one()
, create()
, update()
and delete()
CRUD driver methods.
View Code
abstract public function find_one($options = array());
abstract public function create(array $data);
abstract public function update(array $data);
abstract public function delete();
The Driver Class
This class extends the parent Gateway_Model
adapter class and contains logic to handle the core CRUD methods for Authorize.net - such as the interacting with the SDK to retrieve a payment method from Authorize.net.
View Code
public function find_one($options = array())
{
...
$request = new AuthorizeNetCIM();
$response = $request->getCustomerPaymentProfile($customer_id, $payment_method_id);
if (!$response->isOk()) {
Log::error('Unable to retrieve Authorize.net payment method.');
return null;
}
$credit_card = $response->xml->paymentProfile->payment->creditCard;
return array(
'id' => $response->getPaymentProfileId(),
'customer_id' => $customer_gateway_id,
'account' => '****' . substr($credit_card->cardNumber, -4),
);
}
Similar driver classes could be written to provide support for other payment gateways such as PayPal.
You can then instantiate an instance of a particular driver with something like:
$instance = Gateway::instance($driver_name = 'authorizenet');
Start Designing For Change Today
You shouldn’t over-architect your app and integration points - there’s no need to adopt the adapter pattern for every decision you make. The message here is that designing for change can add great flexibility and power to your application with relatively little effort. Here are three easy steps you can take when adding a major new piece of functionality to your app:
- Define a Standard: Figure out what’s common across all of the options/3rd-party solutions so you can define a standard (e.g., all shipping solutions provide basic price calculation and label generation).
- Abstract Your Code: Write a simple wrapper class rather than directly call SDKs and other 3rd-party specific code (e.g., write a Shipping class that then references the UPS SDK so you don’t have vendor-specific code all over your app).
- Categorize Your Data: Name certain elements of your data structures generically (e.g. use a “provider” field to track which 3rd-party / driver the data is related to).
What Do You Think?
As I’ve described above, the adapter design pattern can have a dramatic impact on your app and development cycle. I’d love to hear your thoughts and experiences with using this approach in your apps. You can find me @smwxforever.