【设计模式】结构型模式其一: 适配器模式

433 阅读11分钟

结构型模式:《适配器模式》

这是我们结构型模式的第一期,所以我打算先讲讲结构型模式

结构型模式

结构型模式
结构型模式(Structural Pattern)关注如何将现有类或对象组织在一起形成更加强大的结构 不同的结构型模式从不同的角度组合类或对象,它们在尽可能满足各种面向对象设计原则的同时为类或对象的组合提供一系列巧妙的解决方案

在面向对象编程中,结构型模式是一类常用的设计模式,用于解决对象之间组合、聚合或继承关系的问题

结构型模式分类
类结构型模式

  • 关心类的组合,由多个类组合成一个更大的系统,在类结构型模式中一般只存在继承关系实现关系

对象结构型模式

  • 关心类与对象的组合,通过关联关系,在一个类中定义另一个类的实例对象,然后通过该对象调用相应的方法

知识点补充

组合关系是一种严格的部分-整体关系。其中,整体对象包含部分对象,且部分对象的生命周期与整体对象相同。换句话说,如果整体对象被删除,那么所有的部分对象也将被删除。例如,一个汽车包含引擎、轮胎、座位等部件,如果汽车被销毁,那么所有的部件也会被销毁。

聚合关系是一种非常松散的“拥有”关系。其中,整体对象包含部分对象,但部分对象的生命周期与整体对象不同。换句话说,如果整体对象被删除,那么部分对象仍然可以存在。例如,一个学校包含教师和学生,但教师和学生可以在学校外单独存在。


了解完结构型模式之后,我们终于可以去学习适配器模式了

适配器模式生活实例

给大家举一个例子:

image.png

现实生活:

  • 不兼容:生活用电220V  笔记本电脑20V
  • 引入 AC Adapter(交流电适配器)

软件开发:

  • 存在不兼容的结构,例如方法名不一致
  • 引入适配器模式

适配器模式定义

适配器模式:将一个类的接口转换成客户希望的另一个接口。

适配器模式让那些接口不兼容的类可以一起工作

别名为包装器(Wrapper)模式
定义中所提及的接口是指广义的接口,它可以表示一个方法或者方法的集合

适配器模式又分为:对象结构型模式 / 类结构型模式

通过下面的类图应该能了解适配器的作用

image.png

适配器模式包含以下3个角色:
Target(目标抽象类): 定义客户端所需的接口,客户端通过这个接口调用特定功能。
Adapter(适配器类):将Adaptee接口转换为Target接口,使得客户端可以通过Target接口调用Adaptee的功能。
Adaptee(适配者类):需要被适配的类,提供客户端所需的功能,但是其接口与Target接口不兼容

通过实例来学习(对象结构型模式)

题目:某公司欲开发一款儿童玩具汽车,为了更好地吸引小朋友的注意力,该玩具汽车在移动过程中伴随着灯光闪烁和声音提示。在该公司以往的产品中已经实现了控制灯光闪烁(例如警灯闪烁)和声音提示(例如警笛音效)的程序。 为了重用先前的代码并且使得汽车控制软件具有更好的灵活性和扩展性,现使用适配器模式设计该玩具汽车控制软件。

分析: 我们的目标是实现小车的移动与发出声响,所以将其作为目标抽象类

适配者代码

适配者需要有自身的独特功能,比如充电器有慢充和快充,但它都叫充电。

//警灯类,充当适配者
public class PoliceLamp {
   public void alarmLamp() {
      System.out.println("呈现警灯闪烁!");
   }
}
//警笛类,充当适配者
public class PoliceSound {
   public void alarmSound() {
      System.out.println("发出警笛声音!");
   }
}

汽车控制类代码(目标抽象类)

//汽车控制类,充当目标抽象类
public abstract class CarController {
   public void move() {
      System.out.println("玩具汽车移动!");
   }
   
   public abstract void phonate(); //发出声音
   public abstract void twinkle(); //灯光闪烁
}

适配器类

这里是选择继承目标抽象类,在里面将其转化为客户端需要的形式。

这里使用了聚合,将警铃与警灯作为属性来进行联系。

//警车适配器,充当适配器
public class PoliceCarAdapter extends CarController {
   private PoliceSound sound;  //定义适配者PoliceSound对象
   private PoliceLamp lamp;   //定义适配者PoliceLamp对象
   
   public PoliceCarAdapter() {
      sound = new PoliceSound();
      lamp = new PoliceLamp();
   }
   
   //发出警笛声音
   public void phonate() {
      sound.alarmSound();  //调用适配者类PoliceSound的方法
   }
   
   //呈现警灯闪烁
   public void twinkle() {
      lamp.alarmLamp();   //调用适配者类PoliceLamp的方法
   }
}

当然,当你所需的汽车类型发生变化,可以选择再写一个适配器,比如以下:

我之前实现了救护车的声音与光照,我现在需要将其适配到车上。

//救护车灯类,充当适配者
public class AmbulanceLamp {
   public void alarmLamp() {
      System.out.println("呈现救护车灯闪烁!");
   }
}
//救护车声音类,充当适配者
public class AmbulanceSound {
   public void alarmSound() {
      System.out.println("发出救护车声音!");
   }
}
//救护车适配器,充当适配器
public class AmbulanceCarAdapter extends CarController {
   private AmbulanceSound sound;  //定义适配者AmbulanceSound对象
   private AmbulanceLamp lamp;    //定义适配者AmbulanceLamp对象
   
   public AmbulanceCarAdapter() {
      sound = new AmbulanceSound();
      lamp = new AmbulanceLamp();
   }
   
   //发出救护车声音
   public void phonate() {
      sound.alarmSound();  //调用适配者类AmbulanceSound的方法
   }
   
   //呈现救护车灯闪烁
   public void twinkle() {
      lamp.alarmLamp();   //调用适配者类AmbulanceLamp的方法
   }
}

上面的代码写完之后,我们就可以根据客户端的需求来创建不同的适配器类,从而调用它的独特方法。最终,代码是通过适配器去调用的,所以在适配器里需要调用具体类的具体方法(使用聚合的形式)

客户端类代码:

public class Client {
   public static void main(String args[]) {
      CarController car ;
      car = new AmbulanceCarAdapter();
      car.move();
      car.phonate();
      car.twinkle();
   }
}

输出:
玩具汽车移动!
发出救护车声音!
呈现救护车灯闪烁!


现在将客户端的new 适配器换成使用配置文件。

XML 文件

<?xml version="1.0"?>
<config>
   <className>designpatterns.adapter.AmbulanceCarAdapter</className>
</config>

XML文件读取类

public class XMLUtil {
   //该方法用于从XML配置文件中提取具体类类名,并返回一个实例对象
   public static Object getBean() {
      try {
         //创建DOM文档对象
         DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
         DocumentBuilder builder = dFactory.newDocumentBuilder();
         Document doc;                    
         doc =builder.parse(new File("./config.xml")); 
      
         //获取包含类名的文本结点
         NodeList nl = doc.getElementsByTagName("className");
         Node classNode=nl.item(0).getFirstChild();
         String cName=classNode.getNodeValue();
        
         //通过类名生成实例对象并将其返回
         Class c=Class.forName(cName);
         Object obj=c.getConstructor().newInstance();
         return obj;
      }   
      catch(Exception e) {
         e.printStackTrace();
         return null;
      }
   }
}

客户端代码

public class Client {
   public static void main(String args[]) {
      CarController car ;
      car = (CarController)XMLUtil.getBean();
      car.move();
      car.phonate();
      car.twinkle();
   }
}

输出:
玩具汽车移动!
发出救护车声音!
呈现救护车灯闪烁!

同样完成了任务,但是使用配置类更好,不需要重新编译代码。

分析一波: 上面的代码属于是适配器驱动,我们需要根据具体的适配者(已经编写好功能,已经实现)去编写具体的适配器类。


通过实例来学习(类结构型模式)

一般代码:

public class Adapter extends Adaptee implements Target {
    public void request() {
        super.specificRequest();
    }
}

具体来说,适配器类 Adapter 扮演了适配器的角色,实现了目标接口 Target,将客户端对目标接口的请求转换为被适配者类 Adaptee 的特定方法 specificRequest()。通过继承被适配者类 Adaptee,适配器类 Adapter 获得了被适配者类的全部方法和属性,同时又实现了目标接口 Target,保证了客户端对适配器和被适配者的透明性。

具体例子:
假设我们有一个已经存在的音乐播放器类 MusicPlayer,它提供了一个 playMusic() 方法用于播放音乐。现在我们想要使用一个新的播放器类 NewPlayer,但它的接口与 MusicPlayer 不同,它提供了一个 play() 方法用于播放音乐。

// 音乐播放器类
public class MusicPlayer {
    public void playMusic() {
        System.out.println("Playing music 哒哒哒");
    }
}

// 新的播放器接口
public interface NewPlayer {
    void play();
}

// 适配器类
public class MusicPlayerAdapter extends MusicPlayer implements NewPlayer {
    @Override
    public void play() {
        super.playMusic();
    }
    // 这里可以重写适配者类的方法,对其进行修改(继承了方法)
    
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        NewPlayer player = new MusicPlayerAdapter();
        player.play();
    }
}

两种写法总结

类适配器使用继承来适配接口,它通过继承适配者类并实现目标接口来实现适配。在适配器类中,它重写了适配者类的方法,并将其转换成目标接口的方法。由于适配器类继承了适配者类,因此可以使用适配者类的方法和属性。

对象适配器使用组合来适配接口,它通过将适配器类中的对象引用指向适配者对象来实现适配。在适配器类中,它实现了目标接口,并将适配者对象作为一个属性引入。由于适配器类仅仅包含一个适配者对象的引用,因此它可以同时适配多个适配者对象。

两种实现方式各有优缺点,类适配器可以对适配者类进行一定的修改(另一个只能调用),使得适配器更加灵活,但是它需要多重继承(搞多个适配器)才能同时适配多个适配者类而对象适配器则不需要多重继承,但是它需要创建适配者对象,可能会增加代码的复杂度。


适配器模式还有一些特别的模式:

缺省适配器模式

缺省适配器模式(Default Adapter Pattern):当不需要实现一个接口所提供的所有方法时,可先设计一个抽象类实现该接口,并为接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可以选择性地覆盖父类的某些方法来实现需求。
适用于不想使用一个接口中的所有方法的情况,又称为单接口适配器模式。

具体怎么使用呢

按上面说的遥控车的例子, 定义一个接口,里面定义方法phonate()twinkle()方法,让CarController去实现接口为空方法体并定义自己的一般方法move(), 然后PoliceCarAdapterAmbulanceCarCarAdapter去继承CarController选择去实现其中的方法。

image.png

双向适配器

适配者也能作为目标对象,目标对象也能作为适配者。

举一个常见的例子, 让猫学会狗叫,让狗学会爬树(即让猫这个对象适配狗叫,猫的爬树技巧作为适配者给狗这个目标对象)

image.png 下面是代码编写:

// 这里面定义需要适配的方法,
public interface ICat {
    void climb();
}
// 这里面定义需要适配的方法,
public interface IDog {
    void woff();
}

狗的具体实现

public class Dog implements IDog {
    @Override
    public void woff() {
        System.out.println("我能狗叫");
    }
}

猫的具体实现

public class Cat implements ICat {
    public void name(){
        System.out.println("我是猫");
    }
    @Override
    public void climb() {
        System.out.println("我会爬树");
    }
}

适配器

public class Adapter implements ICat, IDog {
    private Dog dog;
    private Cat cat;

    // 创建狗对象时,调用猫的爬树
    // 创建猫对象时,调用狗的狗叫
    // 因此,在构造方法里直接传入两个对象
    Adapter(Dog dog, Cat cat){
        this.dog = dog;
        this.cat = cat;
    }
    // 爬树调用的是猫的爬树
    @Override
    public void climb() {
        cat.climb();
    }
    // 狗叫调用的是狗的狗叫
    @Override
    public void woff() {
        dog.woff();
    }
}

客户端:


public class Client {

    public static void main(String[] args) {
        ICat fakeCat = new Adapter(new Dog(), new Cat());
        fakeCat.climb();
        IDog fakeDog = new Adapter(new Dog(), new Cat());
        fakeDog.woff();
    }
}

优点: 可以双向适配

缺点: 适配器任务过重, 编写麻烦,且消耗内存空间,个人认为也不是很直观。

模式优缺点

模式优点

  • 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构
  • 增加了类的透明性和复用性,提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用-
  • 灵活性和扩展性非常好
  • 类适配器模式:置换一些适配者的方法很方便(适合单个适配者对象)
  • 对象适配器模式:可以把多个不同的适配者适配到同一个目标,还可以适配一个适配者的子类

模式缺点

  • 类适配器模式: 一次最多只能适配一个适配者类,不能同时适配多个适配者
  • 适配者类不能为最终类
  • 目标抽象类只能为接口,不能为类
  • 对象适配器模式:在适配器中置换适配者类的某些方法比较麻烦