浅谈设计模式1——策略模式 | 适配器模式 | 工厂模式

前言

最近在看《Think In JAVA》,其中在讲解继承,组合,抽象类和接口的时候,提到了题中的几个设计模式。这几个设计模式也确实让我更好的理解了JAVA中各个数据结构的含义。今天就结合书本还有自己的理解,稍微整理一下这几个设计模式。

Strategy Pattern | 策略模式

这里就必须要提一下向上转化这个概念。在继承和接口中都有提到这个概念。
向上转化在继承中是指子类可以向上转化为父类。比如,有一个Instrument类,以及它的一个子类Flute。子类重写的play方法会覆盖父类的方法。

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

如果这时有一个方法需要接收一个乐器参数并演奏,那么无需写多个重载方法接收各种不同的乐器,只需要一个接收Instrument类的方法。

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

在这个方法中传入一个Flute对象,调用的将是flute中的play方法。这就是向上转化,动态绑定的一个最简单的例子。
其实接口也是同理,只是接口允许多种向上转化。也就是说,JAVA中继承是唯一的,而接口是可以Implement多个的。因此JAVA中继承向上转化的路径唯一,而接口向上转化路径不唯一。

接下来就要讲到策略模式。

策略模式的概念如下:

Defines a set of encapsulated algorithms that can be swapped to carry out a specific behaviour
定义了一组封装好的算法,这些算法分别执行不同的操作。在实际运行中,这些算法可以动态切换来满足不同场景下的需求

策略模式的使用情景有:

  1. 将文件保存为不同的格式

  2. 排序算法的多种实现

  3. 文件压缩的多种实现

也就是说,策略模式将一组完成相同工作的不同方式的代码分别放到不同的类中,并通过策略模式实现在运行中的相互切换。

《浅谈设计模式1——策略模式 | 适配器模式 | 工厂模式》

这是从网上找到的关于策略模式的UML图。
策略模式是JAVA中继承,抽象类以及接口的一种综合应用。在策略模式中,我们可以根据一个“开放接口”设计出多种“具体策略”,然后在调用时只需要输入“开放接口”,程序运行时会根据“开放接口”的具体实现来决定具体的运行结果。

上面设计模式的代码如下:

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

//接口实现类
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;
   }
}

//上下文
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);
   }
}

//具体调用
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));
   }
}

下面我再讲一个具体的例子来说明使用策略模式与不使用策略模式的差距。

假设我们有一个压缩文件的功能,压缩文件有多种算法,如Zip,RAR等等。程序能够根据实际的操作系统以及性能等参数来选择一个压缩算法执行压缩操作。我们假设这个选择具体算法的功能放置CompressionPreference类中。
其实这些对于客户端来说都是透明的。也就是说,客户端只知道会有一个压缩功能,该功能需要客户上传要压缩的文件。如此场景下,服务端只需要提供一个压缩的接口,而无需暴露具体的实现。

代码如下:

//选择压缩方法类,根据具体情况返回压缩的方法
public class CompressionPreference{
     public static CompressionStrategy getPreferedStrategy(){
         //根据系统的情况或是用户的选择返回具体的压缩算法
     }
}

//压缩策略接口
public interface CompressionStrategy{
    void compress(List<File> files);
}

//压缩策略的具体实现
public class ZipCompressionStrategy implements CompressionStrategy{
    @Override
    public void compress(List<File> files){
        //zip 压缩
    }
}

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

public class CompressionContext{
    private CompressionStrategy strategy;
    
    //这里根据CompressionPreference选择压缩策略
    public void setStrategy(CompressionStrategy strategy){this.strategy=strategy);}
    
    public void createArchieve(List<File> files){
        strategy.compress(files);
    }
}

//客户端调用
public class Client{
    public static void main(String[] args) {
    CompressionContext ctx = new CompressionContext();
    //设置压缩上下文
    ctx.setCompressionStrategy(CompressionPreference.getPreferedStrategy());
    ctx.createArchive(fileList);
  }
}

通过这样的设计之后,如果需要添加新的算法,只需要增加一个CompressionStrategy的具体实现类,以及修改一下CompressionPreference中的方法即可。对于客户端的调用不会产生任何影响。

如果不对算法进行封装,直接允许客户端调用的话。一方面,暴露了压缩算法的种种实现,另一方面,也增加了可能造成的错误调用。而且一旦增加新的压缩算法,客户端也需要知道这些本不需要知道的东西,调整自己的调用。这样的代码,可维护性实在是太差了。

适配器模式 | Adapter Design Pattern

定义:

Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.
将两个不想兼容的接口通过适配器进行相互转化。

适配器模式要比策略模式要好理解一些。在书中讲解适配器模式时,实际上是为了补充说明如何面向接口编程。适配器模式,顾名思义,就是将本来并不继承某个接口的类通过适配器转化为可以通过该接口调用,它充当着连个不兼容的接口之间的桥梁。

从原含义上来讲,适配器是指一个接口转换器,在生活中最常见的接口转换器就是你的手机充电线头啦!充电头将从插座中输出的标准220V电压(国内)转化为可以安全充电的电压。并且在另一侧提供了一个USB充电口,从而使手机可以在一切含有USB端口的充电线下进行充电。再举一个例子,也就是SD卡。使用相机的朋友知道,有些电脑是不提供SD卡接口的,那么就需要将SD卡插入SD卡读卡器,再将读卡器通过USB接口插入电脑。这时电脑就可以读取SD卡中的内容了。

在书中的例子,适配器应用的场景是将不可以修改的类改为继承某个接口从而可以作为该接口的一个实现类被调用。书中的适配器模式有两种实现方式,一种是通过代理,另一种是通过继承。两种方式本质上是相同的,如果需要原类中的所有实现,则通过继承方式实现适配器,如果只是一部分实现,则通过代理的方式。具体情况具体分析。

以上讲的都太过抽象了,下面讲一个具体的例子。

比如我有一个扫描类Scanner,他有一个方法,可以接收所有继承了Readable接口的类,并根据类中的情况将其中的数据读取出来。系统中已经有一些类,他们是可以被Scanner读取的,但是他们并没有继承Readable接口。本着开闭原则,我可以给这些类添加一个适配器,使其可以被Scanner读取。通过这种模式,无论是出现新的Scanner读取文件或是读取系统已有的文件,都不必修改Scanner方法。只需要使其支持Readable接口就行。

    public class Scanner{
        public void read(Readable material){
            material.read();
        }
    }
    
    public interface Readable{
        void read();
    }
    public class TXT implements Readable{
        ...
        public void read(){
            //读取txt文件
        }
        ...
    }
    
    public class HTML{
        public void toReadableFormat(){
            //html文件也可以被读取,但是它并没有继承Readable接口,所以无法被Scanner
            识别
        }
    }
    
    //这里才是适配器模式
    public class HTMLAdapter implements Readable{
        ...
        private HTML html;
        public HTMLAdapter(HTML html){this.html = html}
        public void read(){
            html.toReadableFormat();
        }
        ...
    }
    
    //这时候两个文件都可以被读取了
    public class Test{
        public static void main(String[] args){
            Scanner s = new Scanner();
            s.read(new TXT());
            s.read(new HTMLAdapter(new HTML()));
        }
    }

一个例子不够,再来一个~

在自媒体的发展史中,媒体的格式越来越多样化,从最初的文本,到MP3,再到视频格式。如果现在有一个系统,它本来只支持MP3格式的文件的读取,这时候要想该系统可以支持新媒体类的文件的播放。新媒体类文件由另一个团队开发,拥有自己的开发接口和具体实现。如何才能将该模块融入到现有系统中呢?

这时候就需要通过适配器模式来解决这个问题了。

《浅谈设计模式1——策略模式 | 适配器模式 | 工厂模式》

这是这个系统给的UML类图。通过在原系统中新建一个MediaAdapter适配器继承原媒体播放器的接口,从而使原系统可以在不知下层变动的基础上,继续调用原来的play方法来实现播放功能。
具体代码如下:

    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

终于到工厂模式了~~~~写了好久了呀QAQ

工厂模式是为了管理一个接口之下众多实现类。比如最常见的DAO接口。数据库中的表少至10个多可以至百个。在spring框架中,通过依赖倒置和自动注入实现了这么多读取数据库的接口实现类的管理。那么在没有框架的场景下,如何才可以使上层代码和下层具体的DAO接口解耦呢?这时就需要工厂模式。通过工厂模式获得具体DAO接口。

至于为什么要选择这样的一个工厂模式,而不是直接new一个具体的实现类呢?这里举个例子。比方说,有一个DAO接口,实现该接口的有UserDaoImpl, AccountDaoImpl等。假设有两个类均用到UserDaoImpl。如果在这两个类中均使用new来创建一个新的UserDaoImpl,那么一旦有一天,因为需求变更,需要将UserDaoImpl换成AnotherUserDaoImpl,则需要在两个类中分别修改。那么如果有十个类,甚至一百个类都用到了这个Dao呢?这时候如果我是通过工厂来获得这个Dao,也就只需要在工厂中将返回值从原来的UserDaoImpl变成AnotherUserDaoImpl,并不会影响调用方。

简单工厂模式 | Static Factory Method

下面给一个简单的工厂模式的例子。

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();
  }
}

在简单的工厂模式中,工厂根据输入的条件返回给一个接口的具体实现。
简单工厂模式有一个问题,就是一旦工厂出现新的产品,就必须修改工厂中获取产品的方法,这有违开闭原则。而且工厂模式承担的压力过重,可能会导致职责的混乱。最重要的是,简单工厂模式中,获取产品的方法是静态方法,该方法无法通过继承等形式得到扩展。

工厂方法模式 | Factory Method Pattern

这其实是工厂模式的一个简单的升级。考虑一个真实工厂的场景。它的产品Product之下往往还有许多分类,如轴承,轮胎。各个子分类往往也对应着不同的车间,如轴承车间,轮胎车间。如果还用简单工厂模式返回一个Product,且不说向上转型可能丢失的一些数据,而且工厂的压力也太大了,因为可能要根据不同场景返回上百个不同类型但继承了同一接口的类。这不符合设计原则。

这时候就出现了工厂方法模式。不仅仅对产品抽象,还对工厂抽象。对不同的产品提供不同的工厂,将职责进一步细化,满足SRP(单一职责原则)。同时,因为不需要输入无关的判断数据,也解除了控制耦合。

具体例子有最常见的日志系统。日志系统之下往往针对各个不同的子系统,比如数据库日志子系统,比如文件日志子系统。不同的日志系统对应的日志文件也不同。这时通过工厂方法模式就可以很好的解决二者之间的关系。

《浅谈设计模式1——策略模式 | 适配器模式 | 工厂模式》

在这张图中还可以继续延伸,比如数据库包括Mysql数据库,Oracle数据库等等。在UML图中也可以继续根据MySqlLog创建MysqlLogFactory。

还有一个具体的例子就是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");

这里通过DriverManager工厂根据输入的信息返回一个对应的连接。连接中再返回对应的抽象语句statement。根据工厂中的信息可以知道,这个statement的底层实现必定是一个类似SqlServerStatement的实现。

抽象工厂模式 | Abstract Factory Method

产品等级结构:产品等级结构即产品的继承结构,如一个抽象类是电视机,其子类有海尔电视机、海信电视机、TCL电视机,则抽象电视机与具体品牌的电视机之间构成了一个产品等级结构,抽象电视机是父类,而具体品牌的电视机是其子类。
产品族:在抽象工厂模式中,产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品,如海尔电器工厂生产的海尔电视机、海尔电冰箱,海尔电视机位于电视机产品等级结构中,海尔电冰箱位于电冰箱产品等级结构中。

抽象工厂模式与工厂方法模式最大的区别在于,工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构,一个工厂等级结构可以负责多个不同产品等级结构中的产品对象的创建 。当一个工厂等级结构可以创建出分属于不同产品等级结构的一个产品族中的所有对象时,抽象工厂模式比工厂方法模式更为简单、有效率。

在这里的上下文中,抽象工厂之下的子工厂被划分为海尔子工厂,海信子工厂。在海尔自工厂中可以获得海尔冰箱(productA),海尔电视机(productB), 同理,在海信子工厂中,可以获得海信冰箱(productA),海信电视机(productB)。

当然了 在大多数的应用场景下,工厂设计模式已经足够了。

《浅谈设计模式1——策略模式 | 适配器模式 | 工厂模式》

References

Tutorialspoint Design Pattern
stackoverflow : how does the strategy design pattern work
dzon strategy design pattern
dzon adapter pattern
工厂模式的一个简单例子
工厂模式的一篇极好博客
几种工厂模式之间的对比

    原文作者:设计模式
    原文地址: https://segmentfault.com/a/1190000010739127
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞