Book Image

Practical Design Patterns for Java Developers

By : Miroslav Wengner
Book Image

Practical Design Patterns for Java Developers

By: Miroslav Wengner

Overview of this book

Design patterns are proven solutions to standard problems in software design and development, allowing you to create reusable, flexible, and maintainable code. This book enables you to upskill by understanding popular patterns to evolve into a proficient software developer. You’ll start by exploring the Java platform to understand and implement design patterns. Then, using various examples, you’ll create different types of vehicles or their parts to enable clarity in design pattern thinking, along with developing new vehicle instances using dedicated design patterns to make the process consistent. As you progress, you’ll find out how to extend vehicle functionalities and keep the code base structure and behavior clean and shiny. Concurrency plays an important role in application design, and you'll learn how to employ a such design patterns with the visualization of thread interaction. The concluding chapters will help you identify and understand anti-pattern utilization in the early stages of development to address refactoring smoothly. The book covers the use of Java 17+ features such as pattern matching, switch cases, and instances of enhancements to enable productivity. By the end of this book, you’ll have gained practical knowledge of design patterns in Java and be able to apply them to address common design problems.
Table of Contents (14 chapters)
1
Part 1: Design Patterns and Java Platform Functionalities
4
Part 2: Implementing Standard Design Patterns Using Java Programming
8
Part 3: Other Essential Patterns and Anti-Patterns

Understanding the SOLID design principles

In the previous sections, the idea of structured work was introduced. The development pillars of APIE were elaborated on in detail using examples. You have gained a foundational understanding of the concept of class instances in terms of object-oriented principles and how we can create different types of specific objects:

Figure 1.11 – Vehicle N, where N is a positive integer number, represents an instance of the Vehicle class

Figure 1.11 – Vehicle N, where N is a positive integer number, represents an instance of the Vehicle class

Classes can be instantiated so that an instance becomes an object. The object must fit into free memory. We say that the object allocates memory space. When Java is considered, allocated memory is virtual space inside the physical system’s memory.

Just a small note – we previously discussed the existence of the JVM, an interpreter of compiled bytecode for the required platform (see Figure 1.3). We mentioned other JVM features, one of which is memory management. In other words, the JVM assumes responsibility for allocating virtual memory space. This virtual memory space can be used to allocate an instance of a class. This virtual memory and its fragmentation are taken care of by the JVM and an unused object cleans up the selected garbage collection algorithm, but this is beyond the scope of this book and would be the subject of further study (see Reference 1).

Every programmer, although it may not be obvious at first glance, plays the role of a software designer. The programmer creates the code by writing it. The code carries an idea that is semantically transformed into action depending on the text entered.

Over time, software development has gone through many phases and many articles have been written and published on software maintenance and reusability. One of the milestones in software development may be considered the year 2000 when Robert C. Martin published his paper on Design Principles and Design Patterns (see Reference 2). The paper reviews and examines techniques in the design and implementation of software development. These techniques were later simplified in 2004 into the mnemonic acronym SOLID.

The goal of the SOLID principles is to help software designers make software and its structure more sustainable, reusable, and extensible. In the following sections, we will examine each of the individual terms hidden after the initial letter in the abbreviation SOLID.

The single-responsibility principle (SRP) – the engine is just an engine

The first principle is a well-defined class goal. We can say that each class should have only one reason to exist. As in, it has the intention and responsibility for only one part of the functionality. The class should encapsulate this part of the program. Let’s put this in the context of an example. Imagine the previous example of a vehicle and its abstraction. We are now extending this class with the Engine and VehicleComputer classes, as shown:

Figure 1.12 – The Vehicle class instance using Engine and VehicleComputer realization but an engine functionality does not interfere with the lights

Figure 1.12 – The Vehicle class instance using Engine and VehicleComputer realization but an engine functionality does not interfere with the lights

The engine can start and stop, but the instance of the Engine class cannot control vehicle lights, for example. The light control is the responsibility of the vehicle computer class instance.

The open-closed principle (OCP)

This principle states that the class or entity under consideration should be open to extension but closed to modifications. It goes hand in hand with the concepts already mentioned. Let’s put this in the context of an example where we consider the Car and Truck classes. Both classes inherit the Vehicle interface. Both believe that vehicle entities have a move method.

By not thinking about proper abstraction and without respecting the OCP, code can easily bear unexpected difficulties when classes are not easy to reuse or cannot be handled (see Example 1.5):

public interface Vehicle {}
public class Car implements Vehicle{
    public void move(){}
}
public class Truck implements Vehicle {
    public void move(){}
}
-- usage --
List<Vehicle> vehicles = Arrays.asList(new Truck(), new 
    Car());
vehicles.get(0).move() // ERROR, NOT POSISBLE!

The correction of the example at hand is very trivial in this case (see Example 1.6):

public interface Vehicle {
    void move();    // CORRECTION!
}
--- usage ---
List<Vehicle> vehicles = Arrays.asList(new Truck(), new 
    Car());
vehicles.get(0).move() // CONGRATULATION, ALL WORKS!

Obviously, as code evolves, non-compliance leads to unexpected challenges.

The Liskov Substitution Principle (LSP) – substitutability of classes

The previous sections dealt with inheritance and abstraction as two of the key pillars of OOP. It will come as no surprise to those of you who have read carefully that, given the class hierarchy of parent-child relationships, a child may be replaced or represented by its parent and vice versa (see Example 1.7). Let us look at the example of CarWash, where you can wash any vehicle:

public interface Vehicle {
    void move();
}
public class CarWash {
    public void wash(Vehicle vehicle){}      
}
public class Car implements Vehicle{
    public void move(){}
}
public class SportCar extends Car {}
--- usage ---
CarWash carWash = new CarWash();
carWash.wash(new Car());
carWash.wash(new SportCar());

This means that classes of a similar type can act analogously and replace the original class. This statement was first mentioned during a keynote address by Barbara Liskov in 1988 (see Reference 3). The conference focused on data abstraction and hierarchy. The statement was based on the idea of substitutability of class instances and interface segregation. Let’s look at interface segregation next.

The interface segregation principle (ISP)

This principle states that no instance of a class should be forced to depend on methods that are not used or in their abstractions. It also provides instructions on how to structure interfaces or abstract classes. In other words, it controls how to divide the intended methods into smaller, more specific entities. The client could use these entities transparently. To point out a malicious implementation, consider Car and Bike as children of the Vehicle interface, which shares all the abstract methods (see Example 1.8):

public interface Vehicle {
    void setMove(boolean moving);
    boolean engineOn();
    boolean pedalsMove();
}
public class Bike implements Vehicle{
    ...
    public boolean engineOn() {
        throw new IllegalStateException("not supported");
    }
    ...
}
public class Car implements Vehicle {
    ...
    public boolean pedalsMove() {
        throw new IllegalStateException("not supported");
    }
}
--- usage ---
private static void printIsMoving(Vehicle v) {
    if (v instanceof Car) { 
        System.out.println(v.engineOn());}
    if(v instanceof Bike) 
        {System.out.println(v.pedalsMove());}
}

Some of you with a keen eye will already notice that such a software design direction negatively involves software flexibility through unnecessary actions that need to be considered (such as exceptions). The remedy is based on compliance with the ISP in a very transparent way. Consider two additional interfaces, HasEngine and HasPedals, with their respective functions (see Example 1.9). This step forces the printIsMoving method to overload. The entire code becomes transparent to the client and does not require any special treatment to ensure code stability, with exceptions as an example (as seen in Example 1.8):

public interface Vehicle {
    void setMove(boolean moving);
}
public interface HasEngine {
    boolean engineOn();
}
public interface HasPedals {
    boolean pedalsMove();
}
public class Bike implements HasPedals, Vehicle {...}
public class Car implements HasEngine, Vehicle {...}
--- usage --- 
private static void printIsMoving(Vehicle v){
    // no access to internal state
}
private static void printIsMoving(Car c) {
    System.out.println(c.engineOn());
}
private static void printIsMoving(Bike b) {
    System.out.println(b.pedalsMove());
}

Two interfaces, HasEngine and HasPedals, are introduced, which enforce method code overload and transparency.

The dependency inversion principle (DIP)

Every programmer, or rather software designer, will face the challenge of hierarchical class composition throughout their careers. The following DIP is a remarkably simple guide on how to approach it.

The principle suggests that a low-level class should not know about high-level classes. In the opposite direction, this means that the high-level classes, the classes that are above, should have no information about the basic classes at lower levels (see Example 1.10, with the SportCar class):

public interface Vehicle {}
public class Car implements Vehicle{}
public class SportCar extends Car {}
public class Truck implements Vehicle {}
public class Bus implements Vehicle {}
public class Garage {
    private List<Vehicle> parkingSpots = new ArrayList<>();
    public void park(Vehicle vehicle){
        parkingSpots.add(vehicle);
    }
}

It also means that the implementation of a particular functionality should not depend on specific classes, but rather on their abstractions (see Example 1.10, with the Garage class).