Business needs change on a day-to-day basis, which makes it quite a challenge to design software applications that quickly adapt. Well-designed software applications help businesses get ahead of their competitors, but poorly designed applications make processes rigid and can cost too much to reuse.
To help you create better-designed applications, I've identified the object-oriented design (OOD) elements of good and bad designs. Then, I'll introduce two design principles that can help you create more scalable, robust, and reusable applications.
Well-designed applications offer software components that are more robust, more maintainable, and more reusable. Such applications should be able to adapt changing business needs without affecting design. For example, a banking application should be able to support new types of accounts without a change in the existing design.
Three key points of good design are:
- · Maintainability, which is the ease with which a software system or component can be modified to adapt to changing environments, improve performance, correct faults, or other attributes. Well-designed applications require fewer resources for maintenance and changes.
- · Reusability, which is the degree to which a software module or components can be used in more than one computing program or software system. Reusability of software components helps ensure faster development of software applications.
- · Robustness, which is the stability of software applications in extreme situations (e.g., maximum load conditions, erroneous user inputs). Robust applications have less downtime and can reduce maintenance costs.
Nobody plans to create ill-designed applications. It often happens because of a lack of experience or because the app was designed quickly to meet an extremely tight deadline. Poorly designed applications usually have these problems in common:
- · They're rigid. A design is rigid if it cannot be easily changed. For example, a single change to heavily interdependent, rigid software could begin a cascade of changes in dependent packages. When such a program grows in size, the designers or maintainers cannot predict the extent of that cascade of change, and the impact of the change cannot be estimated. This makes the cost of the change impossible to estimate.
- · They're fragile. Poorly created programs have a tendency to break in many places when a single change is made. Simple changes to one part of the application can lead to failures in other parts that appear to be completely unrelated. Fixing those problems leads to even more issues, and the maintenance process begins to resemble a dog chasing its tail. Such fragility greatly decreases the credibility of the design and maintenance organization, which leaves users and managers unable to predict the future quality of the product.
- · They're not reusable. A design is difficult to reuse when its desirable parts are highly dependent upon other details, which aren't desired. If the design is highly interdependent, other designers will also be daunted by the amount of work necessary to separate the desirable portion of the design from the parts that aren't reusable. In most such cases, the cost of the separation is deemed to be higher than the cost of redevelopment of the design.
To realize the benefits of a well-designed application, you can follow one of two fundamental principles: the Open-Closed Principle (OCP) or the Liskov Substitution Principle (LSP).
The Open-Closed Principle is the most fundamental principle of OOD. According to this principle, software entities (e.g., classes, modules, and functions) should be open for extension and closed for modification.
Open for extension
Open for extension means that the behavior of the application/component can be extended. You can make the application behave in new and different ways as the requirements of the application change, or to meet the needs of new applications.
Closed for modification
The source code of an application/component created with this principle is inviolate. No one should be allowed to make source code changes to it. If someone wants to add new features, he or she should do so by extending the existing application but definitely not by changing existing source code.
In structured programming, OCP is hard to achieve. However, in OOD you can follow OCP with the help of an abstraction mechanism. Let's take a look at an example.
In Listing A, I created a small program that has two shape objects, a circle and a square, along with methods for drawing those objects. The code is self-explanatory; the drawAllShapes method checks for the type of shape (circle or square) and calls the appropriate method for drawing it.
But this code doesn’t follow the OCP, because to add a new shape (e.g., a rectangle) to this program, you would need to modify the ShapeType class, modify the code of the drawAllShapes method, and add a new class called Rectangle. Adding the new class results in altered code in various places, which means this program is not closed for modifications. You'll also notice that it is poorly designed code, because it is rigid to changes and difficult to upgrade.
In Listing B, I redesigned this program by abstracting common functionality of all shapes into the Shape interface. In a drawAllShapes method, I used polymorphism for drawing all shapes. In this code, adding a new shape requires the creation of a Rectangle class by extending the Shape interface. But there is no need to edit the drawAllShapes method code. Essentially this design follows the OCP; it is open for extension and closed for modification.
Liskov Substitution Principle
According to Barbara Liskov, who wrote the LSP in the late 1980s, “Programs that use pointers or references to base classes must be able to use objects of derived classes without knowing it.”
For example, the code in Listing C is poorly designed and violates LSP. This method must be aware about every possible subclass of the Shape class and needs to change if you add a new subclass of Shape. Suppose you want to create a new shape, Rectangle, and you want to pass the Rectangle object to the method in Listing C. It can’t draw the rectangle shape without knowing its type. Such a method violates the principle, because it requires modifications in order to add a new shape.
The drawAllShapes method in Listing B conforms to LSP, because drawAllShapes is able to use objects of derived classes (e.g., Circle or Rectangle) without knowing them.
Similar in principle
The OCP is the most basic principle; all well-designed programs should at least conform to this principle. However, LSP actually follows the OCP. So if you use OCP, you should probably follow the theory of LSP as well.