Server Requests
(→Possible Design Flaws) |
m (Reverted edits by Ebybymic (Talk); changed back to last version by Filip Kujikis) |
||
(51 intermediate revisions by 2 users not shown) | |||
Line 1: | Line 1: | ||
− | = | + | = 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 modeled 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 | *something that works | ||
*is as nicely designed as practical | *is as nicely designed as practical | ||
Line 23: | Line 22: | ||
*is flexible so that new server requests can be added without much hassle | *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 == | == Deliverables == | ||
Line 33: | Line 33: | ||
*Classes that allow requests to be sent over the network | *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. | |
[[Image:initial.png]] | [[Image:initial.png]] | ||
− | == | + | == How it Works == |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
=== Representing Requests === | === 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, representing the request parameters and their names (in particular a .NET 2.0 NameValueCollection). | + | 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. | ''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 instances 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 === | === Using the classes together === | ||
Line 55: | Line 63: | ||
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. | 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]] - Possible 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 relationship, 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 focused 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 fictitious 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 relationships between the application and the abstractions of the class model. | ||
+ | |||
+ | (Please excuse the size of the UML diagram. The UML tool I used would not allow for manually routing connections, so this is the best I could do.) | ||
+ | |||
+ | [[Image:filip_improved.png]] | ||
+ | |||
+ | == Design Description == | ||
+ | |||
+ | I split up the design in two parts - the transport mechanism, and the modeling 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 modeling 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. | ||
+ | |||
+ | |||
+ | == Modeling Requests == | ||
+ | |||
+ | This is the improved design for modeling 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 modeling 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 - modeling 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. | ||
+ | |||
+ | === Creating objects in the Request Family === | ||
+ | |||
+ | I created a single class that handles the creation of objects - the RequestBuilder class. The purpose of this class was to serve as an abstraction that can create the different Request objects necessary in the application. This class defines the different roles or the Request class that represent the possible requests to the server. The idea is that by using this class to obtain object instances the client is protected from having to deal with actual details of the particular concrete Request instances used, or how they encode their POST parameters - supported by the [[Information hiding]] maxim. | ||
+ | |||
+ | Another way to think about this class is as a mapper between the different parameters that need to be sent to the server and the concrete state of a Request object that needs to transport those parameters to the server. Because the class acts as a map, I defined it as static - having multiple instances does not make sense. | ||
+ | |||
+ | When designing this class I considered using the '''Abstract Factory''' and '''Builder''' patterns as possible alternatives. '''Builder''' is naturally closer as it is concerned with initializing objects which is sufficient for most requests, however '''Abstract Factory''' would have been more appropriate for creating the two outliers: CalibrationDataUploadRequest and RegistrationDataRetrievalRequest. I finally decided to create what is currently a mash up of both creational ideas into a single class - because of the varying number and type of parameters required for creating each Request object both '''Abstract Factory''' and '''Builder''' were impractical to implement. | ||
+ | |||
+ | RequestBuilder is still useful however, because its coupling to individual objects acts to decouple a client's access to those objects, and provides an abstraction for obtaining each object necessary. | ||
+ | |||
+ | == Overall improvements == | ||
+ | |||
+ | While probably still imperfect, I feel that the design is much improved from the initial version. Modeling requests as objects and roles instead of classes has dealt with issues 1,2,3 from the '''Design Flaws''' section for the initial design. Issue 4 has been dealt with by avoiding abstract base classes which force the creation concrete classes with no unique implementation. Issue 5 - has been remedied by applying the [[Acyclic dependencies principle]], - the Request class now has a dependency on HttpPOSTRequestSender, and the HttpPOSTRequestSender family of objects are now independent. By reworking the abstractions and restructuring the class hierarchy, issue 6 was addressed - abstractions in the design are now useful and make the model more extensible. The abstractions also reduce client code coupling, which was initially forced to couple to classes at the bottom of the inheritance hierarchy. | ||
+ | |||
+ | == Remaining Issues == | ||
+ | |||
+ | *One obvious issue is the presence of the [[Sequential coupling]] anti pattern in the design with the methods of both HttpPOSTRequestSender and Request class families. The order of executing the methods impacts the outcome, and calling them in an incorrect order results in an error. This is not ideal, but seems to be widely accepted in IO and Networking APIs across platforms (Java and .NET both do it). It is possible to create method calls that chain the correct sequence of operations together, though that poses a clash with the [[Command query separation]] principle for methods. | ||
− | + | *RequestBuilder feels a bit strange and there may well be a better approach/pattern/class decomposition that can replace it. It also appears to be violating [[Avoid interface bloat]] and [[Avoid god classes]] because of the lack of better decomposition. | |
− | + | = Code & Installation = | |
+ | No longer available. |
Latest revision as of 03:22, 25 November 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 modeled 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 instances 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 - Possible 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 relationship, 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 focused 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 fictitious 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 relationships between the application and the abstractions of the class model.
(Please excuse the size of the UML diagram. The UML tool I used would not allow for manually routing connections, so this is the best I could do.)
Design Description
I split up the design in two parts - the transport mechanism, and the modeling 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 modeling 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.
Modeling Requests
This is the improved design for modeling 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 modeling 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 - modeling 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.
Creating objects in the Request Family
I created a single class that handles the creation of objects - the RequestBuilder class. The purpose of this class was to serve as an abstraction that can create the different Request objects necessary in the application. This class defines the different roles or the Request class that represent the possible requests to the server. The idea is that by using this class to obtain object instances the client is protected from having to deal with actual details of the particular concrete Request instances used, or how they encode their POST parameters - supported by the Information hiding maxim.
Another way to think about this class is as a mapper between the different parameters that need to be sent to the server and the concrete state of a Request object that needs to transport those parameters to the server. Because the class acts as a map, I defined it as static - having multiple instances does not make sense.
When designing this class I considered using the Abstract Factory and Builder patterns as possible alternatives. Builder is naturally closer as it is concerned with initializing objects which is sufficient for most requests, however Abstract Factory would have been more appropriate for creating the two outliers: CalibrationDataUploadRequest and RegistrationDataRetrievalRequest. I finally decided to create what is currently a mash up of both creational ideas into a single class - because of the varying number and type of parameters required for creating each Request object both Abstract Factory and Builder were impractical to implement.
RequestBuilder is still useful however, because its coupling to individual objects acts to decouple a client's access to those objects, and provides an abstraction for obtaining each object necessary.
Overall improvements
While probably still imperfect, I feel that the design is much improved from the initial version. Modeling requests as objects and roles instead of classes has dealt with issues 1,2,3 from the Design Flaws section for the initial design. Issue 4 has been dealt with by avoiding abstract base classes which force the creation concrete classes with no unique implementation. Issue 5 - has been remedied by applying the Acyclic dependencies principle, - the Request class now has a dependency on HttpPOSTRequestSender, and the HttpPOSTRequestSender family of objects are now independent. By reworking the abstractions and restructuring the class hierarchy, issue 6 was addressed - abstractions in the design are now useful and make the model more extensible. The abstractions also reduce client code coupling, which was initially forced to couple to classes at the bottom of the inheritance hierarchy.
Remaining Issues
- One obvious issue is the presence of the Sequential coupling anti pattern in the design with the methods of both HttpPOSTRequestSender and Request class families. The order of executing the methods impacts the outcome, and calling them in an incorrect order results in an error. This is not ideal, but seems to be widely accepted in IO and Networking APIs across platforms (Java and .NET both do it). It is possible to create method calls that chain the correct sequence of operations together, though that poses a clash with the Command query separation principle for methods.
- RequestBuilder feels a bit strange and there may well be a better approach/pattern/class decomposition that can replace it. It also appears to be violating Avoid interface bloat and Avoid god classes because of the lack of better decomposition.
Code & Installation
No longer available.