上篇文章讲解了5种创建型模式,本文我们来看看7种结构型模式:适配器模式、装饰模式、代理模式、外观模式、桥接模式、组合模式、享元模式。其中对象的适配器模式是各种模式的起源,我们看下面的图:
适配器模式(Adapter)
适配器模式将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容性问题。例如我们手机可以接受的充电电压大概是5V,而我们民用交流电压220V,这时候就需要一个适配器,就是我们的充电器,将220V电压降到5V,再充给手机。
适配器模式主要分为三类:类的适配器模式、对象的适配器模式、接口的适配器模式。下面我们主要看看对象的适配器模式。
写个实例来看下吧,首先一部手机:Mobile.java
public class Mobile
{
//充电
public void inputPower(V5Power power) {
int provideV5Power = power.provideV5Power();
System.out.println("手机:我需要5V电压充电,现在是-->" + provideV5Power + "V");
}
}
可以看出,手机依赖一个提供5V电压的接口:
//提供5V电压的一个接口
public interface V5Power {
public int provideV5Power();
}
然后我们拥有的是220V民用交流电:
public class V220Power {
//提供220V电压
public int provideV220Power() {
System.out.println("现有类:我提供220V交流电压");
return 220 ;
}
}
下面我们需要一个适配器,完成220V转5V的作用,适配器实现了V5Power接口,且持有V220Power类的实例:
public class V5PowerAdapter implements V5Power {
//组合的方式
private V220Power v220Power ;
public V5PowerAdapter(V220Power v220Power) {
this.v220Power = v220Power ;
}
@Override
public int provideV5Power() {
int power = v220Power.provideV220Power();
//power经过变压适配操作 220-->5
System.out.println("适配器:我悄悄的适配了电压");
return 5 ;
}
}
最后测试,我们给手机冲个电:
public class Test {
public static void main(String[] args) {
Mobile mobile = new Mobile();
V5Power v5Power = new V5PowerAdapter(new V220Power()) ;
mobile.inputPower(v5Power);
}
}
输出:
现有类:我提供220V交流电压
适配器:我悄悄的适配了电压
手机(客户端):我需要5V电压充电,现在是–>5V
可以看出,我们使用一个适配器完成了把220V转化了5V然后提供给手机使用,且我们使用了组合(OO设计原则),原有的手机以及220V电压类都不需要变化,且手机和220V电压完全解耦。
最后画个UML类图,便于大家理解:
装饰模式(Decorator)
装饰模式是结构型设计模式之一,在不改变类文件和使用继承的情况下,动态地扩展一个对象的功能,是继承的替代方案之一。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。
动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活。
- Component:抽象组件,给对象动态的添加职责。
- ConcreteComponent:组件具体实现类。
- Decorator:抽象装饰者,继承Component,从外类来拓展Component类的功能,但对于Component来说无需知道Decorator的存在。
- ConcreteDecorator:装饰者具体实现类。
下面举一个例子,Jim本身自学了音乐,有两位老师Marry和Jack,Marry教了他数学,Jack教了他英语,这样Jim除了音乐还会数学和英语。
(1)抽象组件(Component)
我们先定义一个人的抽象类,里面有学习的抽象方法:
public abstract class Person {
public abstract void study();
}
(2)组件具体实现类(ConcreteComponent)
public class Jim extends Person{
@Override
public void study() {
System.out.println("Jim自学了音乐");
}
}
(3)抽象装饰者(Decorator)
抽象装饰者保持了一个对抽象组件的引用,方便调用被装饰对象中的方法。在这个例子中就是老师要持有人的引用,方便教授Jim技能:
public abstract class Teacher extends Person{
private Person mPerson;
public Teacher(Person mPerson){
this.mPerson=mPerson;
}
@Override
public void study() {
mPerson.study();
}
}
(4)装饰者具体实现类(ConcreteDecorator)
这个例子中用两个装饰者具体实现类,分别是Marry和Jack,他们负责来教Jim新的技能:
public class Marry extends Teacher {
public Marry(Person mPerson) {
super(mPerson);
}
public void teach(){
System.out.println("Marry教数学");
System.out.println("Jim学会了数学");
}
@Override
public void study() {
super.study();
teach();
}
}
public class Jack extends Teacher {
public Jack(Person mPerson) {
super(mPerson);
}
public void teach(){
System.out.println("Jack教英语");
System.out.println("Jim学会了英语");
}
@Override
public void study() {
super.study();
teach();
}
}
(5)客户端调用
public class Client {
public static void main(String[] args){
Jim mJim=new Jim(); //创建mJim
Marry mMarry=new Marry(mJim);
mMarry.study(); //Jim自学了音乐,Marry教Jim数学,Jim学会了数学
Jack mJack=new Jack(mJim);
mJack.study(); //Jim自学了音乐,Jack教Jim英语,Jim学会了英语
}
}
优点
- 通过组合而非继承的方式,动态的来扩展一个对象的功能,在运行时选择不同的装饰器,从而实现不同的行为。
- 有效避免了使用继承的方式扩展对象功能而带来的灵活性差,子类无限制扩张的问题。
- 具体组件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体组件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则”。
缺点
- 装饰链不能过长,否则会影响效率。
- 因为所有对象都是继承于Component,所以如果Component内部结构发生改变,则不可避免地影响所有子类(装饰者和被装饰者),如果基类改变,势必影响对象的内部。
- 比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐,所以只在必要的时候使用装饰者模式。
使用场景
- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
- 需要动态地给一个对象增加功能,这些功能可以动态的撤销。
- 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。
代理模式(Proxy)
代理模式也叫委托模式,是结构型设计模式的一种。在现实生活中我们用到类似代理模式的场景有很多,比如代购、代理上网等。
- Subject:抽象主题类,声明真实主题与代理的共同接口方法。
- RealSubject:真实主题类,定义了代理所表示的真实对象,客户端通过代理类间接的调用真实主题类的方法。
- ProxySubject:代理类,持有对真实主题类的引用,在其所实现的接口方法中调用真实主题类中相应的接口方法执行。
- Client:客户端类。
下面举一个例子,小明想要买个Iphone,他找了个代购来帮他海外购买,这样能省好多钱。
(1)抽象主题类(Subject)
抽象主题类具有真实主题类和代理的共同接口方法,小明想要代购,那共同的方法就是购买:
public interface IShop {
//购买
void buy();
}
(2)真实主题类(RealSubject)
这个购买者就是小明,实现了IShop接口提供的 buy()方法:
public class XiaoMing implements IShop {
@Override
public void buy() {
System.out.println("购买");
}
}
(3)代理类(ProxySubject)
小明找的代理类同样也要实现IShop接口,并且要持有被代理者,在buy()方法中调用了被代理者的buy()方法:
public class Purchasing implements IShop {
private IShop mShop;
public Purchasing(IShop shop){
mShop = shop;
}
@Override
public void buy() {
mShop.buy();
}
}
(3)客户端类(Client)
public class Client {
public static void main(String[] args){
//创建XiaoMing
IShop xiaoMing = new XiaoMing();
//创建代购者并将xiaoMing作为构造参数
IShop purchasing = new Purchasing(xiaoMing);
purchasing.buy();
}
}
看完客户端类的代码,其实也是很好理解,就是代理类包含了真实主题类(被代理者),最终调用的都是真实主题类(被代理者)实现的方法,在上面的例子就是XiaoMing类的buy()方法,所以运行的结果就是“购买”。
动态代理的简单实现
从编码的角度来说,代理模式分为静态代理和动态代理,上面的例子是静态代理,在代码运行前就已经存在了代理类的class编译文件,而动态代理则是在代码运行时通过反射来动态的生成代理类的对象,并确定到底来代理谁。也就是我们在编码阶段不需要知道代理谁,代理谁我们将会在代码运行时决定。Java提供了动态的代理接口InvocationHandler,实现该接口需要重写invoke()方法。下面我们在上面静态代理的例子上做修改:
创建动态代理类:
public class DynamicPurchasing implements InvocationHandler{
private Object obj;
public DynamicPurchasing(Object obj){
this.obj = obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = method.invoke(obj, args);
return result;
}
}
在动态代理类中我们声明一个Object的引用,该引用指向被代理类,我们调用被代理类的具体方法在invoke()方法中执行。接下来我们修改客户端类代码:
public class Client {
public static void main(String[] args){
//创建XiaoMing
IShop xiaoMing = new XiaoMing();
//创建动态代理
DynamicPurchasing mDynamicPurchasing = new DynamicPurchasing(xiaoMing);
//创建XiaoMing的ClassLoader
ClassLoader loader = xiaoMing.getClass().getClassLoader();
//动态创建代理类
IShop purchasing = (IShop) Proxy.newProxyInstance(loader,new Class[]{IShop.class},mDynamicPurchasing);
purchasing.buy();
}
}
代理模式类型
代理模式的类型主要有以下几点:
- 远程代理:为一个对象在不同的地址空间提供局部代表,这样系统可以将Server部分的事项隐藏。
- 虚拟代理:使用一个代理对象表示一个十分耗资源的对象并在真正需要时才创建。
- 安全代理:用来控制真实对象访问时的权限。
- 智能指引:当调用真实的对象时,代理处理另外一些事,比如计算真实对象的引用计数,当该对象没有引用时,可以自动释放它;或者访问一个实际对象时,检查是否已经能够锁定它,以确保其他对象不能改变它。
代理模式使用场景
- 无法或者不想直接访问某个对象时可以通过一个代理对象来间接的访问。
外观模式(Facade)
提供一个统一的接口,用来访问子系统中的一群接口,外观定义了一个高层的接口,让子系统更容易使用。其实就是为了方便客户的使用,把一群操作,封装成一个方法。
例如,我们每次启动计算机时都需要启动CPU,启动Memory,启动Disk,关闭计算机时也需要依次关闭它们,好多步骤啊,如果需要按这么多按钮那不是费劲死了。这时候可以使用外观模式,定义一个高层接口,把这些子系统中所有接口封装起来,以后每次开机只需要点击高层接口的开机即可,关机也是。
看下类图:
我们先看下实现类:
public class CPU {
public void startup(){
System.out.println("cpu startup!");
}
public void shutdown(){
System.out.println("cpu shutdown!");
}
}
public class Memory {
public void startup(){
System.out.println("memory startup!");
}
public void shutdown(){
System.out.println("memory shutdown!");
}
}
public class Disk {
public void startup(){
System.out.println("disk startup!");
}
public void shutdown(){
System.out.println("disk shutdown!");
}
}
高层接口实现类:
public class Computer {
private CPU cpu;
private Memory memory;
private Disk disk;
public Computer(){
cpu = new CPU();
memory = new Memory();
disk = new Disk();
}
public void startup(){
System.out.println("start the computer!");
cpu.startup();
memory.startup();
disk.startup();
System.out.println("start computer finished!");
}
public void shutdown(){
System.out.println("begin to close the computer!");
cpu.shutdown();
memory.shutdown();
disk.shutdown();
System.out.println("computer closed!");
}
}
User类如下:
public class User {
public static void main(String[] args) {
Computer computer = new Computer();
computer.startup();
computer.shutdown();
}
}
输出:
start the computer!
cpu startup!
memory startup!
disk startup!
start computer finished!
begin to close the computer!
cpu shutdown!
memory shutdown!
disk shutdown!
computer closed!
可以看到,我们定义了一个类Computer,然后可以让我一键开关机了。
外观模式:一般用于需要简化一个很大的接口,或者一群复杂的接口的时候。
桥接模式(Bridge)
桥接模式就是把事物和其具体实现分开,使他们可以各自独立的变化。桥接的用意是:将抽象化与实现化解耦,使得二者可以独立变化,像我们常用的JDBC桥DriverManager一样,JDBC进行连接数据库的时候,在各个数据库之间进行切换,基本不需要动太多的代码,甚至丝毫不用动,原因就是JDBC提供统一接口,每个数据库提供各自的实现,用一个叫做数据库驱动的程序来桥接就行了。我们来看看关系图:
先定义接口:
public interface Sourceable {
public void method();
}
分别定义两个实现类:
public class SourceSub1 implements Sourceable {
@Override
public void method() {
System.out.println("this is the first sub!");
}
}
public class SourceSub2 implements Sourceable {
@Override
public void method() {
System.out.println("this is the second sub!");
}
}
定义一个桥,持有Sourceable的一个实例:
public abstract class Bridge {
private Sourceable source;
public void method(){
source.method();
}
public Sourceable getSource() {
return source;
}
public void setSource(Sourceable source) {
this.source = source;
}
}
public class MyBridge extends Bridge {
public void method(){
getSource().method();
}
}
测试类:
public class BridgeTest {
public static void main(String[] args) {
Bridge bridge = new MyBridge();
/*调用第一个对象*/
Sourceable source1 = new SourceSub1();
bridge.setSource(source1);
bridge.method();
/*调用第二个对象*/
Sourceable source2 = new SourceSub2();
bridge.setSource(source2);
bridge.method();
}
}
输出:
this is the first sub!
this is the second sub!
这样,就通过对Bridge类的调用,实现了对接口Sourceable的实现类SourceSub1和SourceSub2的调用。接下来我再画个图,大家就应该明白了,因为这个图是我们JDBC连接的原理,有数据库学习基础的,一结合就都懂了。
组合模式(Composite)
组合模式有时又叫部分-整体模式,在处理类似树形结构的问题时比较方便,看看关系图:
直接来看代码:
public class TreeNode {
private String name;
private TreeNode parent;
private Vector<TreeNode> children = new Vector<TreeNode>();
public TreeNode(String name){
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public TreeNode getParent() {
return parent;
}
public void setParent(TreeNode parent) {
this.parent = parent;
}
//添加孩子节点
public void add(TreeNode node){
children.add(node);
}
//删除孩子节点
public void remove(TreeNode node){
children.remove(node);
}
//取得孩子节点
public Enumeration<TreeNode> getChildren(){
return children.elements();
}
}
public class Tree {
TreeNode root = null; //根节点
public Tree(String name) {
root = new TreeNode(name);
}
}
测试类:
public class Test {
public void main(String[] args) {
Tree tree = new Tree("A");
TreeNode nodeB = new TreeNode("B");
TreeNode nodeC = new TreeNode("C");
nodeB.add(nodeC);
tree.root.add(nodeB);
System.out.println("build the tree finished!");
}
}
使用场景:将多个对象组合在一起进行操作,常用于表示树形结构中,例如二叉树等。
享元模式(Flyweight)
享元模式的主要目的是实现对象的共享,即共享池,当系统中对象多的时候可以减少内存的开销,通常与工厂模式一起使用。
FlyWeightFactory负责创建和管理享元单元,当一个客户端请求时,工厂需要检查当前对象池中是否有符合条件的对象,如果有,就返回已经存在的对象,如果没有,则创建一个新对象,FlyWeight是超类。一提到共享池,我们很容易联想到Java里面的JDBC连接池,想想每个连接的特点,我们不难总结出:适用于作共享的一些个对象,他们有一些共有的属性,就拿数据库连接池来说,url、driverClassName、username、password及dbname,这些属性对于每个连接来说都是一样的,所以就适合用享元模式来处理,建一个工厂类,将上述类似属性作为内部数据,其它的作为外部数据,在方法调用时,当做参数传进来,这样就节省了空间,减少了实例的数量。
看个例子:
看下数据库连接池的代码:
public class ConnectionPool {
private Vector<Connection> pool;
/*公有属性*/
private String url = "jdbc:mysql://localhost:3306/test";
private String username = "root";
private String password = "root";
private String driverClassName = "com.mysql.jdbc.Driver";
private int poolSize = 100;
private static ConnectionPool instance = null;
Connection conn = null;
/*构造方法,做一些初始化工作*/
private ConnectionPool() {
pool = new Vector<Connection>(poolSize);
for (int i = 0; i < poolSize; i++) {
try {
Class.forName(driverClassName);
conn = DriverManager.getConnection(url, username, password);
pool.add(conn);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
/* 返回连接到连接池 */
public synchronized void release() {
pool.add(conn);
}
/* 返回连接池中的一个数据库连接 */
public synchronized Connection getConnection() {
if (pool.size() > 0) {
Connection conn = pool.get(0);
pool.remove(conn);
return conn;
} else {
return null;
}
}
}
通过连接池的管理,实现了数据库连接的共享,不需要每一次都重新创建连接,节省了数据库重新创建的开销,提升了系统的性能!