面向对象设计

文章目录
  1. 面向对象
    1. 封装
    2. 继承(extends)
    3. 多态
  2. UML
  3. 设计原则
  4. 设计模式
    1. 创建型模式
    2. 结构型模式
    3. 行为型模式

在之前并没有系统的学习过面向对象的程序设计, 最近在读面向对象设计的书, 一直在听说设计模式这个词, 听起来就很炫酷, 准备每天学习一种设计模式, 采用Java语言, 参考书为设计模式之禅(第二版) , 参考网站为 图说设计模式

首先面向对象的语言的核心就是可扩展性, 那么设计原则的根本目的就是保证程序可拓展性, 设计模式就是提高程序可拓展性, 设计原则和设计模式是互补的关系, 设计原则主要指导类的定义, 而设计模式主要指导类的行为

对于”面向对象”这个概念初学的时候总是跟语言联系在一起, 这种想法是不对的, 面向对象更应该理解为一种编程思维, 也就是说C语言依然可以写成面向对象的, Java也可以写成面向过程的, 是否面向对象这取决于你的编码思维, 世间的万物皆为对象, 即万物都有属性和动作, 接下来我们看看怎样在程序中抽象对象

面向对象

类与对象

对于刚刚接触面向对象语言的新手来说,理解面向对象的概念就是一个门槛,感觉模模糊糊地似懂非懂,其实有时候读偏哲学的书的时候也会给你同样的感觉。世间万物都可以用两个维度来描述,一个是它本身所具有的属性,另一个是本身可以发生的动作,二者结合起来就是一个对象。我们把生物世界分成不同的物种,是因为寻找到了相同物种之间的共同之处,寻找的过程就是一个抽象的过程,举个例子,把不同品种的猫都叫做猫,猫就是一个类,而特定的某一品种的猫就是一个对象,因为他们有共同的属性可以发出相同的动作,而这个品种具体的一只活生生的猫就是这个对象(猫)的实例。对象一定是抽象出来的,而这种设计哲学更加的抽象出了现实世界。

  • 类是一个抽象的概念, 而对象是通过new操作产生的抽象类的一个实例, 可以把对象理解为一个具象的概念.下面比较二者不同:
名称 概念 存放位置
抽象 堆栈(对象引用)
对象 具象 堆内存(对象本身)
  • 为了提高效率, Java的基本类型int,char,boolen等是存放在堆栈中的, 也可以通过包装器类new到堆内存中.

面向对象类

那么我们怎样抽象一个类, 怎样区分属性和方法呢?

对于一种事物, 名词是属性, 动词是方法

  • 设计属性原则 : 属性不可再细分
  • 设计方法原则 : 一个方法只做一件事情

所有面向对象的特性来源于两点:效率设计

接口(interface)

  • 功能 : 不知道一个对象所属的具体类, 只知道这些对象都具备某种功能, 它是一组相关的交互功能点定义的集合
  • 通过实现(implements)接口定义具体的功能

抽象类(abstract)

  • 功能 : 只能用于继承,不能被实例化为具体的类,比如在Java中不能new一个抽象类的实例,但是可以extend
  • 如果普通类是对现实对象的高层次抽象, 那么抽象类就是对普通类的高层次抽象

接口不同的是, 抽象类本质上还是类, 是属性和方法的具有相似性的普通类的一种高层抽象; 但是接口只是定义了方法声明上的相似性, 而没有方法定义上的相似性

封装

  • 把数据和方法包装进类中 , 以及把具体实现的过程隐藏起来 , 成为一个同时带有特征和行为的数据类型
  • 使用封装保护了数据的隐私, 在封装特性中, 对外提供接口,屏蔽数据,对内开放数据。

继承(extends)

  • 继承就是继承了父类的属性和方法, 提高了代码的复用率, 同时对于子类的改写提高了可扩展性, 也正是必须继承父类的属性和方法增强了耦合性在改写父类时要考虑子类
  • 抽象和继承是前后衔接的关系, 先有抽象, 通过抽象得出类, 后通过继承来表达抽象结果

重载Overlord: 函数重载在设计上可以达到简化编程的目的

  • 重载规则:
    1. 函数名相同
    2. 参数个数不同或者参数类型不同或者参数顺序不同, 至少满足其一均可构成重载
    3. 返回值类型不同并不可以构成重载, 因为有时候我们并不关心函数的返回值, 实现功能就可以
  • 重写Override:在继承了父类的方法后, 要重写一个函数, 函数名和参数必须与原函数一致

多态

  • 在面向对象领域中, 多态的真正含义是 : 使用指向父类的指针或者引用, 能够调用子类的对象
  • 多态屏蔽子类对象的差异, 可以使得调用者写出通用性的代码, 而无需对每个子类来写不同的代码
  • 当增加新的子类时候, 调用者无需变动就能适应新的子类

技术流程

在了解了面向对象的基本理论后, 我们来看一看项目的过程:

需求模型 $\to$ 领域模型 $\to$ 设计模型 $\to$ 实现模型

UML

UML是统一建模语言(Unified Modeling Language)的缩写, UML是使得表达程序设计更加形象生动

实例

  • 是一个抽象类;它有两个继承类:小汽车和自行车;它们之间的关系为实现关系,使用带空心箭头的虚线表示;
  • 小汽车为与SUV之间也是继承关系,它们之间的关系为泛化关系,使用带空心箭头的实线表示;
  • 小汽车与发动机之间是组合关系,使用带实心箭头的实线表示;
  • 学生与班级之间是聚合关系,使用带空心箭头的实线表示;
  • 学生与身份证之间为关联关系,使用一根实线表示;
  • 学生上学需要用到自行车,与自行车是一种依赖关系,使用带箭头的虚线表示;

泛化关系(generalization) : 子类继承某个非抽象类, 本质上就是类的继承

  • UML表示 : 带空心三角形的连接实线

实现关系(realize) : 将抽象类实现为具体的某个子类, 是继承的一种特殊实现

  • UML表示 : 带空心三角形的连接虚线

关联关系(association) : 最弱的一种联系, 通常用排除法确定

依赖关系(dependency) : 比关联更强的一种关系, 一个类依赖另一个类, 就意味着一旦被依赖的类改变, 则依赖类也要改变, 依赖的形式是方法调用/数据访问/对象传递

依赖关系可以形象的描述为没有你我寸步难行, 聚合和组合则是没有你我将不复存在, 描述的是一种整体和部分的关系

聚合关系(aggregation) : 描述has a的关系, 就是某个类包含了另外一个类, 但并不负责被包含类的维护(在Java中指的是创建对象实例), 且两个类的生命周期不一样, 整体消失后部分还能继续存在

组合关系(composition) : 描述owns a的关系, 就是某个类包含了另外一个类, 但并负责被包含类的维护, 且两个类的生命周期一样, 整体消失后部分不能继续存在, 组合是一种强耦合关系, 具有相同的生命周期

+代表public; -代表private; #代表protect

设计原则

内聚

内聚指一个模块内部元素彼此结合的紧密程度, 可以判断一个模块(函数/类/接口/包/子系统)的设计是否合理, 是指一个模块内部元素彼此结合的紧密程度, 那么什么是元素呢?

  • 函数的元素是代码
  • 类/接口的元素是属性方法
  • 的元素是类/接口/全局数据
  • 模块的元素是包/命名空间

在判断一个模块(函数/类/接口/包/子系统)的内聚性的高低, 最重要的是关注元素是否是同一职责, 所谓的高内聚也就是说尽量一个类里的函数都只依赖于本类中的其他函数

内聚分类

  • 逻辑内聚 : 从逻辑上划分内聚的高低, 但是这种逻辑并不可控, 比如IO设备(输入设备/输出设备)
  • 时间内聚 : 在时间的顺序上具有相似性, 元素间的顺序是不固定的
  • 过程内聚 : 元素必须按照固定的处理顺序进行, 不能随意调整
  • 信息内聚 : 所有元素都操作相同的数据
  • 顺序内聚 : 某些元素的输出是另外元素的输入
  • 功能内聚 : 所有元素都是为了同一个任务, 缺一不可

耦合

耦合是程序模块之间的依赖程度, 而内聚关注的是模块内部元素间的结合程度, 这里的模块可大可小, 可以指函数/类/接口/包/子系统

所谓依赖指的是某个模块用到了另外一个模块的一些元素, 元素可大可小

耦合分类

  • 消息耦合 : 消息随模块的不同而不同, 比如子系统的消息是系统交互接口, 类的消息是类的方法, 消息耦合是耦合度很低的, 因为调用方仅仅依赖被调用方的消息, 既不需要传参数也不需要了解被调用方的内部逻辑, 更不需要控制被调用方的内部逻辑, 只是单纯的调用关系

  • 数据耦合 : 两个模块间通过参数传递(而不是通过全局数据/配置文件/共享内存等其他方式)基本数据类型(int/double/String等类型)

  • 数据结构耦合 : 两个模块间通过传递数据结构的方式传递数据, 在传递的数据结构中的成员数据并不需要每一个都用到, 可以只用到一部分

  • 控制耦合 : 一个模块通过某种方式来控制另一个模块的行为, 比如通过传入一个控制参数来控制函数的处理流程和输出, 工厂模式

  • 外部耦合 : 两个模块之间没有直接的交互, 而是依赖相同的外部数据格式/通信协议/设备接口等完成分工合作

  • 全局耦合 : 两个模块共享相同的全局数据

  • 内容耦合 : 一个模块依赖另一个模块的数据成员(public属性), 也就是说一个模块的方法操作了另一个模块的数据成员, 完全破坏了模块的封装性

为什么要高内聚低耦合?

这条原则主要是降低模块的复杂性, 增加模块的稳定性, 反向思维来理解这个问题

  • 低内聚的模块意味着在对类的认识上十分困难, 因为低内聚意味着类中的方法相似性很低, 无论是在逻辑或者功能上的相似性, 在阅读代码时候很难理解, 同时一旦类发生变化, 设计/编码/测试/编译的工作量就增大很多;

  • 高耦合意味着模块间的依赖很多, 这样依赖的其他模块任何一个发生变化, 该模块都必须做出修改, 同样使得后续设计/编码/测试/编译的工作量增大很多

牵一发而动全身意味着模块的稳定性很差, 这样的不稳定性导致产品和成本带来很大的风险

注意, 并不是内聚越高越好, 耦合越低越好, 因为高内聚和低耦合是相互矛盾的

为什么高内聚和低耦合是相互矛盾的?

  • 对于内聚来说, 最高的内聚是一个模块只完成一项功能, 比如一个类只有一个方法, 但是会导致类的数量急剧增加, 导致模块间的耦合特别多
  • 对于耦合来说, 最低的耦合是一个模块完成所有的功能, 比如将所有方法全部写入一个类, 导致内聚非常低

所以, 真正好的设计是高内聚和低耦合间的平衡

单一职责

应该有且仅有一个原因引起类的变更, 就是说一个接口或者类只有一个职责, 对于面向对象来说, 一个类负责多个相关功能

好处

  • 类的复杂性降低使得可读性和可维护性提高
  • 变更引起的风险降低, 只对其实现类有影响, 对其它接口没有影响

但是职责的划分没有一个量化标准, 实际上我们很难做到单一职责, 单一职责导致实现类剧增, 反而增加了复杂性从而难以维护, 单一职责只适合那些基础类, 不适合基于基础类的复杂聚合类

里氏替换

只要父类出现的地方子类就可以出现, 并且将子类替换为父类也不会出现任何错误和异常, 使用者不需要知道是子类还是父类, 但是反过来有子类出现的地方父类未必能够替代子类

  • 该原则对继承定义了规范, 这样子类必须完全实现父类的方法, 父类是一个抽象类
  • 在使用子类的时候务必要使用抽象父类拥有的抽象方法, 否则就违背了里氏替换原则
  • 在子类不能完整的实现父子的业务, 或者父类的某些方法在子类中已经发生了畸变, 那么不使用继承, 而采用依赖/聚合/组合等关系
  • 向下转型是不安全的, 在里氏替换原则的角度看就是有子类出现的地方替换为父类未必可以

向上转型: Person p = new Man() ; //向上转型不需要强制类型转化
向下转型: Man man = (Man)new Person() ; //必须强制类型转化

如何判断是否符合里氏替换原则?

对于调用者来说, 和父类的交互无非两点, 调用父类中的方法得到父类方法的输出, 基于这两点如要符合里氏替换原则, 那么子类必须满足以下三点 :

  • 子类必须实现或者继承父类所有的public方法
  • 子类每个方法的输入参数必须和父类一样
  • 子类每个方法的输出必须不比父类少

参数放大问题

  • 在子类中, 重写或实现父类的方法时, 输入参数可以被放大, 就是子类中输入参数的范围大于父类参数的类型范围, 子类可以代替父类将参数传递到父类中定义的方法, 因为通过继承之后子类中定义的方法永远不会执行, 始终保证父类的方法被调用, 这样子类替换掉父类就不会出现问题, 所以子类中方法的前置条件必须与超类中被重写的方法的前置条件相同或者更宽 , 方法中输入的参数就是前置条件

依赖倒置

高层模块不应该依赖低层模块, 两者都依赖其接口或者抽象类, 实现类之间不发生直接的依赖关系, 通过接口或者抽象类产生依赖关系
接口或者抽象类不应该依赖实现类, 实现类应该依赖接口或者抽象类

依赖倒置原则的本质是通过抽象(抽象类或者接口)使得各个类或者模块实现彼此的独立, 实现模块间的松耦合, 遵照一下规则:

  1. 每个类尽量具有接口或者抽象类

  2. 变量的声明类型尽量是接口或者抽象类

  3. 任何类都不应该从具体类派生

  4. 尽量不要重写基类的方法

  5. 结合里氏替换原则

接口负责定义public属性和方法, 并声明与其他对象的依赖关系;
抽象类负责公共构造部分的实现;
实现类负责准确的实现业务逻辑;

接口隔离

接口尽量细化, 同时接口本身的方法尽量少, 而不是建立一个庞大的接口, 允许所及的客户端进行访问

  1. 接口尽量要小, 但是必须满足单一职责原则
  2. 接口要高内聚

最少知识

一个对象应该对其他对象有最少的了解

  1. 如果一个方法放在本类中, 既不影响类间关系, 也对本类不产生负面影响, 那就放置在本类中

开闭原则

软件实体(模块/抽象/类/方法)应该通过扩展来实现变化, 而不是通过修改已有的代码来实现变化, 但并不意味着不做任何修改, 底层模块的变更必然要有高层模块进行耦合

  • 只变化一个逻辑, 不涉及其他模块的变化, 可以通过修改原有的方法来进行

设计模式

基于面向对象的特性寻找原型设计的通用方法, 使得程序的扩展性和效率都得以提高

创建型模式

单例模式 : 确保一个类只有一个实例, 而且自行实例化并向整个系统提供这个实例

  1. 某个类只能有一个实例
  2. 必须自行创建这个实例
  3. 必须自行向整个系统提供这个实例

  • 通过private的构造函数确保只有一个实例
  • 通过getInstance()对系统提供这个实例
  • 如何禁止一个非抽象类在类的外部使用new关键字实例化, 由于使用new关键字进行实例化的时候会调用相应的构造函数, 将构造函数设为private就可以了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton {
private static final Singleton singleton = new Singleton();
//限制产生多个对象
private Singleton(){

}
//通过该方法获得实例对象
public static Singleton getSingleton(){
return singleton;
}
//类中其他方法,尽量是static
public static void doSomething(){

}
}

优点

节约内存开支, 减少系统开销, 避免资源的多重占用, 优化和共享资源访问

  • PS : 线程安全是指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。

缺点

因为要自行实例化, 接口和抽象类是无法进行实例化的, 所以单例模式一般没有接口很难扩展

工厂方法模式 : 在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。

  • 抽象类Product负责定义产品的共性, 实现对事物最抽象的定义
  • 抽象类Factory负责定义创建某类的方法
  • 实现类ConcreteProduct负责实现具体的产品类
  • 实现类ConcreteFactory负责如何创建产品类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public abstract class Creator {
/*
* 创建一个产品对象,其输入参数类型可以自行设置
* 通常为String、Enum、Class等,当然也可以为空
*/
public abstract <T extends Product> T createProduct(Class<T> c);
}
public class ConcreteCreator extends Creator {
public <T extends Product> T createProduct(Class<T> c){
Product product=null;
try {
product = (Product)Class.forName(c.getName()).newInstance();
} catch (Exception e) {
//异常处理
}
return (T)product;
}
}
public abstract class Product {
//产品类的公共方法
public void method1(){
//业务逻辑处理
}
//抽象方法1
public abstract void method2();
}
public class ConcreteProduct extends Product {
public void method() {
//业务逻辑处理
}
}
public class ConcreteProduct2 extends Product {
public void method2() {
//业务逻辑处理
}
}

优势

  • 良好的封装性, 调用者需要一个产品的对象, 只要知道产品名就可以了, 降低模块之间的耦合

  • 优秀的扩展性, 在产品品种增加的情况下, 工厂类不用任何修改

  • 屏蔽产品类的修改, 无论产品类本身如何变化, 调用者不需要任何关心, 只要保证接口不变就可以

抽象工厂模式 : 提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类, 是所有形式的工厂模式中最为抽象和最具一般性的一种形态。

  • 产品等级结构 :产品等级结构即产品的继承结构,如一个抽象类是电视机,其子类有海尔电视机、海信电视机、TCL电视机,则抽象电视机与具体品牌的电视机之间构成了一个产品等级结构,抽象电视机是父类,而具体品牌的电视机是其子类。
  • 产品族 :在抽象工厂模式中,产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品,如海尔电器工厂生产的海尔电视机、海尔电冰箱,海尔电视机位于电视机产品等级结构中,海尔电冰箱位于电冰箱产品等级结构中。

  • AbstractFactory:抽象工厂
  • ConcreteFactory:具体工厂
  • AbstractProduct:抽象产品
  • Product:具体产品

优点

  • 封装性, 高层模块不关心实现类, 只关心接口
  • 产品族内部的约束关系非公开

结构型模式

适配器模式 : 将一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作

  • Target:目标抽象类
  • Adapter:适配器类
  • Adaptee:适配者类
  • Client:客户类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public interface Target {
public void request(); //目标角色有自己的方法
}
public class ConcreteTarget implements Target {
public void request() {
System.out.println("I have nothing to do. if you need any help,pls call me!"); }
}
public class Adaptee {
//原有的业务逻辑
public void doSomething(){
System.out.println("I'm kind of busy,leave me alone,pls!");
}
}
public class Adapter extends Adaptee implements Target {
public void request() {
super.doSomething();
}
}
public class Client {
public static void main(String[] args) {
//原有的业务逻辑
Target target = new ConcreteTarget();
target.request();
//现在增加了适配器角色后的业务逻辑
Target target2 = new Adapter();
target2.request();
}
}

扩展:

组合模式 : 将对象组合成树形结构以表示部分-整体的层次结构,使得用户对单个对象和组合对象的使用具有一致性

  • Component: 抽象构件角色定义参加组合对象的共有方法和属性,可以定义一些默认的行为或属性
  • Leaf: 叶子构件叶子对象,其下再也没有其他的分支,也就是遍历的最小单位。
  • Composite: 树枝构件树枝对象,它的作用是组合树枝节点和叶子节点形成一个树形结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public abstract class Component {	
//个体和整体都具有的共享
public void doSomething(){
//编写业务逻辑
}
public abstract void add(Component component); //增加一个叶子构件或树枝构件
public abstract void remove(Component component); //删除一个叶子构件或树枝构件
public abstract ArrayList<Component> getChildren(); //获得分支下的所有叶子构件和树枝构件
}
public class Composite extends Component {
private ArrayList<Component> componentArrayList = new ArrayList<Component>(); //构件容器
//增加一个叶子构件或树枝构件
public void add(Component component){
this.componentArrayList.add(component);
}
//删除一个叶子构件或树枝构件
public void remove(Component component){
this.componentArrayList.remove(component);
}
//获得分支下的所有叶子构件和树枝构件
public ArrayList<Component> getChildren(){
return this.componentArrayList;
}
}
public class Leaf extends Component {
@Deprecated
public void add(Component component) throws UnsupportedOperationException{
//空实现,直接抛弃一个“不支持请求”异常
throw new UnsupportedOperationException();
}
@Deprecated
public void remove(Component component)throws UnsupportedOperationException{
//空实现
throw new UnsupportedOperationException();
}
@Deprecated
public ArrayList<Component> getChildren()throws UnsupportedOperationException{
//空实现
throw new UnsupportedOperationException();
}
}
public class Client {
public static void main(String[] args) {
//创建一个根节点
Composite root = new Composite();
root.doSomething();
//创建一个树枝构件
Composite branch = new Composite();
//创建一个叶子节点
Leaf leaf = new Leaf();
//建立整体
root.add(branch);
branch.add(leaf);
}
//通过递归遍历树
public static void display(Component root){

for(Component c:root.getChildren()){
if(c instanceof Leaf){ //叶子节点
c.doSomething();
}else{ //树枝节点
display(c);
}
}

}
}

门面模式 : 外部与一个子系统的通信必须通过一个统一的门面对象进行

  • 门面模式提供一个高层次的接口,使得子系统更易于使用

  • 门面(Facade)角色 :客户端可以调用这个角色的方法。此角色知晓相关的(一个或者多个)子系统的功能和责任。在正常情况下,本角色会将所有从客户端发来的请求委派到相应的子系统去
  • 子系统(SubSystem)角色 :可以同时有一个或者多个子系统。每个子系统都不是一个单独的类,而是一个类的集合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ClassA {	
public void doSomethingA(){
//业务逻辑
}
}
public class ClassB {
public void doSomethingB(){
//业务逻辑
}
}
public class ClassC {
public void doSomethingC(){
//业务逻辑
}
}
public class Facade {
//被委托的对象
private ClassA a = new ClassA();
private ClassB b = new ClassB();
private ClassC c = new ClassC();
//提供给外部访问的方法
public void methodA(){
this.a.doSomethingA();
}
public void methodB(){
this.b.doSomethingB();
}
public void methodC(){
this.c.doSomethingC();
}
}
  • 一个子系统可以有多个门面
1
2
3
4
5
6
7
public class Facade2 {
private Facade facade = new Facade(); //引用原有的门面
//对外提供唯一的访问子系统的方法
public void methodB(){
this.facade.methodB();
}
}
  • 门面不参与子系统内的业务逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Facade {
//被委托的对象
private ClassA a = new ClassA();
private ClassB b = new ClassB();
private ClassC c = new ClassC();
//提供给外部访问的方法
public void methodA(){
this.a.doSomethingA();
}
public void methodB(){
this.b.doSomethingB();
}
public void methodC(){
this.a.doSomethingA();
this.c.doSomethingC();
}
}

代理模式 : 给某一个对象提供一个代理,并由代理对象控制对原对象的引用, Proxy Pattern

代理模式也叫委托模式, 是一种基本的设计技巧, 状态模式, 策略模式, 访问者模式本质上就是在特殊场合中采用了委托

  • Subject: 抽象主题类可以是抽象类或者接口, 定义一个业务逻辑
  • RealSubject: 也叫被代理类, 具体主题类是实现业务逻辑并执行具体的业务逻辑
  • Proxy: 代理类, 负责对具体主题类的应用,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public interface Subject {	
public void request(); //定义一个方法
}
public class RealSubject implements Subject {
//实现方法
public void request() {
//业务逻辑处理
}
}
public class Proxy implements Subject {
//要代理哪个实现类
private Subject subject = null;
//默认被代理者
public Proxy(){
this.subject = new Proxy();
}
public Proxy(Subject _subject){
this.subject = _subject;
}
//通过构造函数传递代理者
public Proxy(Object...objects ){

}
//实现接口中定义的方法
public void request() {
this.before();
this.subject.request();
this.after();
}
//预处理
private void preRequest(){
//do something
}
//善后处理
private void afterRequest(){
//do something
}
}

扩展 :

动态代理 , 即在实现阶段不关心代理谁, 而在运行阶段才指定代理哪一个对象; 相对来说, 自己写代理类的方式就是静态代理, AOP(Aspect Oriented Programming) 面向切面编程的核心就是动态代理

  • InvocationHandle是JDK提供的动态代理接口, 对被代理类的方法进行代理, 通过该接口所有方法都由该Handler来进行处理
  • invoke()是接口InvocationHandle中定义的, 在子类中必须实现, 它完成对真实方法的调用

行为型模式

策略模式 : 定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)

使用场景

  • 实现某一个功能有多个途径,此时可以使用一种设计模式来使得系统可以灵活地选择解决途径,也能够方便地增加新的解决途径。

硬编码
在软件系统中,有许多算法可以实现某一功能,如查找、排序等,一种常用的方法是硬编码(Hard Coding)在一个类中,如需要提供多种查找算法,可以将这些算法写到一个类中,在该类中提供多个方法,每一个方法对应一个具体的查找算法;当然也可以将这些查找算法封装在一个统一的方法中,通过if…else…等条件判断语句来进行选择。这两种实现方法我们都可以称之为硬编码,如果需要增加一种新的查找算法,需要修改封装算法类的源代码;更换查找算法,也需要修改客户端调用代码。在这个算法类中封装了大量查找算法,该类代码将较复杂,维护较为困难。

为了解决这些问题,可以定义一些独立的类来封装不同的算法,每一个类封装一个具体的算法,在这里,每一个封装算法的类我们都可以称之为策略(Strategy),为了保证这些策略的一致性,一般会用一个抽象的策略类或接口来做算法的定义,而具体每种算法则对应于一个具体策略类。

  • Context: 环境类, 即策略的选择功能实现
  • Strategy: 抽象策略类, 即策略的接口
  • ConcreteStrategy: 具体策略类, 即策略接口的具体实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public interface Strategy {
//策略模式的运算法则
public void doSomething();
}
public class ConcreteStrategyA implements Strategy {
public void doSomething() {
System.out.println("具体策略1的运算法则");
}
}
public class ConcreteStrategyB implements Strategy {
public void doSomething() {
System.out.println("具体策略2的运算法则");
}
}
public class Context {
//抽象策略
private Strategy strategy = null;
//构造函数设置具体策略
public Context(Strategy _strategy){
this.strategy = _strategy;
}
//封装后的策略方法
public void doAnythinig(){
this.strategy.doSomething();
}
}

扩展 : 策略枚举

实现一个输入为1+2或者1-2, 得出结果的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public enum Calculator {
//加法运算
ADD("+"){
public int exec(int a,int b){
return a+b;
}
},
//减法运算
SUB("-"){
public int exec(int a,int b){
return a - b;
}
};
String value = "";
//定义成员值类型
private Calculator(String _value){
this.value = _value;
}
//获得枚举成员的值
public String getValue(){
return this.value;
}
//声明一个抽象函数
public abstract int exec(int a,int b);
}
  • 定义一个Enum类型, 每个枚举都是public/final/static的,一般担当不经常变化的角色
1
2
3
4
5
6
7
8
9
10
11
public class Client {
public static void main(String[] args) {
//输入的两个参数是数字
int a = Integer.parseInt(args[0]);
String symbol = args[1]; //符号
int b = Integer.parseInt(args[2]);
System.out.println("输入的参数为:"+Arrays.toString(args));

System.out.println("运行结果为:"+a + symbol + b + "=" + Calculator.ADD.exec(a, b));
}
}

Calculator.ADD.exec(a, b)使得代码更加简洁易读

命令模式 : 将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。

  • Command: 抽象命令类, 需要抽象的所有命令都需要在这里声明
  • ConcreteCommand: 具体命令类
  • Invoker: 调用者, 接收到命令并执行命令
  • Receiver: 接收者, 命令传递到这里被执行
  • Client:客户类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public abstract class Receiver {
public abstract void doSomething(); //抽象接收者,定义每个接收者都必须完成的业务, 因为接收者可以有多个所以是抽象类
}
public class ConcreteReciver extends Receiver{
//每个接受者都必须处理一定的业务逻辑
public void doSomething(){

}
}
public abstract class Command {
public abstract void execute(); //每个命令类都必须有一个执行命令的方法
}
public class ConcreteCommand extends Command {
//声明自己的默认接收者
public ConcreteCommand(){
super(new ConcreteReciver());
}
//设置新的接收者
public ConcreteCommand(Receiver _receiver){
super(_receiver);
}
//每个具体的命令都必须实现一个命令
public void execute() {
//业务处理
super.receiver.doSomething();
}
}
public class Invoker {
private Command command;
//接受命令
public void setCommand(Command _command){
this.command = _command;
}
//执行命令
public void action(){
this.command.execute();
}
}
public class Client {
public static void main(String[] args) {
Invoker invoker = new Invoker(); //首先声明出调用者Invoker
Receiver receiver = new ConcreteReciver(); //定义接收者
Command command = new ConcreteCommand1(receiver); //定义一个发送给接收者的命令
invoker.setCommand(command); //把命令交给调用者去执行
invoker.action();
}
}

命令撤回:

  1. 增加一个新的命令实现事件的回滚
  2. 结合备忘录模式还原最后状态

迭代器模式 : 提供一种方法访问一个容器对象中各个元素,而又不暴露该对象的内部细节

  • Aggregate: 抽象容器一般是一个接口,提供一个iterator()方法,例如java中的Collection接口,List接口,Set接口等。
  • ConcreteAggregate: 具体容器就是抽象容器的具体实现类,比如List接口的有序列表实现ArrayList,List接口的链表实现LinkList,Set接口的哈希列表的实现HashSet等
  • Iterator : 抽象迭代器定义遍历元素所需要的方法,一般来说会有这么三个方法:取得第一个元素的方法first(),取得下一个元素的方法next(),判断是否遍历结束的方法isDone()(或者叫hasNext()),移出当前对象的方法remove()
  • ConcreteIterator : 实现迭代器接口中定义的方法,完成集合的迭代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public interface Aggregate {
public void add(Object object); //是容器必然有元素的增加
public void remove(Object object); //减少元素
public Iterator iterator(); //由迭代器来遍历所有的元素
}
public class ConcreteAggregate implements Aggregate {
private Vector vector = new Vector(); //容纳对象的容器
//增加一个元素
public void add(Object object) {
this.vector.add(object);
}
//返回迭代器对象
public Iterator iterator() {
return new ConcreteIterator(this.vector);
}
//删除一个元素
public void remove(Object object) {
this.remove(object);
}
}
public interface Iterator {
public Object next(); //遍历到下一个元素
public boolean hasNext(); //是否已经遍历到尾部
public boolean remove(); //删除当前指向的元素
}
public class ConcreteIterator implements Iterator {
private Vector vector = new Vector();
public int cursor = 0; //定义当前游标
@SuppressWarnings("unchecked")
public ConcreteIterator(Vector _vector){
this.vector = _vector;
}
//判断是否到达尾部
public boolean hasNext() {
if(this.cursor == this.vector.size()){
return false;
}else{
return true;
}
}
//返回下一个元素
public Object next() {
Object result = null;

if(this.hasNext()){
result = this.vector.get(this.cursor++);
}else{
result = null;
}
return result;
}
//删除当前元素
public boolean remove() {
this.vector.remove(this.cursor);
return true;
}
}

观察者模式 : 定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新

观察者模式又叫做发布-订阅(Publish/Subscribe)模式模型-视图(Model/View)模式源-监听器(Source/Listener)模式从属者(Dependents)模式

  • Subject: [抽象定义]被观察者, 必须能够动态的增加或取消观察者, 实现管理观察者和通知观察者的操作都是在该类中完成的
  • ConcreteSubject: 具体的被观察者, 定义自己的业务模型
  • Observer: 观察者, 观察者进行update()操作对接收到的信息进行处理
  • ConcreteObserver: 具体观察者, 每个具体的观察者的处理逻辑不同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*被观察者*/
public abstract class Subject {
//定一个一个观察者数组
private Vector<Observer> obsVector = new Vector<Observer>();
//增加一个观察者
public void addObserver(Observer o){
this.obsVector.add(o);
}
//删除一个观察者
public void delObserver(Observer o){
this.obsVector.remove(o);
}
//通知所有观察者
public void notifyObserver(){
for(Observer o:this.obsVector){
o.update();
}
}
}
/*具体的被观察者*/
public class ConcreteSubject extends Subject {
//具体的业务
public void doSomething(){
/*
* do something
*/
super.notifyObserver();
}
}
/*观察者*/
public interface Observer {
//更新方法
public void update();
}
/*具体的观察者*/
public class ConcreteObserver implements Observer {
//实现更新方法
public void update() {
System.out.println("接收到信息,并进行处理!");
}
}
  • 观察者和被观察者都是抽象耦合, 而且实现了一套触发机制, 根据单一职责原则每个类的职责是单一的

缺点:

  • 由于Java的消息机制默认是顺序执行的, 一个观察者卡壳会影响整体的执行效率, 这是采用异步机制, 尤其要考虑线程安全和队列的问题

状态模式 : 允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

  • State: 接口或者抽象类, 负责对状态的定义, 并且封装环境角色以实现状态的转换
  • ConcreteState: 具体的状态类, 本状态的行为管理和趋向状态的处理
  • Context: 定义客户端需要的接口, 并且负责具体状态的转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public abstract class State {
//定义一个环境角色,提供子类访问
protected Context context;
//设置环境角色
public void setContext(Context _context){
this.context = _context;
}
//行为1
public abstract void handle1();
//行为2
public abstract void handle2();
}
public class ConcreteState1 extends State {
@Override
public void handle1() {
//本状态下必须处理的逻辑
}
@Override
public void handle2() {
//设置当前状态为stat2
super.context.setCurrentState(Context.STATE2);
//过渡到state2状态,由Context实现
super.context.handle2();
}
}
public class ConcreteState2 extends State {
@Override
public void handle1() {
//设置当前状态为stat1
super.context.setCurrentState(Context.STATE1);
//过渡到state1状态,由Context实现
super.context.handle1();
}
@Override
public void handle2() {
//本状态下必须处理的逻辑
}
}
public class Context {
//定义状态: 1. 状态对象声明为静态变量
public final static State STATE1 = new ConcreteState1();
public final static State STATE2 = new ConcreteState2();
//当前状态
private State CurrentState;
//获得当前状态
public State getCurrentState() {
return CurrentState;
}
//设置当前状态
public void setCurrentState(State currentState) {
this.CurrentState = currentState;
//切换状态
this.CurrentState.setContext(this);
}
//行为委托 : 具有状态抽象类的所有行为
public void handle1(){
this.CurrentState.handle1();
}
public void handle2(){
this.CurrentState.handle2();
}
}
public class Client {
public static void main(String[] args) {
//定义环境角色
Context context = new Context();
//初始化状态
context.setCurrentState(new ConcreteState1());
//行为执行
context.handle1();
context.handle2();
}
}
  • 避免使用复杂的逻辑判断语句, 提高了可维护性
  • 良好的封装性, 隐藏了状态的变化过程, 只看到行为的改变而不用知道状态是怎么变化的