Creational vs Structural Patterns: Java Implementation

In software design, design patterns play a crucial role in solving common problems in a reusable and efficient way. Two important categories of design patterns are creational patterns and structural patterns. Creational patterns are concerned with object creation mechanisms, aiming to create objects in a way that is suitable for the situation. Structural patterns, on the other hand, deal with how classes and objects are composed to form larger structures. In this blog post, we will explore the fundamental concepts of creational and structural patterns in Java, their usage methods, common practices, and best practices. We will also provide clear code examples to illustrate these concepts.

Table of Contents

  1. Creational Patterns
  2. Structural Patterns
  3. Conclusion
  4. References

Creational Patterns

Creational Patterns: Fundamental Concepts

Creational patterns provide ways to create objects while hiding the creation logic. They decouple the object creation from the rest of the code, making the code more flexible and maintainable. Some of the well - known creational patterns are Singleton, Factory Method, and Abstract Factory.

  • Singleton: Ensures that a class has only one instance and provides a global point of access to it.
  • Factory Method: Defines an interface for creating an object, but lets subclasses decide which class to instantiate.
  • Abstract Factory: Provides an interface for creating families of related or dependent objects without specifying their concrete classes.

Creational Patterns: Usage Methods

  • Singleton: Use when you need to control the number of instances of a class. For example, in a logging system, you may want only one logger instance throughout the application.
  • Factory Method: When you want to delegate the object creation to subclasses. For example, in a game, different types of enemies can be created using a factory method.
  • Abstract Factory: When you need to create families of related objects. For example, in a GUI framework, an abstract factory can be used to create different types of widgets (buttons, text fields, etc.) for different operating systems.

Creational Patterns: Common Practices

  • Lazy Initialization: In the Singleton pattern, use lazy initialization to create the instance only when it is first requested. This can save resources.
  • Separation of Concerns: In the Factory Method and Abstract Factory patterns, separate the object creation logic from the business logic. This makes the code more modular.

Creational Patterns: Best Practices

  • Thread Safety: In a multi - threaded environment, ensure that the Singleton pattern is thread - safe. You can use techniques like double - checked locking or an enum implementation.
  • Flexibility: Design the Factory Method and Abstract Factory patterns to be flexible enough to accommodate new types of objects without major code changes.

Creational Patterns: Code Examples

Singleton Pattern

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

Factory Method Pattern

// Product interface
interface Shape {
    void draw();
}

// Concrete products
class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a circle");
    }
}

class Square implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a square");
    }
}

// Creator abstract class
abstract class ShapeFactory {
    public abstract Shape createShape();
}

// Concrete creator
class CircleFactory extends ShapeFactory {
    @Override
    public Shape createShape() {
        return new Circle();
    }
}

class SquareFactory extends ShapeFactory {
    @Override
    public Shape createShape() {
        return new Square();
    }
}

Structural Patterns

Structural Patterns: Fundamental Concepts

Structural patterns are used to compose classes or objects into larger structures. They focus on how classes and objects are combined to form more complex structures. Some common structural patterns are Adapter, Decorator, and Proxy.

  • Adapter: Converts the interface of a class into another interface clients expect. It allows classes with incompatible interfaces to work together.
  • Decorator: Attaches additional responsibilities to an object dynamically. It provides a flexible alternative to subclassing for extending functionality.
  • Proxy: Provides a surrogate or placeholder for another object to control access to it.

Structural Patterns: Usage Methods

  • Adapter: When you need to make an existing class work with another class whose interface is incompatible. For example, in a legacy system, you may need to adapt an old data format to a new one.
  • Decorator: When you want to add additional functionality to an object at runtime. For example, in a coffee shop, you can add different toppings (decorators) to a coffee (the base object).
  • Proxy: When you need to control access to an object. For example, in a remote service, a proxy can be used to handle network communication and security.

Structural Patterns: Common Practices

  • Composition over Inheritance: In the Decorator pattern, use composition to add functionality to an object instead of inheritance. This can reduce code duplication.
  • Transparency: In the Proxy pattern, make the proxy transparent to the client. The client should not be aware that it is using a proxy.

Structural Patterns: Best Practices

  • Performance Considerations: In the Proxy pattern, consider the performance overhead introduced by the proxy. Use it only when necessary.
  • Maintainability: In the Adapter pattern, make the adapter code as simple as possible to ensure maintainability.

Structural Patterns: Code Examples

Adapter Pattern

// Target interface
interface MediaPlayer {
    void play(String audioType, String fileName);
}

// Adaptee
class AdvancedMediaPlayer {
    public void playVlc(String fileName) {
        System.out.println("Playing vlc file: " + fileName);
    }

    public void playMp4(String fileName) {
        System.out.println("Playing mp4 file: " + fileName);
    }
}

// Adapter
class MediaAdapter implements MediaPlayer {
    AdvancedMediaPlayer advancedMediaPlayer;

    public MediaAdapter(String audioType) {
        if (audioType.equalsIgnoreCase("vlc")) {
            advancedMediaPlayer = new AdvancedMediaPlayer();
        }
    }

    @Override
    public void play(String audioType, String fileName) {
        if (audioType.equalsIgnoreCase("vlc")) {
            advancedMediaPlayer.playVlc(fileName);
        }
    }
}

// Concrete implementation of Target
class AudioPlayer implements MediaPlayer {
    MediaAdapter mediaAdapter;

    @Override
    public void play(String audioType, String fileName) {
        if (audioType.equalsIgnoreCase("mp3")) {
            System.out.println("Playing mp3 file: " + fileName);
        } else if (audioType.equalsIgnoreCase("vlc")) {
            mediaAdapter = new MediaAdapter(audioType);
            mediaAdapter.play(audioType, fileName);
        }
    }
}

Decorator Pattern

// Component interface
interface Beverage {
    String getDescription();
    double cost();
}

// Concrete component
class Espresso implements Beverage {
    @Override
    public String getDescription() {
        return "Espresso";
    }

    @Override
    public double cost() {
        return 1.99;
    }
}

// Decorator abstract class
abstract class CondimentDecorator implements Beverage {
    protected Beverage beverage;

    public CondimentDecorator(Beverage beverage) {
        this.beverage = beverage;
    }
}

// Concrete decorator
class Milk extends CondimentDecorator {
    public Milk(Beverage beverage) {
        super(beverage);
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", Milk";
    }

    @Override
    public double cost() {
        return beverage.cost() + 0.3;
    }
}

Conclusion

Creational and structural patterns are essential tools in a Java developer’s toolkit. Creational patterns help in creating objects in a flexible and efficient way, while structural patterns assist in composing classes and objects into larger structures. By understanding the fundamental concepts, usage methods, common practices, and best practices of these patterns, developers can write more modular, maintainable, and flexible code.

When using these patterns, it is important to choose the right pattern for the right situation. Also, follow the best practices to ensure that the code is robust and performs well.

References

  • Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object - Oriented Software. Addison - Wesley.
  • Freeman, E., & Freeman, E. (2004). Head First Design Patterns. O’Reilly Media.