Talking about Design Mode 1-Strategy Mode | Adapter Mode | Factory Mode

Preface

Recently, I read Think In JAVA, which talks about inheritance, composition, abstract classes and interfaces, and mentions several design patterns. These design patterns have really helped me better understand the meaning of each data structure in JAVA. Today, I have my own understanding of the book, a little collation of these design patterns.

Strategy Pattern | Policy pattern

Here we must mention the concept of upward transformation. This concept is mentioned in both inheritance and interface.
Upward conversion in inheritance means that subclasses can be upgraded to parent classes. For example, there is an Instrument class and a subclass of Flute. The play method overridden by the subclass overrides the method of the parent class.

    class Instrument{
        public void play(){
            ...
            //play instrument
        }
    }
    
    class Flute extends Instrument{
        public void play(){
            ...
            //play flute
        }
    }

If there is a method that needs to receive an instrument parameter and play, then there is no need to write multiple overload methods to receive different instruments, only one method that receives Instrument class.

    public void play(Instrument instrument){
        instrument.play();
    }

In this method, a Flute object is passed in, and the play method in flute will be called. This is the simplest example of upward conversion, dynamic binding.
In fact, the interface is the same, but the interface allows a variety of upward conversion. That is to say, inheritance is unique in JAVA, and interfaces can be Implement ed more than one. Therefore, the path of inheritance upward transformation is unique in JAVA, and the path of interface upward transformation is not unique.

Next we will talk about the strategy model.

The concepts of strategy patterns are as follows:

Defines a set of encapsulated algorithms that can be swapped to carry out a specific behaviour
A set of encapsulated algorithms are defined, which perform different operations respectively. In practice, these algorithms can be dynamically switched to meet the needs of different scenarios.

The usage scenarios of the strategy pattern are as follows:

  1. Save files in different formats

  2. Various Realizations of Sorting Algorithms

  3. Various Implementation of File Compression

That is to say, the policy pattern puts a group of codes of different ways to accomplish the same work into different classes, and realizes the switching in operation through the policy pattern.

This is a UML diagram of policy patterns found online.
Policy pattern is a comprehensive application of inheritance, abstract class and interface in JAVA. In the policy mode, we can design a variety of "specific policies" according to an "open interface", and then only need to input "open interface" when invoking. The program runs according to the specific implementation of "open interface" to determine the specific running results.

The code for the above design pattern is as follows:

//Interface
public interface Strategy {
   public int doOperation(int num1, int num2);
}

//Interface Implementation Class
public class OperationAdd implements Strategy{
   @Override
   public int doOperation(int num1, int num2) {
      return num1 + num2;
   }
}
public class OperationSubstract implements Strategy{
   @Override
   public int doOperation(int num1, int num2) {
      return num1 - num2;
   }
}
public class OperationMultiply implements Strategy{
   @Override
   public int doOperation(int num1, int num2) {
      return num1 * num2;
   }
}

//context
public class Context {
   private Strategy strategy;

   public Context(Strategy strategy){
      this.strategy = strategy;
   }

   public int executeStrategy(int num1, int num2){
      return strategy.doOperation(num1, num2);
   }
}

//Specific calls
public class StrategyPatternDemo {
   public static void main(String[] args) {
      Context context = new Context(new OperationAdd());        
      System.out.println("10 + 5 = " + context.executeStrategy(10, 5));

      context = new Context(new OperationSubstract());        
      System.out.println("10 - 5 = " + context.executeStrategy(10, 5));

      context = new Context(new OperationMultiply());        
      System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
   }
}

Next, I will give a concrete example to illustrate the gap between using and not using the strategy model.

Suppose we have a function of compressing files. There are many algorithms for compressing files, such as Zip, RAR and so on. The program can select a compression algorithm to perform compression operation according to the actual operating system and performance parameters. Let's assume that the function of selecting a specific algorithm is placed in the Compression Preference class.
In fact, these are transparent to the client. In other words, the client only knows that there will be a compression function, which requires the client to upload compressed files. In such a scenario, the server only needs to provide a compressed interface without exposing the specific implementation.

The code is as follows:

//Select the compression method class and return the compression method according to the specific situation.
public class CompressionPreference{
     public static CompressionStrategy getPreferedStrategy(){
         //Return a specific compression algorithm based on the system or user's choice
     }
}

//Compression Policy Interface
public interface CompressionStrategy{
    void compress(List<File> files);
}

//Implementation of Compression Strategy
public class ZipCompressionStrategy implements CompressionStrategy{
    @Override
    public void compress(List<File> files){
        //zip compression
    }
}

public class RarCompressionStrategy implements CompressionStrategy{
    @Override
    public void compress(List<File> files){
        //RAR Compression
    }
}

public class CompressionContext{
    private CompressionStrategy strategy;
    
    //Here, compression strategy is selected according to Compression Preference
    public void setStrategy(CompressionStrategy strategy){this.strategy=strategy);}
    
    public void createArchieve(List<File> files){
        strategy.compress(files);
    }
}

//Client Call
public class Client{
    public static void main(String[] args) {
    CompressionContext ctx = new CompressionContext();
    //Setting Compression Context
    ctx.setCompressionStrategy(CompressionPreference.getPreferedStrategy());
    ctx.createArchive(fileList);
  }
}

After this design, if you need to add a new algorithm, you only need to add a specific implementation class of Compression Strategy and modify the method in Compression Preference. There will be no impact on client calls.

If the algorithm is not encapsulated, the client can call it directly. On the one hand, it exposes various implementations of compression algorithm, on the other hand, it also increases the number of possible error calls. And once a new compression algorithm is added, the client needs to know what it doesn't need to know and adjust its calls. The maintainability of such code is too bad.

Adapter Mode | Adapter Design Pattern

Definition:

Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.
Convert two incompatible interfaces through adapters.

The adapter pattern is better understood than the policy pattern. When explaining the adapter pattern in the book, it is actually intended to supplement the instructions on how to program for interfaces. The adapter pattern, as its name implies, converts classes that do not inherit an interface into classes that can be invoked through the adapter. It serves as a bridge between incompatible interfaces.

From the original meaning, the adapter refers to an interface converter, the most common interface converter in life is your mobile phone charging head! The charging head will convert the standard 220V voltage (domestic) output from the socket into a voltage that can be safely charged. A USB charging port is provided on the other side, so that the mobile phone can be charged under all charging lines with USB ports. Another example is the SD card. Friends who use cameras know that some computers do not provide SD card interface, so they need to insert SD card into SD card reader, and then the card reader through the USB interface into the computer. Then the computer can read the contents of the SD card.

In the book's example, the adapter application scenario is to change an unmodifiable class to inherit an interface so that it can be invoked as an implementation class of that interface. There are two ways to implement the adapter pattern in the book, one through proxy and the other through inheritance. The two approaches are essentially the same. If all implementations in the original class are needed, the adapter is implemented by inheritance, and if only partially implemented, by proxy. Specific analysis of the specific situation.

All of the above are too abstract. Here's a concrete example.

For example, I have a scanning class Scanner, which has a method to receive all classes inheriting the Readable interface and read out the data according to the situation in the class. There are already some classes in the system that can be read by Scanner, but they do not inherit the Readable interface. Based on the open-close principle, I can add an adapter to these classes so that they can be read by Scanner. In this mode, whether new Scanner reads files or existing files in the system, there is no need to modify the Scanner method. Just make it support the Readable interface.

    public class Scanner{
        public void read(Readable material){
            material.read();
        }
    }
    
    public interface Readable{
        void read();
    }
    public class TXT implements Readable{
        ...
        public void read(){
            //Read txt file
        }
        ...
    }
    
    public class HTML{
        public void toReadableFormat(){
            //The html file can also be read, but it does not inherit the Readable interface, so it cannot be Scanner
            //Distinguish
        }
    }
    
    //Here is the adapter mode
    public class HTMLAdapter implements Readable{
        ...
        private HTML html;
        public HTMLAdapter(HTML html){this.html = html}
        public void read(){
            html.toReadableFormat();
        }
        ...
    }
    
    //At this point, both files can be read.
    public class Test{
        public static void main(String[] args){
            Scanner s = new Scanner();
            s.read(new TXT());
            s.read(new HTMLAdapter(new HTML()));
        }
    }

One example is not enough. Another~

In the history of the development of self-media, media formats have become more and more diverse, from the original text, to MP3, and then to video formats. If there is a system now, it would only support the reading of files in MP3 format. At this time, if the system can support the playback of new media files. New media class files are developed by another team, with its own development interface and specific implementation. How can we integrate this module into the existing system?

At this point, we need to solve this problem through the adapter mode.

This is the UML class diagram given by the system. By creating a new MediaAdapter adapter in the original system to inherit the interface of the original media player, the original system can continue to call the original play method to realize the playback function without knowing the underlying changes.
The code is as follows:

    public interface MediaPlayer {
       public void play(String audioType, String fileName);
    }
    
    public interface AdvancedMediaPlayer {    
       public void playVlc(String fileName);
       public void playMp4(String fileName);
    }
    
    public class VlcPlayer implements AdvancedMediaPlayer{
       @Override
       public void playVlc(String fileName) {
          System.out.println("Playing vlc file. Name: "+ fileName);        
       }

       @Override
       public void playMp4(String fileName) {
          //do nothing
       }
    }
    
    public class Mp4Player implements AdvancedMediaPlayer{

       @Override
       public void playVlc(String fileName) {
          //do nothing
       }

       @Override
       public void playMp4(String fileName) {
          System.out.println("Playing mp4 file. Name: "+ fileName);        
       }
    }
    
    public class MediaAdapter implements MediaPlayer {

       AdvancedMediaPlayer advancedMusicPlayer;

       public MediaAdapter(String audioType){
   
          if(audioType.equalsIgnoreCase("vlc") ){
             advancedMusicPlayer = new VlcPlayer();            
         
          }else if (audioType.equalsIgnoreCase("mp4")){
             advancedMusicPlayer = new Mp4Player();
          }    
       }

       @Override
       public void play(String audioType, String fileName) {
   
          if(audioType.equalsIgnoreCase("vlc")){
             advancedMusicPlayer.playVlc(fileName);
          }
          else if(audioType.equalsIgnoreCase("mp4")){
             advancedMusicPlayer.playMp4(fileName);
          }
       }
    }
    
    public class AdapterPatternDemo {
       public static void main(String[] args) {
          MediaPlayer audioPlayer = new AudioPlayer();
          audioPlayer.play("mp3", "beyond the horizon.mp3");
          MediaPlayer videoPlayer = new MediaAdapter();
          videoPlayer.play("vlc", "far far away.vlc");
   }
}

Factory Design Pattern

It's been a long time since we finally got to the factory mode. QAQ

The factory pattern is designed to manage many implementation classes under an interface. For example, the most common DAO interface. There are fewer than 10 tables in the database and up to 100 tables. In spring framework, the management of classes is realized by relying on inversion and automatic injection. So in the absence of a framework, how can we decouple the upper code from the lower concrete DAO interface? The factory model is needed. Specific DAO interfaces are obtained through factory mode.

Why choose such a factory pattern instead of a concrete implementation class directly new? Here's an example. For example, there is a DAO interface, which is implemented by UserDaoImpl, AccountDaoImpl, etc. Assume that two classes use UserDaoImpl. If you use new to create a new UserDaoImpl in both classes, then once one day you need to replace UserDaoImpl with AnotherUserDaoImpl because of a change in requirements, you need to modify the two classes separately. So what if ten classes, or even a hundred classes, use this Dao? At this point, if I get this Dao through the factory, I just need to change the return value from the original UserDaoImpl to AnotherUserDaoImpl in the factory, without affecting the caller.

Simple Factory Mode | Static Factory Method

Here's an example of a simple factory model.

interface Dog
{
  public void speak ();
}

class Poodle implements Dog
{
  public void speak()
  {
    System.out.println("The poodle says \"arf\"");
  }
}

class Rottweiler implements Dog
{
  public void speak()
  {
    System.out.println("The Rottweiler says (in a very deep voice) \"WOOF!\"");
  }
}

class SiberianHusky implements Dog
{
  public void speak()
  {
    System.out.println("The husky says \"Dude, what's up?\"");
  }
}

class DogFactory
{
  public static Dog getDog(String criteria)
  {
    if ( criteria.equals("small") )
      return new Poodle();
    else if ( criteria.equals("big") )
      return new Rottweiler();
    else if ( criteria.equals("working") )
      return new SiberianHusky();

    return null;
  }
}

public class JavaFactoryPatternExample
{
  public static void main(String[] args)
  {
    // create a small dog
    Dog dog = DogFactory.getDog("small");
    dog.speak();

    // create a big dog
    dog = DogFactory.getDog("big");
    dog.speak();

    // create a working dog
    dog = DogFactory.getDog("working");
    dog.speak();
  }
}

In the simple factory mode, the factory returns to the specific implementation of an interface according to the input conditions.
One problem with the simple factory model is that once new products appear in the factory, it is necessary to modify the method of obtaining products in the factory, which violates the open-close principle. Moreover, the overburdened factory model may lead to confusion of responsibilities. Most importantly, in the simple factory model, the method of obtaining products is static method, which can not be extended by inheritance and other forms.

Factory Method Pattern

This is actually a simple upgrade of the factory model. Consider a real factory scenario. Its products are often classified under Product, such as bearings and tyres. Each sub-classification often corresponds to different workshops, such as bearing workshop and tire workshop. If a product is returned in simple factory mode, not to mention some data that may be lost during the upward transition, the pressure on the factory is too great, because hundreds of different classes may be returned according to different scenarios but inherit the same interface. This does not conform to the design principles.

At this time, the factory method model appeared. Not only to product abstraction, but also to factory abstraction. Provide different factories for different products, further refine responsibilities to meet SRP (Single Responsibility Principle). At the same time, because it does not need to input irrelevant judgment data, it also removes the control coupling.

Specific examples include the most common logging system. Under the log system, it is often aimed at different subsystems, such as database log subsystem, such as file log subsystem. Different log systems have different log files. At this time, the relationship between them can be well solved through the factory method model.

It can also be extended in this diagram, such as Mysql database, Oracle database and so on. You can also continue to create MysqlLogFactory based on MySqlLog in the UML diagram.

Another concrete example is the database connection in JAVA.

Connection conn=DriverManager.getConnection("jdbc:microsoft:sqlserver://loc
alhost:1433; DatabaseName=DB;user=sa;password=");
Statement statement=conn.createStatement();
ResultSet rs=statement.executeQuery("select * from UserInfo");

Here, the DriverManager factory returns a corresponding connection based on the input information. The corresponding abstract statement statement is returned in the connection. According to the information in the factory, the underlying implementation of the state must be a SqlServer Statement-like implementation.

Abstract Factory Method

Product hierarchy structure: product hierarchy structure is the inheritance structure of products. For example, an abstract class is TV. Its subclasses are Haier TV, Haixin TV and TCL TV. Then Abstract TV and specific brand TV constitute a product hierarchy structure. Abstract TV is the parent class, and specific brand TV is its subclass.
Product family: In the abstract factory model, product family refers to a group of products produced by the same factory and located in different product grade structure, such as Haier TV set and Haier refrigerator produced by Haier Electrical Appliance Factory, Haier TV set is located in the product grade structure of TV set, Haier refrigerator is located in the product grade structure of refrigerator.

The biggest difference between the abstract factory model and the factory method model is that the factory method model aims at a product level structure, while the abstract factory model needs to face multiple product level structures. A factory level structure can be responsible for the creation of product objects in different product level structures. When a factory hierarchy structure can create all objects in a product family that belong to different product hierarchies, the abstract factory model is simpler and more efficient than the factory method model.

In this context, the sub-factories under the abstract factory are divided into Haier sub-factories and Haixinzi factories. Haier can obtain Haier Refrigerator (productA), Haier Television (productB) from its factory. Similarly, Haixinzi Refrigerator (productA) and Haixin Television (productB) can be obtained from its factory.

Of course, in most application scenarios, factory design patterns are sufficient.

References

Tutorialspoint Design Pattern
stackoverflow : how does the strategy design pattern work
dzon strategy design pattern
dzon adapter pattern
A simple example of factory mode
An excellent blog on factory mode
Contrast among Several Factory Models

Keywords: Java Database MediaPlayer Mobile

Added by danzene on Mon, 03 Jun 2019 01:29:19 +0300