文章摘要(AI生成)
结构型设计模式概述结构型设计模式包括适配器模式、桥接模式、组合模式、外观模式、享元模式、代理模式、装饰器模式适配器模式定义将一个类的接口转换成希望的另外一个接口,使得原本不兼容的接口可以协同工作角色现有实例(adaptee):被适配的角色,也指业务已经存在的实际情况目标接口(target):定义所需
结构型设计模式概述
结构型设计模式用于处理类或对象的组合,对类的拓展和封装提供指南,包括适配器模式、桥接模式、组合模式、外观模式、享元模式、代理模式、装饰器模式
适配器模式
定义
将一个类的接口转换成希望的另外一个接口,使得原本不兼容的接口可以协同工作
角色
- 现有实例(adaptee):被适配的角色,也指业务已经存在的实际情况
- 目标接口(target):定义所需的方法,即新场景下需要实现的场景
- 适配器(adapter):基于现有实例,转换成所需的目标接口
- 请求者(client):负责使用目标接口进行具体处理
优点
- 适配器模式可以让两个没有任何关系的类在一起运行,只要适配器这个角色能够搞定 他们就成。
- 增加了类的透明性 想想看,我们访问的Target目标角色,但是具体的实现都委托给了源角色,而这些对高 层次模块是透明的,也是它不需要关心的。
- 提高了类的复用度 当然了,源角色在原有的系统中还是可以正常使用,而在目标角色中也可以充当新的演员。
- 灵活性非常好 某一天,突然不想要适配器,没问题,删除掉这个适配器就可以了,其他的代码都不用修改,基本上就类似一个灵活的构件,想用就用,不想就卸载。
使用场景
适配器应用的场景只要记住一点就足够了:你有动机修改一个已经投产中的接口时,适 配器模式可能是最适合你的模式。比如系统扩展了,需要使用一个已有或新建立的类,但这 个类又不符合系统的接口,怎么办?使用适配器模式,这也是我们例子中提到的。
最佳实践
适配器模式是一个补偿模式,或者说是一个“补救”模式,通常用来解决接口不相容的问 题,在百分之百的完美设计中是不可能使用到的,什么是百分之百的完美设计?“千虑”而没 有“一失”的设计,但是,再完美的设计也会遇到“需求”变更这个无法逃避的问题,就以我们 上面的人力资源管理系统为例来说,不管系统设计得多么完美,都无法逃避新业务的发生, 技术只是一个工具而已,是因为它推动了其他行业的进步和发展而具有了价值,通俗地说, 技术是为业务服务的,因此业务在日新月异变化的同时,也对技术提出了同样的要求,在这 种要求下,就需要我们有一种或一些这样的补救模式诞生,使用这些补救模式可以保证我们 的系统在生命周期内能够稳定、可靠、健壮的运行,而适配器模式就是这样的一个“救世主”,它在需求巨变、业务飞速而导致你极度郁闷、烦躁、崩溃的时候横空出世,它通过把 非本系统接口的对象包装成本系统可以接受的对象,从而简化了系统大规模变更风险的存 在
使用spring实现
下面场景是已有广告牌场景有打印功能,借助打印功能实现打印机中的打印功能
现有实例
定义一个banner,用来表示广告牌,对输入的字符进行广告化输入输出
@Component
@Slf4j
public class Banner {
private String string;
public void setString(String string) {
this.string = string;
}
public void showWithParen(){
log.info("("+string+")");
}
public void showWithAster(){
log.info("*"+string+"*");
}
}
目标接口
我们现有场景有弱化字符串显示和强化字符串显示两种场景
public interface Print {
void setString(String string);
void printWeak();
void printStrong();
}
适配器
我们通过编写适配器,通过继承或委托现有实例的方式完成目标接口的实现
继承方式
@Component
public class PrintBanner extends Banner implements Print{
@Override
public void setString(String string) {
super.setString(string);
}
@Override
public void printWeak() {
// TODO Auto-generated method stub
showWithParen();
}
@Override
public void printStrong() {
// TODO Auto-generated method stub
showWithAster();
}
}
委托方式
@Component
public class PrintBanner implements Print{
//通过创建目标对象获取目标对象的功能
@Resource
private Banner banner;
public void setString(String string){
banner.setString(string);
}
@Override
public void printWeak() {
// TODO Auto-generated method stub
banner.showWithParen();
}
@Override
public void printStrong() {
// TODO Auto-generated method stub
banner.showWithAster();
}
}
场景测试
@Controller
public class AdapterController {
@Resource
private Print print;
@GetMapping("/adapter")
public String adapter(){
print.setString("hello");
print.printWeak();
print.printStrong();
return "success";
}
}
输出结果为:
桥接模式
定义
将类的抽象部分和实现分离,使得他们都可以独立地发生变化。即将类的功能层次结构和实现层次结构连接在一起。
功能层次结构:即在子类中增加新的结构
实现层次结构:即在子类中增加新的实现
优点
- 抽象和实现分离。这也是桥梁模式的主要特点,它完全是为了解决继承的缺点而提出的设计模式。在该模 式下,实现可以不受抽象的约束,不用再绑定在一个固定的抽象层次上。
- 优秀的扩充能力。看看我们的例子,想增加实现?没问题!想增加抽象,也没有问题!只要对外暴露的接 口层允许这样的变化,我们已经把变化的可能性减到最小。
- 实现细节对客户透明。客户不用关心细节的实现,它已经由抽象层通过聚合关系完成了封装。
使用场景
-
不希望或不适用使用继承的场景
例如继承层次过渡、无法更细化设计颗粒等场景,需要考虑使用桥梁模式。
-
接口或抽象类不稳定的场景
明知道接口不稳定还想通过实现或继承来实现业务需求,那是得不偿失的,也是比较失 败的做法。
-
重用性要求较高的场景
设计的颗粒度越细,则被重用的可能性就越大,而采用继承则受父类的限制,不可能出 现太细的颗粒度。
最佳实践
不能说继承不好,它非常好,但是有强侵入的缺点,父类的方法子类一定会有,我们可以扬长避短,对于比较明确不发生变 化的,则通过继承来完成;若不能确定是否会发生变化的,那就认为是会发生变化,则通过 桥梁模式来解决,这才是一个完美的世界。
角色
- 抽象实例(abstraction):位于类的功能层次结构的最上层,定义了基本的功能
- 拓展抽象实例(RefinedAbstraction):在抽象实例的基础上增加了新功能的角色。
- 实现者(Implementor):位于实现层次结构的最上层,定义了用于实现抽象实例的接口方法
- 具体实现者(ConcreteImplementor):负责实现在实现者中定义的接口
使用spring实现
程序的现状是已有显示接口去打开文件、打印、显示等功能,需要拓展重复打印功能
抽象实例
已有显示接口为:
@Component
public class Display {
@Resource
private DisplayImpl impl;
public void setImpl(String string){
impl.setStringAndWidth(string);
}
public void open() {
impl.rawOpen();
}
public void print() {
impl.rawPrint();
}
public void close() {
impl.rawClose();
}
public final void display() {
open();
print();
close();
}
}
抽象实例实现者
public abstract class DisplayImpl {
public abstract void setStringAndWidth(String string);
public abstract void rawOpen();
public abstract void rawPrint();
public abstract void rawClose();
}
具体实例实现者
定义使用字符串显示的类
@Component
@Slf4j
public class StringDisplayImpl extends DisplayImpl {
private String string;
private int width;
public void setStringAndWidth(String string){
this.string = string;
this.width = string.getBytes().length;
}
@Override
public void rawOpen() {
// TODO Auto-generated method stub
printLine();
}
@Override
public void rawPrint() {
// TODO Auto-generated method stub
log.info("|" + string + "|");
}
@Override
public void rawClose() {
// TODO Auto-generated method stub
printLine();
}
private void printLine() {
// TODO Auto-generated method stub
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("+");
for (int i = 0; i < width; i++) {
stringBuilder.append("-");
}
stringBuilder.append("+");
log.info(String.valueOf(stringBuilder));
}
拓展实例
拓展后的重复打印的接口为:
@Component
public class CountDisplay extends Display {
public void mutiDIsplay(int times){
open();
for (int i = 0; i < times; i++) {
print();
}
close();
}
}
场景测试
@RestController
public class BridgeController {
@Resource
Display display;
@Resource
CountDisplay countDisplay;
@GetMapping("/bridge")
public String bridge(){
display.setImpl("Hello,China!");
display.display();
display.setImpl("Hello,World.");
display.display();
countDisplay.setImpl("Hello,Universe.");
countDisplay.display();
countDisplay.mutiDIsplay(5);
return "success";
}
}
测试结果如下
组合模式
定义
将对象组合成树形结构以表示部分-整体的层次结构,使容器与内容具有一致性,创造出递归结构
优缺点
优点
-
高层模块调用简单
一棵树形机构中的所有节点都是Component,局部和整体对调用者来说没有任何区别,也就是说,高层模块不必关心自己处理的是单个对象还是整个组合结构,简化了高层模块的代码。
-
节点自由增加
使用了组合模式后,我们可以看看,如果想增加一个树枝节点、树叶节点是不是都很容易,只要找到它的父节点就成,非常容易扩展,符合开闭原则,对以后的维护非常有利。
缺点
部分和整体在嵌套使用时,可能会直接调用实现类。在面向接口编程上是很不恰当的,与依赖倒置原则冲 突,在使用的时候要考虑清楚,它限制了你接口的影响范围。
使用场景
- 维护和展示部分-整体关系的场景,如树形菜单、文件和文件夹管理。
- 从一个整体中能够独立出部分模块或功能的场景。
最佳实践
组合模式在项目中到处都有,比如现在的页面结构一般都是上下结构,上面放系统的 Logo,下边分为两部分:左边是导航菜单,右边是展示区,左边的导航菜单一般都是树形的 结构,比较清晰,有非常多的JavaScript源码实现了类似的树形菜单,大家可以到网上搜索 一下。
还有,大家常用的XML结构也是一个树形结构,根节点、元素节点、值元素这些都与我 们的组合模式相匹配
角色
- 内容实例(Leaf):表示内容的角色,在该角色中不能放入其他对象
- 容器实例(Composite):表示容器的角色,可以在其中放入内容实例和容器实例
- 组件实例(Component):使内容实例和容器实例具有一致性的角色
- 客户端(client):调用组合模式的角色
使用spring实现
实现一个能列出文件和文件夹的预览功能
组件实例
定义抽象实例entry,用来实现file和directory的一致性
public abstract class Entry {
public abstract String getName();
public abstract int getSize();
public Entry add(Entry entry) throws FileTreatmentException {
throw new FileTreatmentException();
}
public void printList() {
printList("");
}
protected abstract void printList(String prefix);
public String toString() {
return getName() + "(" + getSize() + ")";
}
}
容器实例
定义文件夹类
@Component
@Scope(value = BeanDefinition.SCOPE_PROTOTYPE)
@Slf4j
public class Directory extends Entry{
private String name;
private ArrayList<Entry> directory = new ArrayList<Entry>();
public void initDirectory(String name) {
this.name = name;
}
@Override
public String getName() {
// TODO Auto-generated method stub
return name;
}
@Override
public int getSize() {
// TODO Auto-generated method stub
int size = 0;
Iterator<Entry> it = directory.iterator();
while(it.hasNext()){
Entry entry = it.next();
size+=entry.getSize();
}
return size;
}
public Entry add(Entry entry){
directory.add(entry);
return this;
}
@Override
protected void printList(String prefix) {
// TODO Auto-generated method stub
log.info(prefix+"/"+this);
Iterator<Entry> it = directory.iterator();
while (it.hasNext()) {
Entry object = (Entry) it.next();
object.printList(prefix+"/"+name);
}
}
}
内容实例
定义文件类
@Component
@Scope(value = BeanDefinition.SCOPE_PROTOTYPE)
@Slf4j
public class File extends Entry {
private String name;
private int size;
public void initEntry(String name, int size){
this.name = name;
this.size = size;
}
@Override
public String getName() {
// TODO Auto-generated method stub
return name;
}
@Override
public int getSize() {
// TODO Auto-generated method stub
return size;
}
@Override
protected void printList(String prefix) {
// TODO Auto-generated method stub
log.info(prefix + "/" + this);
}
}
组件生成器
定义文件和文件夹的生成器,方便我们生成多实例
@Component
public abstract class EntryGenerator {
@Lookup
public abstract Directory directory();
public Directory generateDir(String name){
Directory directory = directory();
directory.initDirectory(name);
return directory;
}
@Lookup
public abstract File file();
public File generateFile(String name, int size){
File file = file();
file.initEntry(name, size);
return file;
}
}
场景测试
@RestController
@Slf4j
public class CompositeController {
@Resource
private EntryGenerator entryGenerator;
@GetMapping("/composite")
public String composite(){
try {
log.info("Making root entries...");
Directory rootdir = entryGenerator.generateDir("root");
Directory bindir = entryGenerator.generateDir("bin");
Directory tmpir = entryGenerator.generateDir("tmp");
Directory usrdir = entryGenerator.generateDir("usr");
rootdir.add(bindir);
rootdir.add(tmpir);
rootdir.add(usrdir);
bindir.add(entryGenerator.generateFile("vi", 10000));
bindir.add(entryGenerator.generateFile("latex", 20000));
rootdir.printList();
log.info("Making user entries...");
Directory yuki = entryGenerator.generateDir("yuki");
Directory hanako = entryGenerator.generateDir("hanako");
Directory tomura = entryGenerator.generateDir("tomura");
usrdir.add(yuki);
usrdir.add(hanako);
usrdir.add(tomura);
yuki.add(entryGenerator.generateFile("diary.html", 100));
yuki.add(entryGenerator.generateFile("Composite.java", 200));
hanako.add(entryGenerator.generateFile("memo.tex", 300));
tomura.add(entryGenerator.generateFile("game.doc", 400));
tomura.add(entryGenerator.generateFile("junk.mail", 500));
rootdir.printList();
} catch (FileTreatmentException e) {
// TODO: handle exception
e.printStackTrace();
}
return "success";
}
}
测试结果如下:
评论区