资讯详情

Java 设计模式最佳实践:四、结构模式

原文:Design Patterns and Best Practices in Java

协议:CC BY-NC-SA 4.0

贡献者:飞龙

本文来自【ApacheCN Java 译后编辑(MTPE)尽可能提高流程效率。

本章的目的是学习结构模式。结构模式是利用对象与类别之间的关系创建复杂结构的模式。大多数结构模式都是基于继承的。本章只关注以下内容 GOF 模式:

  • 适配器模式
  • 代理模式
  • 桥接模式
  • 装饰模式
  • 复合模式
  • 外观模式
  • 享元模式

我们可能无法详细介绍其他已确定的结构模式,但值得理解。具体如下:

  • :使用空接口标记特定类(如Serializable),因此,您可以根据接口名进行搜索。有关更多信息,请阅读第一篇文章 37 项 -乔舒亚使用标记界面定义类型·布洛赫的《Effective Java(二版)。
  • :将类分组,实现软件模块的概念。模块化结构包种模式,Kirk knorenschild 对此进行了清晰的解释。Java9 模块就是这种模式的例子,请参考这个页面。
  • :在运行时改变现有的对象接口。更多信息请访问这个页面。
  • :这为不支持多重继承的语言增加了多重继承功能。Java8 通过添加默认方法来支持多种类型的继承。即便如此,双胞胎模式在某些情况下仍然有用。Java 在这个页面上,设计模式站点对很好的描述。

适配器模式

适配器模式为代码重用提供了解决方案;它将现有的旧代码适配/包装到新的接口中,这在设计原始代码时是未知的。1987 年,当 PS/2 当设计端口时,没有人认为它会连接到它 9 年后设计的 USB 总线。然而,我们仍然可以使用旧的 PS/2 键盘连接到我们最新的计算机 USB 端口。

适配器模式通常用于处理遗留代码,因为我们可以立即访问已测试的旧功能,包装现有代码并适应新的代码接口。这可以用于多个继承 Java8 可以实现中默认接口),也可以通过使用组合(旧对象成为类属性)来实现。适配器模式也被称为

如果旧代码需要使用新代码,反之亦然,我们需要使用一个叫做双向适配器的特殊适配器来实现两个接口(旧接口和新接口)。

JDK 中的java.io.InputStreamReaderjava.io.OutputStreamWriter类是适配器,因为它们会 JDK1.0 输入/输出流对象适应后期 JDK1.1 读写器对象的定义。

意图

其目的是将现有的旧接口应用于新的客户端接口。目标是尽可能重用旧的和测试过的代码,并自由更改新的接口。

实现

下面的 UML 图建模了新客户端代码与修改后代码之间的交互。通常在其他语言中使用多重继承来实现适配器模式 Java8 一开始这是可能的。我们将使用另一种适用于旧方法的方法 Java 版本;我们将使用聚合物。它比继承更有限,因为我们不能访问受保护的内容,只能访问适配器的公共接口:

以下参与者可以从实现图中区分:

  • Client:代码客户端
  • Adapter:将调用转发给适配器的适配器类别
  • Adaptee:旧代码需要修改
  • Target:新接口需要支持

示例

下面的代码模拟 USB 总线中使用 PS/2 键盘。它定义了一个 PS/2 键盘(适配器) USB 设备接口(目标) PS2ToUSBAdapter(适配器)及使设备工作的连接线:

package gof.structural.adapter; import java.util.Arrays; import java.util.Collections; import java.util.List; class WireCap  { 
           WireCap link = WireCap.LooseCap;   private Wire wire;   publicstatic WireCap LooseCap = new WireCap(null);   public WireCap(Wire wire)    { 
             this.wire = wire;    }
  publicvoid addLinkTo(WireCap link) 
  { 
        
    this.link = link;
  }
  public Wire getWire() 
  { 
        
    return wire;
  }
  public String toString() 
  { 
        
    if (link.equals(WireCap.LooseCap))
    return "WireCap belonging to LooseCap";
    return "WireCap belonging to " + wire + " is linked to " + 
    link.getWire();
  }
  public WireCap getLink() 
  { 
        
    return link;
  }
}

顾名思义,WireCap类模型是每根导线的两端。默认情况下,所有导线都是松的;因此,我们需要一种方法来发出信号。这是通过使用空对象模式来完成的,LooseCap是我们的空对象(一个空替换,它不抛出NullPointerException)。请看下面的代码:

class Wire 
{ 
        
  private String name;
  private WireCap left;
  private WireCap right;
  public Wire(String name) 
  { 
        
    this.name = name;
    this.left = new WireCap(this);
    this.right = new WireCap(this);
  }
  publicvoid linkLeftTo(Wire link) 
  { 
        
    left.addLinkTo(link.getRightWireCap());
    link.getRightWireCap().addLinkTo(left);
  }
  public WireCap getRightWireCap() 
  { 
        
    return right;
  }
  publicvoid printWireConnectionsToRight() 
  { 
        
    Wire wire = this;
    while (wire.hasLinkedRightCap()) 
    { 
        
      wire.printRightCap();
      wire = wire.getRightLink();
    }
  }
  public Wire getRightLink() 
  { 
        
    return getRightWireCap().getLink().getWire();
  }
  publicvoid printRightCap() 
  { 
        
    System.out.println(getRightWireCap());
  }
  publicboolean hasLinkedRightCap() 
  { 
        
    return !getRightWireCap().link.equals(WireCap.LooseCap);
  }
  public String getName() 
  { 
        
    return name;
  }
  public String toString() 
  { 
        
    return "Wire " + name;
  }
}

Wire类对来自 USB 或 PS/2 设备的电线进行建模。它有两端,默认情况下是松散的,如以下代码所示:

class USBPort 
{ 
        
  publicfinal Wire wireRed = new Wire("USB Red5V");
  publicfinal Wire wireWhite = new Wire("USB White");
  publicfinal Wire wireGreen = new Wire("USB Green");
  publicfinal Wire wireBlack = new Wire("USB Black");
}

根据 USB 规范,USBPort 有四根导线:5V 红色、绿色和白色导线用于数据,黑色导线用于接地,如下代码所示:

interface PS2Device
{ 
        
  staticfinal String GND = "PS/2 GND";
  staticfinal String BLUE = "PS/2 Blue";
  staticfinal String BLACK = "PS/2 Black";
  staticfinal String GREEN = "PS/2 Green";
  staticfinal String WHITE = "PS/2 White";
  staticfinal String _5V = "PS/2 5V";
  public List<Wire> getWires();
  publicvoid printWiresConnectionsToRight();
}
class PS2Keyboard implements PS2Device 
{ 
        
  publicfinal List<Wire> wires = Arrays.asList(
  new Wire(_5V),
  new Wire(WHITE),
  new Wire(GREEN),
  new Wire(BLACK),
  new Wire(BLUE),
  new Wire(GND));
  public List<Wire> getWires() 
  { 
        
    return Collections.unmodifiableList(wires); 
  }
  publicvoid printWiresConnectionsToRight() 
  { 
        
    for(Wire wire : wires)
    wire.printWireConnectionsToRight();
  }
}

PS2Keyboard是适配器。我们需要使用的是旧设备,如下代码所示:

interface USBDevice 
{ 
        
  publicvoid plugInto(USBPort port);
}

USBDevice是目标接口。它知道如何与USBPort接口,如下代码所示:

class PS2ToUSBAdapter implements USBDevice 
{ 
        
  private PS2Device device;
  public PS2ToUSBAdapter(PS2Device device) 
  { 
        
    this.device = device;
  }
  publicvoid plugInto(USBPort port) 
  { 
        
    List<Wire> ps2wires = device.getWires();
    Wire wireRed = getWireWithNameFromList(PS2Device._5V, 
    ps2wires);
    Wire wireWhite = getWireWithNameFromList(PS2Device.WHITE,
    ps2wires);
    Wire wireGreen = getWireWithNameFromList(PS2Device.GREEN,
    ps2wires);
    Wire wireBlack = getWireWithNameFromList(PS2Device.GND, 
    ps2wires);
    port.wireRed.linkLeftTo(wireRed);
    port.wireWhite.linkLeftTo(wireWhite);
    port.wireGreen.linkLeftTo(wireGreen);
    port.wireBlack.linkLeftTo(wireBlack);
    device.printWiresConnectionsToRight();
  }
  private Wire getWireWithNameFromList(String name, List<Wire>
  ps2wires) 
  { 
        
    return ps2wires.stream()
    .filter(x -> name.equals(x.getName()))
    .findAny().orElse(null);
  }
}

PS2ToUSBAdapter是我们的适配器类。它知道如何布线,以便新的USBPort仍然可以使用旧的设备,如下代码所示:

publicclass Main
{ 
        
  publicstaticvoid main (String[] args)
  { 
        
    USBDevice adapter = new PS2ToUSBAdapter(new PS2Keyboard());
    adapter.plugInto(new USBPort());
  }
}

输出如下:

正如预期的那样,我们的设备已连接到 USB 端口并准备好使用。所有接线都已完成,例如,如果 USB 端口将红线设置为 5 伏,则该值将到达键盘,如果键盘通过绿线发送数据,则该值将到达 USB 端口。

代理模式

每当您使用企业或 SpringBeans、模拟实例和实现 AOP 时,对具有相同接口的另一个对象进行 RMI 或 JNI 调用,或者直接/间接使用java.lang.reflect.Proxy,都会涉及到代理对象。它的目的是提供一个真实对象的代理,具有完全相同的封装外形。它在调用之前或之后执行其他操作时将工作委托给它。代理类型包括:

  • :将工作委托给远程对象(不同的进程、不同的机器),例如企业 bean。使用 JNI 手动或自动地使用 JNI 包装现有的非 Java 旧代码(例如,使用 SWIG 生成胶粘代码,参见这个页面)是一种远程代理模式,因为它使用句柄(C/C++ 中的指针)访问实际对象。
  • :进行安全/权限检查。
  • :使用记忆加速调用。最好的例子之一是 Spring @Cacheable方法,它缓存特定参数的方法结果,不调用实际代码,而是从缓存返回先前计算的结果。
  • 。这些增加了方法的功能,比如记录性能度量(创建一个@Aspect,为所需的方法定义一个@Pointcut,并定义一个@Around通知)或者进行延迟初始化。

适配器和代理之间的主要区别在于代理提供完全相同的接口。装饰器模式增强了接口,而适配器改变了接口。

意图

其目的是为真实对象提供代理,以便更好地控制它。它是一个实际对象的句柄,其行为类似于它,因此使客户端代码使用它就像使用实际对象一样。

实现

下图对代理模式进行了建模。请注意,由于真实和代理主题都实现了相同的接口,因此它们可以互换:

我们可以在实现图中区分以下参与者:

  • Subject:客户端使用的现有接口
  • RealSubject:真实对象的类
  • ProxySubject:代理类

示例

下面的代码模拟从 localhost EJB 上下文中查找 bean 的远程代理。我们的远程代理是在另一个 JVM 中运行的几何计算器。我们将使用工厂方法来制作代理和真实对象,以证明它们是可互换的。代理版本的计算时间更长,因为我们还模拟 JNI 查找部分并发送/检索结果。看看代码:

package gof.structural.proxy;
publicclass Main
{ 
        
  publicstaticvoid main (String[] args) throws java.lang.Exception
  { 
        
    GeometryCalculatorBean circle = GeometryCalculatorBeanFactory.
    REMOTE_PROXY.makeGeometryCalculator();
    System.out.printf("Circle diameter %fn",    
    circle.calculateCircleCircumference(new Circle()));
  }
}
class Circle 
{ 
        }
interface GeometryCalculatorBean 
{ 
        
  publicdouble calculateCircleCircumference(Circle circle);
}

这是我们的主题,我们要实现的接口。模拟@RemoteInterface@LocalInterface接口的建模,如下代码所示:

class GeometryBean implements GeometryCalculatorBean 
{ 
        
  publicdouble calculateCircleCircumference(Circle circle) 
  { 
        
    return 0.1f;
  }
}

这是我们真正的主题,知道如何执行实际的几何计算,如以下代码所示:

class GeometryBeanProxy implements GeometryCalculatorBean 
{ 
        
  private GeometryCalculatorBean bean;
  public GeometryBeanProxy() throws Exception 
  { 
        
    bean = doJNDILookup("remote://localhost:4447", "user", 
    "password");
  }
  private GeometryCalculatorBean doJNDILookup
  (final String urlProvider, final String securityPrincipal, final  
  String securityCredentials)
  throws Exception 
  { 
        
    System.out.println("Do JNDI lookup for bean");
    Thread.sleep(123);//simulate JNDI load for the remote location
    return GeometryCalculatorBeanFactory.LOCAL.
    makeGeometryCalculator();
  }
  publicdouble calculateCircleCircumference(Circle circle) 
  { 
        
    return bean.calculateCircleCircumference(circle);
  }
}

这是我们的代理主题。请注意,它没有业务逻辑;它在设法建立对它的句柄之后,将它委托给真正的主题,如以下代码所示:

enum GeometryCalculatorBeanFactory 
{ 
        
  LOCAL 
  { 
        
    public GeometryCalculatorBean makeGeometryCalculator() 
    { 
        
      returnnew GeometryBean();
    }
  },
  REMOTE_PROXY 
  { 
        
    public GeometryCalculatorBean makeGeometryCalculator() 
    { 
        
      try 
      { 
        
        returnnew GeometryBeanProxy();
      } 
      catch (Exception e) 
      { 
        
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
      returnnull;
    }
  };
  publicabstract GeometryCalculatorBean makeGeometryCalculator();
}

以下输出显示代理成功链接到真实对象并执行所需的计算:

装饰器模式

有时我们需要在不影响现有代码的情况下,向现有代码添加或从现有代码中删除功能,有时创建子类是不实际的。在这些情况下,装饰器非常有用,因为它允许在不更改现有代码的情况下这样做。它通过实现相同的接口、聚合要修饰的对象、将所有公共接口调用委派给它,并在子类中实现新功能来实现这一点。将此模式应用于具有轻量级接口的类。在其他情况下,通过将所需的策略注入组件(策略模式)来扩展功能是更好的选择。这将保持特定方法的局部更改,而不需要重新实现其他方法。

装饰对象及其装饰器应该是可互换的。装饰器的接口必须完全符合装饰对象的接口。

因为它使用递归,所以可以通过组合装饰器来实现新功能。在这方面,它类似于复合模式,它将多个对象组合在一起,以形成作为一个对象的复杂结构。装饰器可以被视为护照上的一块玻璃或一张卡片(安装在一块玻璃和一张卡片之间的图片或照片),其中图片/照片本身就是装饰对象。另一方面,策略可以看作是艺术家在照片上的签名。

JScrollPaneswing 类是装饰器的一个示例,因为它允许在现有容器周围添加新功能,例如滚动条,并且可以多次执行,如下代码所示:

JTextArea textArea = new JTextArea(10, 50);
JScrollPane scrollPane1 = new JScrollPane(textArea);
JScrollPane scrollPane2 = new JScrollPane(scrollPane1);

意图

其目的是动态扩展现有对象的功能,而不更改其代码。它符合原始接口,并且能够通过使用组合(而不是子类化)在功能上扩展。

实现

下图对装饰器模式进行了建模。结果表明,扩展构件和修饰构件可以相互替换。装饰器可以递归地应用;它可以应用于现有的组件实现,但也可以应用于另一个装饰器,甚至应用于它自己。装饰器接口不是固定到组件接口的;它可以添加额外的方法,装饰器的子级可以使用这些方法,如图所示

我们可以在实现图中区分以下参与者:

  • Component:抽象组件(可以是接口)
  • ComponentImplementation:这是我们要装饰的组件之一
  • Decorator:这是一个抽象的组件Decorator
  • ExtendedComponent:这是添加额外功能的组件装饰器

示例

下面的代码显示了如何增强简单的打印 ASCII 文本,以打印输入的十六进制等效字符串,以及实际文本:

package gof.structural.decorator;
import java.util.stream.Collectors;
publicclass Main
{ 
        
  publicstaticvoid main (String[] args) throws java.lang.Exception
  { 
        
    final String text = "text";
    final PrintText object = new PrintAsciiText();
    final PrintText printer = new PrintTextHexDecorator(object);
    object.print(text);
    printer.print(text);
  }
}
interface PrintText 
{ 
        
  publicvoid print(String text);
}
PrintText is the component interface:
class PrintAsciiText implements PrintText 
{ 
        
  publicvoid print(String text) 
  { 
        
    System.out.println("Print ASCII: " + text); 
  }
}

PrintASCIIText是要装饰的构件。注意,它只知道如何打印ASCII文本。我们想让它也以十六进制打印;我们可以使用下面的代码

class PrintTextHexDecorator implements PrintText 
{ 
        
  private PrintText inner;
  public PrintTextHexDecorator(PrintText inner) 
  { 
        
    this.inner = inner;
  }  
  publicvoid print(String text) 
  { 
        
    String hex = text.chars()
    .boxed()
    .map(x -> "0x" + Integer.toHexString(x))
    .collect(Collectors.joining(" "));
    inner.print(text + " -> HEX: " + hex);
  }
}

PrintTextHexDecorator是装饰师。也可应用于其它PrintText元件。假设我们要实现一个组件PrintToUpperText。我们可能仍然使用我们现有的装饰,使其打印十六进制以及。

以下输出显示当前功能(ASCII)和新添加的功能(十六进制显示):

桥接模式

在软件设计过程中,我们可能会面临一个问题,即同一个抽象可以有多个实现。这在进行跨平台开发时最为明显。例如 Linux 上的换行符换行符或 Windows 上存在注册表。需要通过运行特定操作系统调用来获取特定系统信息的 Java 实现肯定需要能够改变实现。一种方法是使用继承,但这会将子级绑定到特定接口,而该接口可能不存在于不同的平台上。

在这些情况下,建议使用桥接模式,因为它允许从扩展特定抽象的大量类转移到嵌套泛化,这是 Rumbaugh 创造的一个术语,在这里我们处理第一个泛化,然后处理另一个泛化,从而将所有组合相乘。如果所有子类都同等重要,并且多个接口对象使用相同的实现方法,那么这种方法就可以很好地工作。如果由于某种原因,大量代码被复制,这就表明这种模式不是解决特定问题的正确选择。

意图

其目的是将抽象与实现分离,以允许它们独立地变化。它通过在公共接口和实现中使用继承来实现这一点。

实现

下图显示了一个可能的网桥实现。请注意,抽象和实现都可以更改,不仅接口可以更改,实现代码也可以更改。例如,精化抽象可以利用只有SpecificImplementation提供的doImplementation3()

我们可以在实现图中区分以下参与者:

  • Abstraction:这是抽象组件
  • Implementation:这是抽象实现
  • Refined:这是具体组件
  • SpecificImplementation:这是具体实现

示例

下面的代码展示了一个电子邮件客户端,它使用了基于运行平台的实现。可以使用工厂方法模式对其进行增强,以创建特定的平台实现:

package gof.structural.bridge;
publicclass Main
{ 
        
  publicstaticvoid main (String[] args) 
  { 
        
    new AllMessageClient(new WindowsImplementation())
    .sendMessageToAll("abc@gmail.com", "Test");
  }
}
interface PlatformBridge 
{ 
        
  publicvoid forwardMessage(String msg);
}

PlatformBridge是我们的实现抽象类。它指定了每个实现需要提供什么—在我们的例子中,是转发文本给出的消息。以下两种实现(Windows 和 POSIX)都知道如何执行此任务:

class WindowsImplementation implements PlatformBridge 
{ 
        
  publicvoid forwardMessage(String msg) 
  { 
        
    System.out.printf("Sending message n%s nFrom the windows 
    machine", msg) 

标签: 智能型压力变送器cyb

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台