Thyme-设计模式三两例
一、UML简介:
类之间的关系
1、依赖关系
依赖(Dependency)关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。在代码中,某个类的方法通过局部变量、方法的参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责。
在 UML 类图中,依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类。如下是人与手机的关系图,人通过手机的语音传送方法打电话。
2、关联关系
关联(Association)关系是对象之间的一种引用关系,用于表示一类对象与另一类对象之间的联系,如老师和学生、师傅和徒弟、丈夫和妻子等。关联关系是类与类之间最常用的一种关系,分为一般关联关系、聚合关系和组合关系。我们先介绍一般关联。
关联可以是双向的,也可以是单向的。在 UML 类图中,双向的关联可以用带两个箭头或者没有箭头的实线来表示,单向的关联用带一个箭头的实线来表示,箭头从使用类指向被关联的类。也可以在关联线的两端标注角色名,代表两种不同的角色。
在代码中通常将一个类的对象作为另一个类的成员变量来实现关联关系。如下是老师和学生的关系图,每个老师可以教多个学生,每个学生也可向多个老师学,他们是双向关联。
3、聚合关系
聚合(Aggregation)关系是关联关系的一种,是强关联关系,是整体和部分之间的关系,是 has-a 的关系。
聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。例如,学校与老师的关系,学校包含老师,但如果学校停办了,老师依然存在。
在 UML 类图中,聚合关系可以用带空心菱形的实线来表示,菱形指向整体。如下是大学和教师的关系图。
4、组合关系
组合(Composition)关系也是关联关系的一种,也表示类之间的整体与部分的关系,但它是一种更强烈的聚合关系,是 cxmtains-a 关系。
在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在。例如,头和嘴的关系,没有了头,嘴也就不存在了。
在 UML 类图中,组合关系用带实心菱形的实线来表示,菱形指向整体。如下是头和嘴的关系图。
5、泛华关系
泛化(Generalization)关系是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系,是一种继承关系,是 is-a 的关系。
在 UML 类图中,泛化关系用带空心三角箭头的实线来表示,箭头从子类指向父类。在代码实现时,使用面向对象的继承机制来实现泛化关系。例如,Student 类和 Teacher 类都是 Person 类的子类,其类图如下所示。
6、实现关系
实现(Realization)关系是接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作。
在 UML 类图中,实现关系使用带空心三角箭头的虚线来表示,箭头从实现类指向接口。例如,汽车和船实现了交通工具,其类图如下所示。
二、软件设计七大原则:
依赖倒置原则在“顾客购物程序”中的应用。
分析:本程序反映了 “顾客类”与“商店类”的关系。商店类中有 sell() 方法,顾客类通过该方法购物以下代码定义了顾客类通过韶关网店 ShaoguanShop 购物:
class Customer { public void shopping(ShaoguanShop shop) { //购物 System.out.println(shop.sell()); } }
但是,这种设计存在缺点,如果该顾客想从另外一家商店(如婺源网店 WuyuanShop)购物,就要将该顾客的代码修改如下:
class Customer { public void shopping(WuyuanShop shop) { //购物 System.out.println(shop.sell()); } }
代码如下:
package principle; public class DIPtest { public static void main(String[] args) { Customer wang = new Customer(); System.out.println("顾客购买以下商品:"); wang.shopping(new ShaoguanShop()); wang.shopping(new WuyuanShop()); } } //商店 interface Shop { public String sell(); //卖 } //韶关网店 class ShaoguanShop implements Shop { public String sell() { return "韶关土特产:香菇、木耳……"; } } //婺源网店 class WuyuanShop implements Shop { public String sell() { return "婺源土特产:绿茶、酒糟鱼……"; } } //顾客 class Customer { public void shopping(Shop shop) { //购物 System.out.println(shop.sell()); } }
实际上,这些原则的目的只有一个:降低对象之间的耦合,增加程序的可复用性、可扩展性和可维护性。
三、设计模式的分类
1. 根据目的来分
根据模式是用来完成什么工作来划分,这种方式可分为创建型模式、结构型模式和行为型模式 3 种。
- 创建型模式:用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。GoF 中提供了单例、原型、工厂方法、抽象工厂、建造者等 5 种创建型模式。
- 结构型模式:用于描述如何将类或对象按某种布局组成更大的结构,GoF 中提供了代理、适配器、桥接、装饰、外观、享元、组合等 7 种结构型模式。
- 行为型模式:用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎样分配职责。GoF 中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等 11 种行为型模式。
2. 根据作用范围来分
根据模式是主要用于类上还是主要用于对象上来分,这种方式可分为类模式和对象模式两种。
- 类模式:用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时刻便确定下来了。GoF中的工厂方法、(类)适配器、模板方法、解释器属于该模式。
- 对象模式:用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。GoF 中除了以上 4 种,其他的都是对象模式。
四、设计模式二三例
1、建造者模式
在软件开发过程中有时需要创建一个复杂的对象,这个复杂对象通常由多个子部件按一定的步骤组合而成。例如,计算机是由 CPU、主板、内存、硬盘、显卡、机箱、显示器、键盘、鼠标等部件组装而成的,采购员不可能自己去组装计算机,而是将计算机的配置要求告诉计算机销售公司,计算机销售公司安排技术人员去组装计算机,然后再交给要买计算机的采购员。
生活中这样的例子很多,如游戏中的不同角色,其性别、个性、能力、脸型、体型、服装、发型等特性都有所差异;还有汽车中的方向盘、发动机、车架、轮胎等部件也多种多样;每封电子邮件的发件人、收件人、主题、内容、附件等内容也各不相同。
以上所有这些产品都是由多个部件构成的,各个部件可以灵活选择,但其创建步骤都大同小异。这类产品的创建无法用前面介绍的工厂模式描述,只有建造者模式可以很好地描述该类产品的创建。
模式的定义与特点
建造者(Builder)模式的定义:指将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,这样的设计模式被称为建造者模式。它是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。它将变与不变相分离,即产品的组成部分是不变的,但每一部分是可以灵活选择的。
该模式的主要优点如下:
- 封装性好,构建和表示分离。
- 扩展性好,各个具体的建造者相互独立,有利于系统的解耦。
- 客户端不必知道产品内部组成的细节,建造者可以对创建过程逐步细化,而不对其它模块产生任何影响,便于控制细节风险。
其缺点如下:
- 产品的组成部分必须相同,这限制了其使用范围。
- 如果产品的内部变化复杂,如果产品内部发生变化,则建造者也要同步修改,后期维护成本较大。
模式的结构与实现
建造者(Builder)模式由产品、抽象建造者、具体建造者、指挥者等 4 个要素构成,现在我们来分析其基本结构和实现方法。
1. 模式的结构
建造者(Builder)模式的主要角色如下。
- 产品角色(Product):它是包含多个组成部件的复杂对象,由具体建造者来创建其各个零部件。
- 抽象建造者(Builder):它是一个包含创建产品各个子部件的抽象方法的接口,通常还包含一个返回复杂产品的方法 getResult()。
- 具体建造者(Concrete Builder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。
- 指挥者(Director):它调用建造者对象中的部件构造与装配方法完成复杂对象的创建,在指挥者中不涉及具体产品的信息。
其结构图如图 1 所示。
2. 模式的实现
图 1 给出了建造者(Builder)模式的主要结构,其相关类的代码如下。
(1) 产品角色:包含多个组成部件的复杂对象。
1 class Product { 2 private String partA; 3 private String partB; 4 private String partC; 5 public void setPartA(String partA) { 6 this.partA = partA; 7 } 8 public void setPartB(String partB) { 9 this.partB = partB; 10 } 11 public void setPartC(String partC) { 12 this.partC = partC; 13 } 14 public void show() { 15 //显示产品的特性 16 } 17 }
(2) 抽象建造者:包含创建产品各个子部件的抽象方法。
1 abstract class Builder { 2 //创建产品对象 3 protected Product product = new Product(); 4 public abstract void buildPartA(); 5 public abstract void buildPartB(); 6 public abstract void buildPartC(); 7 //返回产品对象 8 public Product getResult() { 9 return product; 10 } 11 }
(3) 具体建造者:实现了抽象建造者接口。
1 public class ConcreteBuilder extends Builder { 2 public void buildPartA() { 3 product.setPartA("建造 PartA"); 4 } 5 6 public void buildPartB() { 7 product.setPartB("建造 PartB"); 8 } 9 10 public void buildPartC() { 11 product.setPartC("建造 PartC"); 12 } 13 }
(4) 指挥者:调用建造者中的方法完成复杂对象的创建。
1 class Director { 2 private Builder builder; 3 4 public Director(Builder builder) { 5 this.builder = builder; 6 } 7 8 //产品构建与组装方法 9 public Product construct() { 10 builder.buildPartA(); 11 builder.buildPartB(); 12 builder.buildPartC(); 13 return builder.getResult(); 14 } 15 }
(5) 客户类。
1 public class Client { 2 public static void main(String[] args) { 3 Builder builder = new ConcreteBuilder(); 4 Director director = new Director(builder); 5 Product product = director.construct(); 6 product.show(); 7 } 8 }
模式的应用实例
StringBuilder源码分析
在jdk中StringBuilder类的实现中,采用建造者模式的思想。具体分析如下:
StringBuilder类继承自AbstractStringBuilder,而AbstractStringBuilder实现了Appendable接口。AbstractStringBuilder虽然是一个抽象类,但是它实现了Appendable接口中的各个append()方法,因此在这里Appendable接口是一个抽象建造者,而AbstractStringBuilder是建造者,只是不能实例化。对于StringBuilder类,它既充当了指挥者角色,同时充当了具体的建造者,建造方法的具体实现是由AbstractStringBuilder完成,StringBuilder继承了AbstractStringBuilder。
Appendable接口:
1 public interface Appendable { 2 Appendable append(CharSequence csq) throws IOException; 3 Appendable append(CharSequence csq, int start, int end) throws IOException; 4 Appendable append(char c) throws IOException; 5 }
AbstractStringBuilder类:
1 abstract class AbstractStringBuilder implements Appendable, CharSequence { 2 3 char[] value;//The value is used for character storage. 4 int count;//The count is the number of characters used. 5 6 AbstractStringBuilder() { } 7 8 AbstractStringBuilder(int capacity) { 9 value = new char[capacity]; 10 } 11 12 /** 13 * Appends the specified string to this character sequence. 14 *80 * 81 * @param srcBegin index of the first character in the string 82 * to copy. 83 * @param srcEnd index after the last character in the string 84 * to copy. 85 * @param dst the destination array. 86 * @param dstBegin the start offset in the destination array. 87 * @exception IndexOutOfBoundsException If any of the following 88 * is true: 89 *15 * The characters of the {@code String} argument are appended, in 16 * order, increasing the length of this sequence by the length of the 17 * argument. If {@code str} is {@code null}, then the four 18 * characters {@code "null"} are appended. 19 *
20 * Let n be the length of this character sequence just prior to 21 * execution of the {@code append} method. Then the character at 22 * index k in the new character sequence is equal to the character 23 * at index k in the old character sequence, if k is less 24 * than n; otherwise, it is equal to the character at index 25 * k-n in the argument {@code str}. 26 * 27 * @param str a string. 28 * @return a reference to this object. 29 */ 30 public AbstractStringBuilder append(String str) { 31 if (str == null) 32 return appendNull(); 33 int len = str.length(); 34 ensureCapacityInternal(count + len); 35 str.getChars(0, len, value, count); 36 count += len; 37 return this; 38 } 39 40 private AbstractStringBuilder appendNull() { 41 int c = count; 42 ensureCapacityInternal(c + 4); 43 final char[] value = this.value; 44 value[c++] = 'n'; 45 value[c++] = 'u'; 46 value[c++] = 'l'; 47 value[c++] = 'l'; 48 count = c; 49 return this; 50 } 51 52 /** 53 * For positive values of {@code minimumCapacity}, this method 54 * behaves like {@code ensureCapacity}, however it is never 55 * synchronized. 56 * If {@code minimumCapacity} is non positive due to numeric 57 * overflow, this method throws {@code OutOfMemoryError}. 58 */ 59 private void ensureCapacityInternal(int minimumCapacity) { 60 // overflow-conscious code 61 if (minimumCapacity - value.length > 0) { 62 value = Arrays.copyOf(value, 63 newCapacity(minimumCapacity)); 64 } 65 } 66 67 /** 68 * Copies characters from this string into the destination character 69 * array. 70 *
71 * The first character to be copied is at index {@code srcBegin}; 72 * the last character to be copied is at index {@code srcEnd-1} 73 * (thus the total number of characters to be copied is 74 * {@code srcEnd-srcBegin}). The characters are copied into the 75 * subarray of {@code dst} starting at index {@code dstBegin} 76 * and ending at index: 77 *
78 * dstBegin + (srcEnd-srcBegin) - 1 79 *
- {
StringBuilder类
public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence { //虽说是重写,但还是调用的AbstractStringBuilder方法 @Override public StringBuilder append(String str) { super.append(str); return this; } }
2、桥接模式
在现实生活中,某些类具有两个或多个维度的变化,如图形既可按形状分,又可按颜色分。如何设计类似于 Photoshop 这样的软件,能画不同形状和不同颜色的图形呢?如果用继承方式,m 种形状和 n 种颜色的图形就有 m×n 种,不但对应的子类很多,而且扩展困难。
当然,这样的例子还有很多,如不同颜色和字体的文字、不同品牌和功率的汽车、不同性别和职业的男女、支持不同平台和不同文件格式的媒体播放器等。如果用桥接模式就能很好地解决这些问题。
桥接模式的定义与特点
桥接(Bridge)模式的定义如下:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
通过上面的讲解,我们能很好的感觉到桥接模式遵循了里氏替换原则和依赖倒置原则,最终实现了开闭原则,对修改关闭,对扩展开放。这里将桥接模式的优缺点总结如下。
桥接(Bridge)模式的优点是:
- 抽象与实现分离,扩展能力强
- 符合开闭原则
- 符合合成复用原则
- 其实现细节对客户透明
缺点是:由于聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,能正确地识别出系统中两个独立变化的维度,这增加了系统的理解与设计难度。
桥接模式的结构与实现
可以将抽象化部分与实现化部分分开,取消二者的继承关系,改用组合关系。
1. 模式的结构
桥接(Bridge)模式包含以下主要角色。
- 抽象化(Abstraction)角色:定义抽象类,并包含一个对实现化对象的引用。
- 扩展抽象化(Refined Abstraction)角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
- 实现化(Implementor)角色:定义实现化角色的接口,供扩展抽象化角色调用。
- 具体实现化(Concrete Implementor)角色:给出实现化角色接口的具体实现。
其结构图如图 1 所示。
2. 模式的实现
桥接模式的代码如下:
1 package bridge; 2 3 public class BridgeTest { 4 public static void main(String[] args) { 5 Implementor imple = new ConcreteImplementorA(); 6 Abstraction abs = new RefinedAbstraction(imple); 7 abs.Operation(); 8 } 9 } 10 11 //实现化角色 12 interface Implementor { 13 public void OperationImpl(); 14 } 15 16 //具体实现化角色 17 class ConcreteImplementorA implements Implementor { 18 public void OperationImpl() { 19 System.out.println("具体实现化(Concrete Implementor)角色被访问"); 20 } 21 } 22 23 //抽象化角色 24 abstract class Abstraction { 25 protected Implementor imple; 26 27 protected Abstraction(Implementor imple) { 28 this.imple = imple; 29 } 30 31 public abstract void Operation(); 32 } 33 34 //扩展抽象化角色 35 class RefinedAbstraction extends Abstraction { 36 protected RefinedAbstraction(Implementor imple) { 37 super(imple); 38 } 39 40 public void Operation() { 41 System.out.println("扩展抽象化(Refined Abstraction)角色被访问"); 42 imple.OperationImpl(); 43 } 44 }
桥接模式的应用实例
百度百科对JDBC是这样介绍的【JDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成。JDBC提供了一种基准,据此可以构建更高级的工具和接口,使数据库开发人员能够编写数据库应用程序
java.sql包不同于其他几乎所有的包,它基本可以说没有定义多少类,但是定义了大量的接口,而由各个数据库公司去写这些接口的实现类。
仅仅是提出了一系列接口规范,数据库公司做出实现,然后当你真的需要连接某个数据库时,你需要先添加数据库公司发布的jar包,如jdbc-mysql.jar,然后在使用时先加载Driver:
1 try { 2 Class.forName("com.mysql.jdbc.Driver"); 3 } catch (ClassNotFoundException e) { 4 System.out.println("找不到驱动程序类 ,加载驱动失败!"); 5 e.printStackTrace(); 6 }
然后就可以通过DriverManager去获取连接:
1 try { 2 conn = (Connection) DriverManager.getConnection(url , username , password ); 3 } catch (SQLException e) { 4 System.out.println("数据库连接失败!"); 5 e.printStackTrace(); 6 }
这里的conn的定义依旧是以java.sql中定义的接口来声明的,也就是说,我们在代码中依旧使用的是接口,只不过通过了最初的加载Driver让添加的jar包中的类成为了真正的实现
Class.forName可以直接加载类中的静态代码块,然后我们来看下com.mysql.jdbc.Driver类:
1 static 2 { 3 try 4 { 5 DriverManager.registerDriver(new Driver()); 6 } 7 catch(SQLException E) 8 { 9 throw new RuntimeException("Can't register driver!"); 10 } 11 }
类在被Class.forName的时候就加载了这一块代码块,将Driver类本身的对象注册进入DriverManager
1 /** 2 * Registers the given driver with the {@code DriverManager}. 3 * A newly-loaded driver class should call 4 * the method {@code registerDriver} to make itself 5 * known to the {@code DriverManager}. If the driver is currently 6 * registered, no action is taken. 7 * 8 * @param driver the new JDBC Driver that is to be registered with the 9 * {@code DriverManager} 10 * @param da the {@code DriverAction} implementation to be used when 11 * {@code DriverManager#deregisterDriver} is called 12 * @exception SQLException if a database access error occurs 13 * @exception NullPointerException if {@code driver} is null 14 * @since 1.8 15 */ 16 public static synchronized void registerDriver(java.sql.Driver driver, 17 DriverAction da) 18 throws SQLException { 19 20 /* Register the driver if it has not already been added to our list */ 21 if(driver != null) { 22 registeredDrivers.addIfAbsent(new DriverInfo(driver, da)); 23 } else { 24 // This is for compatibility with the original DriverManager 25 throw new NullPointerException(); 26 } 27 28 println("registerDriver: " + driver); 29 30 }
可以看到注册的过程无非是把Driver对象存放进入了变量registeredDrivers中,这个变量原本是个List
而当我们getConnection的时候
1 // Worker method called by the public getConnection() methods. 2 private static Connection getConnection( 3 String url, java.util.Properties info, Class<?> caller) throws SQLException { 4 /* 5 * When callerCl is null, we should check the application's 6 * (which is invoking this class indirectly) 7 * classloader, so that the JDBC driver class outside rt.jar 8 * can be loaded from here. 9 */ 10 ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; 11 synchronized(DriverManager.class) { 12 // synchronize loading of the correct classloader. 13 if (callerCL == null) { 14 callerCL = Thread.currentThread().getContextClassLoader(); 15 } 16 } 17 18 if(url == null) { 19 throw new SQLException("The url cannot be null", "08001"); 20 } 21 22 println("DriverManager.getConnection(\"" + url + "\")"); 23 24 // Walk through the loaded registeredDrivers attempting to make a connection. 25 // Remember the first exception that gets raised so we can reraise it. 26 SQLException reason = null; 27 28 for(DriverInfo aDriver : registeredDrivers) { 29 // If the caller does not have permission to load the driver then 30 // skip it. 31 if(isDriverAllowed(aDriver.driver, callerCL)) { 32 try { 33 println(" trying " + aDriver.driver.getClass().getName()); 34 Connection con = aDriver.driver.connect(url, info); 35 if (con != null) { 36 // Success! 37 println("getConnection returning " + aDriver.driver.getClass().getName()); 38 return (con); 39 } 40 } catch (SQLException ex) { 41 if (reason == null) { 42 reason = ex; 43 } 44 } 45 46 } else { 47 println(" skipping: " + aDriver.getClass().getName()); 48 } 49 50 } 51 52 // if we got here nobody could connect. 53 if (reason != null) { 54 println("getConnection failed: " + reason); 55 throw reason; 56 } 57 58 println("getConnection: no suitable driver found for "+ url); 59 throw new SQLException("No suitable driver found for "+ url, "08001"); 60 }
他将会遍历所有注册的driver,并从driver加载connection,
回到com.mysql.jdbc.Driver,我们可以看到它继承了NonRegisteringDriver类,然后这个类中实现了connect方法:
1 public Connection connect(String url, Properties info) 2 throws SQLException 3 { 4 if(url != null) 5 { 6 if(StringUtils.startsWithIgnoreCase(url, "jdbc:mysql:loadbalance://")) 7 return connectLoadBalanced(url, info); 8 if(StringUtils.startsWithIgnoreCase(url, "jdbc:mysql:replication://")) 9 return connectReplicationConnection(url, info); 10 } 11 Properties props = null; 12 if((props = parseURL(url, info)) == null) 13 return null; 14 if(!"1".equals(props.getProperty("NUM_HOSTS"))) 15 return connectFailover(url, info); 16 try 17 { 18 com.mysql.jdbc.Connection newConn = ConnectionImpl.getInstance(host(props), port(props), props, database(props), url); 19 return newConn; 20 } 21 catch(SQLException sqlEx) 22 { 23 throw sqlEx; 24 } 25 catch(Exception ex) 26 { 27 SQLException sqlEx = SQLError.createSQLException((new StringBuilder()).append(Messages.getString("NonRegisteringDriver.17")).append(ex.toString()).append(Messages.getString("NonRegisteringDriver.18")).toString(), "08001", null); 28 sqlEx.initCause(ex); 29 throw sqlEx; 30 } 31 }
至此整个加载流程应该已经清楚了,在Class.forName的过程中执行了静态代码块,在静态代码块中注册了Driver对象,在getConnection中取出注册的Driver并调用其connect方法,在mysql中connect方法的返回值是com.mysql.jdbc.Connection类的对象了,但是我们在调用过程中依旧是用其接口定义,这里充分运用了面向对象多态的性质。
jdbc的类族设计是由sum公司设计了一套接口,再由各个数据库公司实现接口,我们在调用的过程中只需要使用接口去定义,然后在加载Driver的过程中底层代码会给我们选择好接口真正的实现类,以此来实现真正的数据库连接,此后所有的方法,包括获取statement等等,都是由接口声明调用,但是底层返回的是接口实现类。用这种桥接的模式,我们可以很轻松地在不同的数据库连接中进行转化,只需要修改Driver加载的类,如果把加载类的声明放入配置文件中,更是不需要重新去编译,可以很方便地在不同数据库间进行转化。
有两个维度 Driver和Connection,抽象类图如下:
3、命令模式
在软件开发系统中,“方法的请求者”与“方法的实现者”之间经常存在紧密的耦合关系,这不利于软件功能的扩展与维护。例如,想对方法进行“撤销、重做、记录”等处理都很不方便,因此“如何将方法的请求者与实现者解耦?”变得很重要,命令模式就能很好地解决这个问题。
在现实生活中,命令模式的例子也很多。比如看电视时,我们只需要轻轻一按遥控器就能完成频道的切换,这就是命令模式,将换台请求和换台处理完全解耦了。电视机遥控器(命令发送者)通过按钮(具体命令)来遥控电视机(命令接收者)。
再比如,我们去餐厅吃饭,菜单不是等到客人来了之后才定制的,而是已经预先配置好的。这样,客人来了就只需要点菜,而不是任由客人临时定制。餐厅提供的菜单就相当于把请求和处理进行了解耦,这就是命令模式的体现。
命令模式的定义与特点
命令(Command)模式的定义如下:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理。
命令模式的主要优点如下。
- 通过引入中间件(抽象接口)降低系统的耦合度。
- 扩展性良好,增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,且满足“开闭原则”。
- 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
- 方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。
- 可以在现有命令的基础上,增加额外功能。比如日志记录,结合装饰器模式会更加灵活。
其缺点是:
- 可能产生大量具体的命令类。因为每一个具体操作都需要设计一个具体命令类,这会增加系统的复杂性。
- 命令模式的结果其实就是接收方的执行结果,但是为了以命令的形式进行架构、解耦请求与实现,引入了额外类型结构(引入了请求方与抽象命令接口),增加了理解上的困难。不过这也是设计模式的通病,抽象必然会额外增加类的数量,代码抽离肯定比代码聚合更加难理解。
命令模式的结构与实现
可以将系统中的相关操作抽象成命令,使调用者与实现者相关分离,其结构如下。
1. 模式的结构
命令模式包含以下主要角色。
- 抽象命令类(Command)角色:声明执行命令的接口,拥有执行命令的抽象方法 execute()。
- 具体命令类(Concrete Command)角色:是抽象命令类的具体实现类,它拥有接收者对象,并通过调用接收者的功能来完成命令要执行的操作。
- 实现者/接收者(Receiver)角色:执行命令功能的相关操作,是具体命令对象业务的真正实现者。
- 调用者/请求者(Invoker)角色:是请求的发送者,它通常拥有很多的命令对象,并通过访问命令对象来执行相关请求,它不直接访问接收者。
其结构图如图 1 所示。
2. 模式的实现
命令模式的代码如下:
1 package command; 2 3 public class CommandPattern { 4 public static void main(String[] args) { 5 Command cmd = new ConcreteCommand(); 6 Invoker ir = new Invoker(cmd); 7 System.out.println("客户访问调用者的call()方法..."); 8 ir.call(); 9 } 10 } 11 12 //调用者 13 class Invoker { 14 private Command command; 15 16 public Invoker(Command command) { 17 this.command = command; 18 } 19 20 public void setCommand(Command command) { 21 this.command = command; 22 } 23 24 public void call() { 25 System.out.println("调用者执行命令command..."); 26 command.execute(); 27 } 28 } 29 30 //抽象命令 31 interface Command { 32 public abstract void execute(); 33 } 34 35 //具体命令 36 class ConcreteCommand implements Command { 37 private Receiver receiver; 38 39 ConcreteCommand() { 40 receiver = new Receiver(); 41 } 42 43 public void execute() { 44 receiver.action(); 45 } 46 } 47 48 //接收者 49 class Receiver { 50 public void action() { 51 System.out.println("接收者的action()方法被调用..."); 52 } 53 }
命令模式的应用实例
Runnable是一个典型命令模式,
Runnable担当命令的角色,Thread充当的是调用者,start方法就是其执行方法
1 /命令接口(抽象命令角色) 2 public interface Runnable { 3 public abstract void run(); 4 } 5 ? 6 //调用者 7 public class Thread implements Runnable { 8 private Runnable target; 9 10 public synchronized void start() { 11 if (threadStatus != 0) 12 throw new IllegalThreadStateException(); 13 ? 14 group.add(this); 15 ? 16 boolean started = false; 17 try { 18 start0(); 19 started = true; 20 } finally { 21 try { 22 if (!started) { 23 group.threadStartFailed(this); 24 } 25 } catch (Throwable ignore) { 26 } 27 } 28 } 29 30 private native void start0(); 31 }
会调用一个native方法start0(),调用系统方法,开启一个线程。
而接收者是对程序员开放的,可以自己定义接收者。
1 /** 2 * jdk Runnable 命令模式 3 * TurnOffThread : 属于具体 4 */ 5 public class TurnOffThread implements Runnable{ 6 private Receiver receiver; 7 8 public TurnOffThread(Receiver receiver) { 9 this.receiver = receiver; 10 } 11 public void run() { 12 receiver.turnOFF(); 13 } 14 }
1 ** 2 * 测试类 3 */ 4 public class Demo { 5 public static void main(String[] args) { 6 Receiver receiver = new Receiver(); 7 TurnOffThread turnOffThread = new TurnOffThread(receiver); 8 Thread thread = new Thread(turnOffThread); 9 thread.start(); 10 } 11 }Appendable