The design phase produces the design model.
The design model specifies an application's modules and their dependencies upon each other.
The design model is a logical structure. Various UML diagrams provide views of parts of the design model.
During the implementation phase the design model is transformed into code, sometimes called the implementation model:
The gap between the design model and the implementation model should be narrow. (In fact, some code can be auto-generated from a design model.) This means that the design model must try to eliminate ambiguities and missing pieces.
An application (or module) should be correct, efficient, secure, and maintainable. These goals are not negotiable.
Maintainability means the application is not needlessly difficult to repair, extend, or enhance. (Most of a software budget is spent on these activities.)
The following design principles help us create maintainable applications and modules:
· Modularity: Applications should be built from cohesive, loosely coupled modules.
· Abstraction: The interface of a module should be independent of its implementation.
· Open-Closed: A module should be open to extension but closed to modification.
· Reusability: Modules should be reusable, if possible
A module is a named collection of declarations (fields, methods, classes, interfaces, sub-modules, etc.)
In object-oriented programming modules usually correspond to classes, packages, files, and components.
Beyond its name, a module is specified by the interfaces it implements and the interfaces it needs other modules to implement.
An interface is a named collection of type declarations. For example:
interface PassengerManifest {
Passenger getPassenger(int seatNum);
void add(Passenger p, int seatNum);
void rem(Passenger p);
...
}
A module is ultimately specified by its implementation.
class A implements interface1, interface2, ... { ... }
This means that A provides implementations of all of the operations (i.e., methods – implementations) in each of the interfaces listed. Thus, different types of clients can have different views of A.
For example, an airline flight record might aggregate all information about a particular flight: number, route, schedule, passenger records, airplane maintenance records, etc.:
Suppose FlightManifest promises an operation such as:
Passenger getPassenger(int seatNum);
But the user's manual tells us that before the operation can be called, we must set the global variable PassangerFound back to false, as it is used as the condition of a while-loop in the implementation. This would be a blatant violation of the Abstraction Principle. It not only makes the operation hard to use, but it binds the interface to a particular implementing class.
Inheritance is a good example of the Open-Closed Principle. For example, we can extend the scope of our flight management system to include the ability to book chartered flights by extending the Flight class. No modification to the Flight class is needed:
A module is cohesive if its members are closely related to each other. For example, with methods for updating information about passengers, avionics, and routes, our Flight class is probably not very cohesive.
Recall that class A depends on class B if a change to B may require a change to A.
In UML we can represent a dependency as follows:
This can happen if the declaration of A contains any references to B such as a declaration of a superclass, field, parameter, or local variable of type B:
class A extends B {
B someField;
B someMethod(B someParam) {
B someLocal;
...
}
...
}
In these cases we say that B is a provider for A and that A is a client of B.
Of course a dependency can be bi-directional:
Sometimes this is necessary, but it often leads to code that is difficult to maintain because a modification to A can require a modification to B which can require a further modification to A, and so on. Bi-directional dependencies should be eliminated when possible.
If A depends on B, and B is changed, then we might need to change A. So what's the probability that a change to B will require a change to A? If, for example, many members of A reference many members of B, then a change is likely and we say that A is tightly coupled to B. In other words, A is highly dependent on B. Tight coupling should be avoided if possible.
Even if a class is never reused, the fact that it is reusable usually indicates that it is cohesive and independent of or loosely coupled to other reusable classes.
A design pattern is a reusable solution to a recurring design problem.
Design patterns can be found in pattern catalogs.
One recurring problem is deciding where to start. This problem is often solved by the Layered Architecture Pattern. This pattern partitions all of an application's classes and interfaces (future and current) into layers:
Presentation Layer |
Application Layer |
Domain Layer |
Infrastructure Layer |
Foundation Layer |
· The presentation layer contains boundary classes that interface with actors such as users, databases, clients, servers, etc.
· The application layer contains classes related to implementing use cases such as transfer funds, purchase items, search inventory, etc.
· Domain layer classes represent domain-specific concepts such as Account, ShoppingCart, Employee, etc.
· The infrastructure layer is the framework or scaffolding that provides services such as persistence, messaging, event notification, lifecycle management, etc. Typical classes we might find here include Dispatcher, CommandProcessor, Proxy, Publisher, ObjectManager, etc.
· The foundation layer contains library classes and interfaces such as String, Number, Collection, etc.
The layered architecture does not allow upward dependencies. In other words, a class in one layer should not depend on a class in a higher layer. Such dependencies defeat the purpose of layering and make the lower-layer classes less reusable.
For example, if a List class in the foundation level depended on an Employee class in the domain layer:
class List {
Employee[] members;
...
}
then the List class could not be reused in contexts where accounts, posts, or contacts needed to be listed. Furthermore, changes to the Employee class (which occur with far greater frequency than changes to any foundation class) may require changes to the List class, which may propagate a wave of changes back up the hierarchy.
Ideally, the domain layer of a banking application should be reusable in a variety of different banking applications—online banking, account management, customer service, etc.
The Model-View-Controller design pattern can be seen as a special case of the layered architecture given above:
The model class corresponds to the domain layer, controller classes belong to the application layer, and view classes belong to the presentation layer.
The MVC pattern is useful when the domain is small enough to fit into a single class. For example, in a word processor the model corresponds to the document; views might include outline, print, and draft views; controllers implement menu items such as copy, cut, paste, insert, save, open, redo, spell check, etc.
An important feature of the MVC pattern is that the model does not depend on views or controllers. (Of course we already have this feature in the layered architecture.) Sometimes event notification patterns such as Publisher-Subscriber are used to notify views that the model has been updated and they should redraw themselves.