I'm putting together a set of posts on patterns or practices that I've found useful at work. This is the first in that series. I covers how I used the Strategy Pattern to re-write an Application Programming Interface (API) that we use at work.
Background
Communication Manager
Step 1: Define the Family of Algorithms
Thus far we have abstracted out the class that will need to
perform the behaviors (the calling class or connector class) and the behaviors
that anyone who subclasses it will need.
Step 2: Encapsulate each one
3. Make them interchangeable
Pretty simple right? Okay, so now to pull this all
together, they would simply extend the ConnMan abstract class to create the
concrete class. Check this out:
I hope that this article has been a help to you. If you found it helpful or if you have something that you would like to add, feel free.
Background
Ironically, one of the constants of software engineering is
change. The point of developing Application Programming Interfaces (API)
is to create tools that are adaptable and flexible enough to accommodate
anticipated changes.
A good place to start, when determining how to best approach
an API, is to look at aspects of it that change. What behaviors, actions, etc,
are likely to change? Separate those behaviors and provide an interface for
them. In other words, take what varies and encapsulate it so it won't
affect the rest of your code.
In the November/December 2009 Issue of Computer Magazine, an
article titled "What Makes API's Hard to Learn? Answers from Developers",
by Martin P. Robillard conducted a study
that concluded that the majority to problems developers have when learning new
API's is documentation (we'll address this in another article), the other was complexity.
The approach taken in this redesign incorporates the
Strategy Pattern, which is defined as:
Strategy Pattern:
·
Define a family of algorithms
·
Encapsulate each one
·
Make them interchangeable
|
Communication Manager
We are going to look at creating a communication
manager. This will allow developers to
send data to and receive data from another system (perhaps a back end system or
legacy system).
Problem Definition
We need a system that will send messages or commands to
another system and receive messages or responses from that system. However,
there are multiple systems with which we need to communicate. In addition, some
of these systems do not always return data in the form that we need it.
Step 1: Define the Family of Algorithms
It is easy to see that what varies with regard to the
connection components are:
·
The way they choose to send data
·
The way they choose to receive data (to a lesser
extent)
Therefore, these behaviors are separated out and made into
interfaces.
public interface SendBehavior
{
Object sendData(Object
obj);
}
|
public interface
RecBehavior
{
Object recData(Object
obj);
}
|
Step 2: Encapsulate each one
Let's take a closer look at the main class our “caller”, so
to speak. We know it will need to perform the behaviors we have separated
out. So how do we handle that? Check out the code below:
/**
* ConnMan will employ the Strategy Pattern in that behavior will be taken out * of core classes and will allow for "plug-in"s rather than making changes to * the core code. * */
public abstract class
ConnMan {
SendBehavior sendBehavior; RecBehavior recBehavior;
// Constructor
public ConnMan() { }
// Setter for Sending
Behavior
public void setSendBehavior(SendBehavior sendBehavior) { this.sendBehavior = sendBehavior; }
// Setter for Receiving
Behavior
public void setRecBehavior(RecBehavior recBehavior) { this.recBehavior = recBehavior; }
/**
* Perform Send Data (performSendData) This method will perform the sendData * method of the various classes that implement the SendBehavior interface. * We don't need to know which one as its behavior will be set by the * setSendBehavior method. */
public Object
performSend(Object obj) {
return sendBehavior.sendData(obj); }
/**
* Perform Receive Data (performRecData) This method will perform the * recData method of the various classes that implement the RecBehavior * interface. We don't need to know which one as its behavior will be set by * the setRecBehavior method. */
public Object
performRec(Object obj) {
return recBehavior.recData(obj); } } |
3. Make them interchangeable
The implementations that will perform the
"performSend" and "performRec" behaviors are separate and
might look something like this:
public class JMSSend
implements SendBehavior
{ /** * sendData for JMSSend will format * the incoming domain object into the *format expected for Java * Messenger Service and send it to a * pre-configured WebSphere message * queue. @Override public Object sendData(Object obj) {
// TODO:
// 1. Convert domain object to XML // 2. Send message // 3. Retrieve Response // 4. Return response as object
return "I used JMSSend
with " + obj;
}
}
|
public class RecXML
implements RecBehavior
{
/**
* recData for RecXML will take the object returned from * sendData and convert it to (in this case) XML. * */
@Override
public Object recData(Object obj) {
// TODO
// 1. Convert object received from sendData to XML
return "I used RecXML to
convert sendData= " + obj + " to XML";
}
}
|
public class
ConnectionMgr extends ConnMan {
public
ConnectionMgr() {
}
public void
display() {
System.out.println("Send
mechanism is " + sendBehavior.toString()
+ "
and Receive Mechanism is " + recBehavior.toString());
}
} |
A factory
creates the objects:
public class
ConnManFactory {
// Used to create instances of implementations. Instead of
users
// using new to create the instances, the factory will do it based // on their selection. Again, this is just for illustrative purposes, // under true implementation, we would probably use something // other than hardcoded values to find implementation choices.
SendBehavior connManSendFact(ConnectionMgr cm, String simpl) {
SendBehavior ret_s;
ret_s = null;
if (simpl != null)
{
if (simpl
== "JMS")
{ ret_s = new JMSSend(); } if (simpl == "XML") { ret_s = new XMLSend(); }
}
return ret_s;
}
RecBehavior connManRecFact(ConnectionMgr cm, String rimpl) {
RecBehavior ret_r;
ret_r = null;
if (rimpl != null)
{
if (rimpl
== "XML")
{ ret_r = new RecXML(); }
}
return ret_r;
}
}
|
Another solution you could use is to use a Properties or XML file and reflection and the .newinstance() method to create the objects on the fly. You could simply have a setup file for your users that could be used to specify the implementation they'd like to use. I'll cover this in detail in another article.
In short, it all comes together like this:
And to see how this works in action, let's create a small
application class that uses it and see what happens:
public class AppTestConnMan {
/**
* PROOF OF CONCEPT--Strategy Pattern for the Connection Manager * can use whatever send behavior or * receive behavior it wants to. In other words, the application sets the * behaviors (instead of selecting from a hard-coded list) allowing them the * flexibility to send the data in whatever format they want to and to * receive the data in whatever format they want. * * @param args */
public static void main(String[] args) {
ConnectionMgr cm = new ConnectionMgr();
/**
* The send and receive behaviors are set by application developers The * implementations can be either implementations written by us or * written by the AppDev's...it's their choice. The API never needs to * know which behavior they choose to implement. * * In actual implementation, we could set it up so that the value for * impl could come from an XML (preferably) or Properties file, but for * the sake of this test, we'll just hardcode a string. * */
ConnManFactory cmf = new ConnManFactory();
String simpl = "JMS";
String rimpl = "XML";
SendBehavior sret = cmf.connManSendFact(cm, simpl);
RecBehavior rret = cmf.connManRecFact(cm, rimpl);
cm.setSendBehavior(sret);
cm.setRecBehavior(rret);
// Or, they
user can use their own implementation and specify it here:
// cm.setSendBehavior(new
JMSSend());
// cm.setRecBehavior(new
RecXML());
/**
* The AppDev's create the domain object and sends it via the sendData * method that is in all implementations that extend * * The receive behavior can actually be optional. If the AppDev is * fine with receiving the data as an object, they need not go any * further. If, however, they would like the data converted to * something else, they can choose to use a receive behavior to * convert it. */
Object s_obj = "sent
stuff"; // very simple domain object
Object me_sOut = cm.performSend(s_obj); Object me_rOut = cm.performRec(me_sOut);
cm.display();
System.out.println("Output from performSend: " + me_sOut); System.out.println("Output from performRec: " + me_rOut);
}
}
|
And here is the output from running this simple app:
Send mechanism is connMan2.JMSSend and Receive Mechanism is
connMan2.RecXML
Output from performSend: I used JMSSend with sent stuff Output from performRec: I used RecXML to convert sendData= I used JMSSend with sent stuff to XML |
Simple, flexible, easy to maintain. If someone wants
to utilize their own implementation, they can by simply set another value for setSendBehavior
value (even if it is one that they've written).
No comments:
Post a Comment