Patterns in Practice
Adding Functionality to an Object: Designing the Object Model
In my January 2013 column, I proposed a business problem. As part of a sales order system, during the ordering stage, users can select and apply sales options to individual products on their order (for example, expediting shipping on an item or adding gift wrapping). In that column, I suggested that the Roles pattern provided a model for supporting this functionality in an extensible, maintainable and testable way. In my February 2013 column, I provided the data design for that solution and discussed the design and operational issues that would drive the data design. In this column, I focus on designing the object model.
Revising the Initial Design
In the February column, I also discussed an issue that readers had raised about where, in the application’s object model, the SalesOption objects should be made available. I had the Product object handle the SalesOption objects, but at least one reader suggested that the OrderLine object the Product object is attached to should handle the SalesOptions. In that column, while still skeptical, I pointed out that the data design also supported the readers’ point: In the data design, the SalesOption table was tied to the OrderLine table.
I also outlined some criteria for making this decision based on the way the business worked. I sent an email to the client I based the case study on to follow up on those questions, and I started sketching out a sample implementation. When the client got back to me, he confirmed that the readers were right: The business processes supported having the OrderLine object manage the SalesOption objects. My initial sketches for an implementation also showed that having the Product object manage SalesOption objects would generate a lot of ugly code.
Making the change created a problem, however. The SalesOption objects depend on data that is private to the Product. The solution to that problem is to have the Product expose that data using properties declared with a scope of internal. That makes the data behind those properties accessible to other classes in the same project (like the OrderLine class) but not to external clients. Although the SalesOption classes won’t be part of the same project as the Product and OrderLine, the OrderLine can pass the necessary data either as part of instantiating a SalesOption class or by setting properties on a SalesOption object.
I suppose revising the design could be counted as a failure, but that process of design, review and revise is the typical case, at least for me. As I work with a design, I get smarter about the problem (wouldn’t it be sad if I didn’t?) and often end up revisiting the requirements to clarify my understanding of them. And that leads to changes in the design. For me, going from design to an implementation sketch is an iterative process. The feedback from reviewing the design with others or sketching out what an implementation could look like is a critical part of the process.
Design Goals and Costs
Now it’s time to look at the object model design for handling sales options. My primary goal is to create an easily extendible system so that when the company adds new sales options the application can be extended by adding new code rather than by modifying existing, working code—ideally, by adding new classes. When I see code that will need to be modified as the application grows, I want to isolate that code in its own class. Both of those goals (adding new classes and isolating code) make it easy for me to test changes. I don’t have to test the whole Product object because I modified only one small part of it.
I also have a secondary goal: to create easily testable code (classes that can be easily instantiated before having their methods and properties exercised). That’s easiest when I design objects that reflect the single responsibility principal—that is, classes that do one thing well.
The downside of working toward these goals is that my design could have more classes than other designs with different goals—much in the same way that a data design that values flexibility generates more tables than a data design that values ease in reporting.
An analysis of the business shows that very few sales options will be applied to any particular OrderLine/Product combination—most OrderLines will have no sales options. The company has only about a half-dozen sales options right now, and it’s difficult to imagine why more than one or two dozen would ever be needed. The analysis also shows that most processes that work with a SalesOrder will be interested in only specific sales options (for example, the shipping process is interested in expediting and gift-wrapping options but not in others). The one exception is the order-taking process, which must show all the sales options applied to an OrderLine/Product combination. The order-taking process is also the only place where sales options can be applied, at least in the current system.
To support the business, therefore, the order-taking process needs this functionality in an OrderLine object:
- Retrieve all the SalesOptions applied to an OrderLine
- Add new SalesOptions to an OrderLine
- Remove a SalesOption from an OrderLine
Other processes in the sales order system will add an additional requirement: the ability to retrieve a specific SalesOption on request.
I’ll support these four requirements through a property and three methods on the OrderLine class:
- SalesOptions (property): A read-only collection of all the SalesOptions applied to the OrderLine
- AddSalesOption (method): Passed an enumerated value specifying the SalesOption; adds the SalesOption to the OrderLine’s SalesOptions collection
- RemoveSalesOption (method): Passed an enumerated value specifying a SalesOption; removes the SalesOption from the OrderLine’s SalesOptions collection
- GetSalesOption (method): Passed an enumerated value; returns the appropriate SalesOption object
I could collapse these into a single SalesOptions property by providing add, remove and retrieve-by-value functionality to the SalesOptions collection. However, it’s easier for me to explain the code if it’s in individual methods (and the code is almost identical in both implementations). I also suspect that it’s easier to suppress the add and remove functionality in the non-order-taking processes if that functionality is in separate methods. In your implementation, feel free, to divide up the code differently.
The easiest way to support the SalesOptions collection is to create all the SalesOptions objects when the OrderLine is created (“eager loading”). That will also improve the responsiveness of the GetSalesOption method because the objects will already exist. There is a cost, however. Instantiating the OrderLine object will take longer because all the SalesOption objects assigned to the OrderLine will need to be created as part of instantiating the OrderLine object.
In addition, right now, only the order-taking process requires the ability to loop through all the SalesOptions, which gives me another option: load the SalesOptions collection only if the OrderLine is being used in the order-taking process.
My final option is to defer loading the SalesOptions until the first call to the collection (“lazy loading”).
The option of loading the collection only for the order-taking process isn’t attractive. If other processes later require the collection, I’d have to go back into the OrderLine class and change the SalesOption loading code to support those other processes—I’d prefer not to create more work for myself in the future. Really, only the eager loading or lazy loading options are sensible.
If the SalesOption objects are small and cheap to instantiate, the cost of instantiating the few SalesOptions on an OrderLine when an OrderLine is created shouldn’t impose an excessive delay when creating the OrderLine. With small SalesOptions objects, implementing eager loading is a viable option, and both writing and testing the code would be simpler than with the lazy loading option. I’m going to implement eager loading and create all the SalesOptions for an OrderLine when it’s created, but I’m also going to try to keep the size of each SalesOption small.
Designing the SalesOptions
Analysis also shows that only some of the functionality required by a SalesOption object is needed in any part of the sales order system. For example, in the order-taking process, the expediting SalesOption just needs to determine how early the product or service can be sent; it doesn’t need to fill in the shipping details. Later, during the shipping process, the expediting SalesOption will need to set up a special shipping option to meet the date (for example, using an overnight delivery service) but either won’t need to calculate the “expected delivery” date or will use a different process for the calculation. In addition, as I noted in my February column, the functionality required in a SalesOption can vary not only by which process is using the OrderLine but also by the type of Product on the OrderLine.
With that in mind, I don’t need one big “expediting SalesOption” object. Instead, I can create several “expediting SalesOption” objects. I would want, for example, one SalesOption class to hold the code that supports the order-taking process and a separate SalesOption class to hold the code for the shipping process. This approach also aligns with the way development teams are structured. The two developers who support the order-taking process don’t want to be responsible for writing the code required for shipping—they’d rather leave that to the developer responsible for the shipping process.
If I do need expediting-related code across multiple parts of the process, I can use a base “expediting SalesOption” class that the other expediting classes can inherit from.
Although the number of classes is increasing, I’m not generating extra code. The code in each object is required by the business. However, I am distributing this code over multiple classes. Managing these additional objects does incur associated costs. In general, I prefer having a lot of simple objects rather than fewer but larger objects. Among other benefits, when the application requires additional functionality, I shouldn’t need to modify any existing class. Typically, I’ll be able to add new functionality by adding a new class. But you might feel that the costs of “drowning in objects” outweigh the benefits I value.
Regardless, I do know that it will be essential to implement this design with a toolset that makes it easy to instantiate the correct object.
In my January column, I specified an interface that makes all SalesOptions objects look alike, but I suspect I’ll have to expand that interface once I see how the applications using the SalesOptions need to interact with the SalesOption.
Designing the OrderLine
For the OrderLine to retrieve the right object, the OrderLine needs to accept an enumerated value that specifies which process is using the OrderLine object—which raises the question of when that value should be passed to the OrderLine.
If an OrderLine object is used in just one process (for example, order taking) when it’s created and never passed to an application that supports another part of the process, the environment value can be passed to the OrderLine’s constructor (“constructor injection”). If an instantiated OrderLine can be passed among processes, however, a property on the OrderLine that accepts the enumerated value (“setter injection”) should be used so that the receiving application can change the value. In this organization, OrderLine objects aren’t passed among processes, so a constructor injection makes sense. If that changes down the line, I’ll be forced to go into the OrderLine and change it to support setter injection.
That’s a lot of talk without getting to any code! However, I now have a “good enough” design to start looking at implementation. See you next month.
Peter Vogel is the principal system architect in PH&V Information Services, specializing in SharePoint and service-oriented architecture (SOA) development, with expertise in user interface design. In addition, Peter is the author of four books on programming and wrote Learning Tree International’s courses on SOA design ASP.NET development taught in North America, Europe, Africa and Asia.