Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Ajax Patterns And Best Practices (2006)

.pdf
Скачиваний:
39
Добавлен:
17.08.2013
Размер:
15.94 Mб
Скачать

C H A P T E R 1 1 R E S T - B A S E D M O D E L V I E W C O N T R O L L E R P A T T E R N

349

}

catch (IOException e) {

System.out.println( "oop error (" + e.getMessage() + ")");

}

The type NameValuePair defines an array of key value pairs that are assembled and URLencoded into a query string. The method setQueryString converts the array into a query string. To execute the HTTP request, the method executeMethod is called. If the return code is 200, the request was successful. Because the HTTP request was successful does not mean that the response will contain any data. To know if there are any results, a parser will need to inspect the response. The undefined method processResults converts the response from an XML stream into a result that is added to the controller (further details of this method are beyond the scope of this chapter). The method getQueryIdentifier is used to identify which query identifier the result is associated with. The query identifier is part of the Persistent Communications pattern and is used to identify which query a result belongs to.

Using Google to Search for Something

Google allows outside developers to access their search engine technologies by using the SOAP web service API. In the example, the Java-based Axis 1.x engine was used to convert a Web Services Description Language (WSDL) file into a client stub. The client stub performs an automatic serialization of the XML data. In essence, a WSDL file does the same thing as an XML schema file used to generate a serialization stub. The serialization stub contains a number of types that are used to serialize and deserialize XML. For reference purposes, a WSDL file does contain an XML schema file.

The following source code illustrates how to call the Google search engine web service:

String queryIdentifier = _parent.getQueryIdentifier(); GoogleSearch searchRequest = new GoogleSearch();

if( _endPoint.length() > 0) { searchRequest.setSoapServiceURL( _endPoint);

}

searchRequest.setKey( _key); searchRequest.setQueryString( _request.getQueryString()); try {

GoogleSearchResult searchResult = searchRequest.doSearch(); if( searchResult != null) {

GoogleSearchResultElement[] results = searchResult.getResultElements();

for( int c1 = 0; c1 < results.length; c1 ++) { _parent.addResult( new SearchResult(

results[ c1].getURL(), results[ c1].getTitle(), results[ c1].getSnippet(), transactionIdentifier));

}

}

}

catch (GoogleSearchFault e) { return; }

350

C H A P T E R 1 1 R E S T - B A S E D M O D E L V I E W C O N T R O L L E R P A T T E R N

The Google search implementation is simpler than Amazon.com’s because it uses the generated client stub. All of the classes that are prefixed with the Google identifier are the generated classes. The variables _endpoint and _key are values from a configuration file. The variable _endpoint is used to define the server called to execute a search. The variable _key is the Google access identifier that serves the exact same purpose of identification as the Amazon.com access identifier key. The variable queryIdentifier is the client-provided query identifier if an asynchronous request is made. If a synchronous request is made, the query identifier length is zero. In the implementation, though, the method addResult is always called with a query identifier. This is okay, because the servlet or handler that converts the results into XML (or HTML, or other content returned to the client) will know whether or not to process the query identifier.

When the executed search responds, the found entries are added to a result set by converting the results to the type SearchResult. The found types are not converted into XML because that would couple the results to a specific data format. This would be problematic for the Permutations pattern, which generates the format that the client wants to see and thus prefers to manipulate objects and not have to parse an XML file again.

Creating a Search Engine Client Infrastructure

I have very quickly described the implementations that execute a search on Amazon.com and Google. My objective is not to explain how the Amazon.com and Google search engine APIs function. My objective is to illustrate the following requirements used to finish implementing

acontroller:

The request information provided by the client will in most cases not be enough to perform a request to the remote servers. A client can provide the extra information, but that should be avoided whenever possible because a dependency to a specific implementation is created.

Extra request information would be stored as configuration items that are loaded by the controller and passed to the local client. Hard-coding any of the parameters is not advised.

The local clients should not couple themselves to specific data formats or types. This means the local clients should not assume XML, and should not assume being called from a specific controller technology such as a Java servlet or ASP.NET handler.

These requirements dictate that the controller implementation should be kept as general as possible. Individual model details are managed by the local clients that convert the specifics into a general model used by the controller. However, the reality is that there are specifics. For example, the Amazon access identifier requires extra information stored in a configuration file or sent by the client to the controller that is then sent to the local client. Programmatically, being generic and specific at the same time is impossible or at least it can seem impossible. The solution to this dilemma is to use the Extension pattern.

The purpose of the Extension pattern is to be both a general and a specific solution. The best way to understand this is to consider the following source code:

C H A P T E R 1 1 R E S T - B A S E D M O D E L V I E W C O N T R O L L E R P A T T E R N

351

interface General { }

interface Specialization { public void Method();

}

class Implementation implements General, Specialization { public Implementation( String extraInfo) { }

public void Method() { }

}

class Factory {

public static General CreateInstance( String extraInfo) { return new Implementation( extraInfo);

}

}

The interface General is a minimal interface, a sort of placeholder. In the example, the General interface has no methods, but there could have been methods. The aim is to keep methods and property declarations to a minimum. The other interface, Specialization, has a single method, but is an interface used to specialize or provide a specific functionality not offered by the General interface.

Where the Extension pattern comes into play is when the class Implementation implements both General and Specialization. A user of Implementation would see the General interface, but could carry out a typecast that converts General into Specialization, as illustrated by the following source code:

Specialization specialized = (Specialization)genericInstance;

Notice that a typecast was made from one interface to another interface, and not to the implementation type Implementation. This is the essence of the Extension pattern, where interface instances are typecast to the required interface, assuming that the interface instance implements all of the required interfaces. By using the Extension pattern, a framework can deal with objects generically, and then by using typecasting can ask for specialized functionality. You might ask, “Why not just pass around the type Object, because Object is very generic—and after all, you are typecasting, and typecasting an Object is easy.” Passing around Object is not suitable because Object is too generic. Even though the Generic interface had no methods, it is still a type that indicates whoever implements Generic does realize that there are other interfaces that could be implemented as well. Using Object says that any object can be stored, even an object that has absolutely nothing to do with the problem being solved.

Although I’ve said that you do not typecast to Implementation, but to an interface, there are occasions when typecasting to Implementation would be acceptable. For example, sometimes it would be silly to implement an interface for the sake of implementing an interface, because the derived type would be used only in a single solution domain space. What’s more, that scenario will be illustrated by the types SearchResult and SearchRequest.

With the advent of Java 1.5 and .NET 2.0, another programming technique called generics is available. Generics, in conjunction with constraints, could very well be used to implement the Extension pattern, but it is beyond the scope of this chapter. Those interested in further

352

C H A P T E R 1 1 R E S T - B A S E D M O D E L V I E W C O N T R O L L E R P A T T E R N

details should seek a book on that topic. For the .NET developers, I recommend my book,

Foundations of Object-Oriented Programming Using .NET 2.0 Patterns (Apress, 2005) as it goes into detail regarding the use of .NET generics.

Defining the Abstracted REST-Based Model View Controller Pattern

Finishing the controller implementation means applying the Extension pattern for two levels of abstraction. The first level is the general case of implementing the REST-Based Model View Controller pattern. The second level is the case of implementing a search engine based on the REST-Based Model View Controller pattern. This section focuses on the first level of abstraction.

The controller manages the local clients. In the context of the REST-Based Model View Controller pattern, the controller fulfills the abstract role of executing the local clients, managing the local clients, managing the request, and managing the results that will be returned to the client. The controller exposes itself to the local client by using an interface called Parent that is defined as follows:

public interface Parent {

public void addResult( Result result); public Request getRequest();

public void addCommand( Command cmd); public Iterator getCommands();

public void processRequest( Request request);

public void processRequest( String type, Request request); public String getTransactionIdentifier();

}

The methods of Parent use general types such as Result, Command, and Request. Result defines a result generated by a local client. Request defines the HTTP request parameters such as the query string. And the local clients implement Command. There are two variations of the method processRequest. The processRequest with a single parameter will execute a search on all local clients. The processRequest with two parameters has as a first parameter the identifier of the local client that will process the request and generate the results (for example, amazon).

The Request and Result interfaces are defined as follows:

public interface Result {

}

public interface Request {

}

The interfaces have no method implementations and therefore represent pure general types, as illustrated by the Extension pattern example. The local clients implement the Command interface, which is defined as follows:

public interface Command {

public void setRequest( Request request); public void assignParent( Parent parent); public String getIdentifier();

}

C H A P T E R 1 1 R E S T - B A S E D M O D E L V I E W C O N T R O L L E R P A T T E R N

353

The method assignParent is used to assign the parent controller with the local client. The association is needed when the local client generates a result and wants to pass the result to the controller, which then passes it to the client. The method getIdentifier is used by the implementation of the method Parent.processRequest( String type, Request request) to identify which Command instance is executed.

Implementing the Search Abstractions

The search engine local clients (Amazon.com and Google) implement two interfaces: Command and Runnable. The search engine local clients are managed by Parent using the Command interface, but Parent executes the local client by using the Runnable interface. The reason is that the controller executes each local client on its own thread. The reason for an individual thread will be discussed shortly. An example implementation of the Amazon.com search engine local client would be as follows (note that some details have been removed for clarity):

public class AmazonSearchCommand implements Command, Runnable { private String _endpoint;

private String _accessKey; private String _secretAccessKey; private Parent _parent;

public void assignParent( Parent parent) { _parent = parent;

}

public AmazonSearchCommand( String endpoint, String accessKey, String secretAccessKey) { _endpoint = endpoint;

_accessKey = accessKey; _secretAccessKey = secretAccessKey;

}

public String getIdentifier() { return "amazon";

}

}

The run method implementation has been removed and was already shown in the section “Using Amazon.com to Search for Something.” What has been kept are the details relating to the instantiating and configuring of the Amazon.com local client. The method getIdentifier is a hard-coded string that returns the identifier amazon. Normally, hard-coded strings are a bad idea, but because the Amazon.com local client is being referenced, the identifier is not going to change. Let’s put it this way: you are not going to reference the Amazon.com local client as Google or Barnes & Noble. The identifier amazon is identical to the URL /search/impl/amazon. The same value is not a coincidence because when the /search/impl is retrieved, the generated links are generated by the controller that iterates the local clients, which in turn are queried by using the method getIdentifier.

In the section “Using Amazon.com to Search for Something,” there were references to configuration items such as the access key. The configuration items are passed to the client using the constructor. The constructor was chosen so that under no circumstances can the

354 C H A P T E R 1 1 R E S T - B A S E D M O D E L V I E W C O N T R O L L E R P A T T E R N

Amazon.com local client be instantiated without having a valid configuration. The use of passing configuration items by using the constructor would apply to the Google search engine client and any other local client.

To wire the local clients to the controller, another method that implements the Builder pattern is used. An example of implementing the Builder pattern for the local clients is as follows:

public class SearchBuilder {

private static String _amazonEndPoint; private static String _googleEndPoint; private static String _amazonAccessKey; private static String _amazonSecretKey; private static String _googleAccessKey; private static boolean _didAssign = false;

public static void assignConfiguration( String amazon, String amazonAccessKey, String amazonSecretKey, String google, String googleAccessKey) { _amazonEndPoint = amazon;

_amazonAccessKey = amazonAccessKey; _amazonSecretKey = amazonSecretKey; _googleEndPoint = google; _googleAccessKey = googleAccessKey;

if( _amazonEndPoint == null || _amazonEndPoint.length() == 0 || _googleEndPoint == null || _googleEndPoint.length() == 0 || _amazonAccessKey == null || _amazonAccessKey.length() == 0 || _amazonSecretKey == null || _amazonAccessKey.length() == 0 || _googleAccessKey == null || _googleAccessKey.length() == 0) {

throw new IllegalStateException( "configuration data invalid");

}

_didAssign = true;

}

public static void buildCommands( Parent parent) { if( ! _didAssign) {

throw new IllegalStateException( "configuration data not assigned");

}

parent.clearAllCommands(); parent.addCommand( new AmazonSearchCommand(

_amazonEndPoint, _amazonAccessKey, _amazonSecretKey)); parent.addCommand( new GoogleSearchCommand(

_googleEndPoint, _googleAccessKey));

}

}

The class SearchBuilder has two static methods: assignConfiguration and buildCommands. The method assignConfiguration assigns the default configuration to the Amazon.com or Google local clients when the local clients are instantiated. In the example, the configuration values are referenced as simple strings, but those strings could have been converted into types, and the method assignConfiguration could have referenced those types. Converting the strings would probably have been a good idea because five parameters can become a bit

C H A P T E R 1 1 R E S T - B A S E D M O D E L V I E W C O N T R O L L E R P A T T E R N

355

tedious to maintain. Shown only with a basic amount of code is the validation of the data in the assignConfiguration method. Validating the data is good practice so that whenever local clients are instantiated, they are instantiated with valid values.

The other method, buildCommands, adds local client-instantiated objects to the controller that can be executed whenever a request for execution happens. In the implementation of buildCommands, the method clearAllCommands removes all of the past instantiated Command instances. The old local client instances are cleared so that multiple threads do not use the same local client instances. The method addCommand is called to add the Amazon.com local client and Google local client instances to the controller. When the method buildCommands returns, the Parent interface instance contains a collection of Command implementations that can be called to perform some action and generate results.

One last detail is to explain the implementations of the Result and Request interfaces, which are illustrated as follows:

public class SearchRequest implements Request { private String _query;

public SearchRequest( String query) { _query = query;

}

public String getQueryString() { return _query;

}

}

public class SearchResult implements Result { String _url;

String _title; String _snippet;

String _transactionIdentifier;

public SearchResult( String url, String title, String snippet, String transId) { _url = url;

_title = title; _snippet = snippet;

_transactionIdentifier = transId;

}

public String getTransactionIdentifier() { return _transactionIdentifier;

}

public String getURL() { return _url;

}

public String getTitle() { return _title;

}

public String getSnippet() { return _snippet;

}

}

356

C H A P T E R 1 1 R E S T - B A S E D M O D E L V I E W C O N T R O L L E R P A T T E R N

While I was explaining the Extension pattern, I recommended that you use interfaces to perform typecasts, but in this case there are only the base Request and Result interfaces. There are no SearchResult and SearchRequest interfaces because the classes SearchResult and SearchRequest are specific to the domain of searching. The likelihood that the classes SearchResult and SearchRequest would be used in a different context is fairly unlikely. Though we still want to implement the Extension pattern, we don’t need to use an interface, but can use an interface and a class declaration.

The other item to note is that both SearchResult and SearchRequest are immutable types. An immutable type is a type that once assigned cannot be modified. In the case of SearchResult and SearchRequest, the data members are assigned in the constructor. The only methods exposed allow the retrieval of the data members, but not modification or assignment.

Putting All of the Pieces Together

The final step after defining the architecture and implementing the individual pieces is to put everything together into a working solution that can be called a REST-Based Model View Controller pattern. From an architectural perspective, a Java servlet or ASP.NET handler will interact with a Parent implementation. The Parent implementation is what pulls everything together and defines the model, view, and controller. Putting it all together, the architecture would appear similar to Figure 11-8.

Figure 11-8. Architecture with all of the pieces assembled

The UML diagram in Figure 11-8 looks complicated, but it can be separated into two blocks of functionality. There is an inner circle of interfaces and an outer circle of implementations. The inner circle has the types Request, Result, Parent, and Command. The outer circle has the types

Amazon, Google, SearchResult, SearchRequest, ParentImpl, HttpServlet, and AsynchronousServlet.

C H A P T E R 1 1 R E S T - B A S E D M O D E L V I E W C O N T R O L L E R P A T T E R N

357

Parent is the core of the entire system and is the bridge that binds all the pieces together. However, to keep it simple for Parent, Parent knows about only the inner circle of types. In the diagram, Google, Amazon, and HttpServlet know about the outer circle of types (SearchRequest, SearchResult) that are passed across the bridge.

Implementing a Parent

Implementing the Parent interface is a two-step process because the Parent interface plays the central role of processing the data. Let’s consider the context. The Parent interface instance is responsible for executing the Command implementations, gathering the results, and making the request information available. Through all of these responsibilities, the Parent interface cannot use specific types but must use the general defined types. Additionally, the Parent interface implementation has to function whether the request is asynchronous or synchronous.

The first step when implementing Parent is to define a base class that provides a certain amount of common functionality. The second step is then to create either an asynchronous or synchronous implementation. You need to separate an asynchronous implementation from a synchronous one because of how the results and threads are managed.

Implementing the Base Class

Before the synchronous and asynchronous Parent implementations are outlined, the first step is to outline the base type. The class ParentBase implements Parent, and a subset of the implemented functionality is outlined as follows (the remaining pieces will be explained in a moment):

public abstract class ParentBase implements Parent { private List _commands = new LinkedList();

public void addCommand( Command cmd) { _commands.add( cmd);

}

public Iterator getCommands() { return _commands.iterator();

}

public void clearAllCommands() { _commands.clear();

}

The code excerpt shows that the individual local client instances (Command) are managed in a LinkedList. To add a local client, the method addCommand is used. To remove all local client instances, the method clearAllCommands is used. Because we are coding in a managed code environment, removing the Command instances does not equate to deleting them. They will be deleted when there are no references to the local client instances. This is important because when the local client instances are cleared, the threads referencing the local client instances will still be executing. It would be very inappropriate to have to wait until all of the old local client instances have finished executing, or to stop the execution in midstream.

The remaining functionality implemented by ParentBase relates to executing the local clients through the Command interface. The execution of the local clients is on a per thread basis. Each local client is allocated a thread so that the individual executions can occur concurrently. Some readers may comment that spinning off an individual thread in a heavily multithreaded environment is inefficient. Granted, the statement is true, but consider the context, where the

358

C H A P T E R 1 1 R E S T - B A S E D M O D E L V I E W C O N T R O L L E R P A T T E R N

greater cost is the waiting time of the network communications. Therefore, to be robust, it is best if each local client waits individually and indicates to the controller when they have completed. While an individual thread is waiting, it is not consuming resources, and therefore having multiple waiting threads is not a problem for the server. Following is the implementation of the command execution:

protected List _runningThreads = new LinkedList(); public abstract void addResult(Result result);

public void processRequest( Request request) { Iterator iter = _commands.iterator(); _runningThreads.clear();

while( iter.hasNext()) {

Command cmd = (Command)iter.next(); cmd.setRequest( request); cmd.assignParent( this);

Thread thrd = new Thread((Runnable)cmd); _runningThreads.add( thrd); thrd.start();

}

}

public void processRequest( String impl, Request request) { Iterator iter = _commands.iterator(); _runningThreads.clear();

while( iter.hasNext()) {

Command cmd = (Command)iter.next();

if( cmd.getIdentifier().compareTo( impl) == 0) { cmd.setRequest( request);

cmd.assignParent( this);

Thread thrd = new Thread( (Runnable)cmd); _runningThreads.add( thrd);

thrd.start();

break;

}

}

}

The data member _runningThreads is a list of threads that are executing. The list is required by the synchronous or asynchronous controller implementations to know when a thread has completed. The method addResult, which is used to add a result to the controller, is defined as abstract because the synchronous or asynchronous implementations define their own way of managing the results. You will see this difference shortly. The processRequest methods are used to execute the Command interface instances. There are two versions of the processRequest method. The version with a single parameter executes all local clients. The version with two parameters executes a specific local client.

Regardless of whether a single local client or all local clients are executed, they are executed on their own threads. This keeps the architecture simple so you don’t have to deal with too many architectural variations.