Designing Software  «Prev  Next»
Lesson 1

Designing Software with Design Patterns

You have now explored five patterns in depth and looked at capsule summaries of quite a few more. This module discusses some general issues that arise in using patterns to develop software.
In this module, you will learn:
  1. How to choose the right pattern for the job
  2. How to modify documented patterns to fit your problem
  3. How to combine design patterns
  4. The pitfalls and limitations of patterns

General Issues that arise when using Design Patterns

Patterns, specifically in the realm of software design, are powerful tools that encapsulate solutions to recurring problems within a particular context. However, the application of patterns isn't without its issues. These issues can manifest themselves in various ways and at different points in the software development lifecycle. Here are some general issues that may arise:
  1. Misuse and Overuse of Patterns: One of the most common issues is the misuse or overuse of patterns. This usually occurs when developers treat patterns as the ultimate solution to every problem, leading to their overuse. This could result in an overly complex design, which is hard to understand, maintain, and modify.
  2. Context Ignorance: Each pattern is designed for a specific context. Applying a pattern outside its appropriate context can lead to a counterproductive design.
  3. Impediment to Creativity: Strict adherence to design patterns may stifle creativity and inhibit the development of novel, more efficient solutions to a problem.
  4. Inefficiency due to Pattern Overhead: Every design pattern comes with its own overhead. While the overhead might be negligible in larger systems, in smaller systems it could lead to inefficiency.
  5. Lack of Flexibility: A pattern-oriented approach might sometimes result in rigid, tightly-coupled systems that are resistant to change, thereby limiting flexibility.

How to select an appropriate Design Pattern for a specific Problem

Given the potential pitfalls, one might wonder how to select an appropriate design pattern for a specific problem. Here are some steps that may guide the selection process:
  1. Understand the Problem Domain: Prior to selecting a design pattern, it is critical to gain a comprehensive understanding of the problem domain. It is important to identify the problem's key attributes, such as its scale, the number of entities involved, and the type of operations that need to be performed.
  2. Classify the Problem: After understanding the problem, the next step is to classify it based on certain parameters. For instance, is it a problem of object creation, or is it about behavior of objects, or is it about the structure and relationship of classes and objects?
  3. Research Appropriate Patterns: Once the problem is classified, research the patterns that are typically used to address such problems. For example, if the problem involves object creation, consider creational patterns such as Factory, Abstract Factory, Builder, Prototype, or Singleton.
  4. Evaluate Patterns: After identifying potential patterns, evaluate them based on the specific requirements of your problem and the system you're working on. Some patterns may be more suitable for larger systems, while others are better for smaller ones. Similarly, some patterns might lend themselves better to specific programming languages or paradigms.
  5. Iteratively Apply and Refine: Lastly, after selecting a pattern, apply it iteratively and be ready to refine it based on the evolving needs of your system. It's important to remember that patterns are not one-size-fits-all solutions, and they might need to be adapted to fit the specific needs of your problem.

Software design is the process by which an agent creates a specification of a software artifact, intended to accomplish goals, using a set of primitive components and subject to constraints. Software design may refer to either
  1. "all the activities involved in conceptualizing, framing, implementing, commissioning, and ultimately modifying complex systems" or
  2. "the activity following requirements specification and before programming, as in the style of a software engineering process."
Software design usually involves problem solving and planning a software solution. This includes both low-level component and algorithm design as well as high-level, architecture design.

The Goal of Patterns

Cost, customer satisfaction, productivity, and development time are some objectives of software development. Patterns contribute indirectly to many of these goals:
  1. Productivity: By providing domain expertise, patterns shorten the time interval for many important design structures.
    Discovery includes the activities of a designer to find out how the current system works as a basis for maintenance changes.
    More importantly, patterns avoid rework that comes from inexpert design decisions.
  2. Development Interval. Many software patterns are a form of design-level reuse. Patterns can reduce the amount of time required to build solution structures because they allow designers to use design chunks that are larger than functions or objects. Patterns also provide road maps to the structure of existing systems, making it easier for the inexpert designer to understand and navigate existing software. This can reduce discovery costs. Our studies suggest that as much as half of software development effort can be attributed to discovery.
  3. Cost. Cost reduction follows in a straightforward way from development interval reduction.
  4. Customer Satisfaction. Customer satisfaction is largely a result of the other factors.

Counted Body Idiom (CBI)

The is a software design pattern that provides a way to manage the lifetime of shared objects. It works by keeping track of the number of references to an object, and deleting the object when the count reaches zero. This pattern is often used in languages like C++, where manual memory management is required. The CBI works by adding a reference count to the object being shared, called the body. Access to the body is only allowed through a handle object. When a handle is created, it increments the reference count of the body. When a handle is destroyed, it decrements the reference count. When the reference count reaches zero, the body is deleted.
The CBI has a number of advantages over other memory management techniques, such as garbage collection. It is more efficient, because it does not require the runtime to scan for and delete unreachable objects. It is also more flexible, because it allows the programmer to control exactly when objects are deleted. However, the CBI also has some disadvantages. It is intrusive, because it requires the programmer to modify the code for the body class. It is also prone to errors, if the programmer does not correctly increment and decrement the reference count.
Here is a simple example of how to use the CBI in C++:
class Body {
public:
  Body() {}
  ~Body() {}

  int getReferenceCount() const { return referenceCount_; }
  void incrementReferenceCount() { referenceCount_++; }
  void decrementReferenceCount() { referenceCount_--; }

private:
  int referenceCount_ = 0;
};

class Handle {
public:
  Handle(Body* body) : body_(body) { body_->incrementReferenceCount(); }
  ~Handle() { body_->decrementReferenceCount(); }

  Body* getBody() const { return body_; }

private:
  Body* body_;
};
int main() {
  Body* body = new Body();

  Handle handle(body);

  // Use the body object here.

  // Destroy the handle.
  handle.reset();

  // The body object will be deleted automatically when the reference count reaches zero.

  return 0;
}

The Counted Body Idiom is a powerful tool for managing the lifetime of shared objects. However, it is important to be aware of its limitations and use it carefully.