结构性模式

  • 适配器模式
  • 桥接模式
  • 组合模式
    • 组合模式中的安全方式与透明方式
  • 装饰模式
    • 1. 用于增强功能的装饰模式
    • 2. 用于添加功能的装饰模式
    • I/O 中的装饰模式
  • 外观模式

适配器模式:用于有相关性但不兼容的接口

桥接模式:用于同等级的接口互相组合

组合模式:用于整体与部分的结构


适配器模式

将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。通俗地讲,适配器模式适用于 有相关性但不兼容的结构,源接口通过一个中间件转换后才可以适用于目标接口,这个转换过程就是适配,这个中间件就称之为适配器。
//家庭电源提供 220V 的电压:
class HomeBattery {int supply() {// 家用电源提供一个 220V 的输出电压return 220;}
}//USB 数据线只接收 5V 的充电电压:
class USBLine {void charge(int volt) {// 如果电压不是 5V,抛出异常if (volt != 5) throw new IllegalArgumentException("只能接收 5V 电压");
// 如果电压是 5V,正常充电System.out.println("正常充电");}
}//适配之前,用户如果直接用家庭电源给手机充电:
public class User {@Testpublic void chargeForPhone() {HomeBattery homeBattery = new HomeBattery();int homeVolt = homeBattery.supply();System.out.println("家庭电源提供的电压是 " + homeVolt + "V");USBLine usbLine = new USBLine();usbLine.charge(homeVolt);}
}
运行程序,输出如下:
家庭电源提供的电压是 220V
java.lang.IllegalArgumentException:只能接收 5V 电压
//加入电源适配器:
class Adapter {int convert(int homeVolt) {// 适配过程:使用电阻、电容等器件将其降低为输出 5Vint chargeVolt = homeVolt - 215;return chargeVolt;}
}//用户再使用适配器将家庭电源提供的电压转换为充电电压:
public class User {@Testpublic void chargeForPhone() {HomeBattery homeBattery = new HomeBattery();int homeVolt = homeBattery.supply();System.out.println("家庭电源提供的电压是 " + homeVolt + "V");Adapter adapter = new Adapter();int chargeVolt = adapter.convert(homeVolt);System.out.println("使用适配器将家庭电压转换成了 " + chargeVolt + "V");USBLine usbLine = new USBLine();usbLine.charge(chargeVolt);}
}
运行程序,输出如下:
家庭电源提供的电压是 220V
使用适配器将家庭电压转换成了 5V
正常充电我们日常的开发中经常会使用到各种各样的 Adapter,都属于适配器模式的应用。
但适配器模式并不推荐多用。因为未雨绸缪好过亡羊补牢,如果事先能预防接口不同的问题,不匹配问题就不会发生,只有遇到源接口无法改变时,才应该考虑使用适配器。比如现代的电源插口中很多已经增加了专门的充电接口,让我们不需要再使用适配器转换接口,这又是社会的一个进步。

桥接模式

考虑这样一个需求:绘制矩形、圆形、三角形这三种图案。按照面向对象的理念,我们至少需要三个具体类,对应三种不同的图形,一个图形接口。
接下来我们有了新的需求,每种形状都需要有四种不同的颜色:红、蓝、黄、绿。容易想到,使用子类继承父类,出现12个类
形状和颜色,都是图形的两个属性。他们两者的关系是平等的,所以不属于继承关系。更好的的实现方式是:将形状和颜色分离,根据需要对形状和颜色进行组合,这就是桥接模式的思想。
桥接模式:将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体模式或接口模式。
通俗地说,如果一个对象有两种或者多种分类方式,并且两种分类方式都容易变化,比如本例中的形状和颜色。这时使用继承很容易造成子类越来越多,所以更好的做法是把这种分类方式分离出来,让他们独立变化,使用时将不同的分类进行组合即可。
说到这里,不得不提一个设计原则:合成 / 聚合复用原则。虽然它没有被划分到六大设计原则中,但它在面向对象的设计中也非常的重要:合成 / 聚合复用原则:优先使用合成 / 聚合,而不是类继承。
继承虽然是面向对象的三大特性之一,但继承会导致子类与父类有非常紧密的依赖关系,它会限制子类的灵活性和子类的复用性。而使用合成 / 聚合,也就是使用接口实现的方式,就不存在依赖问题,一个类可以实现多个接口,可以很方便地拓展功能。
//新建接口类 IColor,仅包含一个获取颜色的方法:
public interface IColor {String getColor();
}//每种颜色都实现此接口:
public class Red implements IColor {@Overridepublic String getColor() {return "红";}
}public class Blue implements IColor {@Overridepublic String getColor() {return "蓝";}
}//在每个形状类中,桥接 IColor 接口: (IShape为图形接口)
class Rectangle implements IShape {private IColor color;void setColor(IColor color) {this.color = color;}@Overridepublic void draw() {System.out.println("绘制" + color.getColor() + "矩形");}
}class Round implements IShape {private IColor color;void setColor(IColor color) {this.color = color;}@Overridepublic void draw() {System.out.println("绘制" + color.getColor() + "圆形");}
}
//测试
public class Test {@Testpublic void drawTest() {Rectangle rectangle = new Rectangle();rectangle.setColor(new Red());rectangle.draw();Round round = new Round();round.setColor(new Blue());round.draw();Triangle triangle = new Triangle();triangle.setColor(new Yellow());triangle.draw();}
}
因为颜色是多边,图形多变,所以无法使用枚举,那么颜色作为一个具体的类需要new出来  ??????
这时我们再来回顾一下官方定义:将抽象部分与它的实现部分分离,使它们都可以独立地变化。抽象部分指的是父类,对应本例中的形状类,实现部分指的是不同子类的区别之处。将子类的区别方式 —— 也就是本例中的颜色 —— 分离成接口,通过组合的方式桥接颜色和形状,这就是桥接模式,它主要用于 两个或多个同等级的接口。

组合模式

组合模式用于 整体与部分的结构,当整体与部分有相似的结构,在操作时可以被一致对待时,就可以使用组合模式。例如:1.文件夹和子文件夹的关系:文件夹中可以存放文件,也可以新建文件夹,子文件夹也一样。
2.总公司子公司的关系:总公司可以设立部门,也可以设立分公司,子公司也一样。
3.树枝和分树枝的关系:树枝可以长出叶子,也可以长出树枝,分树枝也一样。在这些关系中,虽然整体包含了部分,但无论整体或部分,都具有一致的行为。
组合模式:又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。

例如:公司人员结构

公司人员分布结构,董事长管理部门总管,部门总管管理各部门的经理等人员,经理管理普通员工等..其中部分管理人员不仅是管理者,也是被管理者,管理者和职员有共同的属性和方法:即职位,工作内容等,将这些共同的内容提取成父类(抽象类),然后管理者类和职员类继承父类即可。
这种称为透明方式,职员类和管理员类可以使用同样的接口。但是这样违背了接口隔离原则:客户端不应依赖它不需要的接口。如果一个接口在实现时,部分方法由于冗余被客户端空实现,则应该将接口拆分,让实现类只需依赖自己需要的接口方法。
即职员没有管理的人,不应该有管理的接口。只需要去掉管理的接口就行,管理的方法在子类中写即可。
public abstract class Component {// 职位private String position;// 工作内容private String job;public Component(String position, String job) {this.position = position;this.job = job;}// 做自己的本职工作public void work() {System.out.println("我是" + position + ",我正在" + job);}abstract void addComponent(Component component);abstract void removeComponent(Component component);abstract void check();
}
//管理者继承自此抽象类:public class Manager extends Component {// 管理的组件private List<Component> components = new ArrayList<>();public Manager(String position, String job) {super(position, job);}@Overridepublic void addComponent(Component component) {components.add(component);}@Overridevoid removeComponent(Component component) {components.remove(component);}// 检查下属@Overridepublic void check() {work();for (Component component : components) {component.check();}}
}
//职员同样继承自此抽象类:public class Employee extends Component {public Employee(String position, String job) {super(position, job);}@Overridevoid addComponent(Component component) {System.out.println("职员没有管理权限");}@Overridevoid removeComponent(Component component) {System.out.println("职员没有管理权限");}@Overridevoid check() {work();}
}
//修改客户端如下:public class Client {@Testpublic void test() {Component boss = new Manager("老板", "唱怒放的生命");Component HR = new Employee("人力资源", "聊微信");Component PM = new Manager("产品经理", "不知道干啥");Component CFO = new Manager("财务主管", "看剧");Component CTO = new Manager("技术主管", "划水");Component UI = new Employee("设计师", "画画");Component operator = new Employee("运营人员", "兼职客服");Component webProgrammer = new Employee("程序员", "学习设计模式");Component backgroundProgrammer = new Employee("后台程序员", "CRUD");Component accountant = new Employee("会计", "背九九乘法表");Component clerk = new Employee("文员", "给老板递麦克风");boss.addComponent(HR);boss.addComponent(PM);boss.addComponent(CFO);PM.addComponent(UI);PM.addComponent(CTO);PM.addComponent(operator);CTO.addComponent(webProgrammer);CTO.addComponent(backgroundProgrammer);CFO.addComponent(accountant);CFO.addComponent(clerk);boss.check();}
}

组合模式中的安全方式与透明方式

Employee 类虽然继承了父类的 addComponent 和 removeComponent 方法,但是仅仅提供了一个空实现,因为 Employee 类是不支持添加和移除组件的。违背了接口隔离原则,这种方式在组合模式中被称作透明方式.

透明方式:在 Component 中声明所有管理子对象的方法,包括 add 、remove 等,这样继承自 Component 的子类都具备了 add、remove 方法。对于外界来说叶节点和枝节点是透明的,它们具备完全一致的接口。

//将抽象类修改为:
public abstract class Component {// 职位private String position;// 工作内容private String job;public Component(String position, String job) {this.position = position;this.job = job;}// 做自己的本职工作public void work() {System.out.println("我是" + position + ",我正在" + job);}abstract void check();
}//可以看到,我们在父类中去掉了 addComponent 和 removeComponent 这两个抽象方法。
//Manager 类修改为:public class Manager extends Component {// 管理的组件private List<Component> components = new ArrayList<>();public Manager(String position, String job) {super(position, job);}public void addComponent(Component component) {components.add(component);}void removeComponent(Component component) {components.remove(component);}// 检查下属@Overridepublic void check() {work();for (Component component : components) {component.check();}}
}//Manager 类单独实现了 addComponent 和 removeComponent 这两个方法,去掉了 @Override 注解。
//Employee 类修改为:public class Employee extends Component {public Employee(String position, String job) {super(position, job);}@Overridevoid check() {work();}
}//客户端建立人员结构关系:public class Client {@Testpublic void test() {Manager boss = new Manager("老板", "唱怒放的生命");Employee HR = new Employee("人力资源", "聊微信");Manager PM = new Manager("产品经理", "不知道干啥");Manager CFO = new Manager("财务主管", "看剧");Manager CTO = new Manager("技术主管", "划水");Employee UI = new Employee("设计师", "画画");Employee operator = new Employee("运营人员", "兼职客服");Employee webProgrammer = new Employee("程序员", "学习设计模式");Employee backgroundProgrammer = new Employee("后台程序员", "CRUD");Employee accountant = new Employee("会计", "背九九乘法表");Employee clerk = new Employee("文员", "给老板递麦克风");boss.addComponent(HR);boss.addComponent(PM);boss.addComponent(CFO);PM.addComponent(UI);PM.addComponent(CTO);PM.addComponent(operator);CTO.addComponent(webProgrammer);CTO.addComponent(backgroundProgrammer);CFO.addComponent(accountant);CFO.addComponent(clerk);boss.check();}
}

安全方式:在 Component 中不声明 add 和 remove 等管理子对象的方法,这样叶节点就无需实现它,只需在枝节点中实现管理子对象的方法即可。

安全方式遵循了接口隔离原则,但由于不够透明,Manager 和 Employee 类不具有相同的接口,在客户端中,我们无法将 Manager 和 Employee 统一声明为 Component 类了,必须要区别对待,带来了使用上的不方便。
安全方式和透明方式各有好处,在使用组合模式时,需要根据实际情况决定。但大多数使用组合模式的场景都是采用的透明方式,虽然它有点不安全,但是客户端无需做任何判断来区分是叶子结点还是枝节点,用起来是真香。

装饰模式

提到装饰,我们先来想一下生活中有哪些装饰:女生的首饰:戒指、耳环、项链等装饰品
家居装饰品:粘钩、镜子、壁画、盆栽等
我们为什么需要这些装饰品呢?很容易想到是为了美,戒指、耳环、项链、壁画、盆栽等都是为了提高颜值或增加美观度。但粘钩、镜子不一样,它们是为了方便我们挂东西、洗漱。所以我们可以总结出装饰品共有两种功能:增强原有的特性:我们本身就是有一定颜值的,添加装饰品提高了我们的颜值。同样,房屋本身就有一定的美观度,家居装饰提高了房屋的美观度。
添加新的特性:在墙上挂上粘钩,让墙壁有了挂东西的功能。在洗漱台装上镜子,让洗漱台有了照镜子的功能。
并且,我们发现装饰品并不会改变物品本身,只是起到一个锦上添花的作用。装饰模式也一样,它的主要作用就是:
1.增强一个类原有的功能
2.为一个类添加新的功能
并且 装饰模式也不会改变原有的类。

装饰模式:动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。其别名也可以称为包装器,与适配器模式的别名相同,但它们适用于不同的场合。根据翻译的不同,装饰模式也有人称之为“油漆工模式”。

1. 用于增强功能的装饰模式

模拟戴上装饰品提高我们颜值的过程: 新建颜值接口:

public interface IBeauty {int getBeautyValue();
}

新建 Me 类,实现颜值接口:

public class Me implements IBeauty {@Overridepublic int getBeautyValue() {return 100;}
}

戒指装饰类,将 Me 包装起来:

public class RingDecorator implements IBeauty {private final IBeauty me;public RingDecorator(IBeauty me) {this.me = me;}@Overridepublic int getBeautyValue() {return me.getBeautyValue() + 20;}
}

客户端测试:

public class Client {@Testpublic void show() {IBeauty me = new Me();System.out.println("我原本的颜值:" + me.getBeautyValue());IBeauty meWithRing = new RingDecorator(me);System.out.println("戴上了戒指后,我的颜值:" + meWithRing.getBeautyValue());}
}

运行程序,输出如下:

我原本的颜值:100
戴上了戒指后,我的颜值:120
这就是最简单的增强功能的装饰模式。以后我们可以添加更多的装饰类,比如:

耳环装饰类:

public class EarringDecorator implements IBeauty {private final IBeauty me;public EarringDecorator(IBeauty me) {this.me = me;}@Overridepublic int getBeautyValue() {return me.getBeautyValue() + 50;}
}

项链装饰类:

public class NecklaceDecorator implements IBeauty {private final IBeauty me;public NecklaceDecorator(IBeauty me) {this.me = me;}@Overridepublic int getBeautyValue() {return me.getBeautyValue() + 80;}
}

客户端测试:

public class Client {@Testpublic void show() {IBeauty me = new Me();System.out.println("我原本的颜值:" + me.getBeautyValue());// 随意挑选装饰IBeauty meWithNecklace = new NecklaceDecorator(me);System.out.println("戴上了项链后,我的颜值:" + meWithNecklace.getBeautyValue());// 多次装饰IBeauty meWithManyDecorators = new NecklaceDecorator(new RingDecorator(new EarringDecorator(me)));System.out.println("戴上耳环、戒指、项链后,我的颜值:" + meWithManyDecorators.getBeautyValue());// 任意搭配装饰IBeauty meWithNecklaceAndRing = new NecklaceDecorator(new RingDecorator(me));System.out.println("戴上戒指、项链后,我的颜值:" + meWithNecklaceAndRing.getBeautyValue());}
}

运行程序,输出如下:

我原本的颜值:100
戴上了项链后,我的颜值:180
戴上耳环、戒指、项链后,我的颜值:250
戴上戒指、项链后,我的颜值:200

可以看到,装饰器也实现了 IBeauty 接口,并且没有添加新的方法,也就是说这里的装饰器仅用于增强功能,并不会改变 Me 原有的功能,这种装饰模式称之为 透明装饰模式,由于没有改变接口,也没有新增方法,所以透明装饰模式可以无限装饰。

装饰模式是 继承 的一种替代方案。本例如果不使用装饰模式,而是改用继承实现的话,戴着戒指的 Me 需要派生一个子类、戴着项链的 Me 需要派生一个子类、戴着耳环的 Me 需要派生一个子类、戴着戒指 +
项链的需要派生一个子类…各种各样的排列组合会造成类爆炸。而采用了装饰模式就只需要为每个装饰品生成一个装饰类即可,所以说就 增加对象功能 来说,装饰模式比生成子类实现更为灵活。

2. 用于添加功能的装饰模式

我们用程序来模拟一下房屋装饰粘钩后,新增了挂东西功能的过程:

新建房屋接口:

public interface IHouse {void live();
}

房屋类:

public class House implements IHouse {@Overridepublic void live() {System.out.println("房屋原有的功能:居住功能");}
}

新建粘钩装饰器接口,继承自房屋接口:

public interface IStickyHookHouse extends IHouse {void hangThings();
}

粘钩装饰类:

public class StickyHookDecorator implements IStickyHookHouse {private final IHouse house;public StickyHookDecorator(IHouse house) {this.house = house;}@Overridepublic void live() {house.live();}@Overridepublic void hangThings() {System.out.println("有了粘钩后,新增了挂东西功能");}
}

客户端测试:

public class Client {@Testpublic void show() {IHouse house = new House();house.live();IStickyHookHouse stickyHookHouse = new StickyHookDecorator(house);stickyHookHouse.live();stickyHookHouse.hangThings();}
}

运行程序,显示如下:

房屋原有的功能:居住功能
房屋原有的功能:居住功能
有了粘钩后,新增了挂东西功能

这就是用于 新增功能 的装饰模式。我们在接口中新增了方法:hangThings,然后在装饰器中将 House 类包装起来,之前 House 中的方法仍然调用 house
去执行,也就是说我们并没有修改原有的功能,只是扩展了新的功能,这种模式在装饰模式中称之为 半透明装饰模式。

为什么叫半透明呢?由于新的接口 IStickyHookHouse 拥有之前 IHouse 不具有的方法,所以我们如果要使用装饰器中添加的功能,就不得不区别对待 装饰前的对象和装饰后的对象。也就是说客户端要使用新方法,必须知道具体的装饰类
StickyHookDecorator,所以这个装饰类对客户端来说是可见的、不透明的。而被装饰者不一定要是 House,它可以是实现了 IHouse
接口的任意对象,所以被装饰者对客户端是不可见的、透明的。由于一半透明,一半不透明,所以称之为半透明装饰模式。

我们可以添加更多的装饰器:

新建镜子装饰器的接口,继承自房屋接口:

public interface IMirrorHouse extends IHouse {void lookMirror();
}

镜子装饰类:

public class MirrorDecorator implements IMirrorHouse {private final IHouse house;public MirrorDecorator(IHouse house) {this.house = house;}@Overridepublic void live() {house.live();}@Overridepublic void lookMirror() {System.out.println("有了镜子后,新增了照镜子功能");}
}

客户端测试:

public class Client {@Testpublic void show() {IHouse house = new House();house.live();IMirrorHouse mirrorHouse = new MirrorDecorator(house);mirrorHouse.live();mirrorHouse.lookMirror();}
}

运行程序,输出如下:

房屋原有的功能:居住功能
房屋原有的功能:居住功能
有了镜子后,新增了照镜子功能

现在我们仿照 透明装饰模式 的写法,同时添加粘钩和镜子装饰试一试:

public class Client {@Testpublic void show() {IHouse house = new House();house.live();IStickyHookHouse stickyHookHouse = new StickyHookDecorator(house);IMirrorHouse houseWithStickyHookMirror = new MirrorDecorator(stickyHookHouse);houseWithStickyHookMirror.live();houseWithStickyHookMirror.hangThings(); // 这里会报错,找不到 hangThings 方法houseWithStickyHookMirror.lookMirror();}
}

我们会发现,第二次装饰时,无法获得上一次装饰添加的方法。原因很明显,当我们用 IMirrorHouse 装饰器后,接口变为了 IMirrorHouse,这个接口中并没有 hangThings 方法。

那么我们能否让 IMirrorHouse 继承自 IStickyHookHouse,以实现新增两个功能呢?

可以,但那样做的话两个装饰类之间有了依赖关系,那就不是装饰模式了。装饰类不应该存在依赖关系,而应该在原本的类上进行装饰。这就意味着,半透明装饰模式中,我们无法多次装饰。

有的同学会问了,既增强了功能,又添加了新功能的装饰模式叫什么呢?

—— 举一反三,肯定是叫全不透明装饰模式!

—— 并不是!只要添加了新功能的装饰模式都称之为 半透明装饰模式,他们都具有不可以多次装饰的特点。仔细理解上文半透明名称的由来就知道了,“透明”指的是我们无需知道被装饰者具体的类,既增强了功能,又添加了新功能的装饰模式仍然具有半透明特性。

看了这两个简单的例子,是不是发现装饰模式很简单呢?恭喜你学会了 1 + 1 = 2,现在你已经掌握了算数的基本思想,接下来我们来做一道微积分题练习一下。

I/O 中的装饰模式

I/O 指的是 Input/Output,即输入、输出。我们以 Input 为例。先在 src 文件夹下新建一个文件 readme.text,随便写点文字:

禁止套娃 禁止禁止套娃 禁止禁止禁止套娃 然后用 Java 的 InputStream 读取,代码一般长这样:

public void io()throws IOException{InputStream in=new BufferedInputStream(new FileInputStream("src/readme.txt"));byte[]buffer=new byte[1024];while(in.read(buffer)!=-1){System.out.println(new String(buffer));}in.close();
}

这样写有一个问题,如果读取过程中出现了 IO 异常,InputStream 就不能正确关闭,所以我们要用try…finally来保证 InputStream 正确关闭:

public void io()throws IOException{InputStream in=null;try{in=new BufferedInputStream(new FileInputStream(" src/readme.txt"));byte[]buffer=new byte[1024];while(in.read(buffer)!=-1){System.out.println(new String(buffer));}}finally{if(in!=null){in.close();}}
}

这种写法实在是太丑了,而 IO 操作又必须这么写,显然 Java 也意识到了这个问题,所以 Java 7
中引入了try(resource)语法糖,IO 的代码就可以简化如下:

public void io()throws IOException{try(InputStream in=new BufferedInputStream(new FileInputStream(" src/readme.txt"))) { byte[] buffer = new byte[1024]; while (in.read(buffer) != -1) { System.out.println(new String(buffer));}}
}

这种写法和上一种逻辑是一样的,运行程序,显示如下:

禁止套娃 禁止禁止套娃 禁止禁止禁止套娃 观察获取 InputStream 这句代码:

InputStream in = new BufferedInputStream(new FileInputStream(“src/readme.txt”));

是不是和我们之前多次装饰的代码非常相似:

// 多次装饰 IBeauty meWithManyDecorators = new NecklaceDecorator(new RingDecorator(new EarringDecorator(me))); 事实上,查看 I/O
的源码可知,Java I/O 的设计框架便是使用的 装饰者模式,InputStream 的继承关系如下:

其中,InputStream 是一个抽象类,对应上文例子中的 IHouse,其中最重要的方法是 read 方法,这是一个抽象方法:

public abstract class InputStream implements Closeable {public abstract int read() throws IOException;// ...
}

这个方法会读取输入流的下一个字节,并返回字节表示的 int 值(0~255),返回 -1 表示已读到末尾。由于它是抽象方法,所以具体的逻辑交由子类实现。

上图中,左边的三个类 FileInputStream、ByteArrayInputStream、ServletInputStream 是 InputStream 的三个子类,对应上文例子中实现了 IHouse 接口的 House。

右下角的三个类 BufferedInputStream、DataInputStream、CheckedInputStream 是三个具体的装饰者类,他们都为 InputStream 增强了原有功能或添加了新功能。

FilterInputStream 是所有装饰类的父类,它没有实现具体的功能,仅用来包装了一下 InputStream:

public class FilterInputStream extends InputStream { protected volatile InputStream in;

protected FilterInputStream(InputStream in) {this.in = in;
}

public int read() throws IOException { return in.read(); }

//...

} 我们以 BufferedInputStream 为例。原有的 InputStream
读取文件时,是一个字节一个字节读取的,这种方式的执行效率并不高,所以我们可以设立一个缓冲区,先将内容读取到缓冲区中,缓冲区读满后,将内容从缓冲区中取出来,这样就变成了一段一段读取,用内存换取效率。BufferedInputStream
就是用来做这个的。它继承自 FilterInputStream:

public class BufferedInputStream extends FilterInputStream { private static final int DEFAULT_BUFFER_SIZE = 8192;
protected volatile byte buf[];

public BufferedInputStream(InputStream in) { this(in, DEFAULT_BUFFER_SIZE); }

public BufferedInputStream(InputStream in, int size) { super(in); if (size <= 0) { throw new IllegalArgumentException("
Buffer size <= 0"); } buf = new byte[size]; }

//...

} 我们先来看它的构造方法,在构造方法中,新建了一个 byte[] 作为缓冲区,从源码中我们看到,Java 默认设置的缓冲区大小为 8192 byte,也就是 8 KB。

然后我们来查看 read 方法:

public class BufferedInputStream extends FilterInputStream { //…

public synchronized int read() throws IOException { if (pos >= count) { fill(); if (pos >= count)
return -1; } return getBufIfOpen()[pos++] & 0xff; }

private void fill() throws IOException { // 往缓冲区内填充读取内容的过程 //… } } 在 read 方法中,调用了 fill 方法,fill
方法的作用就是往缓冲区中填充读取的内容。这样就实现了增强原有的功能。

在源码中我们发现,BufferedInputStream 没有添加 InputStream 中没有的方法,所以 BufferedInputStream 使用的是 透明的装饰模式。

DataInputStream 用于更加方便地读取 int、double 等内容,观察 DataInputStream 的源码可以发现,DataInputStream 中新增了 readInt、readLong 等方法,所以
DataInputStream 使用的是 半透明装饰模式。

理解了 InputStream 后,再看一下 OutputStream 的继承关系,相信大家一眼就能看出各个类的作用了:

这就是装饰模式,注意不要和适配器模式混淆了。两者在使用时都是包装一个类,但两者的区别其实也很明显:

纯粹的适配器模式 仅用于改变接口,不改变其功能,部分情况下我们需要改变一点功能以适配新接口。但使用适配器模式时,接口一定会有一个 回炉重造 的过程。 装饰模式 不改变原有的接口,仅用于增强原有功能或添加新功能,强调的是 锦上添花。
掌握了装饰者模式之后,理解 Java I/O 的框架设计就非常容易了。但对于不理解装饰模式的人来说,各种各样相似的 InputStream
非常容易让开发者感到困惑。这一点正是装饰模式的缺点:容易造成程序中有大量相似的类。虽然这更像是开发者的缺点,我们应该做的是提高自己的技术,掌握了这个设计模式之后它就是我们的一把利器。现在我们再看到 I/O 不同的 InputStream
装饰类,只需要关注它增强了什么功能或添加了什么功能即可。

外观模式

外观模式非常简单,体现的就是 Java 中封装的思想。将多个子系统封装起来,提供一个更简洁的接口供外部调用。

外观模式:外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。外观模式又称为门面模式。

类似于类中的init方法,比如初始化需要做的事情封装到init中。


#享元模式
享元模式体现的是 程序可复用 的特点,为了节约宝贵的内存,程序应该尽可能地复用,就像《极限编程》作者 Kent 在书里说到的那样:Don’t repeat yourself. 简单来说 享元模式就是共享对象,提高复用性,

享元模式:运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式。

有些对象本身不一样,但通过一点点变化后就可以复用,我们编程时可能稍不注意就会忘记复用这些对象。比如《超级玛丽》,草和云更改一下颜色就可以实现复用,还有里面的三种乌龟,换一个颜色、加一个装饰就变成了不同的怪。


#代理模式
我们在客户端和Xxx类之间新增了一个中间件 XxxProxy,这个类就叫做代理类,他实现了和Xxx类一模一样的行为。

代理模式:给某一个对象提供一个代理,并由代理对象控制对原对象的引用。

我们可能会遇到这样的需求:在网络请求前后,分别打印将要发送的数据和接收到数据作为日志信息。此时我们就可以新建一个网络请求的代理类,让它代为处理网络请求,并在代理类中打印这些日志信息。
###静态代理:

//新建网络请求接口:
public interface IHttp {void request(String sendData);void onSuccess(String receivedData);
}
//新建 Http 请求工具类:
public class HttpUtil implements IHttp {@Overridepublic void request(String sendData) {System.out.println("网络请求中...");}@Overridepublic void onSuccess(String receivedData) {System.out.println("网络请求完成。");}
}
//新建 Http 代理类:
public class HttpProxy implements IHttp {private final HttpUtil httpUtil;public HttpProxy(HttpUtil httpUtil) {this.httpUtil = httpUtil;}@Overridepublic void request(String sendData) {httpUtil.request(sendData);}@Overridepublic void onSuccess(String receivedData) {httpUtil.onSuccess(receivedData);}
}
//到这里,和我们上述吃饭睡觉的代码是一模一样的,现在我们在 HttpProxy 中新增打印日志信息:
public class HttpProxy implements IHttp {private final HttpUtil httpUtil;public HttpProxy(HttpUtil httpUtil) {this.httpUtil = httpUtil;}@Overridepublic void request(String sendData) {System.out.println("发送数据:" + sendData);httpUtil.request(sendData);}@Overridepublic void onSuccess(String receivedData) {System.out.println("收到数据:" + receivedData);httpUtil.onSuccess(receivedData);}
}
//客户端验证:
public class Client {@Testpublic void test() {HttpUtil httpUtil = new HttpUtil();HttpProxy proxy = new HttpProxy(httpUtil);proxy.request("request data");proxy.onSuccess("received result");}
}

运行程序,输出如下:

发送数据:request data
网络请求中...
收到数据:received result
网络请求完成。

这就是代理模式的一个应用,除了 打印日志,它还可以用来做权限管理。读者看到这里可能已经发现了,这个代理类看起来和装饰模式的 FilterInputStream 一模一样,但两者的目的不同,装饰模式是为了 增强功能或添加功能,代理模式主要是为了加以控制。

###动态代理:
动态代理与静态代理的原理一模一样,只是换了一种写法。使用动态代理,需要把一个类传入,然后根据它正在调用的方法名判断是否需要加以控制。

public class HttpProxy implements InvocationHandler {private HttpUtil httpUtil;public IHttp getInstance(HttpUtil httpUtil) {this.httpUtil = httpUtil;return (IHttp) Proxy.newProxyInstance(httpUtil.getClass().getClassLoader(), httpUtil.getClass().getInterfaces(), this);}// 调用 httpUtil 的任意方法时,都要通过这个方法调用@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {Object result = null;if (method.getName().equals("request")) {// 如果方法名是 request,打印日志,并调用 request 方法System.out.println("发送数据:" + args[0]);result = method.invoke(httpUtil, args);} else if (method.getName().equals("onSuccess")) {// 如果方法名是 onSuccess,打印日志,并调用 onSuccess 方法System.out.println("收到数据:" + args[0]);result = method.invoke(httpUtil, args);}return result;}
}

先看 getInstance 方法,Proxy.newProxyInstance 方法是 Java 系统提供的方法,专门用于动态代理。其中传入的第一个参数是被代理的类的 ClassLoader,第二个参数是被代理类的 Interfaces,这两个参数都是 Object 中的,每个类都有,这里就是固定写法。我们只要知道系统需要这两个参数才能让我们实现我们的目的:调用被代理类的任意方法时,都通过一个方法间接调用。现在我们给系统提供了这两个参数,系统就会在第三个参数中帮我们实现这个目的。

第三个参数是 InvocationHandler 接口,这个接口中只有一个方法:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

现在我们调用被代理类 httpUtil 的任意方法时,都会通过这个 invoke 方法调用了。invoke 方法中,第一个参数我们暂时用不上,第二个参数 method 就是调用的方法,使用 method.getName() 可以获取到方法名,第三个参数是调用 method 方法需要传入的参数。本例中无论 request 还是 onSuccess 都只有一个 String 类型的参数,对应到这里就是 args[0]。返回的 Object 是 method 方法的返回值,本例中都是无返回值的。

我们在 invoke 方法中判断了当前调用方法的方法名,如果现在调用的方法是 request,那么打印请求参数,并使用这一行代码继续执行当前方法:

result = method.invoke(httpUtil, args);

这就是 反射调用函数 的写法,如果不了解可以记做固定写法,想要了解的同学可以看之前的这篇文章:详解面试中常考的 Java 反射机制。虽然这个函数没有返回值,但我们还是将 result 返回,这是标准做法。

如果现在调用的方法是 onSuccess,那么打印接收到的数据,并反射继续执行当前方法。

修改客户端验证一下:

public class Client {@Testpublic void test() {HttpUtil httpUtil = new HttpUtil();IHttp proxy = new HttpProxy().getInstance(httpUtil);proxy.request("request data");proxy.onSuccess("received result");}
}

运行程序,输出与之前一样:

发送数据:request data

网络请求中…

收到数据:received result

网络请求完成。

动态代理本质上与静态代理没有区别,它的好处是 节省代码量。比如被代理类有 20 个方法,而我们只需要控制其中的两个方法,就可以用动态代理通过方法名对被代理类进行动态的控制,而如果用静态方法,我们就需要将另外的 18 个方法也写出来,非常繁琐。这就是动态代理的优势所在。

。invoke 方法中,第一个参数我们暂时用不上,第二个参数 method 就是调用的方法,使用 method.getName() 可以获取到方法名,第三个参数是调用 method 方法需要传入的参数。本例中无论 request 还是 onSuccess 都只有一个 String 类型的参数,对应到这里就是 args[0]。返回的 Object 是 method 方法的返回值,本例中都是无返回值的。

我们在 invoke 方法中判断了当前调用方法的方法名,如果现在调用的方法是 request,那么打印请求参数,并使用这一行代码继续执行当前方法:

result = method.invoke(httpUtil, args);

这就是 反射调用函数 的写法,如果不了解可以记做固定写法,想要了解的同学可以看之前的这篇文章:详解面试中常考的 Java 反射机制。虽然这个函数没有返回值,但我们还是将 result 返回,这是标准做法。

如果现在调用的方法是 onSuccess,那么打印接收到的数据,并反射继续执行当前方法。

修改客户端验证一下:

public class Client {@Testpublic void test() {HttpUtil httpUtil = new HttpUtil();IHttp proxy = new HttpProxy().getInstance(httpUtil);proxy.request("request data");proxy.onSuccess("received result");}
}

运行程序,输出与之前一样:

发送数据:request data

网络请求中…

收到数据:received result

网络请求完成。

动态代理本质上与静态代理没有区别,它的好处是 节省代码量。比如被代理类有 20 个方法,而我们只需要控制其中的两个方法,就可以用动态代理通过方法名对被代理类进行动态的控制,而如果用静态方法,我们就需要将另外的 18 个方法也写出来,非常繁琐。这就是动态代理的优势所在。

设计模式-结构型模式相关推荐

  1. 设计模式-结构型模式-装饰模式

    设计模式-结构型模式-装饰模式 栗子 以成绩单需要家长签字为要求. 成绩单类图 // 抽象成绩单 public abstract class SchoolReport{// 成绩单你的成绩情况publ ...

  2. 设计模式-结构型模式篇

    设计模式 目录: 一.代理模式 二.适配器模式 三.装饰者模式 四.桥接模式 五.外观模式 六.组合模式 七.享元模式 注:学习视频:黑马程序员Java设计模式 结构型模式 结构性模式描述如何将类或对 ...

  3. JAVA设计模式--结构型模式

    2019独角兽企业重金招聘Python工程师标准>>> 我们接着讨论设计模式,上篇文章我讲完了5种创建型模式,这章开始,我将讲下7种结构型模式:适配器模式.装饰模式.代理模式.外观模 ...

  4. 设计模式 结构型模式 外观模式(Facade Pattern)

    在软件开发过程中,客户端程序经常会与复杂系统的内部子系统进行耦合,从而导致客户端程序随着子系统的变化而变化. 这时为了将复杂系统的内部子系统与客户端之间的依赖解耦,从而就有了外观模式,也称作 &quo ...

  5. 设计模式--结构型模式

    总体来说设计模式分为三大类:创建型模式.结构型模式和行为型模式. 博主的上一篇文章已经提到过创建型模式,此外该文章还有设计模式概况和设计模式的六大原则.设计模式的六大原则是设计模式的核心思想,详情请看 ...

  6. JAVA设计模式--结构型模式--代理模式

    1.代理模式(Proxy Pattern) 一个类代表另一个类的功能.这种类型的设计模式属于结构型模式.在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口. 1.1意图: 为其他对象提供 ...

  7. 设计模式——结构型模式

    七大 - 结构型模式 汇总篇 一.适配器模式 1.基本介绍 2.代码示例 3.在springmvc中使用适配器模式 二.桥接模式 1.基本介绍 2.代码示例 3.桥接模式在JDBC的源码中使用 三.装 ...

  8. 设计模式 - 结构型模式_外观模式

    文章目录 结构型模式 概述 Case Bad Impl Better Impl 小结 结构型模式 结构型模式主要是解决如何将对象和类组装成较大的结构, 并同时保持结构的灵活和⾼效. 结构型模式包括:适 ...

  9. 设计模式——结构型模式之代理模式和适配器模式(类比+图解,从无到有,一文看懂几种模式的区别)

    设计模式 系列文章: 一.创建型模式--工厂模式 二.创建型模式--单例模式.原型模式 三.创建型模式--建造者模式 四.结构型模式--装饰者模式 五.结构型模式--代理模式.适配器模式 文章目录 设 ...

  10. 设计模式 - 结构型模式_适配器模式

    文章目录 结构型模式 概述 Case 场景模拟⼯程 Bad Impl Better Impl (适配器模式重构代码) MQ消息适配 接口适配 小结 结构型模式 结构型模式主要是解决如何将对象和类组装成 ...

最新文章

  1. TCP协议客户端读取文本文件,服务器端输出到文本文件
  2. 台湾炸鸡连锁店供应过期肉品 工厂停业违规品下架
  3. kubernetes资源控制与及ingress插件安装(容忍策略)
  4. The absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved in either web.xm
  5. lucene 异常 Lock obtain timed out 解决方法
  6. Nginx+PHP-FPM——Nginx日志:[Primary script unknown] 网页:[file not found. ]解决方案
  7. 常用PHP函数整理!
  8. python调用c函数传字符串参数_Python使用ctypes模块调用DLL函数之传递数值、指针与字符串参数...
  9. 小米公布Q1手机出货量:驳斥暴跌谣言
  10. python格式化输出类型_Python print 格式化输出
  11. C++ 多线程编程 封装多线程api 类似java多线程风格
  12. 电视直播源码2.0-全站HTML文件
  13. 没钱充会员 百度网盘下载限速怎么办?这些招可以突破百度网盘非会员限速限制
  14. TM2008预览版试用 速度快完美兼容Vista
  15. Pigx官方文档地址
  16. 移动周刊第 176 期:Android 知识梳理
  17. 15天助你掌握问卷统计与Spss实战
  18. 按位与运算()和求余运算(%)的联系
  19. 关于无法在驱动器0分区上安装Windows
  20. 【Scrum模式语言9】准备就绪的定义(Definition of Ready - DoR )

热门文章

  1. 意大利农业股份与中国海南永利实业共同组建国际贸易产品流通枢纽中心
  2. 2023年全国最新安全员精选真题及答案66
  3. 深度学习和Keras 简介
  4. UE4 脚部IK实现
  5. 职业梦想是计算机的英语作文,高中英语作文我的梦想职业
  6. 大神教你如何剪辑热播剧《你是我的荣耀》,3分钟精彩片段变现!【覃小龙课堂】
  7. js长度超过1逗号分开_js怎么用逗号或者换行分割字符串
  8. 米联客PCIE的读写
  9. HTML马里奥小游戏
  10. sublime改成中文简体