Patterns have always captivated my attention, as they can be found in almost every aspect of life. The presence of recurring patterns suggests a level of thought and intention behind what we observe. In essence, patterns imply design, and design inherently implies the existence of a designer. As a Christian, this holds profound significance for me because the intricate design prevalent in nature, such as the Fibonacci sequence, points to a purposeful Creator who orchestrated the formation of our world. This concept resonates with the realm of software design as well.

In the realm of software engineering, a well-crafted software solution is the result of deliberate and skilled thinking by software engineers. They contemplate the optimal structure and functionality of the software before building it accordingly. It is essential to recognize that software solutions do not emerge haphazardly; they are meticulously designed. A software engineer must make informed choices, adhere to industry best practices, and employ their expertise to construct a solution that effectively addresses the given problem.

Much like the intricate design found in creation, the process of software design is guided by intention, knowledge, and expertise. It is through the thoughtful choices and adherence to best practices that a software engineer crafts a solution that not only solves the problem at hand but also exhibits qualities of efficiency, scalability, and maintainability.

What are Design Patterns?

Software Design Patterns are established principles in software design that offer proven and reusable solutions to common problems encountered in software development. These patterns primarily apply to Object-Oriented Programming languages like Java or Smalltalk. One of the most influential books on Design Patterns is the “Gang of Four” book, published in 1994 and copyrighted in 1995. It originated from an informal session at OOPSLA ’90, an ACM research conference, led by Bruce Anderson. While I had the privilege of working with Bruce Anderson during my time at IBM, it is unlikely that he remembers me today.

The “Design Patterns” book remains the de facto reference manual for design patterns, although it has not been without controversy. Paul Graham, a prominent advocate of Lisp and Arc, expresses his skepticism towards design patterns, stating that patterns in code indicate a potential issue. He believes that the structure of a program should solely reflect the problem it aims to solve, and any additional regularity in the code may indicate inadequate abstractions or the manual expansion of macros.

Interestingly, even the original authors of the “Gang of Four” book admitted that they would make some changes if they were to rewrite it today. They would consider re-categorizing certain patterns, introducing new ones, and even eliminating the Singleton pattern altogether.

In my upcoming series of posts on design patterns, I plan to delve into various patterns, starting with the Singleton design pattern, which happens to be one of the most controversial patterns. Before addressing the criticisms surrounding the Singleton, it is essential to provide a concise explanation of what this pattern entails.

Singleton Explained

The Singleton pattern, as defined in the Design Patterns book, aims to ensure that a class has only one instance while providing a global point of access to that instance. The book presents examples such as a printer spooler, file system, and window manager as potential use cases for the Singleton pattern. Additionally, it suggests that the Singleton pattern is well-suited for use by an Abstract Factory class to create single instances of corresponding Factory classes or to create a single instance of a Facade class, which acts as a unified interface for complex subsystem functionality.

From a software engineering perspective, the implementation of the Singleton pattern is intriguing. It involves making the constructor of the Singleton class private and exposing a public static method that returns an instance of the Singleton, ensuring that the constructor is only called once. The following UML sketch illustrates this concept:

When discussing the implementation of a Singleton design pattern, two crucial points should be noted. Firstly, there is a private static property in the Singleton class that holds the instance. Secondly, the method used to retrieve an instance of the Singleton checks whether the private static instance property is populated or null. If it is null, the Singleton constructor is invoked. If it is already populated, the constructor is bypassed, and the existing instance is returned to the requester. Here is an example implementation of a Singleton class in Java:

    /**
     * An example of one way to implement the classic
     * Singleton Design Pattern in Java
     *
     * @author: Brad Jones
     */
    class Singleton
    {
        // static variable instance Singleton
        private static Singleton instance = null;
        private String state = null;

        // private constructor only available within this class
        private Singleton(){
            state = "initialized";
        }

        // static method to return instance of singleton
        public static Singleton getInstance(){
            if (instance == null){
                instance = new Singleton();
            }
            return instance;
        }

        //Method that can be called on the instance
        public String getState(){
            return state;
        }
    }

This Java implementation showcases the key aspects of the Singleton pattern, ensuring that only one instance of the class is created and providing a global access point to that instance.

Pattern or Anti-Pattern?

Now that we have a clear understanding of what a Singleton Pattern is, let’s discuss whether or not it is advisable to use a Singleton in the software you’re developing. Opinions on this matter differ, and I personally belong to the camp that believes using a Singleton is generally not a good idea in most cases. I have several reasons for taking this stance, as the pattern itself has inherent flaws:

  1. Violation of Single Responsibility Principle (SRP): The Singleton pattern combines the responsibility of managing its own instance with the primary responsibility of the class it represents. This violates the SRP, which states that a class should have only one reason to change. It becomes challenging to modify or extend the class’s functionality without affecting the Singleton instance management.
  2. Global State: Singletons introduce a global state, making it difficult to track and reason about dependencies and side effects. Any part of the application can access and modify the Singleton instance, leading to potential issues with concurrency, data consistency, and testing.
  3. Tight Coupling and Dependency Inversion: Using Singletons creates tight coupling between classes, as they directly depend on the Singleton instance. This hampers flexibility, modularity, and the ability to substitute dependencies, making the codebase less maintainable and harder to test.
  4. Difficulty in Unit Testing: Singleton instances are often accessed statically, making it challenging to write isolated unit tests. Unit tests should focus on testing individual units of code in isolation, but Singletons introduce dependencies that cannot be easily mocked or substituted.
  5. Hidden Dependencies and Potential Side Effects: Singletons can hide dependencies on external resources or services, making it harder to identify and manage those dependencies. Additionally, modifications to the Singleton instance can have unintended side effects throughout the application, making it harder to reason about the system’s behavior.
  6. Reduced Flexibility for Scaling and Extending: The use of Singletons can limit scalability and extensibility. When multiple instances are required or when changes in the application’s requirements demand different variations of the Singleton, refactoring becomes complex and may require significant modifications.

While there may be exceptional cases where the Singleton pattern is deemed appropriate, it is crucial to carefully evaluate the trade-offs and consider alternative design approaches such as dependency injection, inversion of control containers, or using stateless services. These alternatives can provide better separation of concerns, improved testability, and more flexible and maintainable code.

Conclusion

While it is true that understanding the drawbacks of a design pattern is essential, it is important to exercise caution when considering the use of the Singleton pattern. While it may not be classified as an outright anti-pattern, its potential issues and trade-offs should be carefully evaluated before deciding to use it.

As a software engineer, it is crucial to weigh the benefits and drawbacks of any design pattern and consider alternative approaches. The Singleton pattern’s drawbacks, such as violating the Single Responsibility Principle, introducing global state, and hindering testability and flexibility, can have significant implications on the maintainability and scalability of the codebase.

Using the Singleton pattern should not be a default choice, but rather a deliberate decision based on a thorough analysis of the specific problem at hand. It is important to consider whether there are alternative patterns or architectural approaches that better address the problem and align with best practices in software engineering.

In some cases, the Singleton pattern may indeed provide a suitable solution, especially if the potential issues have been carefully considered and mitigated. However, it is crucial to exercise caution, ensure the design aligns with the requirements and future scalability needs of the system, and be prepared to refactor or reconsider the approach if necessary.

Ultimately, the goal should be to create software that is maintainable, testable, and flexible, and to use design patterns thoughtfully and judiciously to achieve those objectives.