Server Requests
Line 174: | Line 174: | ||
==== Design Decisions ==== | ==== Design Decisions ==== | ||
+ | |||
+ | === Object vs Class to represent a request === | ||
+ | It is apparent that there is a reduction in the number of classes modelling server requests. This was as a result of altering the design so that it complies with the design maxims [[Model classes not roles]] and [[Define classes by behavior, not state pattern]] (issues 1 and 2 in '''Design Flaws''' section for initial design. I decided that a single Request class was sufficient to model most of the requests, because it became apparent that the behaviour of all the subclasses in the initial design was identical. The only difference was the amount of data sent with each request, thus the design maxims mentioned here applied well - modelling the bulk of requests as objects and roles was more appropriate than having a class per role. Currently, instead of having separate classes, each Request can be created from the required data appropriate to its desired role. This also reduces the class hierarchy and lazy class problems identified in issues 3 and 4 in the '''Design Flaws''' section. | ||
+ | |||
+ | === The abstract base class discussion === | ||
+ | I decided Riel's heuristic [[Abstract classes should be base classes]] does not apply in this case, because artificially adding an abstract base for requests would cause further maxim violations. The base class Request is perfectly valid as concrete because it itself can be used to model a wide range of generic requests to the server. It's method implementations suffice for both this general application and for possible subclasses. Having an abstract base class with the share methods implemented would have violated [[Eliminate irrelevant classes]], as concrete instances with no implementation would have been needed to begin using the class. | ||
+ | |||
+ | === The no-op methods in the inheritance hierarchy === | ||
+ | Representing requests as objects instead of separate classes is sufficient for most of the requests, so an elaborate hierarchy is not needed. The only two requests that cannot be fully represented by the Request class are requests for uploading calibration data (CalibrationDataUploadRequest) and for retrieving registration data from the server (RegistrationDataRetrievalRequest). CalibrationDataUploadRequest needs to send an additional confirmation key after a successful execution, while RegistrationDataRetrieval needs to populate a data object share with the rest of the application. The ConfirmOutcome() and PopulateData() methods were defined for every Request to allow this highest level abstraction to be used even with the concrete subclasses [[Stable abstractions principle]]. These methods are implemented as no-ops for most requests except for the specific subclasses mentioned. | ||
+ | |||
+ | This introduces a coupling of the base abstraction (Request) to the two specific subclasses, and introduces methods to the interface are unnecessary for most instances. However, this design choice was made as a result of following the [[Tell, don't ask]] principle, which suggests that each object should be responsible for decisions made based on its own state. In this manner, subclasses can be handled through the base Request abstraction and conform to the hierarchy. The alternative would have been creating subclasses for CalibrationDataUploadRequest and RegistrationDataRetrievalRequest that add methods outside the base abstraction, which means that downcasting would be required to access the additional functionality required. This would violate the [[Avoid downcasting]] principle, as well as introduce much stronger coupling between the client and the Request model. | ||
+ | |||
+ | |||
+ | As such these two requests do not conform to the original generic Request interface. Trying to mould them to fit the interface would have eventually required downcasting, so that | ||
+ | |||
+ | 1. Model roles as object not classes | ||
+ | 2. Use tell don't ask to get work done by classes, not move it around - hence having to add methods | ||
+ | 3. Not using abstract base | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
Revision as of 02:50, 1 October 2010
Contents |
Background
My design study is related to my part time job, where I am developing an application that can retrieve and plot data from a device that monitors air quality. The application runs on the user's PC and connects to the device via a USB to serial bridge. The following features of the application/device combination are of interest to this study:
Handling Device Registrations
- When the device is connected to the app, internet connectivity can be used to electronically register the device.
- If a user has problems with registering a device, they can use the app to send an assistance request to the manufacturer.
- Lastly, a user can view the registration details of the current device, if they know the e-mail address the device was registered with.
Handling Device Calibrations
- The application allows the user to calibrate a device using an automated process. Only authenticated users may perform calibrations.
- Further, the manufacturer stores a complete copy of the calibration process, and final calibration parameters.
- Lastly, a device may only be calibrated (new parameters applied) after automated approval. The manufacturer is notified when the new parameters are applied to the device.
All notifications/data exchanges with the manufacturer are via HTTPS POST traffic to a webserver. The focus of this design study is the design of the application-side model for the various requests needed to interact with the server in handling registration and calibration requirements as above. In the initial design, the server request necessary for each of these bullet points is modelled as a class. In the revised design, I look at improving that model by using objects to represent the different requests instead of a class-per-request approach.
Requirements
Practically, for the purposes of my work on the application the requirements are largely generic and as follows:
- something that works
- is as nicely designed as practical
- is as easy to understand and maintain as practical
- is flexible so that new server requests can be added without much hassle
Of course, for the purposes of this design study the goal is additionally to:
- achieve a good balance of flexibility, practicality, and a decent level of adherence to good-OO principles
Server Requirements
POST request strings sent to the server may additionally need to contain a checksum parameter that will be used to ensure requests have not been altered during the network transmission process.
Deliverables
- Classes that model the different request types
- Classes that allow requests to be sent over the network
Initial Design
I built this design over a short period a few months ago. At the time I had just read a bunch of articles that didn't clearly define the advantages of using composition over inheritance, so I proceeded and made a mess of interfaces/composition and created the following design. It doesn't feel elegant, but it works and is what has been running app-server interactions since. Here I have renamed some of the classes to more general names.
How it Works
Representing Requests
The IRequest interface is the most generic representation of a server request. In the style of HTTP POST/GET requests, a request is defined simply as something which can return a list of name-value pairs (i.e. something that has a GetPOSTParameters() method), representing the request parameters and their names (in particular a .NET 2.0 NameValueCollection). In the context of the application, the children of XRequest define exactly which parameters are required/optional for a particular request and their types.
XRequest is the abstract implementation of IRequest, implementing the shared functionality (send/get response) for all child classes. Each concrete request is then defined by a particular implementation of GetPOSTParameters() method - returning a list with different name-value parameter contents.
The requests represented by the classes in the diagram above are as follows:
- CalibrationAuthReq - represents an authentication request; POST data: username, password, device serial number;
- CalibrationUploadReq - represents an attempt to upload calibration data to the server; POST data: username, password, serial, calibration log-1, calibration log-2, calibration value-1, calibration value-2; This request returns a confirmation code, which must be re-sent to the server to confirm that calibration parameters were applied to a device.
- CalibrationConfirmationReq - represents the request that notifies the server that calibration parameters have been applied to a device. POST data: username, password, serial, calibration confirmation code;
- RegistrationAssistanceReq - the request which submits an assistance request to the server; POST data: user's name, user's phone, user's email, device serial, user notes;
- RegistrationDataReq - used to request the details of an existing registration; POST data: email the device was registered under, device serial;
- RegistrationSubmitReq - used to submit data for registration. POST data: company name, contact name, address, country, email, phone, distributor, distributor phone, serial, user notes;
Sending Requests
Server requests are sent, and responses received using concrete insteances of IRequestSender. HttpPOSTRequestSender is the most generic concrete implementation of this interface - its methods implement the actual sending/receiving mechanisms. XRequestSender is a class with a specific implementation of the IRequestSender interface for the application's purpose. It uses a composition relationship, containing an HttpPOSTRequestSender instance. Functionally, after computing and attaching a checksum to a given IRequest, the class is used to delegate Send/Receive calls to the contained HttpPOSTRequestSender.
Using the classes together
The application creates concrete instances of the request classes, and interacts with them using the Send/GetResponse methods. The individual request objects then interact with the server using supplied instances of IRequestSender (concretely, XRequestSender). The application does not need knowledge of the IRequestSender family of objects, except for creating them and supplying them to request objects. Perhaps a factory method/class could tidy this up slightly.
Design Flaws
At a very superficial glance this does not feel right. The following are the issues & violated heuristics that I can verbalize about the bad feeling:
- 1.Model classes not roles - Many small classes with the same behaviour, just work on different values of the particular data. Better suited as objects.
- 2.Define classes by behavior, not state pattern - Similar point as above. Classes should model behaviour (or an interface) more so than different type of data (state).
- 3.Class hierarchies should be deep and narrow - Possile to have overly specialized classes at few instead of multiple layers of the inheritance hierarchy.
- 4.Lazy class smell - The numerous small classes inheriting from XRequest could be considered lazy. They don't do much extra from the superclass.
- 5.Violates Acyclic dependencies principle, nearly has Circular dependency between IRequestSender and XRequest - from their replationship, it is unclear which actually performs the sending.
- 6.A client consumes the services of the request classes by directly instatiating instances of the concrete classes, because there is no creation mechanism. This violates the Stable abstractions principle and Program to the interface not the implementation, and couples the client to specific derived class implementations. Additionally this also violates Avoid inheritance for implementation - the XRequest class is used only to factor out common functionality for its children.
- 7.Sequential coupling exists in both hierarchies of IRequestSender and IRequest.
Use of Patterns
The use of patterns was very restricted (I didn't have patterns in mind when designing this initial version). The following are roughly present:
- Bridge pattern used to separate interface/implementation dependencies between IRequest and IRequestSender instances.
- A distorted Proxy for composition: XRequestSender is composed of, and forwards most method calls to an HttpPOSTRequestSender, additionally containing (private) methods for calculating & adding a checksum.
Improved Design
With the improved design I tried to produce a more general and more elegant representation for request and the sending system. The approach I took focussed on trying to create a design that reduces many of the flaws listed above. The largest design differences are in:
- The addition of creation mechanisms that decouple a client from having to deal with concrete classes, and maintain the strength of the design's abstractions.
- Moving to a generic Request class to represent behaviour common to all requests, and can be used to represent the different requests as variations in data/state instead of by needing separate classes.
The UML diagram of the revised design follows. ClientApp is a fictituous class I invented to generally summarize the kind of operations the application performs. It is during and as a result of these operations that the classes here are used. In addition, ClientApp shows the dependency replationships between the application and the abstractions of the class model.
Design Diescription
I split up the design in two parts - the transport mechanism, and the modelling the requests to be transmitted through the transport mechanism. This section gives a tour of the design.
Request transport mechanism
Since the requirement is to use HTTP POST requests, the obvious choice was to create a set of better wrappers for the useful components of the .NET Web API than the initial design offered. On a functional level, the original design functioned, so the revised version restructures the structure of the interaction between the required classes. The transport mechanism occupies the left hand side of the UML diagram.
HttpPOSTRequestSender Family
Walkthrough
- HttpPOSTRequestSender is the abstract base class for concrete implementations of classes capable of sending POST requests to a server. For our purposes, POST requests are sent through POST query strings forming a chain of name-value pairs. An example query string might be "param1=value1¶m2=value2¶mN=valueN". For the purposes of this class, name-value data defining a query string is stored as entries in a NameValueCollection. This is the parameter the Send() method receives, and it returns a boolean indicating whether the POST string was sent successfully. The ReceiveResponse() method represents the means of obtaining the server response to a POST request, and has the most generic return type allowable - an array of bytes containing the response. Thus, the HttpPOSTRequestSender class acts as a base for a generic way of sending HTTP POST query strings to a server.
- BasicSender is the simplest concrete implementation of HttpPOSTRequestSender. It is constructed with a URL string which specifies the server location instances will transfer data to/from. It can send any request that is presented as a NameValueCollection as described above.
- DecoratedSender is the abstract baseclass from a Decorator pattern, and allows for the creation of decorated subclasses of HttpPOSTRequestSender. It adds the SetComponent() and a componentSender field that can be used to set/contain the HttpPOSTRequestSender instance whose behaviour will be decorated.
- ChecksummedSender adds the concrete decorator behaviour of appending a checksum prior to sending a request from a typical HttpPOSTRequestSender.
Design Decisions
Summary
The Decorator pattern is used to represent possible variations of HttpPOSTRequestSender instances. This was necessary to model the requirement of having checksums added to query strings before they are sent to the server. By using the pattern, the checksum could be added to an basic instance of HttpPOSTRequestSender via the AppendChecksum() method in ChecksummedSender. The roles in the pattern as applied are:
- Component - HttpPOSTRequestSender
- ConcreteComponent - BasicSender
- Decorator - DecoratedSender
- ConcreteDecorator - ChecksummedSender
Rationale
Inserting this behaviour in the HttpPOSTRequestSender hierarchy is a natural fit, because it is purely an issue with attaching a parameter needed by the server, and is not fit for modelling by the Request family of objects which represent the meaning of requests - Coupling and cohesion. Adding it anywhere else would reduce cohesion and increase coupling.
I considered two other alternatives to using the decorator pattern.
- Using a concrete HttpPOSTRequestSender with subclass ChecksummedSender that only does the extra step of adding a checksum. Problems: would violate Avoid concrete base classes.
- Using an abstract HttpPOSTRequestSender with two baseclasses: BasicSender and ChecksummedSender. HttpPOSTRequestSender holds the method implementations as they are the same for both subclasses. ChecksummedSender additionally adds a checksum before sending. Problems: this would go against Eliminate irrelevant classes and introduce a Lazy class smell as BasicSender and ChecksummedSender would have all inherited methods blank (implemented in superclass), but would have to exist to create a concrete instance of the abstract superclass.
Using the Decorator pattern allowed the desired flexibility without violating design maxims.
Creation of HttpPOSTRequestSender Family
Walkthrough
The AbstractFactory pattern is used to model the creation process.
- HttpPOSTRequestSenderFactory is the base abstract class for the creation of concrete HttpPOSTRequestSender instances. It has a single method, CreateHttpPOSTRequestSender() which takes in a URL string and returns a concrete HttpPOSTRequestSender configured for the specified URL.
- BasicSenderFactory is the concrete implementation of HttpPOSTRequestSenderFactory for creating instances of BasicSender.
- ChecksummedSenderFactory is the concrete implementation of HttpPOSTRequestSenderFactory for creating instances of ChecksummedSender.
Design Decisions
Summary
The Abstract Factory pattern is used to represent the different ways of creating an HttpPOSTRequestSender.
- AbstractFactory - HttpPOSTRequestSenderFactory
- ConcreteFactory - BasicSenderFactory, ChecksummedSenderFactory
- AbstractProduct - HttpPOSTRequestSender
- ConcreteProduct - BasicSender, ChecksummedSender
Rationale
I chose to employ a creational pattern as a direct way of remedying issue 6 from the Design Flaws section for the initial design. Employing the pattern decouples the client from having to know about the concrete subclasses that are being initialized, and allows access to HttpPOSTRequestSender instances via the highest level abstraction. Thus, the client can program to the interface, and not the implementation. This allows future extensibility and flexibility with adding new classes without having to change the client's dependencies. Employing the Abstract Factory pattern allows the principles from issue 6 to be now be supported instead of violated.
Modelling Requests
This is the improved design for modelling the different requests to the server.
Request Family
Walkthrough
- Request is the concrete base class for all requests. It represents a generic server request - meaning anything that is representable as a list of HTTP POST parameter name/value pairs. It represents request data as a NameValueCollection, and internally holds a reference to a HttpPOSTRequestSender used to interact with the server. The class allows the current request to be sent/executed and to receive an indication of whether the attempt was successful. This class implements two methods. The AddParameter() method can be used to populate/append to the NameValueCollection and hence define the data of the current instance. The Execute() method sends an instance's data to the server using the resident HttpPOSTRequestSender , and returns a boolean indicating whether the request executed successfully on the server. The ConfirmOutcome() and PopulateData() methods are implemented as no-ops, and are left as stubs for subclasses which respecitively need to further interact with the server or receive data from the server.
- CalibrationDataUploadRequest is a sublclass of Request that additionally implements the stub for ConfirmOutcome(). A CalibrationDataUploadRequest represents an attempt to transfer calibration data to the server, during which process the server returns a confirmation code. The additional responsibility of this class is to re-transmit the confirmation code to the server after a successful execution, indicating that the previously transmitted calibration parameters have been applied to a device.
- RegistrationDataRetrievalRequest represents Request instances that need to retrieve registration data from the server. It implements the PopulateData() method which reads in the transmitted server data into a an object shared with the rest of the application.
Design Decisions
Object vs Class to represent a request
It is apparent that there is a reduction in the number of classes modelling server requests. This was as a result of altering the design so that it complies with the design maxims Model classes not roles and Define classes by behavior, not state pattern (issues 1 and 2 in Design Flaws section for initial design. I decided that a single Request class was sufficient to model most of the requests, because it became apparent that the behaviour of all the subclasses in the initial design was identical. The only difference was the amount of data sent with each request, thus the design maxims mentioned here applied well - modelling the bulk of requests as objects and roles was more appropriate than having a class per role. Currently, instead of having separate classes, each Request can be created from the required data appropriate to its desired role. This also reduces the class hierarchy and lazy class problems identified in issues 3 and 4 in the Design Flaws section.
The abstract base class discussion
I decided Riel's heuristic Abstract classes should be base classes does not apply in this case, because artificially adding an abstract base for requests would cause further maxim violations. The base class Request is perfectly valid as concrete because it itself can be used to model a wide range of generic requests to the server. It's method implementations suffice for both this general application and for possible subclasses. Having an abstract base class with the share methods implemented would have violated Eliminate irrelevant classes, as concrete instances with no implementation would have been needed to begin using the class.
The no-op methods in the inheritance hierarchy
Representing requests as objects instead of separate classes is sufficient for most of the requests, so an elaborate hierarchy is not needed. The only two requests that cannot be fully represented by the Request class are requests for uploading calibration data (CalibrationDataUploadRequest) and for retrieving registration data from the server (RegistrationDataRetrievalRequest). CalibrationDataUploadRequest needs to send an additional confirmation key after a successful execution, while RegistrationDataRetrieval needs to populate a data object share with the rest of the application. The ConfirmOutcome() and PopulateData() methods were defined for every Request to allow this highest level abstraction to be used even with the concrete subclasses Stable abstractions principle. These methods are implemented as no-ops for most requests except for the specific subclasses mentioned.
This introduces a coupling of the base abstraction (Request) to the two specific subclasses, and introduces methods to the interface are unnecessary for most instances. However, this design choice was made as a result of following the Tell, don't ask principle, which suggests that each object should be responsible for decisions made based on its own state. In this manner, subclasses can be handled through the base Request abstraction and conform to the hierarchy. The alternative would have been creating subclasses for CalibrationDataUploadRequest and RegistrationDataRetrievalRequest that add methods outside the base abstraction, which means that downcasting would be required to access the additional functionality required. This would violate the Avoid downcasting principle, as well as introduce much stronger coupling between the client and the Request model.
As such these two requests do not conform to the original generic Request interface. Trying to mould them to fit the interface would have eventually required downcasting, so that
1. Model roles as object not classes 2. Use tell don't ask to get work done by classes, not move it around - hence having to add methods 3. Not using abstract base
Code & Installation
Since my study concerns server requests, testing it out depends on interaction with an actual server/server pages. I've provided a heavily stubbed set of pages that return sample data. A local web-server has to be installed in order to test my design. The code implementation is not 100% complete and I have taken some shortcuts for demo purposes, but most of the expected functionality is there and it largely resembles the UML design above.
Steps:
- Download and install Xampp - http://www.apachefriends.org/en/xampp-windows.html
- Run the Xampp control application (xampp-control.exe) from the install directory
- Ensure the Apache module is running
- Create a directory called "requests" inside xampp_install_dir\htdocs (e.g. C:\xampp\htdocs\requests)
- Copy the php pages from the following archive to the "requests" directory
- The source and VS2008 solution are available in this archive. Open the solution and run the program. The program tries to connect to pages in "http://localhost/requests/", so you must have completed the above steps successfully.