设计模式个人解读2(结构型模式)
创建型模式请查看该地址:https://blog.csdn.net/zyfhhhw/article/details/108650710
文章目录
(2)结构模式(7种)
结构型模式:用于描述如何将类或对象按某种布局组成更大的结构。
结构型模式分为以下 7 种:
- 代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
- 适配器(Adapter)模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
- 桥接(Bridge)模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现的,从而降低了抽象和实现这两个可变维度的耦合度。
- 装饰(Decorator)模式:动态地给对象增加一些职责,即增加其额外的功能。
- 外观(Facade)模式:为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
- 享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用。
- 组合(Composite)模式:将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。
一、代理模式
代理模式定义:由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
概念理解可能会有些抽象,如果我们举个例子,大家就可以更好的理解。比如我们购买火车票不一定要去火车站买,可以通过 12306 网站或者去火车票代售点买。我们缴纳的社保,不是我们自己去直接找国家机关的相关单位进行的缴纳,是由公司代替我们缴纳的。
主要解决的问题:直接访问我们需要访问的对象时有很多限制,或者我们不想直接访问目标对象时,就需要我们使用代理模式。例如,我们想访问一个远程服务器,但是由于各种安全问题,我们不能直连该服务器,我们必须要经过一个跳板机,然后跳转到我们需要访问的目标服务器。
代理模式分为三类:
1. 静态代理
2. 动态代理
3. CGLIB代理(不进行讲解,主要讲解动态和静态代理类)
1、代理模式的优缺点
代理模式的主要优点有:
- 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
- 代理对象可以扩展目标对象的功能;
- 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度,增加了程序的可扩展性
其主要缺点是:
- 代理模式会造成系统设计中类的数量增加
- 在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢;
- 增加了系统的复杂度;
2、代理模式的实现
我们依次对三种代理模式,进行代码讲解分析。
(1)静态代理
使用代理类访问到真实需要访问的类
public class ProxyDemo {
public static void main(String[] args) {
SubjectProxy subjectProxy = new SubjectProxy();
subjectProxy.Request();
}
//抽象主题
interface Subject {
void Request();
}
//真实主题
static class RealSubject implements Subject {
public void Request() {
System.out.println("访问真实主题方法...");
}
}
//代理类
static class SubjectProxy implements Subject {
private RealSubject realSubject;
//实现接口方法
@Override
public void Request() {
if(realSubject==null){
realSubject = new RealSubject() ;
}
System.out.println("访问真实主题方法的前置语句");
realSubject.Request();
System.out.println("访问真实主题方法的后置语句");
}
}
}
(2)动态代理
动态代理更有代码参考意义,里面涉及到的类加载器以及InvocationHandler接口等知识,大家可以自行查找一下各自的功能。
①、动态代理类
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
//动态代理类
public class DynamicProxyHandler implements InvocationHandler {
private Object object;
public DynamicProxyHandler(final Object object) {
this.object = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("实例执行前");
Object result = method.invoke(object, args);
System.out.println("实例执行后");
return result;
}
}
②、测试类
import java.lang.reflect.Proxy;
测试类
public class ProxyDemo {
public static void main(String[] args) {
//1、静态代理
/*SubjectProxy subjectProxy = new SubjectProxy();
subjectProxy.Request();*/
//2、动态代理
Subject proxySubject = new RealSubject();
Subject instance = (Subject) Proxy.newProxyInstance(Subject.class.getClassLoader(),
new Class[]{Subject.class},
new DynamicProxyHandler(proxySubject));
instance.Request();
}
}
二、适配器(Adapter)模式
概念:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
生活中的例子:例如,用直流电的笔记本电脑接交流电源时需要一个电源适配器,用计算机访问照相机的 SD 内存卡时需要一个读卡器等。
主要解决:主要解决在软件系统中,常常要将一些"现存的对象"放到新的环境中,而新环境要求的接口是现对象不能满足的。
解决方式:继承或依赖(推荐)。
1、适配器模式的优缺点。
优点:
- 客户端通过适配器可以透明地调用目标接口。
- 复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类。
- 将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。
缺点:
- 对类适配器来说,更换适配器的实现过程比较复杂。
2、实现:
适配器主要分为三类:类适配器模式、对象适配器模式、接口适配器模式。
①、类适配器模式
package com.test;
public class AdaptDemo {
//接口
interface USB{
public void showPPT();
}
interface VGA{
public void projection();
}
//实现类
public static class USBImpl implements USB{
@Override
public void showPPT() {
// TODO Auto-generated method stub
System.out.println("PPT内容演示");
}
}
public static class VGAImpl implements VGA{
@Override
public void projection() {
}
}
//适配器类,继承USBImpl获取USB的功能,然后实现了VGA接口。
public static class AdapterUSB2VGA extends USBImpl implements VGA {
@Override
public void projection() {
super.showPPT();
}
}
public static class Projector<T> {
public void projection(T t) {
if (t instanceof VGA) {
System.out.println("开始投影");
VGA v = new VGAImpl();
v = (VGA) t;
v.projection();
} else {
System.out.println("接口不匹配,无法投影");
}
}
}
public static void main(String[] args) {
//通过适配器创建一个VGA对象,这个适配器实际是使用的是USB的showPPT()方法
AdaptDemo.VGA a=new AdaptDemo.AdapterUSB2VGA();
//进行投影
Projector p1=new Projector();
p1.projection(a);
}
}
②、对象适配器模式
对象适配器和类适配器使用了不同的方法实现适配,对象适配器使用组合,类适配器使用继承。
上面有的代码不再进行讲解,下面是对象适配器和类适配器不同的地方的修改
public class AdapterUSB2VGA implements VGA {
//其实这里就是替换了继承USBImpl,改成了在类里面写。
USB u = new USBImpl();
@Override
public void projection() {
u.showPPT();
}
}
③、接口适配器
当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求,它适用于一个接口不想使用其所有的方法的情况。
这里有个重点就是,我们这个AdapterUSB2VGA的类是抽象类。这个类其实就过滤了我们不需要去使用的testA()的方法。
public abstract class AdapterUSB2VGA implements VGA {
USB u = new USBImpl();
@Override
public void projection() {
u.showPPT();
}
@Override
public void b() {
};
@Override
public void c() {
};
}
AdapterUSB2VGAImpl 继承AdapterUSB2VGA,实现了里面的projection()方法即可。
public class AdapterUSB2VGAImpl extends AdapterUSB2VGA {
public void projection() {
super.projection();
}
}
类适配器模式:当希望将一个类转换成满足另一个新接口的类时,可以使用类的适配器模式,创建一个新类,继承原有的类,实现新的接口即可。
对象适配器模式:当希望将一个对象转换成满足另一个新接口的对象时,可以创建一个包装类(Wrapper),持有原类的一个实例,在包装(Wrapper)类的方法中,调用实例的方法就行。
接口适配器模式:当不希望实现一个接口中所有的方法时,可以创建一个抽象类包装(Wrapper),实现所有方法,我们写别的类的时候,继承抽象类即可。
三、桥接(Bridge)模式
概念:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现的,从而降低了抽象和实现这两个可变维度的耦合度。
主要解决:在有多种可能会变化的情况下,用继承会造成类爆炸问题,扩展起来不灵活。
其实上面的概念都是比较模糊的,从逻辑上我们还是难以理解。桥接模式,简单的说就是我们在抽象对象的特征时,对象的特征属性又很抽象,不得不把属性再次抽象。否则的话,具体子类的数量将会成几何增长,而且不易扩展。
举例,我们在抽象手机这二个对象时,它的几个属性,如操作系统,手机软件,cpu,屏幕,运营商网络等都很复杂。我们不能简单的把这几个属性直接定义。比如手机软件这个属性,我们直接定义属性是不行的,手机软件太多了,微信、QQ、相机等等,都是手机软件,所以必须再次将属性抽象化。而具体的一个手机对象就是这些属性的组合,但不是简单的组合,属性需要实现自己作为属性的功能。在这样的设计下,代码的维护和扩展也就容易了。
1、桥接(Bridge)模式优缺点
优点:
- 由于抽象与实现分离,所以扩展能力强;
- 其实现细节对客户透明。
缺点:由于聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,这增加了系统的理解与设计难度。
2、实例
我们就用手机和手机软件的例子来进行实例说明,这个例子很好的描述了桥接模式是如何实现的。
手机是一个抽象的概念,但是是有实体的,比如小米手机和华为手机是可以摸得到的实物,所以我们把手机设置为抽象类,继承了这个抽象类的有小米和华为手机;手机里面的软件是虚拟的,手机软件运行在手机中,是一种包含与被包含关系,而不是一种父与子或者说一般与特殊的关系,于是我们把软件定义为接口,而应用商城(AppStore)和相机(Camera)是众多软件中的两个。
package com.test;
public class BridgeTest {
//创建软件的接口
public interface Software {
public void run();
}
//创建应用商店类
public static class AppStore implements Software {
@Override
public void run() {
System.out.println("应用商店在运行。");
}
}
//创建相机类
public static class Camera implements Software {
@Override
public void run() {
System.out.println("相机程序在运行。");
}
}
//创建了手机的抽象类。
public static abstract class Phone {
protected Software software;
public void setSoftware(Software software){
this.software = software;
}
public abstract void run();
}
//小米手机继承手机抽象类
public static class Xiaomi extends Phone {
@Override
public void run() {
software.run();
}
}
//华为手机继承手机抽象类
public static class Huawei extends Phone {
@Override
public void run() {
software.run();
}
}
//创建工厂类,
public static class PhoneFactory{
public Phone getPhone(String type){
if(type==null){
return null;
}
if("xiaomi".equals(type)){
return new Xiaomi();
}else if("huawei".equals(type)){
return new Huawei();
}
return null;
}
}
//测试类
public static void main(String[] args) {
//这里我们只对手机进行工厂模式,其实这里可以做抽象工厂模式。
PhoneFactory phoneFactory = new PhoneFactory();
Phone xiaomi = phoneFactory.getPhone("xiaomi");
xiaomi.setSoftware(new Camera());
xiaomi.run();
Phone huawei = phoneFactory.getPhone("huawei");
huawei.setSoftware(new AppStore());
huawei.run();
}
}
将抽象部分(手机类)与它的实现部分(手机软件类)分离,将实现部分抽象成单独的类,手机类和软件类都可独立的进行变化,不会互相影响。整个类图看起来像一座桥,所以称为桥接模式
四、装饰(Decorator)模式
在现实生活中,常常需要对现有产品增加新的功能或美化其外观,如房子装修、相片加相框等。在软件开发过程中,有时想用一些现存的组件。这些组件可能只是完成了一些核心功能。但在不改变其结构的情况下,可以动态地扩展其功能。所有这些都可以釆用装饰模式来实现。装饰者和被装饰者之间必须是一样的类型,也就是要有共同的超类。
概念:动态地给对象增加一些职责,即增加其额外的功能。
主要解决:一般的,我们为了扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀。
1、装饰(Decorator)模式的优缺点
优点:
- 采用装饰模式扩展对象的功能比采用继承方式更加灵活。
- 可以设计出多个不同的具体装饰类,创造出多个不同行为的组合。
- 装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式。
缺点:
- 装饰模式增加了许多子类,如果过度使用会使程序变得很复杂。
2、实例:
我们创建一个Shape 接口和实现了 Shape 接口的实体类(Circle,Rectangle ),先我们要给形状增加一个颜色的修饰功能。我们创建一个实现了 Shape 接口的抽象装饰类 ShapeDecorator,把 Shape 对象作为它的实例变量,然后我们创建一个继承了 ShapeDecorator 的实体类RedShapeDecorator 。在不修改原代码逻辑的基础上,我们增加了对形状颜色的装饰。
package com.test;
public class DecoratorTest {
//创建形状的接口
public interface Shape {
void draw();
}
//实现了Shape接口的类
public static class Circle implements Shape {
@Override
public void draw() {
System.out.println("形状为:圆形");
}
}
//创建实现了Shape接口的抽象装饰类
public static abstract class ShapeDecorator implements Shape{
//实例变量
protected Shape decoratedShape;
public ShapeDecorator(Shape decoratedShape) {
this.decoratedShape = decoratedShape;
}
public void draw(){
decoratedShape.draw();
}
}
//创建扩展了 ShapeDecorator 类的实体装饰类。
public static class RedShapeDecorator extends ShapeDecorator{
public RedShapeDecorator(Shape decoratedShape) {
super(decoratedShape);
}
@Override
public void draw() {
// decoratedShape.draw();其实就是父类的draw()方法
super.draw();
//在实现原来代码逻辑的基础上,扩展了装饰的方法。
setRedBorder(decoratedShape);
}
private void setRedBorder(Shape decoratedShape){
System.out.println("装饰类方法,颜色设置为: 红色");
}
}
public static void main(String[] args) {
//创建形状的对象
Shape circle = new Circle();
//直接执行的结果
circle.draw();
System.out.println("===============");
//使用装饰类执行代码
ShapeDecorator redShapeDecorator = new RedShapeDecorator(new Circle());
redShapeDecorator.draw();
}
}
这里我没有给写出最后的执行结果,其实是为了让初学者自己试一下,看看结果是什么,然后再自己想想逻辑的实现方式。一定要注意的是,装饰类和被装饰类是同一个类型,所以我们装饰类(ShapeDecorator )也实现了Shape接口,因此装饰者可以取代被装饰者,这样就使被装饰者拥有了装饰者独有的行为。如果我们直接使用继承,当我们要增加新的一些功能,比如增加形状大小的属性,我们就需要对原程序进行修改,使用装饰模式可以避免我们修改原代码。
五、外观(Facade)模式
在现实生活中,我们有很多例子,比如我们开一家公司,我们需要联系很多个单位(工商局、刻章厂、地税局、国税局、银行等等),要是有一个综合部门能解决一切手续问题是不是就省了客户的很多事情。软件设计亦如此,当一个系统功能点越来越多,子系统就越来越多,那客户对系统的访问就会变得越来越复杂。我们为了简化客户的访问,我们只给客户提供一个接口供客户使用,从而降低系统的耦合度,这就是外观模式的实现逻辑。
概念:为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
1、外观模式优缺点
外观(Facade)模式是“迪米特法则”的典型应用,它有以下主要优点。
- 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
- 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
- 降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程,因为编译一个子系统不会影响其他的子系统,也不会影响外观对象。
外观(Facade)模式的主要缺点如下。
- 不能很好地限制客户使用子系统类。
- 增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。
2、实例
我们将创建一个 Shape 接口和实现了 Shape 接口的实体类(Circle、Square)。下一步是定义一个外观类 ShapeMaker。
ShapeMaker 类使用实体类来代表用户对这些类的调用。
package com.test;
/**
* 外观模式
*
*/
public class FacadeDemo {
public interface Shape{
void draw();
}
//实现类
public static class Circle implements Shape{
@Override
public void draw() {
System.out.println("圆形的");
}
}
public static class Square implements Shape{
@Override
public void draw() {
System.out.println("方形的");
}
}
//外观类,包含了上面两种图形的引用。这个类可以直接调用子系统的功能
public static class ShapeMaker{
private Shape circle;
private Shape square;
public ShapeMaker(Shape circle, Shape square) {
this.circle = circle;
this.square = square;
}
public void drawCircle(){
circle.draw();
}
public void drawSquare(){
square.draw();
}
}
public static void main(String[] args) {
ShapeMaker shapeMaker = new ShapeMaker(new Circle() ,new Square());
shapeMaker.drawCircle();
shapeMaker.drawSquare();
}
}
外观(Facade)模式的结构比较简单,主要是定义了一个高层接口。它包含了对各个子系统的引用,客户端可以通过它访问各个子系统的功能。这里我们定义了一个接口,其实我们直接写类也是可以的,灵活运用。
在外观模式中,当增加或移除子系统时需要修改外观类,这违背了“开闭原则”。如果引入抽象外观类,则在一定程度上解决了该问题。
六、享元(Flyweight)模式
概念:运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。
主要解决:在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。
在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题。创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈。例如,围棋和五子棋中的黑白棋子,教室里的桌子和凳子等。这些对象有很多相似的地方,如果能把它们相同的部分提取出来共享,则能节省大量的系统资源,这就是享元模式的产生背景。
1、享元模式优缺点
优点:
- 相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。
缺点:
- 为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
- 读取享元模式的外部状态会使得运行时间稍微变长。
2、实例:
我们将创建一个 Shape 接口和实现了 Shape 接口的实体类 Circle。下一步是定义工厂类 ShapeFactory。ShapeFactory 有一个 Circle 的 HashMap,其中键名为 Circle 对象的颜色。无论何时接收到请求,都会创建一个特定颜色的圆。ShapeFactory 检查它的 HashMap 中的 circle 对象,如果找到 Circle 对象,则返回该对象,否则将创建一个存储在 hashmap 中以备后续使用的新对象,并把该对象返回到客户端。
package com.test;
import java.util.HashMap;
/**
* 享元模式
*/
public class FlyweightDemo {
//接口
public interface Shape {
void draw();
}
//实现接口,设置对应属性。
public static class Circle implements Shape {
private String color;
private int x;
private int y;
private int radius;
public Circle(String color) {
this.color = color;
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
public void setRadius(int radius) {
this.radius = radius;
}
@Override
public void draw() {
System.out.println("画图,颜色是:"+color+",X:"+x+",Y:"+y+"radius:"+radius+"。");
}
}
//工厂类,创建圆实体类。
public static class ShapeFactory{
//创建一个静态HashMap对象。
static final HashMap<String, Shape> circleMap = new HashMap<>();
//得到指定了颜色的圆形。
public static Shape getCircle(String color) {
Circle circle = (Circle)circleMap.get(color);
//如果circle是空的,就创建。不是空的就不再创建。
if(circle == null) {
circle = new Circle(color);
circleMap.put(color, circle);
System.out.println("创建Circle,颜色是: " + color);
}
return circle;
}
}
private static final String colors[] ={ "Red", "Green" };
private static String getRandomColor() {
return colors[(int)(Math.random()*colors.length)];
}
private static int getRandomX() {
return (int)(Math.random()*100 );
}
private static int getRandomY() {
return (int)(Math.random()*100);
}
//测试
public static void main(String[] args) {
for (int i=0;i<10;i++){
Circle circle = (Circle)ShapeFactory.getCircle(getRandomColor());
circle.setX(getRandomX());
circle.setY(getRandomY());
circle.setRadius(100);
circle.draw();
}
}
}
测试结果可以看出,当我们创建了已经存在的对象后,我们不再重复创建,而是直接拿来用。
七、组合(Composite)模式
概念:有时又叫作部分-整体模式,将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。
在现实生活中,存在很多“部分-整体”的关系,例如,总公司中的部门与分公司、学习用品中的书与书包、厨房中的锅碗瓢盆等。在软件开发中也是这样,例如,文件系统中的文件与文件夹、窗体程序中的简单控件与容器控件等。对这些简单对象与复合对象的处理,如果用组合模式来实现会很方便。
1、组合模式的优缺点:
优点:
- 组合模式使得客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,这简化了客户端代码;
- 更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足“开闭原则”;
缺点:
- 设计较复杂,客户端需要花更多时间理清类之间的层次关系;
- 不容易限制容器中的构件;
- 不容易用继承的方法来增加构件的新功能;
2、实例
我们有一个类 Employee,该类被当作组合模型类。我们需要理解组合模式的具体使用方式。下面我们用实例进行具体的阐述。
package com.test;
import java.util.ArrayList;
import java.util.List;
/**
* 组合模式
*/
public class CompositeDemo {
//创建组合的具体实现类Employee
public static class Employee {
private String name;
private String dept;
private int salary;
private List<Employee> childOrdinates;
//构造函数。
public Employee(String name, String dept, int salary) {
this.name = name;
this.dept = dept;
this.salary = salary;
//下属对象
this.childOrdinates = new ArrayList<Employee>();
}
//添加
public void add(Employee employee) {
childOrdinates.add(employee);
}
//删除
public void remove(Employee employee) {
childOrdinates.remove(employee);
}
public List<Employee> getChildOrdinates(){
return childOrdinates;
}
public String toString(){
return ("Employee :[ Name : "+ name
+", dept : "+ dept + ", salary :"
+ salary+" ]");
}
}
//测试
public static void main(String[] args) {
//我们用公司职务进行组合,这个是为了让下面的组织机构成为树形结构。相当于根目录。
Employee total = new Employee("公司","总部",0);
// 创建CEO张三,我们创建了CEO的时候,其实我们已经创建了CEO下面的下属组织对象。
Employee ceo = new Employee("张三","CEO", 30000);
//创建CEO下面的小部门领导李四
Employee headSales = new Employee("李四","Head Sales", 20000);
//创建小部门下的员工王五
Employee clerk1 = new Employee("王五","Sales", 10000);
//然后对上面的人进行组合。
total.add(ceo);
ceo.add(headSales);
headSales.add(clerk1);
//组合好以后,我们打印数据,看一下效果。
for (Employee childOrdinate : total.getChildOrdinates()) {
System.out.println(childOrdinate);
for (Employee ordinate : childOrdinate.getChildOrdinates()) {
System.out.println(ordinate);
for (Employee ordinateChildOrdinate : ordinate.getChildOrdinates()) {
System.out.println(ordinateChildOrdinate);
}
}
}
}
}
数据结果如下图,自己想试试的可以试一下。我们需要注意的是,根节点是没有打印的。这个可以优化,其实这就类似于打印组织机构树的写法,具体深入的研究,大家可以自行再找资料进行学习。