目录
创建型设计模式
------------------------------------------创建型设计模式
1. Builder模式
- 使用多参数构造器,初始化时比较麻烦,需要书写多个构造参数,并且记住这些参数之间的次序;
- 使用set方法会使得类属性在被创建之后被修改,给程序带来了一些不安全性;
2. 单例模式
- 保证一个类只能有一个实例
应用场景:
- 统计当前在线人数(计数器):用一个全局对象来记录。
- 管理器
- 数据库连接池(控制资源):比如Mysql、jedis,因为数据库连接是一种连接数据库资源,不易频繁创建和销毁,使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗。
- 应用程序的日志(资源共享):一般日志内容是共享操作,需要在后面不断写入内容所以通常单例设计。
优点:
- 减少内存开销,尤其是频繁的创建和销毁实例
- 避免对资源对过多占用。
缺点:
- 没有抽象层,不利于继承扩展很难。
- 不符合了“单一职责原则”,一个类只重视内部关系,而忽略外部关系。
- 滥用单例会出现一些负面问题,如为节省资源将数据库连接池对象设计为单例,可能会导致共享连接池对象对程序过多而出现连接池溢出。如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这样将导致对象状态丢失。
实现有饿汉式和懒汉式两种:
- 饿汉式
优点:线程安全,不用加锁,效率很高;
缺点:类一加载就初始化实例,浪费内存
1 public class Singleton { 2 private static Singleton instance = new Singleton(); 3 private Singleton (){} 4 5 public static Singleton getInstance() { 6 return instance; 7 } 8 }
- 懒汉式(双重检验锁)
实现复杂,在多线程下能保证高性能。
synchronized: 在getInstance方法上加同步锁,进行线程控制
双重if判断: 外层if是保证只有第一次创建实例的时候才进入锁块,否则每次获取实例对象时都排队执行,导致效率的低下 。内层if判断是控制只有第一个获取到锁的线程发现instance是null,才去创建实例,双重if判断本质上是对效率的优化。
volatile : 为了避免JVM在指令优化时指令重排序,尤其是在创建对象(new)过程出现的指令重排序现象
1 public class Singleton { 2 private volatile static Singleton singleton; 3 private Singleton (){} 4 5 public static Singleton getSingleton() { 6 if (singleton == null) { 7 synchronized (Singleton.class) { 8 if (singleton == null) { 9 singleton = new Singleton(); 10 } 11 } 12 } 13 return singleton; 14 } 15 }
从网上找了一段话,再次说明为啥使用volatile:
new singleton()操作在Java中不是一个原子性的操作。这个动作大概分了3步
1.申请一个内存区域(空白内存)
2.调用构造方法等对singleton进行初始化(写内存)
3.将变量指针指向该对象内存区域(变量声明)
虽然双重验证锁保证了进入锁时进行判断,但如果线程1在进行new操作时,由于指令重排序先执行了1、3而没有执行初始化的步骤。此时线程2进来判断instance非null直接返回了。那么线程2获得的是个不完整的对象,使用时就会报错。
3. 原型模式
- 为了性能和安全要求,比如通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式;
- 为了保存对象的状态。
应用场景:自己太笨至今没发现, 网上说一般是和工厂方法模式一起出现,具有大量共有信息的类,然后可以拷贝出副本,修正细节信息,建立了一个完整的个性对象。
特点:
- 原型模式创建出来的对象不仅有相同的结构,而且有相同的值;
- 对象被拷贝时,不会执行构造函数
实现参见:https://www.cnblogs.com/lcmichelle/p/10743330.html中的深拷贝与浅拷贝
4. 工厂模式
- 让使用者不需关心创建过程,在改变创建过程时,不影响使用者
优点:
- “使用者” 无需关心创建过程,便于创建过程的修改维护;
- 统一管理创建相同类型的对象,提高代码可读性。
缺点:
当添加新的对象时,需要修改工厂类,违反了“开闭原则”,即违背了“系统对扩展开放,对修改关闭”的原则。
示例:
1 // 抽象产品类 2 public abstract class Video { 3 public abstract void produce(); 4 }
1 // 具体产品类 2 public class JavaVideo extends Video { 3 @Override 4 public void produce() { 5 System.out.println("录制Java课程视频"); 6 } 7 } 8 9 public class PythonVideo extends Video { 10 @Override 11 public void produce() { 12 System.out.println("录制Python课程视频"); 13 } 14 }
1 // 工厂类 2 public class VideoFactory { 3 /** 4 * 使用if else 判断类型,type 为 Java 则返回 JavaVideo, type为Python则返回 PythonVideo 5 */ 6 public Video getVideo(String type) { 7 if ("java".equalsIgnoreCase(type)) { 8 return new JavaVideo(); 9 } else if ("python".equalsIgnoreCase(type)) { 10 return new PythonVideo(); 11 } 12 return null; 13 } 14 15 /** 16 * 使用反射来创建对象 17 */ 18 public Video getVideo(Class c) { 19 Video video = null; 20 try { 21 video = (Video) Class.forName(c.getName()).newInstance(); 22 } catch (InstantiationException e) { 23 e.printStackTrace(); 24 } catch (IllegalAccessException e) { 25 e.printStackTrace(); 26 } catch (ClassNotFoundException e) { 27 e.printStackTrace(); 28 } 29 return video; 30 } 31 }
1 // 客户端调用 2 public class Test { 3 public static void main(String[] args) { 4 VideoFactory videoFactory = new VideoFactory(); 5 Video video1 = videoFactory.getVideo("python"); 6 if (video1 == null) { 7 return; 8 } 9 video1.produce(); 10 11 Video video2 = videoFactory.getVideo(JavaVideo.class); 12 if (video2 == null) { 13 return; 14 } 15 video2.produce(); 16 } 17 }
// 输出 录制Python课程视频 录制Java课程视频
- 对简单工厂模式改进,弥补简单工厂模式的缺点
优点:
- “使用者” 无需关心创建过程,便于创建过程的修改维护;
- 增加新的产品时只需要添加具体产品类和对应的具体工厂类,不用对原工厂进行任何修改,满足开闭原则。
缺点:
- 每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度。
应用场景:
没想到呢
示例:
参见工厂方法模式(Factory Method),也可以参见log4j的实现原理。
最后,介绍抽象工厂模式:
为什么存在该模式:
- 工厂方法模式中一个工厂只能创建一类“具体产品”,这将会导致工厂类特别多,增加系统的开销。而在实际过程中,一个工厂往往需要生产多种相关的产品,所以有了抽象工厂模式;
优点:
- “使用者” 无需关心创建过程,便于创建过程的修改维护;
- 符合单一职责原则,每个具体工厂类只负责创建对应的产品;
- 增加新的产品族,只需要增加相应的具体产品类和相应的工厂子类,符合开-闭原则
缺点:
- 抽象工厂接口中已经确定了可以被创建的产品集合,如果需要添加新产品,就必须修改抽象工厂的接口,这样就涉及到抽象工厂类及所有子类的改变
示例:
主要参见设计模式 | 抽象工厂模式及典型应用和初学Java设计模式随记 -- 抽象工厂(Abstract Factory)模式(比较通俗)
------------------------------------------结构型设计模式
1. 代理模式
为什么做代理
为了明确职责、增加扩展性(只需要丰富代理功能而不用修改自己方法的代码),把自己需要但是不适合自己做的东西交给代理来做;
比如常见的两类情况:
(a) 在一个方法调用前和调用后搞些事情,比如权限控制、日志打印等;
(b) 客户端无法直接操作对象,需要通过网络来访问,可以建立远程对象的代理,像调用本地方法一样调用远程对象等
- 静态代理
特点:程序员手动地定义代理类,代理类继承业务接口,实现方法;
优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展;
缺点:
- 要为每一个服务都得创建代理类,工作量太大,不易管理;
- 同时接口一旦发生改变,代理类也要相应改变。
示例:
如下所示,利用代理对sing方法进行了功能扩展(黄色部分)
1 public interface ISinger { 2 void sing(); 3 } 4 5 /** 6 * 目标对象实现了某一接口 7 */ 8 public class Singer implements ISinger{ 9 public void sing(){ 10 System.out.println("唱一首歌"); 11 } 12 } 13 14 /** 15 * 代理对象和目标对象实现相同的接口 16 */ 17 public class SingerProxy implements ISinger{ 18 // 接收目标对象,以便调用sing方法 19 private ISinger target; 20 public UserDaoProxy(ISinger target){ 21 this.target=target; 22 } 23 // 对目标对象的sing方法进行功能扩展 24 public void sing() { 25 System.out.println("向观众问好"); 26 target.sing(); 27 System.out.println("谢谢大家"); 28 } 29 }
1 /** 2 * 测试类 3 */ 4 public class Test { 5 public static void main(String[] args) { 6 //目标对象 7 ISinger target = new Singer(); 8 //代理对象 9 ISinger proxy = new SingerProxy(target); 10 //执行的是代理的方法 11 proxy.sing(); 12 } 13 }
- 动态代理
特点:利用反射机制,在运行的时候动态地构建代理,所谓动态就是不用手动编写代理类了
优点:拥有静态代理的优点,省去了很多代码,并且扩展性更强,通过反射可以执行任意类型的被代理类方法
缺点:
- 增加了代理对象,有些类型的代理模式可能会造成请求的处理速度变慢,例如保护代理;
- 有些代理模式的实现过程比较复杂,例如远程代理
应用场景:
- 远程代理为位于两个不同地址空间对象的访问提供了一种实现机制,可以将一些消耗资源较多的对象和操作移到性能更好的计算机上,提高系统的整体运行效率。
- 虚拟代理通过一个消耗资源较少的对象代替一个消耗资源较多的对象,可以在一定程度上节省系统的运行开销。
- 缓冲代理为某一个操作的结果提供临时的缓存存储空间,以便在后续使用中能够共享这些结果,优化系统性能,缩短执行时间。
- 保护代理可以控制对一个对象的访问权限,为不同用户提供不同级别的使用权限。
实现方式:JDK动态代理,其实底层原理就是自动帮你生成了一个代理类,这个代理类继承了所有接口,重写了每个方法,保证它们都经过invoke方法
2. 适配器模式
为什么有适配器:
为了使原本因为接口不兼容而不能一起工作、不能统一管理的那些类可以一起工作、进行统一管理;
优点:
- 可以让任何两个没有关联的类一起运行;
- 体现了对象组合原则;
- 提高类的复用;
- 增加类的透明度。
缺点:
-
过多使用适配器,会让系统非常零乱。比如,明明看到调用的是A接口,内部却被适配成了B接口的实现,因此如果可以不使用适配器,最好直接对系统进行重构。
- 增加了系统复杂度,过多时会降低运行时的性能。
应用场景:
- 使用一个已经存在的类,但该类不符合现有的接口规范,导致无法直接去访问, 比如猪和飞禽,现在多了一个飞猪,可以增加一个适配器,在里面包容一个猪对象,实现飞的接口;
- 一般在一个系统或者设计已经定型时用,而不是在初始设计时。一般是因为不影响现在业务情况下,为了解决统一客户端调用接口的代码,并且所调用的接口具有不兼容问题时使用适配器模式,可以更加紧凑;
- 想要使用接口中的某个或某些方法,但是接口中有太多方法,如果使用时必须实现接口中所有方法,可以使用抽象类来实现接口,并不对方法进行实现(仅置空),然后我们再继承这个抽象类来通过重写想用的方法的方式来实现。这个抽象类就是适配器(接口适配器);
实现:
客户(Client):只能调用目标接口。
目标接口(Target):客户看到的接口,适配器必须实现该接口才能被客户使用。
适配器(Adapter):适配器把被适配者接口转换为目标接口,提供给客户使用。
被适配者(Adaptee)
有对象适配器、类适配器和接口适配器。类适配器就是继承,如图所示
例如有Adaptee
1 public class Adaptee { 2 public void adapteeRequest() { 3 System.out.println("被适配者的方法"); 4 } 5 }
有Target,
1 public interface Target { 2 void request(); 3 }
最后Adapter
1 public class Adapter extends Adaptee implements Target{ 2 @Override 3 public void request() { 4 //...一些操作... 5 super.adapteeRequest(); 6 //...一些操作... 7 } 8 }
对象适配器是组合依赖(常用吧)
1 public class Adapter implements Target{ 2 // 适配者是对象适配器的一个属性 3 private Adaptee adaptee = new Adaptee(); 4 5 @Override 6 public void request() { 7 //... 8 adaptee.adapteeRequest(); 9 //... 10 } 11 }
接口适配器是抽象类
1 public interface Interfaces { 2 public void method1(); 3 public void method2(); 4 public void method3(); 5 public void method4(); 6 public void method5(); 7 }
public abstract class AbstractClass implements Interfaces { @Override public void method1() { } @Override public void method2() { } @Override public void method3() { } @Override public void method4() { } @Override public void method5() { } }
1 public class ImplementClass2 extends AbstractClass { 2 @Override 3 public void method2() { 4 System.out.println("method2 called"); 5 } 6 7 @Override 8 public void method4() { 9 System.out.println("method4 called"); 10 } 11 }
Spring AOP中也使用了适配器模式,看了半天仍然一知半解,先放上链接以后再回顾吧
https://blog.csdn.net/wwwdc1012/article/details/82780560
https://zuiyanwangyue.iteye.com/blog/348286
https://blog.csdn.net/qq_38526573/article/details/88238550#2_144
https://blog.csdn.net/zuiyanwangyue/article/details/83362087
3. 桥接模式
为什么有桥接模式:
桥接模式像桥一样连接两头,两头的变化相互独立,它将抽象化(Abstraction)与实现化(Implementation)脱耦,使得二者独立的变化(官方说法),也可以说是从多种维度分类,各维度独立变化,再将各个维度组合,减少了维度间的耦合;
优点:
- 桥接模式可以取代多层继承,极大的减少子类的个数,降低管理和维护的成本,符合单一职责原则;
- 桥接模式提高了系统可扩展性,扩展任意一个维度都不用修改原有的系统,符合开闭原则。
缺点:
- 桥接模式的引入增加了系统的理解和设计难度,要求开发者针对抽象进行设计和编程;
- 桥接模式要求正确识别出系统中各个独立变化的维度,因此使用范围有局限性。
应用场景:
- 处理多层继承结构,处理多维度变化(都要扩展)的场景,可以将各个维度设计成独立的继承结构;
- 不希望因为多层次继承导致系统类的个数急剧增加的场景。
示例:
品牌类
1 public interface Brand { 2 void sale(); 3 } 4 5 class Lenovo implements Brand { 6 7 @Override 8 public void sale() { 9 System.out.println("销售联想电脑"); 10 } 11 12 } 13 14 class Dell implements Brand { 15 16 @Override 17 public void sale() { 18 System.out.println("销售Dell电脑"); 19 } 20 21 } 22 23 class Shenzhou implements Brand { 24 25 @Override 26 public void sale() { 27 System.out.println("销售神舟电脑"); 28 } 29 30 }
电脑类
1 public class Computer2 { 2 3 protected Brand brand; 4 5 public Computer2(Brand b) { 6 this.brand = b; 7 } 8 9 public void sale(){ 10 brand.sale(); 11 } 12 13 } 14 15 class Desktop2 extends Computer2 { 16 17 public Desktop2(Brand b) { 18 super(b); 19 } 20 21 @Override 22 public void sale() { 23 super.sale(); 24 System.out.println("销售台式机"); 25 } 26 } 27 28 class Laptop2 extends Computer2 { 29 30 public Laptop2(Brand b) { 31 super(b); 32 } 33 34 @Override 35 public void sale() { 36 super.sale(); 37 System.out.println("销售笔记本"); 38 } 39 }
客户端
1 public class Client { 2 public static void main(String[] args) { 3 //销售联想的笔记本电脑 4 Computer2 c = new Laptop2(new Lenovo()); 5 c.sale(); 6 //销售神舟的台式机 7 Computer2 c2 = new Desktop2(new Shenzhou()); 8 c2.sale(); 9 } 10 }
参考:Java桥接模式(bridge)和java设计模式-桥梁模式(桥接模式 Bridge)
4. 装饰者模式
为什么有装饰模式:
在不动原有业务的前提下,动态扩展对象的功能,是继承关系的替代方案;
优点:
- 装饰模式可以提供比继承更多的灵活性,而继承关系是静态的;
- 装饰类和被装饰类相互独立。
缺点:
- 装饰模式会导致设计中出现许多小类,过度使用会使程序变得复杂。
应用场景:
- 需要扩展一个类的功能;
- 需要动态透明的给一个对象增加功能,这些功能可以再动态地撤销。
- 需要增加一些基本功能的排列组合而产生的非常大量的功能,从而使继承变得不现实。
示例:
中文图比较直观些,装饰者模式中有四个角色:
- Component(被装饰对象的基类):定义一个对象接口,可以给这些对象动态地添加职责。
- ConcreteComponent(具体被装饰对象):定义一个对象,可以给这个对象添加一些职责。
- Decorator(装饰者抽象类):定义一个与Component接口一致的接口。
- ConcreteDecorator(具体装饰者):具体的装饰对象,给内部持有的具体被装饰对象,增加具体的职责。
例如,抽象组件
1 /** 2 * 装饰组件 3 * 提供操作方法 4 */ 5 public interface Component { 6 void operation(); 7 }
具体组件
1 /** 2 * 具体组件 3 * 喝水 4 */ 5 public class ConcreateComponent implements Component { 6 @Override 7 public void operation() { 8 System.out.println("喝水"); 9 } 10 }
装饰者
1 /** 2 * 装饰者 3 */ 4 public class Decorator implements Component { 5 /** 6 * 持有要装饰的组件 7 */ 8 private Component component; 9 10 public Decorator(Component component) { 11 this.component = component; 12 } 13 14 /** 15 * 拥有与组件一样的方法,但是在前后和扩展功能 16 */ 17 @Override 18 public void operation() { 19 System.out.println("吃鸡腿"); 20 component.operation(); 21 System.out.println("吃鸡翅"); 22 } 23 }
测试类
1 public static void main(String[] strings) { 2 3 System.out.println("一开始,直接操作功能组件"); 4 Component component = new ConcreateComponent(); 5 component.operation(); 6 7 8 System.out.println("扩展功能,通过装饰者调用"); 9 Component decorator = new Decorator(component); 10 decorator.operation(); 11 }
输出:
1 一开始,直接操作功能组件 2 喝水 3 扩展功能,通过装饰者调用 4 吃鸡腿 5 喝水 6 吃鸡翅
还有其他应用,比如在Java中IO(https://www.cnblogs.com/lcmichelle/p/10743330.html)和Spring开发中一些应用(https://www.liangzl.com/get-article-detail-3227.html)
5. 门面模式
为什么有门面模式:
为子系统的一组接口提供一个统一的访问接口,使子系统更容易被访问或使用;
优点:
- 降低子系统与客户端的耦合度,子系统的变化不会影响客户类;
-
提高客户端使用的便捷性,对客户屏蔽了子系统组件。
缺点:
- 增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”;
应用场景:
- 为复杂的模块或子系统提供外界访问的模块;
- 保证子系统相互独立;
- 在层析结构中,可以在层与层之间建立外观模式。
示例:
像我们的Service层与DAO层,就是用到了门面模式,Controller层本来是需要跟一个个DAO打交道,但是有了Service层,它直接与DAO打交道,Controller就可以直接使用Service,我们只需专注在Service上写业务逻辑与操作DAO的各种方法,分离了责任。门面模式主要包含以下角色:
门面(Facade)角色:为多个子系统对外提供一个共同的接口;
子系统(Sub System)角色:实现系统的部分功能,客户可以通过门面角色访问它;
客户(Client)角色:通过一个门面角色使用子系统的功能。
比如:
1 package facade; 2 public class FacadePattern 3 { 4 public static void main(String[] args) 5 { 6 Facade f=new Facade(); 7 f.method(); 8 } 9 } 10 //门面角色 11 class Facade 12 { 13 private SubSystem01 obj1=new SubSystem01(); 14 private SubSystem02 obj2=new SubSystem02(); 15 private SubSystem03 obj3=new SubSystem03(); 16 public void method() 17 { 18 obj1.method1(); 19 obj2.method2(); 20 obj3.method3(); 21 } 22 } 23 //子系统角色 24 class SubSystem01 25 { 26 public void method1() 27 { 28 System.out.println("子系统01的method1()被调用!"); 29 } 30 } 31 //子系统角色 32 class SubSystem02 33 { 34 public void method2() 35 { 36 System.out.println("子系统02的method2()被调用!"); 37 } 38 } 39 //子系统角色 40 class SubSystem03 41 { 42 public void method3() 43 { 44 System.out.println("子系统03的method3()被调用!"); 45 } 46 }
运行结果为:
1 子系统01的method1()被调用! 2 子系统02的method2()被调用! 3 子系统03的method3()被调用!
6. 享元模式
为什么有享元模式:
支持大量细粒度对象的复用;
优点:
-
减少了对象的创建,降低了程序内存的占用
缺点:
- 需要区分外部状态和内部状态,使应用程序更加复杂了;
应用场景:
- 系统中有大量相同或者相似的对象时;
- 对象消耗大量内存时;
- 对象的状态大部分可以外部化时
示例:
享元模式有以下几个角色:
抽象享元类(Flyweight):具体享元类的超类或者接口;
具体享元类(ConcreteFlyweight):指定内部状态,为内部状态增加存储空间;
非共享具体享元类(UnsharedConcreteFlyweight):指出那些不需要共享的Flyweight子类;
享元工厂类(FlyweightFactory):用来创建并管理Flyweight对象。
享元模式的应用有各种池技术,String常量池、数据库连接池、缓冲池等等,举个例子:
具体享元类的超类
1 public abstract class Flyweight { 2 3 //内部状态 4 public String intrinsic; 5 //外部状态 6 protected final String extrinsic; 7 8 //要求享元角色必须接受外部状态 9 public Flyweight(String extrinsic) { 10 this.extrinsic = extrinsic; 11 } 12 13 //定义业务操作 14 public abstract void operate(int extrinsic); 15 16 public String getIntrinsic() { 17 return intrinsic; 18 } 19 20 public void setIntrinsic(String intrinsic) { 21 this.intrinsic = intrinsic; 22 } 23 24 }
1 public class ConcreteFlyweight extends Flyweight { 2 //接受外部状态 3 public ConcreteFlyweight(String extrinsic) { 4 super(extrinsic); 5 } 6 7 //根据外部状态进行逻辑处理 8 @Override 9 public void operate(int extrinsic) { 10 System.out.println("具体Flyweight:" + extrinsic); 11 } 12 13 }
享元工厂
1 public class FlyweightFactory { 2 3 //定义一个池容器 4 private static HashMap<String, Flyweight> pool = new HashMap<>(); 5 6 //享元工厂 7 public static Flyweight getFlyweight(String extrinsic) { 8 Flyweight flyweight = null; 9 10 if(pool.containsKey(extrinsic)) { //池中有该对象 11 flyweight = pool.get(extrinsic); 12 System.out.print("已有 " + extrinsic + " 直接从池中取---->"); 13 } else { 14 //根据外部状态创建享元对象 15 flyweight = new ConcreteFlyweight(extrinsic); 16 //放入池中 17 pool.put(extrinsic, flyweight); 18 System.out.print("创建 " + extrinsic + " 并从池中取出---->"); 19 } 20 21 return flyweight; 22 } 23 }
客户端
1 public class Client { 2 public static void main(String[] args) { 3 int extrinsic = 22; 4 5 Flyweight flyweightX = FlyweightFactory.getFlyweight("X"); 6 flyweightX.operate(++ extrinsic); 7 8 Flyweight flyweightY = FlyweightFactory.getFlyweight("Y"); 9 flyweightY.operate(++ extrinsic); 10 11 Flyweight flyweightZ = FlyweightFactory.getFlyweight("Z"); 12 flyweightZ.operate(++ extrinsic); 13 14 Flyweight flyweightReX = FlyweightFactory.getFlyweight("X"); 15 flyweightReX.operate(++ extrinsic); 16 } 17 }
看结果,注意最后一次创建X,因为在池中已经存在了,所以直接从池中取出:
1 创建X 并从池中取出---->具体Flyweight:23 2 创建Y 并从池中取出---->具体Flyweight:24 3 创建Z 并从池中取出---->具体Flyweight:25 4 创建X 并从池中取出---->具体Flyweight:26
7. 组合模式
为什么有组合模式:
将对象组合成树形结构以表示“部分-整体”的层次结构;
优点:
-
更容易在组合体内加入新的对象;
-
简化客户端操作,不必关心自己处理的是单个对象还是还是组合对象
缺点:
- 在使用组合模式时,其叶子和树枝的声明都是实现类,而不是接口,违反了依赖倒置原则(这个还不明白,以后再看哈);
- 使得设计更加复杂。客户端需要花更多时间理清类之间的层次关系。(几乎是所有设计模式所面临的问题);
- 要求较高的抽象性,如果节点和叶子有很多差异性的话,比如很多方法和属性都不一样,难以实现组合模式
应用场景:
- 当想表达对象的部分-整体的层次结构时;
- 希望用户忽略组合对象与单个对象的不同,用户统一地使用组合结构中的所有对象时。
示例:
组合模式包含以下主要角色。
- 抽象构件(Component)角色:它的主要作用是为树叶和树枝声明公共接口,并实现它们的默认行为;
- 树叶构件(Leaf)角色:是组合中的叶节点对象,它没有子节点;
- 树枝构件(Composite)角色:是组合中的分支节点对象,它有子节点。它的主要作用是存储和管理子部件,通常包含 Add()、Remove()、GetChild() 等方法。
实现方式有透明式和安全式两种,首先安全式:
在Component中不去声明Add和Remove方法,那么子类的Leaf就不需要实现它,而是在Composit声明所有用来管理子类对象的方法。
1 // 抽象构件类、节点类 2 abstract class Component { 3 public String name; 4 5 public Component(String name) { 6 this.name = name; 7 } 8 9 // 公有操作 10 public void getName() { 11 System.out.println(this.name); 12 } 13 }
1 // 树枝构件类 2 class Composite extends Component { 3 private LinkedList<Component> children; 4 5 public Composite(String name) { 6 super(name); 7 this.children = new LinkedList<Component>(); 8 } 9 10 // 添加一个节点,可能是树枝、叶子 11 public void add(Component child) { 12 this.children.add(child); 13 } 14 15 // 删除一个节点,可能是树枝、叶子 16 public void remove(String child) { 17 this.children.remove(child); 18 } 19 20 // 获取子树 21 public LinkedList<Component> getChildren() { 22 return this.children; 23 } 24 }
1 // 叶子构件类 2 class Leaf extends Component { 3 public Leaf(String name) { 4 super(name); 5 } 6 }
1 // 测试类,负责构建整棵树 2 public class Client { 3 public static void main(String[] args) { 4 5 Composite root = new Composite("树根"); 6 7 Composite branch01 = new Composite("树枝01"); 8 Composite branch02 = new Composite("树枝02"); 9 10 root.add(branch01); 11 root.add(branch02); 12 13 Component leaf01 = new Leaf("树叶01"); 14 Component leaf02 = new Leaf("树叶02"); 15 Component leaf03 = new Leaf("树叶03"); 16 Component leaf04 = new Leaf("树叶04"); 17 Component leaf05 = new Leaf("树叶05"); 18 19 branch01.add(leaf01); 20 branch01.add(leaf02); 21 22 branch02.add(leaf03); 23 branch02.add(leaf04); 24 branch02.add(leaf05); 25 26 displayTree(root); 27 } 28 29 // 递归遍历整棵树 30 public static void displayTree(Composite root) { 31 LinkedList<Component> children = root.getChildren(); 32 33 for (Component c : children) { 34 if (c instanceof Leaf) { 35 System.out.print("\t"); 36 c.getName(); 37 } else { 38 c.getName(); 39 // 递归 40 displayTree((Composite)c); 41 } 42 } 43 } 44 }
叶节点无需在实现Add与Remove这样的方法,但是对于客户端来说,必须对叶节点和枝节点进行判定,为客户端的使用带来不便。
透明式:
在Component中声明所有方法,其中包括Add,Remove等。这样叶节点和枝节点对于外界没有区别,它们具备完全一致的接口。
1 // 叶子构件类 2 class Leaf extends Component { 3 4 public Leaf(String name) { 5 super(name); 6 } 7 8 @Deprecated // 抛出不支持的操作异常 9 public void add(Component child) throws UnsupportedOperationException{ 10 throws new UnsupportedOperationException(); 11 } 12 13 @Deprecated 14 public void remove(String child) throws UnsupportedOperationException{ 15 throws new UnsupportedOperationException(); 16 } 17 18 @Deprecated 19 public LinkedList<Component> getChildren() throws UnsupportedOperationException{ 20 throws new UnsupportedOperationException(); 21 } 22 }
Leaf 类在具体实现时就必须将继承而来的add、remove等不可用、不合逻辑的方法给注解Deprecated掉,并抛出适当的异常,不提供给用户使用,这样在遍历整棵树的时候可以不进行强制类型转换。