模式是一条由三部分组成的通用规则:它代表了一个特定环境、一类问题和一个解决方案之间的关系。每一个模式描述了一个不断重复发生的问题,以及该问题解决方案的核心设计。
软件领域的设计模式定义:设计模式是对处于特定环境下,经常出现的某类软件开发问题的,一种相对成熟的设计方案。
由来:有资深的软件设计师,它们积累了足够的经验,这些经验让他们快速、优雅地解决软件开发中的大量重复问题。而设计模式的最终目标就是帮助人们利用软件设计师的集体经验,从而设计出更加优秀的软件。
设计模式通产分为三类:
- 创建型:创建对象时,不再直接实例化对象;而是根据特定场景,由程序来确定创建对象的方式,从而保证更高的性能、更好的架构优势。主要有:工厂模式(简单工厂模式&抽象工厂模式)、单例模式、生成器模式和原型模式。
- 结构型:用于帮助将多个对象组织成更大的结构。主要有:适配器模式、桥接模式、组合器模式、装饰器模式、门面模式、享元模式和代理模式。
- 行为型:用于帮助系统间个对象的通信,以及如何控制复杂系统中的流程。主要有:命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、模板模式和访问者模式。
设计模式五大原则:
单一职责原则:一个类应该只有一个引起它变化的原因。也就是说,一个类只负责一个功能或任务,从而实现高内聚、低耦合的设计。
开闭原则:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。也就是说,当需要新增功能时,应该通过扩展现有代码来实现,而不是修改现有代码。
里氏替换原则:任何时候,子类都可以替换父类并且表现出类似的行为。也就是说,在任何需要使用父类对象的地方,都可以使用子类对象来替换,而不会导致程序出错。
接口隔离原则:一个类不应该强迫客户端(调用它的代码)依赖于它不需要的方法。也就是说,一个接口应该只包含必要的方法,而不应该包含不必要的方法。
依赖倒置原则:高层模块不应该依赖底层模块,二者都应该依赖抽象接口。也就是说,依赖关系应该是通过抽象接口来实现,而不是通过具体实现来实现。这样可以降低模块之间的耦合性,提高代码的灵活性和可维护性。
1,创建型设计模式
1.1,单例模式
有些时候,允许自由创建某个类的实例没有意义,还可能造成系统性能下降(因为创建对象会带来系统开销问题)。在JAVA EE应用中可能只需要一个数据库引擎访问点,Hibernate访问时只需要一个SessionFactory实例,如果在系统中为它们创建多个实例就没有太大意义。如果一个类始终只能创建一个实例,则这个类被称为单例类,这种模式被称为单例模式。
单例和多例:
- 你用杯子喝可乐,喝完了不刷,继续去倒果汁喝,就是单例。
- 你用杯子喝可乐,直接扔了杯子,换个杯子去倒果汁喝,就是多例。
- 用单例和多例的标准只有一个:当对象含有可改变的状态时(更精确的说就是在实际应用中该状态会改变),则多例,否则单例。
实现方式:
- 为了保证该类只能产生一个实例,程序不能允许自由创建该类的对象,而是只允许为该类创建一个对象。为了避免程序自由创建该类的实例,使用private修饰该类的构造器,从而将该类的构造器隐藏起来。
- 将该类的构造器隐藏起来,则需要体用一个public方法作为该类访问点,用于创建该类的对象,且该方法必须使用static修饰(因为调用该方法之前不存在对象,因此调用该方法的不可能是对象,只能是类)。
- 除此之外,该类还必须缓存已经创建的对象,否则该类无法知道是否曾经创建过实例,也就无法保证只创建一个实例。为此该类需要使用一个静态属性来保证曾经创建的实例,且该属性需要被静态方法访问,所以该属性也应使用static修饰。
使用单例模式的优势:
- 减少创建Java实例所带来的系统开销。
- 便于系统跟踪单个Java实例的生命周期、实例状态。
懒汉式(双重sync或者在instance变量加上volatile修饰):
class Singleton { //使用一个类变量缓存曾经创建的实例 private static Singleton instance; //将构造器使用private修饰,隐藏该构造器 private Singleton() {} //提供一个静态方法,用于返回Singleton实例,该方法可以加入自定义控制,保证只产生一个Singleton对象 synchronized public static Singleton getInstance() { //如果instance为null,表明还不曾创建Singleton对象 //如果instance不为null,则表明已经创建了Singleton对象,将不会执行该方法 if (instance == null) { synchronized (Singleton.class) { // 双重检查,确保只有一个线程创建实例 if (instance == null) { instance = new Singleton(); } } } return instance; } }
饿汉式(通过静态变量保证了线程安全):
class Singleton { private static Singleton instance = new Singleton(); //私有化构造方法,防止外部访问 private Singleton() { } //通过共有的静态方法获取对象实例 public static Singleton getInstance() { return instance; } }
Java Web项目中的单例模式:
- 数据库连接池就是单例模式,有且仅有一个连接池管理者,管理多个连接池对象。
- 单例在spring中是默认的,如果要产生多例,则在配置文件的bean中添加scope="prototype"。
- 对于struts2来说,action必须用多例,因为action本身含有请求参数的值,即可改变的状态;Struts2会对每一个请求,产生一个Action的实例来处理。如果是单例的话,若出现两个用户都修改一个对象的属性值,则会因为用户修改时间不同,两个用户访问得到的属性不一样,操作得出的结果不一样。(有一块布长度300cm,能做一件上衣(用掉100cm)和一件裤子(用掉200cm);甲和乙同时访问得到的长度都是300cm,甲想做上衣和裤子,他先截取100cm去做上衣,等上衣做完再去做裤子,而乙这时正好也拿100cm去做上衣,那好,等甲做完上衣再做裤子的时候发现剩下的布(100cm)已经不够做裤子了…..这就是影响系统的性能,解决的办法就是给甲和乙一人一块300cm的布,就不会出现布被别人偷用的事情)
- servlet采用单实例多线程模式开发,减少产生servlet实例的开销;当容器收到一个Servlet请求,调度线程从线程池中选出一个工作者线程,将请求传递给该工作者线程,然后由该线程来执行Servlet的service方法。当这个线程正在执行的时候,容器收到另外一个请求,调度线程同样从线程池中选出另一个工作者线程来服务新的请求,容器并不关心这个请求是否访问的是同一个Servlet。当容器同时收到对同一个Servlet的多个请求的时候,那么这个Servlet的service()方法将在多线程中并发执行。
Spring推荐将所有业务逻辑组件、DAO组件、数据源组件等配置成单例的行为方式,因为这些组件无须保存任何用户状态,故所有客户端都可共享这些逻辑组件、DAO组件,因此推荐讲这些组件配置成单例模式的行为方式。
1.2,工厂模式
模式动机:
- 在软件系统中,经常面临着“某个对象”的创建工作;由于需求的变化,这个对象经常面临着剧烈的变化,但是它却拥有比较稳定的接口。
- 如何应对这种变化?如何提供一种“封装机制”来隔离出“这个易变对象”的变化,从而保持系统中“其他依赖该对象的对象”不随着需求改变而改变。
- 当你需要什么,只需要传入一个正确的参数,就可以获取你所需要的对象,而无须知道其创建细节。
工厂模式的用意是定义一个创建产品对象的工厂接口,将实际创建性工作推迟到子类中。工厂模式可分 为简单工厂、工厂方法和抽象工厂模式。
注意:我们常说的23种经典设计模式,包含了工厂方法模式和抽象工厂模式,而并未包含简单工厂模式。另外,我们平时说的工厂模式,一般默认是指工厂方法模式。
1.3,简单工厂
简单工厂模式其实并不算是一种设计模式,更多的时候是一种编程习惯。简单工厂的实现思路是,定义 一个工厂类,根据传入的参数不同返回不同的实例,被创建的实例具有共同的父类或接口。增加新产品时,需要修改工厂类、增加产品类。
使用简单工厂模式的优势:让对象的调用者和对象创建过程分离,当对象调用者需要对象时,直接向工厂请求即可。从而避免了对象的调用者与对象的实现类以硬编码方法耦合,以提高系统的可维护性、可扩展性。
简单工厂模式的缺点:当产品修改时,工厂类也要做相应的修改。
简单工厂的适用场景:
- 工厂类负责创建的对象比较少;
- 客户只知道传入工厂类的参数,对于如何创建对象(逻辑)不关心;
- 由于简单工厂很容易违反高内聚责任分配原则,因此一般只在很简单的情况下应用。
简单工厂的使用场景:
- 日志记录器:记录可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。
- 数据库访问:当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。
- 设计一个连接服务器的框架:需要三个协议,"POP3"、"IMAP"、"HTTP",可以把这三个作为产品类,共同实现一个接口。
简单工厂包含的角色:
- Factory:工厂:工厂类在客户端的直接调用下创建产品对象,它往往是由一个具体的java类实现。
- Product:抽象产品:担任模式所创建的对象的父类或它们的共同的接口,抽象产品角色可以用一个java接口或者java抽象类实现。
- ConcreteProduct:具体产品:所创建的任何对象都是这个角色实例,具体产品角色是由一个具体的java类实现。
【问题】创建一个可以绘制不同形状的绘图工具,可以绘制圆形,正方形,三角形,每个图形都会有一个draw() 方法用于绘图,不看代码先考虑一下如何通过该模式设计完成此功能。
【分析】由题可知圆形,正方形,三角形都属于一种图形,并且都具有draw方法,所以首先可以定义一个接口 或者抽象类,作为这三个图像的公共父类,并在其中声明一个公共的draw方法:
抽象产品:
public interface Shape { void draw(); }
具体产品:
//圆形 class CircleShape implements Shape{ public CircleShape(){ System.out.println("CircleShape: create"); } public void draw() { System.out.println("draw: CircleShape"); } } //正方形 class RectShape implements Shape{ public RectShape(){ System.out.println("RectShape: create"); } public void draw() { System.out.println("draw: RectShape"); } } //三角形 class TriangleShape implements Shape{ public TriangleShape(){ System.out.println("TriangleShape: create"); } public void draw() { System.out.println("draw: TriangleShape"); } }
工厂:
class ShapeFactory { public static Shape getShape(String type) { Shape shape = null; if (type.equalsIgnoreCase("circle")) { shape = new CircleShape(); } else if (type.equalsIgnoreCase("rect")) { shape = new RectShape(); } else if (type.equalsIgnoreCase("triangle")) { shape = new TriangleShape(); } return shape; } }
为工厂类传入不同的type可以new不同的形状,返回结果为Shape 类型,这个就是简单工厂核心的地方了。
1.4,工厂方法
工厂方法模式:工厂方法模式是简单工厂的仅一步深化, 在工厂方法模式中,我们不再提供一个统一的工厂类来创建所 有的对象,而是针对不同的对象提供不同的工厂。也就是说每个对象都有一个与之对应的工厂。
工厂方法的实现思路:定义一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式 让一个类的实例化延迟到其子类。
工厂方法适用场景:
- 客户端不需要知道它所创建的对象的类。
- 客户端可以通过子类来指定创建对应的对象。
模式定义:
- 工厂方法模式去掉了简单工厂模式中工厂方法的静态属性,使得它可以被子类继承。这样在简单工厂模式里集中在工厂方法上的压力可以由工厂方法模式里不同的工厂子类来分担。
- 工厂方法模式又称为工厂模式,在该模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象,这样可以将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品。
简单工厂包含的角色:
- Product:抽象产品:它是具体产品继承的父类或者是实现的接口。在 java 中一般有抽象类或者接口来实现。
- ConcreteProduct:具体产品:具体工厂角色所创建的对象就是此角色的实例。在 java 中由具体的类来实现。
- Factory:抽象工厂:这是工厂方法模式的核心,它与应用程序无关。是具体工厂角色必须实现的接口或者必须继承的父类。在 java 中它由抽象类或者接口来实现。
- ConcreteFactory:具体工厂:它含有和具体业务逻辑有关的代码。由应用程序调用以创建对应的具体产品的对象。
【问题】现在需要设计一个这样的图片加载类,它具有多个图片加载器,用来加载jpg,png,gif格式的图片, 每个加载器都有一个read()方法,用于读取图片。下面我们完成这个图片加载类。
简单工厂-抽象产品:
public interface Reader { void read(); }
简单工厂-具体产品:
class JpgReader implements Reader{ public void read() { System.out.println("read jpg"); } } class PngReader implements Reader{ public void read(){ System.out.println("read png"); } } class GifReader implements Reader{ public void read(){ System.out.println("read gif"); } }
抽象工厂-抽象产品:
interface ReaderFactory{ Reader getReader(); }
简单工厂-具体产品:
// jpg加载器工厂 class JpgReaderFactory implements ReaderFactory { @Override public Reader getReader() { return new JpgReader(); } } // png加载器工厂 class PngReaderFactory implements ReaderFactory { @Override public Reader getReader() { return new PngReader(); } } // gif加载器工厂 class GifReaderFactory implements ReaderFactory { @Override public Reader getReader() { return new GifReader(); } }
在每个工厂类中我们都通过重写的getReader()方法返回各自的图片加载器对象。和简单工厂对比一下,最根本的区别在于,简单工厂只有一个统一的工厂类,而工厂方法是针对每个要 创建的对象都会提供一个工厂类,这些工厂类都实现了一个工厂基类。
1.5,抽象工厂
这个模式最不好理解,而且在实际应用中局限性也蛮大的,因为这个模式并不符合开闭原则。实际开发 还需要做好权衡。抽象工厂模式是工厂方法的仅一步深化,在这个模式中的工厂类不单单可以创建一个对象,而是可以创建一组对象。这是和工厂方法最大的不同点。
抽象工厂的实现思路是,提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。 抽象工厂和工厂方法一样可以划分为4大部分:
- AbstractFactory(抽象工厂):声明了一组用于创建对象的方法,注意是一组。
- ConcreteFactory(具体工厂):它实现了在抽象工厂中声明的创建对象的方法,生成一组具体对象。
- AbstractProduct(抽象产品):它为每种对象声明接口,在其中声明了对象所具有的业务方法。
- ConcreteProduct(具体产品):它定义具体工厂生产的具体对象。
抽象工厂适用场景:
- 和工厂方法一样客户端不需要知道它所创建的对象的类。
- 需要一组对象共同完成某种功能时。并且可能存在多组对象完成不同功能的情况。
- 系统结构稳定,不会频繁的增加对象。
至于Spring IoC容器,到底是简单工厂,还是抽象工厂?我倾向于认为Spring IoC是抽象工厂,因为Spring IoC容器可以包括万象,它不仅可以管理普通Bean实例,也可管理工厂实例。不用过于纠结于简单工厂模式、抽象工厂模式这些概念,可以把他们统称为工厂模式。如果工厂直接生产被调用对象,那就是简单工厂模式;如果工厂生产了工厂对象,那就回升级成抽象工厂模式。
【问题】现在需要做一款跨平台的游戏,需要兼容Android,Ios,Wp三个移动操作系统,该游戏针对每个系统 都设计了一套操作控制器(OperationController)和界面控制器(UIController),下面通过抽闲工厂 方式完成这款游戏的架构设计。
【分析】由题可知,游戏里边的各个平台的UIController和OperationController应该是我们最终生产的具体产品。所以新建两个抽象产品接口。
抽象操作控制器&抽象界面控制器:
interface OperationController{ void control(); } interface UIController{ void display(); }
然后完成各个系统平台的具体操作控制器和界面控制器:Android&IOS&WP
class AndroidOperationController implements OperationController { public void control() { System.out.println("AndroidOperationController"); } } class AndroidUIController implements UIController { public void display() { System.out.println("AndroidInteraceController"); } }
class IosOperationController implements OperationController { public void control() { System.out.println("IosOperationController"); } } class IosUIController implements UIController { public void display() { System.out.println("IosInterfaceController"); } }
class WpOperationController implements OperationController { public void control() { System.out.println("WpOperationController"); } } class WpUIController implements UIController { public void display() { System.out.println("WpInterfaceController"); } }
下面定义一个抽闲工厂,该工厂需要可以创建OperationController和UIController。
public interface SystemFactory { public OperationController createOperationController(); public UIController createInterfaceController(); }
在各平台具体的工厂类中完成操作控制器和界面控制器的创建过程。
class AndroidFactory implements SystemFactory { public OperationController createOperationController() { return new AndroidOperationController(); } public UIController createInterfaceController() { return new AndroidUIController(); } } class IosFactory implements SystemFactory{ public OperationController createOperationController() { return new IosOperationController(); } public UIController createInterfaceController() { return new IosUIController(); } } class wpFactory implements SystemFactory{ public OperationController createOperationController() { return new WpOperationController(); } public UIController createInterfaceController() { return new WpUIController(); } }
简单工厂模式和抽象工厂模式有什么区别?
(1)简单工厂模式其实并不算是一种设计模式,更多的时候是一种编程习惯。简单工厂的实现思路是,定义 一个工厂类,根据传入的参数不同返回不同的实例,被创建的实例具有共同的父类或接口。
(2)工厂方法模式是简单工厂的仅一步深化, 在工厂方法模式中,我们不再提供一个统一的工厂类来创建所 有的对象,而是针对不同的对象提供不同的工厂。也就是说每个对象都有一个与之对应的工厂。工厂方 法的实现思路是,定义一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式让一个 类的实例化延迟到其子类。
(3)抽象工厂模式是工厂方法的仅一步深化,在这个模式中的工厂类不单单可以创建一个对象,而是可以创 建一组对象。这是和工厂方法最大的不同点。抽象工厂的实现思路是,提供一个创建一系列相关或相互 依赖对象的接口,而无须指定它们具体的类。
2,结构型设计模式
2.1,代理模式
代理模式是一种应用非常广泛的设计模式,当客户端代码需要调用某个对象时,客户端实际上也不关心是否准确得到对象,它只要一个能提供该功能的对象即可,此时就可返回该对象的代理(Proxy)。
这种设计方式下,系统为某个对象提供一个代理对象,并由代理对象控制对源对象的应用。代理就是一个Java对象代表另一个Java对象来采取行动。在某些情况下,客户端代码不想或不能够直接调用被调用者,代理对象可以在客户和目标对象之间起到中介作用。
对于客户端而言,它不能分辨出代理对象与真实对象的区别,它也无须分辨代理对象和真实对象的区别,客户端代码并不知道真正的被代理对象,客户端代码面向接口编程,它仅仅持有一个被代理对象的接口。
总而言之,只要客户端代码不能或不想直接访问被调用对象——这种情况有很多原因,比如需要创建一个系统开销很大的对象,或者被调用对象在远程主机上,或者目标对象的功能还不足以满足需求......而是额外创建一个代理对象返回给客户端使用,那么这种设计模式就是代理模式。
代理模式的作用:阻止对目标对象的直接访问,或者就是在执行目标对象时,在执行目标对象前后进行一系列的操作。举个例子来说,1:在执行一个方法之前必须要对该“方法”做“日记”,记录每个操作,这样可以在执行目标对象之前,先执行一个的操作,然后执行该方法。2:又如明星唱歌例子,商家首先找到该明星的代理公司,谈妥后再找该明星去唱歌,中间代理起拦截等的操作。
代理模式包含如下角色:
- Subject:抽象主题角色,声明了真实主题和代理主题的共同接口。
- Proxy:代理主题角色,内部包含对真实主题的引用,并且提供和真实主题角色相同的接口。
- RealSubject:真实主题角色,定义真实的对象。
静态代理:手动添加被代理对象的方法。
- 写一个普通类,继承目标对象的接口。
- 写一个实例变量,记住代理谁,即目标对象。
- 使用构造方法为实例变量赋值,或者可以直接赋值。
- 写一个普通方法,该方法的返回值是接口,该接口是目标对象的实现接口。
//歌迷 public class Fans { public static void main(String[] args) { //创建代理对象 ZhangYuProxy zhangyuProxy = new ZhangYuProxy(); //通过代理对象找目标对象 //业务方法 zhangyuProxy.sing(30000); } } ------------------------------------------------- public interface Star { public void sing(int m); } ------------------------------------------------- public class ZhangYu implements Star { public void sing(int m) { System.out.println("宇哥唱歌"); } } ------------------------------------------------- public class ZhangYuProxy implements Star { private ZhangYu zhangyu = new ZhangYu(); public void sing(int m) { if(m>=3000) zhangyu.sing(m); else System.out.println("出场费不足"); } //基于接口编程 public Star getProxy() { return zhangyu; } }
这种静态代理的方法不推荐使用,分析一下,如果说,star里面有多少个方法,在代理对象的类中就有多少个方法。不利于扩展。举个例子,如果说在A类是目标对象,B类是代理对象,A类每个方法执行前都需要做”日志“操作,那么代理对象需要扩展A类所有方法,在执行前加上日志的代码。当A类增加方法时,B类仍然要手动添加一个方法做”日志“。所以这里不推荐使。
动态代理:自动生成被代理对象的方法,无须手动生成。
- 通过实现 InvocationHandler 接口创建自己的调用处理器。
- 通过为 Proxy 类指定 ClassLoader 对象和一组 interface 来创建动态代理类。
- 通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型。
- 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。
为了简化对象创建过程,Proxy类中的newProxyInstance方法封装了2~4,只需两步即可完成代理对象的创建。
代理对象的生成,是利用JDKAPI, 动态的在内存中构建代理对象。Proxy类与普通类的唯一区别就是其字节码是由JVM在运行时动态生成的而非预存在于任何一个.class文件中。每次生成动态代理类对象时都需要指定一个类装载器对象:newProxyInstance()方法第一个参数。
(1)Proxy类:Proxy类提供了用于创建动态代理类和实例对象的方法,它是所创建的动态代理类的父类,它最常用的方法如下:
- public static Class getProxyClass(ClassLoader loader,Class[] interfaces):该方法用于返回一个Class类型的代理类,在参数中需要提供类加载器并需要指定代理的接口数组(与真实主题类的接口列表一致)。
- public Static Object newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h):该方法用于返回一个动态创建的代理类的实例,方法中第一个参数loader表示代理类的类加载器,第二个参数interfaces表示代理类所实现的接口列表(与真实主题类的接口列表一致),第三个参数h表示所指派的调用处理程序类。
(2)InvocationHandler接口:InvocationHandler接口是代理处理程序类的实现接口,该接口作为代理实例的调用处理者的公共父类,每一个代理类的实例都可以提供一个相关的具体调用处理者(InvocationHandler接口的子类)。在该接口中声明了如下方法:
- public Object invoke(Object proxy, Method method, Object[] args):该方法用于处理对代理的类实例的方法调用并返回相应的结果,当一个代理实例中的业务方法被调用时将自动调用该方法。invoke()方法包含三个参数,其中第一个参数proxy表示代理的类的实例,第二个参数method表示需要代理的方法,第三个参数args表示代理方法的参数数组。
动态代理类需要在运行时指定所代理真实主题类的接口,客户端在调用动态代理对象的方法时,调用请求会将请求自动转发给InvocationHandler对象的invoke()方法,由invoke()方法来实现对请求的统一处理。
//哥/歌迷 public class Fans { public static void main(String[] args) { //创建代理对象 ZhangYuProxy zhangyuProxy = new ZhangYuProxy(); //通过代理对象找目标对象 Star star = zhangyuProxy.getProxy(); //业务方法 star.sing("30"); //业务方法 star.dance("5","恰恰舞"); //业务方法 star.eat("8"); } } ----------------------------------------------------- //接口 public interface Star { public void sing(String money); public void dance(String money,String what); public void eat(String money); } ----------------------------------------------------- //目标对象 public class ZhangYu implements Star { public void sing(String money) { System.out.println("宇哥唱歌"); } public void dance(String money, String what) { System.out.println("宇哥跳舞,名称为" + what); } public void eat(String money) { System.out.println("宇哥吃饭"); } } ----------------------------------------------------- import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; //代理类 public class ZhangYuProxy{ //代理谁,通过构造方法赋值 private ZhangYu zhangyu = new ZhangYu(); //动态产生代理对象 public Star getProxy(){ return (Star) Proxy.newProxyInstance( ZhangYuProxy.class.getClassLoader(), zhangyu.getClass().getInterfaces(), new InvocationHandler(){ public Object invoke( Object proxy, Method method, Object[] args) throws Throwable { String money = (String) args[0]; System.out.println(method.getName()+"在调用"); if("sing".equals(method.getName())){ if(new Integer(money)>3){ //调用宇哥的sing()方法 return method.invoke(zhangyu,args); }else{ System.out.println("不够出场费"); } }else if("dance".equals(method.getName())){ if(new Integer(money)>5){ return method.invoke(zhangyu,args); }else{ System.out.println("不够出场费"); } }else if("eat".equals(method.getName())){ System.out.println("宇哥今天有事,不能来"); } return null; } }); } } //参数一:loader表示动态代理对象是由哪个类加载器完成的 //参数二:interfaces表示动态代理对象与目标对象(宇哥)有一样的方法 //参数三:表示动态代理对象的栏截方法,即每次调用目标对象都会执行该invoke()方法(难点) //invoke()方法的三个参数含义如下 //参数一:动态产生的代理对象本身 //参数二:method表示方法 //参数三:args表示方法参数
【案例】Spring框架中的AOP技术,基于代理模式。
组成:
- 调用者Beans - 即调用发起者,它只知道目标方法所在Bean,并不清楚代理以及Advice的存在。
- 目标方法所在Bean - 被调用的目标方法。
- 生成的代理 - 由Spring AOP为目标方法所在Bean生成的一个代理对象。
- Advice - 切面的执行逻辑。
过程:
- 调用者Bean尝试调用目标方法,但是被生成的代理截了胡。
- 代理根据Advice的种类(本例中是@Before Advice),对Advice首先进行调用。
- 代理调用目标方法。
- 返回调用结果给调用者Bean(由代理返回,没有体现在图中)。
【案例】Hibernate延迟加载所采用的的设计模式,Hibernate关联映射时。
当A实体与B实体之间存在关联关系时,Hibernate默认启用延迟加载,当系统加载A实例时,A实例关联B实例的B实例并未被加载出来,A实体所关联的B实体全部是代理对象——只有等到A实体真正需要访问B实体时,系统才会去数据库里抓取B实体所对应的记录。
【案例】Struts2中的AOP实现
大部分web应用都涉及权限控制,当浏览者需要执行某个操作时,应用需要先检查浏览者是否登录,以及是否有足够的权限来执行该操作。
本示例要求用户登录,必须为制定用户名才可以查看系统中某个视图资源,否则直接转入登录页面。对于上面的需求,可以在每个action执行业务逻辑之前,先执行权限检查逻辑,但这种做法不利于代码复用。因为大部分action中检查权限代码都大同小异,故将这些代码逻辑放在拦截器中,将会更加优雅。
检查用户是否登录,通常都是跟踪用户的Session来完成的,通过ActionContext即可访问到Session中的属性,拦截器的intercep(ActionInvocation invocation)方法的invocation参数可以很轻易的访问到请求相关的ActionContex实例。
2.2,装饰器模式
代理模式是一种结构型模式,它通过使用一个代理对象来控制对实际对象的访问。在这种模式下,客户端通过与代理对象交互来访问实际对象,从而使得代理对象可以在访问前或访问后加入一些额外的处理逻辑。代理模式常被应用于需要控制或限制对某个对象的访问,或者需要在访问前后执行额外的逻辑的场合。
装饰器模式也是一种结构型模式,它允许动态地向一个现有对象添加新的功能,同时又不改变其结构。在这种模式下,我们可以创建一个装饰器类,该类包装了实际对象,并提供了额外的功能。装饰器模式常被应用于需要动态地修改对象的行为或增加对象的功能的场合。
两种模式的主要异同点如下:
目的不同:代理模式的主要目的是控制或限制对某个对象的访问,或在访问前后执行额外的逻辑,而装饰器模式的主要目的是动态地修改对象的行为或增加其功能。
装饰器模式更加灵活:装饰器模式允许我们动态地为一个对象添加功能,而代理模式则更多地是控制或限制已有的对象的访问。因此,装饰器模式更加灵活,可以随时动态地为对象添加/删除功能,而代理模式则需要预先声明代理对象。
装饰器模式中的装饰器可叠加:在装饰器模式中,我们可以创建多个装饰器类,它们可以按照一定的规划来组合,从而实现更丰富的功能。而在代理模式中,我们只能使用一个代理对象。
2.3,门面模式
随着系统的不断改进和开发,他们会变得越来越复杂,系统会生成大量的类,这使得程序流程更难被理解。门面模式可为这些类提供一个简单的接口,从而简化访问这些类的复杂性,有时这种简化可能降低访问这些底层的灵活性,但除了要求特别苛刻的客户端之外,它通常都可以提供所需的全部功能。
门面模式:也被称为正面模式、外观模式,这种模式用于将一组复杂的类包装到一个简单的外部接口中。
场景:有一个顾客需要到饭店用餐,这就需要定义一个Customer类,并为该类定义一个havaDinner()方法。考虑该饭店有三个部门:收银部、厨师部和服务商部,用户就餐需要这三个部门协调才能完成。
public class Payment { public String pay(){ String food = "快餐"; System.out.println("你已经向收银员支付了费用,您购买的是:"+food); return food; } } ---------------------------------- public class Cook { public String cook(String food) { System.out.println("厨师蒸菜烹饪:" + food); return food; } } ---------------------------------- public class Waiter { public void server(String food) { System.out.println("服务生已将" + food + "端过来了,请慢用..."); } }
public class Customer { public void haveDinner() { Payment pay = new Payment(); Cook cook = new Cook(); Waiter waiter = new Waiter(); String food = pay.pay(); food = cook.cook(food); waiter.server(food); } }
Customer需要依次调用三个部门的方法才可实现这个haveDinner()方法。如果这个饭店有更多的部门,那么程序就需要调用更多部门的方法来实现这个haveDinner()方法——这就会增加haveDinner()方法的实现难度了。
为了解决这个问题,可以为Payment、Cook、Writer三个部门提供一个门面(Facade),使用该Facade来包装这个类,对外提供一个简单的访问方法。
public class Facade { Payment pay; Cook cook; Waiter waiter; public Facade() { this.pay = new Payment(); this.cook = new Cook(); this.waiter = new Waiter(); } public void serverFood() { String food = pay.pay(); food = cook.cook(food); waiter.server(food); } }
该门面类保证了Payment、Cook、Waiter三个部门,serverFood()方法对外提供了一个用餐方法,而底层则依赖于三个部门的pay()、cook()、server()三个方法。如果不采用门面模式,客户端需要自行决定需要调用那些类、那些方法,并需要合理的顺序来调用它们才可实现所需的功能。
2.4,桥接模式
桥接模式是一种结构型模式,它主要应对的是:由于实际的需要,某个类具有两个或两个以上的维度变化,如果只是使用继承将无法实现这种需要,或者使得设计变得相当臃肿。
场景:假设现在需要为某个餐厅制造菜单,餐厅供应牛肉面、猪肉面,而且顾客可以根据自己的口味选择是否添加辣椒。此时就产生了一个问题,如何应对这种变化:是否需要定义辣椒牛肉面、无辣牛肉面、辣椒猪肉面、无辣猪肉面4个子类?如果有其他种类的面种和不同的辣度?那岂不是要一直忙于定义子类?
为了解决这个问题,可以使用桥接模式,桥接模式的做法是把变化部分抽象出来,使变化部分与主类分离开来,从而将多个维度的变化彻底分离。最后提供一个管理类来组合不同维度上的变化,通过这种组合来满足业务的需求。
public interface Peppery { String style(); }
public class PepperyStyle implements Peppery{ public String style() { return "辣味很重,很过瘾!"; } } ================================== public class PlainStyle implements Peppery{ public String style() { return "味道清淡,很养胃!"; } }
从上面可以看出,该Peppery接口代表了面条在辣味风格这个维度上的变化,不论面条在该维度上有多少种变化,程序只需要为这几种变化分别提供实现类即可。对于系统而言,辣味风格这个维度上的变化时固定的,程序必须面对的,程序使用桥接模式将辣味风格这个维度的变化分离出来,避免与牛肉、猪肉材料风格这个维度的变化耦合在一起。
程序还需要一个AbstractNoodle抽象类,该抽象类将会持有一个Peppery属性,该属性代表该面条的辣味风格。
public abstract class AbstractNoodle { protected Peppery style; public AbstractNoodle(Peppery style){ this.style = style; } public abstract void eat(); } --------------------------------------------- public class PorkyNoodle extends AbstractNoodle { public PorkyNoodle(Peppery style) { super(style); } public void eat() { System.out.println("这是一碗猪肉面条" + super.style.style()); } } --------------------------------------------- public class BeefNoodle extends AbstractNoodle { public BeefNoodle(Peppery style) { super(style); } public void eat() { System.out.println("这是一碗牛肉面" + super.style.style()); } }
public class Main { public static void main(String[] args) { //辣味牛肉面 AbstractNoodle noodle1 = new BeefNoodle(new PepperyStyle()); //不辣猪肉面 AbstractNoodle noodle2 = new PorkyNoodle(new PlainStyle()) } }
3,行为型设计模式
3.1,命令模式
假如存在某个场景:某个方法需要完成某一个功能,完成这个功能的大部分步骤已经确定了,但可能有少量具体步骤无法确定,必须等到执行该方法时才可以确定。具体来说:假设有个方法需要遍历某个数组元素,但无法确定在遍历元素时如何处理这些元素,需要在调用该方法时指定具体的处理行为。这个方法不仅要求参数可以变化,甚至要求方法执行体的代码可以变化,需要能把“处理行为”作为一个参数传入该方法。
对于这样的需求,要求把“处理行为”作为参数传入该方法,而“处理行为”用编程来实现就是一段代码。在Java语言中,类才是一等公民,方法不能独立存在,所以实际传入该方法的应该是一个对象,该对象通常是某个接口的匿名实现类的实例,该接口通常被称为命令接口,这种设计模式被称为命令模式。
public interface Command { void process(int[] target); }
public class ProcessArray { //定义一个each()方法,用于处理数组 public void each(int[] target, Command cmd) { cmd.process(target); } }
public class Main { public static void main(String[] args) { ProcessArray pa = new ProcessArray(); int[] target = {3, -4, 6, 4}; pa.each(target, new Command() { public void process(int[] target) { for (int tmp : target) { System.out.println("迭代输出目标数组的元素:" + tmp); } } }); pa.each(target, new Command() { public void process(int[] target) { int sum = 0; for (int tmp : target) { sum += tmp; } System.out.println("数组元素的总和是:" + sum); } }); } }
这样就实现了process()方法和”处理行为“的分离,两次不同的处理行为分别由不同的Command对象来提供。
命令模式在MVC中的应用:Struts中,在模型层都要继承一个Action接口,并实现execute方法,其实这个Action就是命令类。为什么Struts会应用命令模式,是因为Struts的核心控制器ActionServlet只有一个,相当于Invoker(调用者),而模型层的类会随着不同的应用有不同的模型类,相当于具体的Command。这样,就需要在ActionServlet和模型层之间解耦,而命令模式正好解决这个问题。
3.2,策略模式
策略模式用于封装系列的算法,这些算法通常被封装在一个被称为Context的类中,客户端程序可以自由选择其中一种算法,或让Context为客户端选择一个最佳的算法——使用策略模式的优势为了支持算法的自由切换。
场景:加入正在开发一个网上书店,该书店为了更好地促销,经常需要对图书馆进行打折促销,程序需要考虑各种打折促销的计算方法。
public double discount(double price) { switch (getDiscontType()){ case VIP_DISCOUNT: return vipDiscount(price); break; case OLD_DISCOUNT: return oldDiscount(price); break; } }
这段代码没有太大问题,但有明显的不足,程序中各种打折方法都被直接写入discount(double price)方法中。如果有一天,新增打折方法就需要修改三处地方:(1)首先增加常量,代表打折类型。(2)在switch中增加一个case语句。(3)需要实现xxxDiscount()方法。
public interface DiscountStrategy { double getDiscount(double originPrice); }
public class VipDiscount implements DiscountStrategy { public double getDiscount(double originPrice) { System.out.println("使用VIP打折算法..."); return originPrice * 0.5; } }
public class OldDiscount implements DiscountStrategy { public double getDiscount(double originPrice) { System.out.println("使用默认打折..."); return originPrice * 0.7; } }
提供了两个折扣策略之后,程序还需要提供一个DiscountContext类,该类用于为客户端代码选择合适折扣策略,当然也允许用户自由选择折扣策略。
public class DiscountContext { private DiscountStrategy strategy; public DiscountContext(DiscountStrategy strategy) { this.strategy = strategy; } public double getDiscountPrice(double price) { if (strategy == null) { strategy = new OldDiscount(); } return this.strategy.getDiscount(price); } public void changeDiscount(DiscountStrategy strategy){ this.strategy = strategy; } }
public class Main { public static void main(String[] args) { DiscountContext dc = new DiscountContext(null); double pricel = 79; System.out.println("78元默认打折后的价格:" + dc.getDiscountPrice(pricel)); dc.changeDiscount(new VipDiscount()); System.out.println("78元VIP打折后的价格:" + dc.getDiscountPrice(pricel)); } } ============================================= 使用默认打折... 78元默认打折后的价格:55.3 使用VIP打折算法... 78元VIP打折后的价格:39.5
再次考虑前面的需求:当业务需要新增一种打折类型时,系统只需要重新定义一个DiscountStrategy实现类,该实现类实现getDiscount()方法,用于实现新的打折算法即可。客户端程序需要切换为新的打折策略时,则需要先调用DiscountContext的changeDiscount()方法切换为新的打折策略。
3.3,观察者模式
观察者模式定义了对象间的一对多依赖关系,让一个或多个观察者对象观察一个主题对象,当主题对象的状态发生改变时,系统能通知所有的依赖于此对象的观察者对象,从而使得观察者对象能够自动观察。(被观察的对象/被依赖的对象常常被称为目标或主题,依赖的对象被称为观察者)
public interface Observer { void update(Observable o, Object arg); } ------------------------------------------ import java.util.ArrayList; import java.util.List; public class Observable { //用一个List来保存该对象上所有绑定的事件监听器 List<Observer> observers = new ArrayList<>(); public void registObserver(Observer o){ observers.add(o); } public void removeObserver(Observer o){ observers.remove(o); } public void notifyObservers(Object value){ for (Observer o:observers){ o.update(this,value); } } }
registObserver()方法用于注册一个新的观察者;并提供了removeObserver()方法用于删除一个已注册的观察者;当具体被观察对象的状态改变时,具体被观察对象调用notifyObservers()方法来通知所有观察者。
public class Product extends Observable{ private String name; private double price; public Product(){} public Product(String name,double price){ this.name = name; this.price = price; } public String getName() { return name; } public void setName(String name) { this.name = name; notifyObservers(name); } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; notifyObservers(price); } }
提供两个观察者,一个用于观察Product对象的name成员变量,另一个用于观察Product对象的price成员变量。
import javax.swing.*; public class NameObserver implements Observer { public void update(Observable o, Object arg) { if (arg instanceof String) { String name = (String) arg; JFrame f = new JFrame("观察者"); JLabel l = new JLabel("名称改变为:" + name); f.add(l); f.pack(); f.setVisible(true); System.out.println("名称观察者:" + o + "物品名称已改变为:" + name); } } } ----------------------------------------- public class PriceObserver implements Observer { public void update(Observable o, Object arg) { if (arg instanceof Double){ System.out.println("价格观察者:" + o + "物品价格变为:" + arg); } } }
public class Main { public static void main(String[] args) { Product p = new Product("电视机",176); NameObserver no = new NameObserver(); PriceObserver po = new PriceObserver(); p.registObserver(no); p.registObserver(po); p.setName("书桌"); p.setPrice(50); } }
可以看到当Product的成员变量发生改变时,注册在该Product上的NameObserver和PriceObserver将被触发。
可以发现观察者模式通常包含4个角色:
- 被观察者的抽象基类:它通常持有多个观察者对象的引用。Java提供了java.util.Observable基类来代表被观察者的抽象基类,所以实际开发中无须自己开发这个角色。
- 观察者接口:该接口是所有被观察对象应该实现的接口,通常它只包含一个抽象方法update()。Java同样提供了java.util.Observer接口来代表观察者接口,实际开发中也无须开发该角色。
- 被观察者实现类:该类继承Observable基类。
- 观察者实现类:实现Observer接口,实现update()抽象方法。