We should not forget that one of the main advantages of the object-oriented design approach was that we have the same concept of class in each SDLC phase (even if different characteristics of classes are emphasized at different phases). So the domain classes we identified during analysis can and should be revisited from the design perspective. Recall that now we should deal with not the problem but the solution space so we need a design that can tell how to achieve the requested functionalities (after analysing the what part earlier). A design-level model should still be independent of concrete implementation platforms since this will ensure that the design is reusable.
As a reminder, here is our latest figure that shows the domain classes (augmented with the class Catalog that has been discussed but never shown on a diagram):
Refinement of the analysis-level model to design-level model will include the following steps (this is not intended to be an exhaustive list):
check of classes to have a more precise set of attributes with proper types,
identification of the most important methods that decribe the object's behavior (along with finding names and types of arguments, and, in an ideal case, providing contracts for those methods),
introduction, deletion or rearrangement of classes as needed: some classes that are good in describing a domain might not be useful in providing a solution, and, which is more often the case, additional classes are needed,
especially when thinking of controller classes or classes of the UI,
introduction of interfaces when possible and reasonable,
assign solution classes to the packages identified during architectural design (separation of concerns),
refactor the model to patterns,
develop sequence diagrams that show how the identified objects will communicate with each other (using the methods that we identified) when a use case is executed: this is to “prove” that the functionality described by the use case is realized.
The UML itself does not provide a naming convention for attribute and method names. However, we often use capitalized names in analysis-level diagrams. In design-level diagrams the names of properties approach those that we will need in the implementation so here we switch to the use of the Java naming conventions. The most of the changes we made during this step is very straightforward. They are the direct translations of analysis-level elements onto design-level elements. At design level you should assign a data type to each of your attributes! Therefore we have selected some appropriate types for those that were omitted earlier.
We have decided to use String instead of PersonalName and TelephoneNumber and ISBNumber, the respective primitive types that are no longer used has been deleted. It is a design-level decision to decide on data types that are not motivated by the domain, that is why we used them earlier (hence the designer can make assumptions and find pros and cons of the change including the introduction of some kind of validation if needed). Some other types motivated by the domain like Address and Money stayed in the model: on one hand, they occur several places and changing Addresss to String would disallow the possibility of finding customers from a particular city since if it only stored as a String we can never be sure how to parse it properly in order to retrieve the name of the city. On the other hand, the use of a number type like float or double instead of type Money would mean that a single currency is supported only (that is not necessarily a problem but will limit future possibilities).
Enumerations PaymentMethod and OrderState has been
included into class Order as internal enumerations so we
can meet the notation of nesting a classifier inside another one.
Few method specifications have also been decided. It is common to have much more methods in the design levl diagram than we had during analysis because analysis classes might have only used some high-level business processes as methods. Now we are looking for the solution (how to solve ) therefore appropriate behavior (responsibility) needs to be assigned to each class.
At design level, these decisions must be documented. When following a
plan-driven approach these pieces of information usually go to the software
design descriptions document (SDD) which (like SRS) has IEEE-recommended
structure and contents. The best practice says that each attribute and operation
of our classes should be documented. Of course this is about the final design
not about such an intermediary step we execute right now. However, due to space
limits we will not provide full details on each property at the end of design
therefore we show an example documentation for classes
Order and Manager.
Table 5.1. Class Order
| The Order class represents an order. An instance of this class will be created when the contents of a non-empty shopping cart are checked out. | |||
|---|---|---|---|
| Name of property | Type of property | Default value | Description |
| datePlaced | Date | The actual date | The date when the order has been placed. |
| billingAddress | Address | The address to which the invoice needs to be prepared. | |
| shippingAddress | Address | The address products need to be delivered to. | |
| customerName | String | Name of the customer who placed the order. | |
| totalValue | Money | Derived attribute containing the total price of the order. | |
| status | OrderState | NEW | The status of the order. |
| paymentType | PaymentMethod | CREDIT_CARD | The selected payment method. |
| pay() | void | Initiate payment with the default payment method. Payment is implemented | |
| pay(PaymentMethod) | void | Initiate payment using the given payment method. | |
From the specification it is clear that managers can set some discounts. Setting at least two kinds of discounts (per-Customer and per-Product) should be possible.
In fact, this was underspecified. The besz we can do in such cases collect enough information on the expected behavior during requirements analysis. Since our case study serves examplary goals it was not our goal to provide a full set of well-specified requirements.
Let us consider the expected behavior of dicount settings: managers can set discounts to individual products, individual customers and product categories. If a discount is set to a category then all products belonging to this category will be discounted. The amount of discount can be specified either using an absolute value (e.g., the discount is x EURs regardless of the actual price) or by providing a percentage for specifying a relative value (e.g., y % discount is given from the actual price). In cases when multiple discounts are present (e.g., when a discounted customer buys a discounted product), it should not be possible to give more than one discounts. Instead the one which is more friendly to the customer should be applied.
Table 5.2. Class Manager
| The Manager class represents a user of the system. All new... methods return with an int that is an id for that rule which can be used later for deleting it. | |||
|---|---|---|---|
| Name of property | Type of property | Default value | Description |
| newDiscount(Customer, Money) | int | Sets customer-based absolute discount. | |
| newDiscount(Customer, int) | int | Sets customer-based relative discount (int is the percentage). | |
| newDiscount(Product, Money) | int | Sets product-based absolute discount. | |
| newDiscount(Product, int) | int | Sets product-based relative discount (int is the percentage). | |
| newDiscount(ProductCategory, Money) | int | Sets absolute discount to all products of the given category. | |
| newDiscount(ProductCategory, int) | int | Sets relative discount to all products of the given category (int is the percentage). | |
| removeDiscount(int) | void | Discount rule with the provided id is removed. | |
| removeDiscount(int, int) | void | Discount rules with ids of the given range are removed. | |
| removeDiscounts() | void | All discounts are removed. | |
This design also implies some further design. The ids returned by the
newDiscount methods needs to be stored if we ever
want to remove them. On the other hand, this solution for setting discounts is
not generic enough. For more sophisticated cases some kind of rule engines
should be applied.
Of course we identified only a subset of attributes and methods. Readers should continue this work by introducing attributes and methods they found. But be warned! At design level we are not interested in methods like getters/setters. They simply do not add any value to the model. They are important from the implementation point of view since they provide access to attribute values that have been declared as private for achieving encapsulation. Modeling on the design level is done on a higher level of abstraction.
We should also store the discounts set for customers and products therefore we
need additional attributes (and discounts : double of
Product has to completely replaced). We need two
attributes for each discount since otherwise we could not find out which is
better for the buyer.
Now we can define the contracts of the methods listed above. We only give a
single yet informal example for Manager's
newDiscount(Product, int), the rest is left to the
reader. The precondition for this newDiscount method is
that the first parameter is a valid product while the second parameter is
between 0 and 100 (as it depicts a percentage value). If this is fulfilled, then
the contract's postcondition part will be provided to guarantee that the product
is looked up and it's discountRelative attribute is set to the value given in
the second parameter if it is bigger than tha actual value of that attribute (so
this is a bigger discount than the earlier one if such existed—remember
our business rule to provide the best case for customers).
A relatively big change to the analysis model has also been introduced: we changed the way how shopping cart is handled. For analysis purposes it was OK but from design considerations we had to change. Our analysis model did not use the concept of an item (let it be shopping cart item or order item). This is not a problem: in the real world we still rarely use that concept. We talk about orders, products, shopping carts but we do not talk about order items. It proves that our initial analysis model was good since it used the same concepts as people do so noone was forced to use unfamiliar concepts. It is not a real part of the problem domain,
However, it is an integral part of the solution itself. When an invoice is needed these items should be printed on them, for example. Therefore, instead of the solution used in the analysis model (an association class if you remember) needs to be replaced with a functionally equivalent solution that allows printing invoices if we need. An item is for sure connected to a product but then the question arises: what has an item? A shopping cart? An order? Any of those but not both! If you consider the activities of ordering, the shopping cart should be populated first. An order object will not even be instantiated until the contents of the cart are checked out. But checkout is a final stage of the cart's lifecycle: the order is instantiated, the items will become order items and the shopping cart will never be needed again. That is why we decided not to differentiate between cart items or order items: they are only items that can hold a given amount of quantity of a product. First these item instances will be attached to the cart and upon checkout they will be provided to the order. UML provides a notation to denote that participation in those two associations (i.e., compositions in this particular case) is mutually exclusive. A proper notation is to use a note that is attached to both associations and the note should contain the constraint {XOR} to tell that these associations are exclusively or-ed together.
The following sequence diagram illustrates this concept: when the customer visits a product's page, he/she might want to add some items of this product to the cart. Later, when c
The class Catalog has been introduced in the analysis
model to somehow provide access to all products if it was needed. This is
acceptable at analysis level (strictly because we do not execute any code then)
but really having such an object might become a bit more
than a nightmare. The maintenance costs of such an object will increase together
with the number of products (it might be even worse) since we should guarantee
that no product objects are left out the catalog, none of them are duplicated
and the problems of having a really huge number of objects instantiated in the
memory seems pointless, especially if we know that only few of them is really
needed.
Hopefully it is clear why we need to get rid of that class. But what should we do then? How can perform such operations like search for a particular product if we do not have such a class? We need a persistent storage where huge amounts of unnecessary data can be stored. Use of persistent stores provide further advantages: our domain object can survive the stoppage of the application (when the contents of the memory are lost) let it be wanted or unwanted. We also need persistent stores because the size of memory is limited and it is possible that some objects would not fit if everything stored in the memory. Disks (that we predominantly use as a persistent store are bigger with some orders of magnitude so it is less to be feared that we run out of space there (however, it might happen, of course).
Persistent storage of data can be considered as a necessary evil. Ideally we would have enough memory (probably the same amount of memory that Turing-machines have would be enough), we would not fear our machine freeze and so on. Then we would not need persistent storage. Or we still would since today's database management systems provide transactional capabilities and locking mechanisms for concurent data access.
We just want to underline that dealing with persistent data is not preordinated. Even if we often use persistent stores, not all of the applications will make use of them.
So we need a persistent data storage solution. The hig majority of domain objects should be persisted. The logic of the persistent data access can change over time. We start persisting our small set of data in XML files but later when the amount of data will dramatically inrease, we can look for a database-backed solutions. We can regularly change our databases, as well: upgrading to a new version, finding one of a better perfomance, switching platforms might all result in that our code which depends on the persistence layer immplementation needs to be modified.
Therefore high-level data services our applications require (such like give me a list of all products belonging to a given category or create a report on the orders that have been place in calendar year of 2013, etc.) should be decoupled from the data access logic. Even SQL has several dialects but is you consider that the schema can also change it is easy to understand that we should only concentrate on high-level data requirements first that are not subject to such frequent changes.
A well-known example is the use of the Data Access Object (DAO) pattern.
Such a DAO with a well-defined interface will be a much better alternative to our Catalog object.
You can add as many opoerations as are needed in your domain. Notice that where multiple values are returned we used the array notation. This is not because it is not acceptable to retrieve a java.util.List (as an example) in Java but the design models are intended to be language agnostic. UML does not support collection directly, however, one of its supporting standards called the Object Constraint Language (OCL) defines Set(T) and Bag(T) (among others) so it would be enough to write Set(Product) but Product[] is equally OK as all programming languages support arrays. Later, when you further refine your design onto implementation level models you can map this array to any collection type of the given language.
In our design model, we will replace class Catalog with interface ProductDAO and during implementation programmers should implement this interface with a technology-specific class. You can also decide to introduce the factory, too, if the anticipated benefits outweigh increased complexity. You should not introduce a solution into your design just because you can. Each decisions have pros and cons and if you deal with unnecessary things then probably you will be at a pinch due to the consequences of them.
You should create DAOs for encapsulating all of your data access including an OrderDAO that can be used by managers to create various reports based on the order history or a CustomerDAO that offers a method as a part of its API to retrieve the list of customers whose user account is locked.
For similar reasons why we introduced DAOs (for decoupling our business-related system parts from technology-specific or implementation-dependent parts, we can also introduce two additional modules even if they might seem surprising for the first sight. These modules should be:
Inventory, and
Shipping
As the requirements clearly stated, these are indepedent (probably legacy) systems. However, we can decide to capture them as subsystems that will delegate invocations towards the legacy systems. The intent of these decisions was that this way we can decouple our modules (especially Order management) from these external systems since both Inventory and Shipping subsystems will encapsulate the complexity of those external systems. This is the reason why Order management does not need to implement 3 different methods to access the traceablility information of orders if there are 3 different delivery service providers. Instead of the need for multiple implementations, the Shipping subsystem will provide a simple interface to Order management and will implement the appropriate mappings to the legacy systems inside. If a new delivery company will start to ship then it is will be enough to add the mapping to its interface but subsystem Order management will not be bothered with the change,This is, however, a special combination of the Façade and the Adapter design patterns as there is an interface transformation which is hidden behind a façade.
Of course, a design can never be final. It is not just about that the environment and the requirements are always change but we should take into consideration that the design of a software product is unique. It carries and captures the designer's internals like a fingerprint. There are lots of issues that could be designed differently. There are hundreds of design decisions that must be made in order to “finalize” a design, which is a step towards completing the system. However, our goal was not to provide a fully detailed design but to show the major steps, emphasize the most important activities and provide an approach how you should perform your modeling tasks.