Horse first and look later! An article summarizing 23 design patterns of Java

JavaLanguages ​​have many options to choose from when using design patterns, the most commonly used of which are 23 design patterns. In this article, we will give a brief introduction to these 23 design patterns in Java.

The following three articles introduce in detail Javathe introduction and usage scenarios of 23 design patterns, including specific code implementations.

  • Java Common Design Patterns (1) - Nuggets (juejin.cn)[1]

  • Java Common Design Patterns (2) - Nuggets (juejin.cn)[2]

  • Java Common Design Patterns (3) - Nuggets (juejin.cn)[3]

JavaThe 23 design patterns in are mainly divided into three categories:

  • Creational pattern: mainly solves the problem of object creation

  • Structural pattern: mainly solves the problem of object combination

  • Behavioral pattern: mainly solves the interaction problems between objects

creational pattern

JavaThe creational design pattern in is mainly used for the creation and assembly of objects. These patterns can make systems more flexible and extensible by abstracting and decoupling the object creation process. Here are Java5 creative design patterns:

  • Singleton pattern : ensures that a class has only one instance and provides a global access point.

  • Factory pattern : Use factory methods to create objects without exposing the logic of creating objects.

  • Abstract Factory Pattern : Provides an interface for creating a family of related or dependent objects without specifying the actual implementation class.

  • Builder pattern : Separates the construction and representation of complex objects so that the same construction process can create different representations.

  • Prototype pattern : Create objects by cloning, avoiding the overhead newof explicitly calling the constructor through keywords.

structural pattern

JavaThe structural design pattern in is mainly used to describe the relationship between objects, including the combination of classes and objects, interfaces and inheritance. These patterns can help us better organize and manage code, and improve code reusability and maintainability. The following are Java7 structural design patterns:

  • Adapter pattern : Convert the interface of a class into another interface that the customer wants, so that classes that originally could not work together due to incompatible interfaces can work together.

  • Bridge pattern : Separates the abstract part from its implementation part so that they can vary independently.

  • Combination mode : Combine objects into a tree structure to represent a "part-whole" hierarchy, allowing the client to use a single object or combined objects with consistency.

  • Decorator pattern : Dynamically add some additional responsibilities to an object. In terms of adding functionality, the decorator pattern is more flexible than generating subclasses.

  • Facade pattern : Provides a consistent interface for a set of interfaces in a subsystem, making the subsystem easier to use.

  • Flyweight pattern : Use sharing technology to effectively support the reuse of a large number of fine-grained objects.

  • Proxy pattern : Provides a proxy for other objects to control access to this object.

behavioral patterns

JavaThe behavioral design pattern in is mainly used to describe the communication and collaboration methods between objects, including algorithms, responsibility chains, status, etc. These patterns can help us better organize and manage code, and improve the maintainability and scalability of code. Here are Java11 behavioral design patterns:

  • Chain of responsibility pattern : In order to decouple the sender and receiver of the request, the request processing objects are connected into a chain, and the request is passed along this chain until an object handles it.

  • Command pattern : Encapsulates a request as an object, allowing you to parameterize clients with different requests; queue or log requests, and support undoable operations.

  • Interpreter pattern : Given a language, define a representation of its grammar, and define an interpreter that is used to interpret sentences in the language.

  • Iterator pattern : Provides a way to sequentially access the elements of an aggregate object without exposing the object's internal representation.

  • Mediator pattern : Use a mediator object to encapsulate a series of object interactions so that these objects do not need to explicitly reference each other, thus reducing the degree of coupling.

  • Memento pattern : Capture the internal state of an object and save this state outside the object without destroying encapsulation.

  • Observer pattern : defines a one-to-many dependency relationship between objects. When the state of an object changes, all objects that depend on it are notified and automatically updated.

  • State Pattern : Allows an object to change its behavior when its internal state changes, making the object appear to have modified its class.

  • Strategy pattern : Define a series of algorithms, encapsulate each algorithm, and make them interchangeable.

  • Template method pattern : Define the algorithm skeleton in an operation and defer some steps to subclasses. Template methods allow subclasses to redefine certain steps in an algorithm without changing the structure of the algorithm.

  • Filter design pattern : allows the behavior of dynamically adding or removing objects without changing the original objects.

After understanding the design patterns, let’s learn more about: six design principles

  1. Single Responsibility Principle : A class should have only one reason for its change. In other words, a class should have only one responsibility. This can ensure the cohesion of classes and reduce the coupling between classes.

  2. Open-closed principle : A software entity such as a class, module, and function should be open for extension and closed for modification. This means that when new functionality needs to be added, it should be done by extending the existing code rather than modifying the existing code.

  3. Liskov Substitution Principle : A subclass should be able to replace a parent class without affecting the correctness of the program. This means that when using inheritance, the subclass cannot modify the existing behavior of the parent class, but can only extend the functionality of the parent class.

  4. Interface isolation principle : A client should not depend on interfaces it does not need. A class should only provide the interfaces it needs and should not force clients to depend on interfaces it does not need.

  5. Dependency Inversion Principle : High-level modules should not depend on low-level modules, they should all depend on abstractions. Abstraction should not depend on concrete implementation, but concrete implementation should depend on abstraction.

  6. Demeter's Law : An object should keep the least knowledge about other objects. In other words, an object should only interact with the objects it interacts with directly, and not with any other objects. This can reduce the coupling between classes and improve the flexibility and maintainability of the system.

设计模式与设计原则他们有什么不同呢?

Design principles and design patterns are two important concepts in object-oriented design. They are related to each other, but have different meanings and functions:

  • Design principles are general design guidelines that provide the basic ideas and rules for how to design an excellent software system. It guides designers on how to organize code to achieve high cohesion, low coupling, easy expansion and easy maintenance of software systems.

  • Design patterns are empirical solutions to common problems in specific situations. They provide specific ways of implementing these design principles.

Design patterns are often applied based on meeting design principles. Design patterns can be thought of as a concrete way of implementing design principles.

In actual development, we rarely only use a single design pattern to solve a problem, but mix multiple design patterns to achieve better results.

Factory pattern + singleton pattern

Use the factory pattern to create objects, and use the singleton pattern to ensure that the factory has only one instance, thereby reducing the overhead of creating objects.

First, create a factory class that uses the singleton pattern to ensure that there is only one instance, which is responsible for creating the object. Then, create as many factory methods as needed, each method used to create a different object.

public class SingletonFactory {
    private static volatile SingletonFactory instance;

    private SingletonFactory() {
        
    }

    public static SingletonFactory getInstance() {
        if (instance == null) {
            synchronized (SingletonFactory.class) {
                if (instance == null) {
                    instance = new SingletonFactory();
                }
            }
        }
        return instance;
    }

    public Object createObject(String type) {
        if ("type1".equals(type)) {
            return new Type1();
        } else if ("type2".equals(type)) {
            return new Type2();
        } else {
            throw new IllegalArgumentException("Unsupported type: " + type);
        }
    }
}

public class Type1 {
    
}

public class Type2 {
    
}


SingletonFactoryThe class implements the singleton pattern using double-checked locking and provides a createObject()method that creates different objects based on the input parameters. Type1 和 Type2Classes represent different types of objects, and they contain their own implementation logic.

Template method pattern + strategy pattern

Use the Template Method pattern to define the skeleton of the algorithm, while using the Strategy pattern to define different implementations of the algorithm to achieve greater flexibility.

Suppose we want to implement an image processing program that can process different types of images, including scaling, rotating, and cropping operations. The specific processing algorithm can vary according to different types of images.

First, we define an abstract class ImageProcessor, which contains a template method processImage()that defines a series of processing steps, including opening pictures, executing specific processing algorithms, and saving pictures. Among them, the specific processing algorithm is implemented by the strategy pattern, and we use an abstract strategy interface ImageProcessingStrategyto define different processing algorithms.

public abstract class ImageProcessor {

    public void processImage() {
        BufferedImage image = openImage();
        ImageProcessingStrategy strategy = createImageProcessingStrategy();
        BufferedImage processedImage = strategy.processImage(image);
        saveImage(processedImage);
    }

    protected BufferedImage openImage() {
        
    }

    protected abstract ImageProcessingStrategy createImageProcessingStrategy();

    protected void saveImage(BufferedImage image) {
        
    }
}

public interface ImageProcessingStrategy {

    BufferedImage processImage(BufferedImage image);
}


Then, define specific image processing classes JpegProcessorand PngProcessor, which respectively inherit from ImageProcessorand implement createImageProcessingStrategy()methods to return different processing algorithm strategies.

public class JpegProcessor extends ImageProcessor {

    @Override
    protected ImageProcessingStrategy createImageProcessingStrategy() {
        return new JpegProcessingStrategy();
    }
}

public class PngProcessor extends ImageProcessor {

    @Override
    protected ImageProcessingStrategy createImageProcessingStrategy() {
        return new PngProcessingStrategy();
    }
}


Finally, define different processing algorithm strategies, such as JpegProcessingStrategyand PngProcessingStrategy, which implement ImageProcessingStrategythe interface and provide specific processing algorithm implementations.

public class JpegProcessingStrategy implements ImageProcessingStrategy {

    @Override
    public BufferedImage processImage(BufferedImage image) {
        
    }
}

public class PngProcessingStrategy implements ImageProcessingStrategy {

    @Override
    public BufferedImage processImage(BufferedImage image) {
        
    }
}

In this way, an extensible and customizable image processing program is implemented. At runtime, we can select different processing algorithms according to different image types to achieve different image processing effects.

Strategy mode + factory mode

Use the factory pattern to create different strategy objects, and then use the strategy pattern to select different strategies to achieve different functions. Let's implement a simple calculator.

First, we define an CalculatorStrategyinterface, which contains two methods: calculateused to calculate the results of two numbers, and getDescriptionused to obtain the description information of the current strategy.

public interface CalculatorStrategy {
    double calculate(double num1, double num2);
    String getDescription();
}


Then, we implement several specific calculation strategy classes, such as addition, subtraction, multiplication and division:

public class AddStrategy implements CalculatorStrategy {
    @Override
    public double calculate(double num1, double num2) {
        return num1 + num2;
    }

    @Override
    public String getDescription() {
        return "加法";
    }
}

public class SubtractStrategy implements CalculatorStrategy {
    @Override
    public double calculate(double num1, double num2) {
        return num1 - num2;
    }

    @Override
    public String getDescription() {
        return "减法";
    }
}

public class MultiplyStrategy implements CalculatorStrategy {
    @Override
    public double calculate(double num1, double num2) {
        return num1 * num2;
    }

    @Override
    public String getDescription() {
        return "乘法";
    }
}

public class DivideStrategy implements CalculatorStrategy {
    @Override
    public double calculate(double num1, double num2) {
        return num1 / num2;
    }

    @Override
    public String getDescription() {
        return "除法";
    }
}

Next, we use the factory pattern to create specific policy objects. We create a CalculatorStrategyFactoryfactory class, which defines a getCalculatorStrategymethod that returns the corresponding calculation strategy object based on the incoming operator.

public class CalculatorStrategyFactory {
    public static CalculatorStrategy getCalculatorStrategy(String operator) {
        switch (operator) {
            case "+":
                return new AddStrategy();
            case "-":
                return new SubtractStrategy();
            case "*":
                return new MultiplyStrategy();
            case "/":
                return new DivideStrategy();
            default:
                throw new IllegalArgumentException("无效的操作符:" + operator);
        }
    }
} 

We create a Calculatorclass that contains a calculatemethod that returns the calculation result based on the two numbers and operators passed in.

public class Calculator {
    public static double calculate(double num1, double num2, String operator) {
        CalculatorStrategy calculatorStrategy = CalculatorStrategyFactory.getCalculatorStrategy(operator);
        System.out.println("正在执行 " + calculatorStrategy.getDescription() + " 计算");
        return calculatorStrategy.calculate(num1, num2);
    }
}



double result = Calculator.calculate(10, 5, "+"); 
 
System.out.println(result);

Adapter pattern + Decorator pattern

The adapter pattern is used to convert one interface into another interface, while the decorator pattern is used to dynamically add some additional responsibilities to an object. When we need to convert an existing interface into a new interface, and also need to add some additional responsibilities to the object, we can use a mixture of these two patterns.

First define an interface that needs to be adapted:

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

Then define a specific class that can play mp3 files:

public class Mp3Player implements MediaPlayer {

   @Override
   public void play(String audioType, String fileName) {
      if(audioType.equalsIgnoreCase("mp3")){
         System.out.println("Playing mp3 file. Name: " + fileName);         
      } 
   }
}

Next define an adapter to AdvancedMediaPlayerconvert interface into MediaPlayerinterface

public class MediaPlayerAdapter implements MediaPlayer {

   AdvancedMediaPlayer advancedMusicPlayer;

   public MediaPlayerAdapter(AdvancedMediaPlayer advancedMusicPlayer){
      this.advancedMusicPlayer = advancedMusicPlayer;
   }

   @Override
   public void play(String audioType, String fileName) {
      if(audioType.equalsIgnoreCase("vlc") 
         || audioType.equalsIgnoreCase("mp4")){
         advancedMusicPlayer.play(audioType, fileName);
      }
      else if(audioType.equalsIgnoreCase("mp3")){
         System.out.println("Playing mp3 file. Name: " + fileName);
      }
   }
}

Then define a decorator class to dynamically add some additional functions when playing mp3 files:

public class Mp3PlayerDecorator implements MediaPlayer {

   private MediaPlayer mediaPlayer;

   public Mp3PlayerDecorator(MediaPlayer mediaPlayer) {
      this.mediaPlayer = mediaPlayer;
   }

   @Override
   public void play(String audioType, String fileName) {
      if(audioType.equalsIgnoreCase("mp3")){
         System.out.println("Playing mp3 file with additional features. Name: " + fileName);
         
         System.out.println("Adding equalizer to the mp3 file.");
         mediaPlayer.play(audioType, fileName);
      }
      else {
         mediaPlayer.play(audioType, fileName);
      }
   }
}

Finally, we can use adapters and decorators to play different types of audio files:

public static void main(String[] args) {
   MediaPlayer mediaPlayer = new Mp3Player();
   mediaPlayer.play("mp3", "song.mp3");

   AdvancedMediaPlayer advancedMediaPlayer = new VlcPlayer();
   MediaPlayer mediaPlayerAdapter = new MediaPlayerAdapter(advancedMediaPlayer);
   mediaPlayerAdapter.play("vlc", "movie.vlc");

   MediaPlayer decoratedMediaPlayer = new Mp3PlayerDecorator(mediaPlayer);
   decoratedMediaPlayer.play("mp3", "song.mp3");
}


Output results

Playing mp3 file. Name: song.mp3
Playing vlc file. Name: movie.vlc
Playing mp3 file with additional features. Name: song.mp3
Adding equalizer to the mp3 file.
Playing mp3 file. Name: song.mp3

Observer mode + command mode

The observer pattern is used to observe state changes of objects and notify observers in time. The command mode is used to encapsulate a request into an object, which can dynamically switch the recipient of the command at runtime. When we need to observe the state changes of an object and execute some commands when the state changes, we can use a mixture of these two modes.

First, we create an interface Observerto represent the observer object, which contains a update()method for updating the observer state.

public interface Observer {
    void update();
}

Next, we create a class Subjectto represent the observed object, which contains references to some observer objects and some methods for registering, unregistering and notifying observers.

import java.util.ArrayList;
import java.util.List;

public class Subject {
    private List<Observer> observers = new ArrayList<>();

    public void register(Observer observer) {
        observers.add(observer);
    }

    public void unregister(Observer observer) {
        observers.remove(observer);
    }

    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update();
        }
    }
}

Next, we create an interface Commandto represent the command object, which contains a execute()method for executing the command.

public interface Command {
    void execute();
}

Then, we create a concrete command class ConcreteCommandthat implements Commandthe interface, which contains a Subjectreference to the object and a execute()method that calls Subjectthe object's notifyObservers()method to notify the observer object.

public class ConcreteCommand implements Command {
    private Subject subject;

    public ConcreteCommand(Subject subject) {
        this.subject = subject;
    }

    @Override
    public void execute() {
        subject.notifyObservers();
    }
}

Finally, we create a concrete observer class ConcreteObserverthat implements Observerthe interface and contains a execute()method that outputs a message.

public class ConcreteObserver implements Observer {
    @Override
    public void update() {
        System.out.println("ConcreteObserver received notification.");
    }
}

Now we can mix observer pattern and command pattern using the following code:

public class Client {
    public static void main(String[] args) {
        
        Subject subject = new Subject();

        
        Observer observer = new ConcreteObserver();

        
        subject.register(observer);

        
        Command command = new ConcreteCommand(subject);

        
        command.execute();
    }
}


summary

It should be noted that you should be careful when mixing design patterns and do not overuse design patterns to avoid the code becoming too complex and difficult to maintain. Choosing the right design pattern and the right combination can make the code more concise, efficient and easier to maintain.

References

[1]

https://juejin.cn/post/7198700701952983100: https://juejin.cn/post/7198700701952983100

[2]

https://juejin.cn/post/7199907126688825400: https://juejin.cn/post/7199907126688825400

[3]

https://juejin.cn/post/7202820042688528445: https://juejin.cn/post/7202820042688528445

Guess you like

Origin blog.csdn.net/weixin_54542328/article/details/133351812