设计模式详解(4)-- 结构型模式详解下

本系列文章:
设计模式详解(1)-- 初识设计模式
设计模式详解(2)-- 创建型模式详解
设计模式详解(3)-- 结构型模式详解上
设计模式详解(4)-- 结构型模式详解下
设计模式详解(5)-- 行为型模式详解上
设计模式详解(6) – 行为型模式详解下

本博客专门介绍结构型模式中的装饰器模式,外观模式,享元模式,代理模式

装饰器模式(Decorator Pattern)

装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。

功能

动态地给一个对象添加一些额外的职责。

优点

装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。

现在我们尝试为不同的文件提供杀毒功能,并以此让大家对装饰模式有更深刻的认识。

我们先建立一个文件接口

public interface File {

	void enter();
}

接着我们创建两个文件的实现类,分别是图像文件和文本文件

public class ImageFile implements File {

	@Override
	public void enter() {
		System.out.println("enter image file");
	}
}

public class TextFile implements File {

	@Override
	public void enter() {
		System.out.println("enter text file");
	}
}

然后我们构建了一个实现文件接口的抽象装饰类。

public abstract class FileDecorator implements File {

	protected File file;
	
	public FileDecorator(File file) {
		this.file = file;
	}
	
	@Override
	public void enter() {
		file.enter();
	}
}

然后我们创建一个继承了抽象装饰类的实体装饰类。

public class AntivirusDecorator extends FileDecorator {

	public AntivirusDecorator(File file) {
		super(file);
	}

	@Override
	public void enter() {
		System.out.println("Antivirus");
		file.enter();
	}
}

测试一下

public class Demo {

	public static void main(String[] args) {
		File file1 = new AntivirusDecorator(new ImageFile());
		file1.enter();
		
		System.out.println("-----------------");
		
		File file2 = new AntivirusDecorator(new TextFile());
		file2.enter();
	}
}

Antivirus
enter image file
-------分割线----------
Antivirus
enter text file

外观模式(Facade Pattern)

外观模式(Facade Pattern,也叫门面模式)是一种构造型设计模式,为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

在软件系统开发中经常会遇到这样的情况,在一个系统中实现了一些接口(模块),而这些接口(模块)都分布在几个类或几个子系统模块中比如A和B、C、D,各自实现了一些功能接口。然后对于客户,并不知道或并不想知道,这些功能接口是怎么样实现的,是在哪里实现的。外观模式可以为一个复杂的系统(有很多分布在不同类或子系统中的方法)提供一个简单的供客户端调用的接口,使耦合大大降低。

优点
  • 对客户屏蔽子系统组件,因而减少了客户处理的对象的数目并使得子系统使用起来更加方便。
  • 实现了子系统与客户之间的松耦合关系,而子系统内部的功能组件往往是紧耦合的。
缺点
  • 增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。
  • 对客户访问子系统类做太多的限制则减少了可变性和灵活性。

我们用一个简单的案例来讲解外观模式。假设小明要睡觉了,需要把电视机,台灯,大门全部关掉,固然我们可以写三个类并通过直接调用了实现功能,但有没有更好的方法呢?是的,我们可以建立一个统一的入口,来调用电视机,台灯,大门这三个实现类的业务逻辑,这样对于外面控制层来说,看到的就只有这么一个门面,通过调用这个门面的方法,就能完成实现类的全部工作,从而实现了子系统与客户之间的松耦合关系。

示例

我们先建立三个实现类

public class Door {

	public void on() {
		System.out.println("大门打开");
	}
	
	public void off() {
		System.out.println("大门关闭");
	}
}

public class Lamp {

	public void on() {
		System.out.println("打开台灯");
	}
	
	public void off() {
		System.out.println("关闭台灯");
	}
}

public class Television {

	public void on() {
		System.out.println("打开电视机");
	}
	
	public void off() {
		System.out.println("关闭电视机");
	}
}

接着我们实现一个外观类

public class Facade {

	private Door door;
	private Lamp lamp;
	private Television television;
	
	public Facade(Door door,Lamp lamp,Television television) {
		this.door = door;
		this.lamp = lamp;
		this.television = television;
	}
	
	public void on() {
		door.on();
		lamp.on();
		television.on();
	}
	
	public void off() {
		door.off();
		lamp.off();
		television.off();
	}
}

测试一下

public class Demo {

	public static void main(String[] args) {
		Facade facade = new Facade(new Door(), new Lamp(), new Television());
		
		System.out.println("调用门面的on方法");
		facade.on();
		
		System.out.println("调用门面的off方法");
		facade.off();
	}
}

调用门面的on方法
大门打开
打开台灯
打开电视机
调用门面的off方法
大门关闭
关闭台灯
关闭电视机

享元模式(Flyweight Pattern)

享元模式(Flyweight),运用共享技术有效地支持大量细粒度的对象。

相信大家对池技术不陌生吧,实际上,享元模式是池技术的重要实现方式,比如String常量池、数据库连接池等等啥的,都与享元模式有密不可分的关系。

优点

享元模式可以避免大量非常相似类的开销,在程序设计中,有时需要生成大量细粒度的类实例来表示数据,如果能发现这些实例除了几个参数外基本上都是相同的,有时就能够大幅度的减少需要实例化类的数量。如果能把那些参数移到类实例的外面,在方法调用时将它们传进来,就可以通过共享大幅度地减少单个实例的数目。

缺点

提高了系统的复杂度。需要分离出内部状态和外部状态,而外部状态具有固化特性,不应该随着内部状态的改变而改变。

我们可以假设一个场景,现在要进行网站的开发,为了简化,可假定网站只有页面名称这个属性,而用户只有用户名这个属性。若我们对每个网页对象都建立一个实例,那么服务器的内存开销可能会非常大,在实际中是不可能的。既然如此,我们便自然而然地想到,能不能每个页面都只建立一个实例,不同的用户调用时,若访问相同的页面,返回给用户的页面是同一个实例?相信大家看到这里已经猜到了,这是可行的,而其中的关键便是享元模式了。下面我们用代码进行模拟。

示例

先建立一个用户类

//用户
public class User {

	private String name;
	
	public User(String name) {
		this.name = name;
	}
	
	public String getName() {
		return name;
	}
}

然后我们构建一个网站接口

public interface WebSite {

	//业务逻辑的实现
	void enter(User user);
}

写一个实现网站接口的实现类

public class ConcreteWebSite implements WebSite {

	private String webSiteName;
	
	public ConcreteWebSite(String webSiteName) {
		this.webSiteName = webSiteName;
	}
	
	@Override
	public void enter(User user) {
		System.out.println("这是" + webSiteName + "网站, 当前访问用户是" + user.getName());
	}

}

实现享元模式的关键,建立一个网站工厂

public class WebsiteFactory {

	private static HashMap<String, WebSite> factory = new HashMap<String, WebSite>();
	
	public WebSite getWebsite(String websiteName) {
		if(!factory.containsKey(websiteName)) {
			WebSite webSite = new ConcreteWebSite(websiteName);
			factory.put(websiteName, webSite);
		}
		return factory.get(websiteName);
	}
	
	public int getSize() {
		return factory.size();
	}
}

测试

public class Client {

	public static void main(String[] args) {
		WebsiteFactory websiteFactory = new WebsiteFactory();
		
		WebSite webSite1 = websiteFactory.getWebsite("登录页面");
		webSite1.enter(new User("王晓晓"));
		
		WebSite webSite2 = websiteFactory.getWebsite("登录页面");
		webSite2.enter(new User("阿三"));
		
		WebSite webSite3 = websiteFactory.getWebsite("注册页面");
		webSite3.enter(new User("李四"));
		
		WebSite webSite4 = websiteFactory.getWebsite("注册页面");
		webSite4.enter(new User("王五"));
		
		System.out.println("当前页面总数" + websiteFactory.getSize());
	}
}

这是登录页面网站, 当前访问用户是王晓晓
这是登录页面网站, 当前访问用户是阿三
这是注册页面网站, 当前访问用户是李四
这是注册页面网站, 当前访问用户是王五
当前页面总数2

我们再回到代码,在网站中,一共需要两个属性,为用户名与网站名,其中网站名是可以共享的,而用户名唯一,在享元模式中,我们分别将其称作外部状态与内部状态。

内部状态与外部状态
  • 内部状态:在享元内部并且不会随环境改变而改变的共享部分
  • 外部状态:随环境改变而改变的、不可能共享的状态就是外部状态
应用场景

如果一个应用程序使用了大量的对象,而大量的这些对象造成了很大的存储开销时就应该考虑使用;还有就是对象的大多数状态可以外部状态,如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象,此时可以考虑使用享元模式。

代理模式(Proxy Pattern)

转自 代理模式实现方式及优缺点对比

代理,顾名思义,即代替被请求者来处理相关事务。代理对象一般会全权代理被请求者的全部职能,客户访问代理对象就像在访问被请求者一样,虽然代理对象最终还是可能会访问被请求者,但是其可以在请求之前或者请求之后进行一些额外的工作,或者说客户的请求不合法,直接拒绝客户的请求。

代理分为静态代理和动态代理,其中动态代理又可以分为jdk代理和Cglib代理。

静态代理

我们先写一个接口并构建一个实现类

public interface Use {

	void operation();
}

public class UseImp implements Use{

	@Override
	public void operation() {
		System.out.println("UseImp的业务逻辑正在被调用");
	}
}

再实现一个代理类

public class UseProxy implements Use {

	private Use use;
	
	public UseProxy(Use use) {
		this.use = use;
	}
	
	@Override
	public void operation() {
		System.out.println("在业务逻辑被调用之前,出来实现一些功能");
		use.operation();
		System.out.println("在业务逻辑被调用之后,出来实现一些功能");
	}

}

最后测试一下

public class Demo {

	public static void main(String[] args) {
		Use use = new UseImp();
		Use useProxy = new UseProxy(use);
		useProxy.operation();
	}
}

在业务逻辑被调用之前,出来实现一些功能
UseImp的业务逻辑正在被调用
在业务逻辑被调用之后,出来实现一些功能

优点

静态代理是效率最高的一种方式,因为所有的类都是已经编写完成的,客户端只需要取得代理对象并且执行即可。

缺点

可以看到,客户端在调用代理对象时,使用的是代理对象和被代理对象都实现的一个接口,我们可以将该接口理解为定义了某一种业务需求的实现规范。如果有另外一份业务需求,其与当前需求并行的,没有交集的,但是其在进行正常业务之外所做的工作与当前需求是一致的,我们还要为它再写一个代理类,尽管代理类中的代码几乎是完全一样的,这是一个很大的问题。

动态代理
1 jdk代理

所谓的jdk代理指的是借助jdk所提供的相关类来实现代理模式,其主要有两个类:InvocationHandler和Proxy。在实现代理模式时,只需要实现InvocationHandler接口即可。

示例

写一个代理类

public class UseInvocationHandler implements InvocationHandler {

	//要代理的对象
	private Object target;
	
	public UseInvocationHandler(Object target) {
		this.target = target;
	}
	
	@Override
	public Object invoke(Object arg0, Method method, Object[] args) throws Throwable {
		System.out.println("在业务逻辑被调用之前,出来实现一些功能");
		Object result = method.invoke(target, args);
		System.out.println("在业务逻辑被调用之后,出来实现一些功能");
		return result;
	}

}

测试

public class Demo {

	public static void main(String[] args) {
		Use use = new UseImp();
		Use proxyUse = (Use) Proxy.newProxyInstance(Demo.class.getClassLoader(), new Class[]{Use.class}, new UseInvocationHandler(use));
		proxyUse.operation();
	}
}

与上面测试的结果相同。

jdk代理解决了静态代理需要为每个业务接口创建一个代理类的问题,虽然使用反射创建代理对象效率比静态代理稍低,但其在现代高速jvm中也是可以接受的,在Spring的AOP代理中默认就是使用的jdk代理实现的。这里jdk代理的限制也是比较明显的,即其需要被代理的对象必须实现一个接口。这里如果被代理对象没有实现任何接口,或者被代理的业务方法没有相应的接口,我们则可以使用另一种方式来实现,即Cglib代理。

2 Cglib代理

Cglib代理是功能最为强大的一种代理方式,因为其不仅解决了静态代理需要创建多个代理类的问题,还解决了jdk代理需要被代理对象实现某个接口的问题。对于需要代理的类,如果能为其创建一个子类,并且在子类中编写相关的代理逻辑,因为“子类 instanceof 父类”,因而在进行调用时直接调用子类对象的实例,也可以达到代理的效果。Cglib代理的原理实际上是动态生成被代理类的子类字节码,由于其字节码都是按照jvm编译后的class文件的规范编写的,因而其可以被jvm正常加载并运行。这也就是Cglib代理为什么不需要为每个被代理类编写代理逻辑的原因。这里需要注意的是,根据Cglib实现原理,由于其是通过创建子类字节码的形式来实现代理的,如果被代理类的方法被声明final类型,那么Cglib代理是无法正常工作的,因为final类型方法不能被重写。如下是使用Cglib代理的一个示例:

/**
 * 被代理类
 */
public class Suject {
  public void request() {
    System.out.println("update without implement any interface.");
  }
}
/**
 * 代理类
 */
public class SafetyCheckCallback implements MethodInterceptor {
  @Override
  public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
    System.out.println("before safety check.");
    Object result = methodProxy.invokeSuper(o, objects);
    System.out.println("after safety check.");
    return result;
  }
}

测试

public class Client {
  @Test
  public void testCglibProxy() {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(Suject.class);
    enhancer.setCallback(new SafetyCheckCallback());
    Suject proxy = (Suject) enhancer.create();
    proxy.request();
  }
}

可以看到,客户端代码中首先创建了一个Enhancer对象,并且设置了父类及代理回调类对象。该Enhancer对象会为目标类创建相关的子类字节码,并且将代理代码植入该子类字节码中。

适配器模式,装饰模式,代理模式的异同

菜鸟版JAVA设计模式—适配器模式,装饰模式,代理模式异同

我的下一篇设计模式博客:设计模式详解(5)-- 行为型模式详解上

参考:http://www.runoob.com/design-pattern/decorator-pattern.html
https://www.cnblogs.com/ejiyuan/archive/2012/07/02/2573295.html
https://www.cnblogs.com/adamjwh/p/9070107.html
https://www.cnblogs.com/0201zcr/p/4612345.html
https://my.oschina.net/zhangxufeng/blog/1633187

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值