(完)- 书籍已经出版
最后更新于:2022-04-01 07:30:14
好消息,书籍已经由电子工业出版社出版,品牌是博文视点。书籍相比之前的博客,很多内容作了完善和丰富,还增加了“面向对象架构设计”章节,更加全面,请朋友们多多支持。
京东:[购买链接](http://item.jd.com/11826518.html)
亚马逊:[购买链接](http://www.amazon.cn/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%91%B5%E8%8A%B1%E5%AE%9D%E5%85%B8-%E6%80%9D%E6%83%B3-%E6%8A%80%E5%B7%A7%E4%B8%8E%E5%AE%9E%E8%B7%B5-%E6%9D%8E%E8%BF%90%E5%8D%8E/dp/B018SRCFFE/ref=sr_1_1?ie=UTF8&qid=1450785446&sr=8-1&keywords=%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%91%B5%E8%8A%B1%E5%AE%9D%E5%85%B8+%E6%80%9D%E6%83%B3+%E6%8A%80%E5%B7%A7%E4%B8%8E%E5%AE%9E%E8%B7%B5)
当当:[购买链接](http://product.dangdang.com/23823147.html)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-20_569f5ccb59254.jpg)
================================================================================
终于到了要结束的时候了,感谢那些一路关注和跟随的朋友们,面向对象葵花宝典系列的博文到这里就要结束了。
本系列的博文只是我个人写作的面向对象葵花宝典的摘录,面向对象葵花宝典的全文请下载PDF,里面的**内容更全、阅读更方便**。
如果你是新手,这里有详尽和通俗易懂的概念和方法介绍,让你能够快速入门
如果你是老手,这里有完整的面向对象开发流程,让你能够得心应手,精益求精
如果你是高手,这里有独树一帜的理论解读,让你大开眼界
CSDN下载链接(点击下载):
[面向对象葵花宝典:思想、技巧与实践 - ](http://download.csdn.net/detail/yah99_wolf/7966665)节选版
**【一点小小的倡议**】
如果觉得本博客系列或者PDF对你有帮助,想略表心意的话,可以向 @免费午餐 捐一顿午餐(RMB 3元)
http://weibo.com/freelunch?topnav=1&wvr=6&topsug=1
(40) – DECORATOR模式
最后更新于:2022-04-01 07:30:12
## 连载:面向对象葵花宝典:思想、技巧与实践(40) - DECORATOR模式
**掌握了设计模式之道后,我们将以全新的方法来理解设计模式,这个方法更简单、更直观,不信?看几个样例就知道了**
=====================================================================
**DECORATOR模式(以设计模式之道来理解)**
**【业务】**
假设你进入了一个信息安全管理非常严格的公司,这家公司不允许员工自行打印文档,所有的文档打印都需要交给文档打印系统统一管理。文档打印系统会记录每次打印的时间、内容、打印人员。。。。。。等等,以便后续出现问题的时候进行追查。
由于公司有很多的部门,每个部门的安全要求并不完全一样,同时每个部门关于文档打印也有自己的一些规定。
我们的任务就是要开发一套能够支持整个公司文档打印需求的系统。
**【发现变化】**
文档打印系统面对的变化主要体现在:文档打印要求是变化的,不同的部门有不同的要求,同一个部门也可能修改自己的打印需求。
例如:
A部门是一个战略规划的部门,里面的资料都非常重要,打印的时候需要在页眉位置打印“绝密”,在页脚的位置打印“密级申明”,同时要加上“绝密文档”的水印;
B部门是内部培训部门,打印培训材料的时候需要在页眉位置打印“内部公开”,但不需要密级申明,同时加上“培训资料”的水印
C部门是对外宣传部门,打印宣传材料的时候只需要加上“公司logo”的水印;
**【传统方法】**
传统方法使用类继承来封装打印请求,为每个部门创建一个打印的子类。详细示例代码如下:
PrintTask.java -- 文档打印系统开发小组负责维护
~~~
package com.oo.designpattern.decorator;
/**
* 打印任务类
*
*/
abstract public class PrintTask {
abstract public void print(String text);
}
~~~
SecretPrint.java -- 文档打印系统开发小组负责维护:
~~~
package com.oo.designpattern.decorator;
/**
* 绝密文档的打印
*
*/
public class SecretPrint extends PrintTask{
@Override
public void print(String text) {
Printer.printHeader("绝密");
Printer.printText(text);
Printer.printFooter("本文包含绝密信息,请勿泄露!");
Printer.printTextWaterMark("绝密文档");
}
}
~~~
InternalPrint.java -- 文档打印系统开发小组负责维护:
~~~
package com.oo.designpattern.decorator;
/**
* 内部公开的文档打印
*
*/
public class InternalPrint extends PrintTask {
@Override
public void print(String text) {
Printer.printHeader("内部公开");
Printer.printText(text);
Printer.printTextWaterMark("培训资料");
}
}
~~~
PublicPrint.java -- 文档打印系统开发小组负责维护:
~~~
package com.oo.designpattern.decorator;
import java.awt.Image;
/**
* 对外宣传的文档打印
*
*/
public class PublicPrint extends PrintTask {
private Image _logo;
@Override
public void print(String text) {
Printer.printText(text);
Printer.printImgWaterMark(_logo);
}
}
~~~
文档打印系统实现如下:
PrintServer -- 文档打印系统开发小组负责维护
~~~
package com.oo.designpattern.decorator;
/**
* 文档打印系统
*
*/
public class PrintServer {
/**
* 执行打印任务
* @param task
* @param text
*/
public static void executePrintTask(PrintTask task, String text){
log();
task.print(text);
audit();
}
/**
* 记录日志
*/
private static void log(){
//省略具体实现代码
}
/**
* 记录审计相关信息
*/
private static void audit(){
//省略具体实现代码
}
}
~~~
定义好不同的打印任务后,每个部门根据自己的需要,选择不同的任务发给文档打印系统。
例如,A部门的打印处理如下:
SecretDepartment.java -- A部门负责维护
~~~
package com.oo.designpattern.decorator;
/**
* A部门的打印处理
*
*/
public class SecretDepartment {
public void print(String text){
PrintTask task = new SecretPrint();
//PrintServer即“文档打印系统”
PrintServer.executePrintTask(task, text);
}
}
~~~
传统方法使用类继承来封装变化的打印需求,当面对变化时,存在如下问题:
1)新增部门的时候,需要文档打印系统提供一个新的打印任务类,将导致出现大量的***Print类;
例如:新建了一个D部门,D部门只需要打印纯文本即可,那么已有的SecretPrint、InternalPrint、PublicPrint类都无法满足需求,必须新增一个PurePrint的类;
2)某个部门的打印需求变更的时候,需要改变已有的***Print类;
例如:C部门希望在对外宣传材料的页眉上打印公司名称,则需要修改PublicPrint类。
**【设计模式方法】**
设计模式封装变化的方法就是Decorator模式。Decorator模式定义如下:
“动态的给一个对象添加一些额外的职责”
《设计模式》一书中关于Decorator模式的描述并不很直观,我理解Decorator模式为“通过聚合的方式将动态变化的职责组合起来”。
我们详细看看Decorator模式是如何封装变化的。
首先,将变化的职责封装为独立的类。传统方式实现中,不同的职责是对应不同的函数调用,而设计模式中,不同的职责是不同的类;
其次,通过聚合将变化的职责组合起来。传统方式中,不同职责的组合是通过在一个函数中写多行代码来体现的,而设计模式中,通过对象的聚合将不同职责组合起来。
**【Decorator模式结构】**
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-20_569f5ccb46f4b.jpg)
Component:定义一个对象接口(对应结构图中的operation函数),可以给这些对象动态添加职责
ConcreteComponent:定义一个对象,这个对象是实际的Component,将被Decorator修饰
Decorator:定义修饰对象的接口,Decorator实现的关键在于聚合了一个Component对象
ConcreteDecorator:具体的修饰对象
**【代码实现】**
使用Decorator设计模式实现的文档打印系统代码如下:
*********************类设计*****************************
PrintComponent.java -- 文档打印系统开发小组负责维护
~~~
package com.oo.designpattern.decorator2;
/**
* 打印组件的父类
*
*/
abstract public class PrintComponent {
abstract public void print();
}
~~~
PrintDecorator.java -- 文档打印系统开发小组负责维护
~~~
package com.oo.designpattern.decorator2;
/**
* 修饰的打印任务,对应Decorator模式中的Decorator
* Decorator可以聚合ConcreteComponent或者其他Decorator
* 这样可以使得打印任务能够嵌套执行下去,直到最后完成所有打印任务
*
*/
public abstract class PrintDecorator extends PrintComponent {
abstract public void print();
}
~~~
TextComponent.java -- 文档打印系统开发小组负责维护
~~~
package com.oo.designpattern.decorator2;
import com.oo.designpattern.decorator.Printer;
/**
* 文本打印,对应Decorator模式中的ConcreteComponent
* 打印任务到ConcreteComponent就算真正完成了
*
*/
public class TextComponent extends PrintComponent {
private String _text;
TextComponent(String text){
this._text = text;
}
@Override
public void print() {
Printer.printText(this._text);
}
}
~~~
HeaderDecorator.java -- 文档打印系统开发小组负责维护
~~~
package com.oo.designpattern.decorator2;
import com.oo.designpattern.decorator.Printer;
/**
* 页眉打印
*
*/
public class HeaderDecorator extends PrintDecorator {
private PrintComponent _comp; //被修饰的打印组件
private String _text; //需要打印的页眉内容
/**
* 初始化的时候,必须包含其它打印组件comp,这是实现Decorator模式的前提
* 同时也需要指定当前组件所需的参数,不能在print函数的参数中指定,
* 因为每个Decorator所需的参数是不一样的
* @param comp
* @param text
*/
HeaderDecorator(PrintComponent comp, String text) {
this._comp = comp;
this._text = text;
}
/**
* 打印
*/
@Override
public void print() {
//打印的时候将当前Decorator和被修饰的Component分开,这是Decorator模式的关键
Printer.printHeader(_text); //打印页眉
//_comp本身如果是Decorator,就会嵌套打印下去
//_comp本身如果不是Decorator,而是ConcreteComponent,则打印任务到此结束
_comp.print();
}
}
~~~
FooterDecorator.java
~~~
package com.oo.designpattern.decorator2;
import com.oo.designpattern.decorator.Printer;
/**
* 页脚打印,和页眉打印类似,此处省略相同的注释代码
*
*/
public class FooterDecorator extends PrintDecorator {
private PrintComponent _comp;
private String _text;
FooterDecorator(PrintComponent comp, String text) {
this._comp = comp;
this._text = text;
}
/**
* 打印
*/
@Override
public void print() {
Printer.printFooter(_text); //打印页脚
_comp.print();
}
}
~~~
TextWatermarkDecorator.java
~~~
package com.oo.designpattern.decorator2;
import com.oo.designpattern.decorator.Printer;
/**
* 文本水印打印,和页眉打印类似,此处省略相同的注释代码
*
*/
public class TextWatermarkDecorator extends PrintDecorator{
private PrintComponent _comp;
private String _text;
TextWatermarkDecorator(PrintComponent comp, String text) {
this._comp = comp;
this._text = text;
}
/**
* 打印
*/
@Override
public void print() {
Printer.printTextWaterMark(_text); //打印文本水印
_comp.print();
}
}
~~~
ImgWatermarkDecorator.java
~~~
package com.oo.designpattern.decorator2;
import java.awt.Image;
import com.oo.designpattern.decorator.Printer;
/**
* 图片水印打印,和页眉打印类似,此处省略相同的注释代码
*
*/
public class ImgWatermarkDecorator extends PrintDecorator {
private PrintComponent _comp;
private static Image _logo; //图片水印只能是公司logo
ImgWatermarkDecorator(PrintComponent comp) {
this._comp = comp;
}
/**
* 打印
*/
@Override
public void print() {
Printer.printImgWaterMark(ImgWatermarkDecorator._logo); //打印图片水印
_comp.print();
}
}
~~~
PrintServer.java
~~~
package com.oo.designpattern.decorator2;
public class PrintServer {
/**
* 执行打印任务
* @param comp
*/
public static void executePrintTask(PrintComponent comp){
log();
comp.print();
audit();
}
/**
* 记录日志
*/
private static void log(){
//省略具体实现代码
}
/**
* 记录审计相关信息
*/
private static void audit(){
//省略具体实现代码
}
}
~~~
*********************类使用*****************************
A部门的打印处理如下(**如下代码请仔细阅读,特别是注释部分**):
SecretDepartment.java -- A部门负责维护
~~~
package com.oo.designpattern.decorator2;
/**
* A部门的打印处理,注意与传统方法中的SecretDepartment类对比
*
*/
public class SecretDepartment {
/**
* 打印任务1,对应传统方式的SecretePrint类
* @param text
*/
public void print(String text){
/**
* 使用Decorator设计模式后,打印任务不再是一个单独的类SecretPrint类,
* 而是通过将多个打印项目聚合成一个打印任务
*/
PrintComponent textComp = new TextComponent(text);
//注意header聚合了textComp
PrintDecorator header = new HeaderDecorator(textComp, "绝密");
//注意footer聚合了header,而不是textComp,这样就能够嵌套执行下去
PrintDecorator footer = new FooterDecorator(header, "本文包含绝密信息,请勿泄露!");
//注意watermark聚合了footer,而不是textComp,这样就能够嵌套执行下去
PrintDecorator watermark = new TextWatermarkDecorator(footer, "绝密文档");
//PrintServer即“文档打印系统”,与传统的PrintServer相比,这里不需要知道打印的text内容
//text内容已经封装到TextComponent中去了(对应代码行14行)
PrintServer.executePrintTask(watermark); //注意这里传递给打印系统的是最后一个Decorator对象watermark
}
/**
* A部门的第二个打印任务,将文本水印改为图片水印,并且不再打印页脚
* @param text
*/
public void print2(String text){
/**
* 新增打印任务,无需文档管理系统增加新的类,只要A部门自己修改代码即可
*/
PrintComponent textComp = new TextComponent(text);
//注意header聚合了textComp
PrintDecorator header = new HeaderDecorator(textComp, "绝密");
//注意watermark聚合了header,而不是textComp,这样就能够嵌套执行下去
PrintDecorator watermark = new ImgWatermarkDecorator(header);
PrintServer.executePrintTask(watermark);
}
}
~~~
**可以看到,使用了设计模式的方法后,打印业务的变化,可以通过类似数学上的排列组合已有的打印功能来完成,而不再需要新的打印类了。**
(39) – 设计原则 vs 设计模式
最后更新于:2022-04-01 07:30:09
## 连载:面向对象葵花宝典:思想、技巧与实践(39) - 设计原则 vs 设计模式
又是设计原则,又是设计模式,到底该用哪个呢? =============================================================================
在“设计模型”一章中,我们提到设计原则和设计模式是互补的,设计原则和设计模式互补体现在:设计原则主要用于指导“类的定义”的设计,而设计模式主要用于指导“类的行为”的设计。
举一个很简单的例子:假设我们要设计一个图形类Shape,这个类既支持三角形,又支持矩形,其代码如下:
~~~
package com.oo.designpattern.diagram;
/**
* 设计不好的Shape类,同时兼顾三角形和矩形的职责,不符合SRP设计原则
*
*/
public class BadShape {
//三角形的属性
Position a;
Position b;
Position c;
//矩形的属性
Position m;
int length;
int width;
public void drawTriangle(){
//TODO: 画出三角形
}
public void drawRectangle(){
//TODO: 画出矩形
}
}
~~~
有经验的朋友都会觉得这个设计不太合理,因为其不符合类的SRP设计原则。因此,合理的做法是将这个类按照SRP原则拆分,具体拆分方法如下:
NormalShape.java
~~~
package com.oo.designpattern.diagram;
/**
* 将BadShape拆开为三角形和矩形两个图形,并提取出NormalShape这个父类
*
*/
abstract class NormalShape {
abstract void draw();
}
~~~
NormalTriangle.java
~~~
package com.oo.designpattern.diagram;
/**
* 三角形类
*
*/
public class NormalTriangle extends NormalShape {
//三角形的属性
Position a;
Position b;
Position c;
@Override
public void draw() {
// TODO:绘画三角形
if(Config.CURRENT_SYSTEM == Config.WINDOWS){
//TODO: 调用Windows的画图方法
}
else if( Config.CURRENT_SYSTEM == Config.LINUX){
//TODO: 调用Linux的画图方法
}
else if( Config.CURRENT_SYSTEM == Config.MAC){
//TODO: 调用Mac的画图方法
}
}
}
~~~
NormalRectangle.java
~~~
package com.oo.designpattern.diagram;
/**
* 矩形类
*
*/
public class NormalRectangle extends NormalShape {
//矩形的属性
Position m;
int length;
int width;
@Override
public void draw() {
// TODO: 绘画矩形
if(Config.CURRENT_SYSTEM == Config.WINDOWS){
//TODO: 调用Windows的画图方法
}
else if( Config.CURRENT_SYSTEM == Config.LINUX){
//TODO: 调用Linux的画图方法
}
else if( Config.CURRENT_SYSTEM == Config.MAC){
//TODO: 调用Mac的画图方法
}
}
}
~~~
这样拆分之后,从类的设计原则来看,已经是符合要求了。
接下来我们再使用设计模式来继续完善这个设计,这里就需要使用设计模式之道来指导我们设计了,即:**找到变化,封装变化**。
关于图形类一个比较明显的变化是跨平台,比如说要同时支持Windows、Linux、Mac三个桌面操作系统,那么实际画图的方法和需要调用的函数可能就随着平台的不同而变化,因此我们要找出一种方法来封装这种变化。
参考《设计模式》,可以知道这种方法就是“Bridge模式”,使用了Bridge后,会多出几个接口和实现类。
具体实现如下:
GoodShape.java
~~~
package com.oo.designpattern.diagram;
/**
* 在NormalShape的基础上,增加Bridge设计模式的实现,使其更加适应于跨平台
*
*/
abstract public class GoodShape {
protected ShapeDraw _draw; //将不同平台的实现封装到一个新的接口ShapeDraw
abstract void draw();
}
~~~
GoodTriangle.java
~~~
package com.oo.designpattern.diagram;
/**
* 按照Bridge设计模式设计的三角形类
*
*/
public class GoodTriangle extends GoodShape {
GoodTriangle(ShapeDraw draw){
this._draw = draw;
}
@Override
void draw() {
// TODO Auto-generated method stub
this._draw.drawTriangle();
}
}
~~~
GoodRectangle.java
~~~
package com.oo.designpattern.diagram;
/**
* 按照Bridge设计模式设计的矩形类
*
*/
public class GoodRectangle extends GoodShape {
GoodRectangle(ShapeDraw draw){
this._draw = draw;
}
@Override
void draw() {
// TODO Auto-generated method stub
this._draw.drawRectangle();
}
}
~~~
ShapeDraw.java
~~~
package com.oo.designpattern.diagram;
/**
* 按照Bridge设计模式进行设计的画图的接口,封装了跨平台不同的实现
*
*/
interface ShapeDraw {
public void drawTriangle();
public void drawRectangle();
}
~~~
WindowsDraw.java
~~~
package com.oo.designpattern.diagram;
/**
* Windwos上的画图实现
*
*/
public class WindowsDraw implements ShapeDraw {
@Override
public void drawTriangle() {
// TODO Auto-generated method stub
}
@Override
public void drawRectangle() {
// TODO Auto-generated method stub
}
}
~~~
LinuxDraw.java
~~~
package com.oo.designpattern.diagram;
/**
* Linux上的画图实现
*
*/
public class LinuxDraw implements ShapeDraw {
@Override
public void drawTriangle() {
// TODO Auto-generated method stub
}
@Override
public void drawRectangle() {
// TODO Auto-generated method stub
}
}
~~~
MacDraw.java
~~~
package com.oo.designpattern.diagram;
/**
* Mac上的画图实现
*
*/
public class MacDraw implements ShapeDraw {
@Override
public void drawTriangle() {
// TODO Auto-generated method stub
}
@Override
public void drawRectangle() {
// TODO Auto-generated method stub
}
}
~~~
可以看到,按照设计原则和设计模式进行重构后,原来不合理的设计逐步演变为一个优秀的设计了
(38) – 设计模式之道
最后更新于:2022-04-01 07:30:07
## 连载:面向对象葵花宝典:思想、技巧与实践(38) - 设计模式之道
很多人能够熟练背诵出所有的设计模式,能够快速画出各种设计模式的UML类图,也能够熟练的写出《设计模式》一书中各个模式的样例代码。但一到实际的项目设计和开发的时候,往往都会陷入迷茫:要么无从下手,不知道哪个地方要用设计模式;要么生搬硬套,胡乱使用设计模式,将方案和代码搞得一团乱麻。
===========================================================================
### 【知易行难 —— 设计模式应用的问题】
形而下者谓之器,形而上者谓之道。
---《易经·系辞》
正如很多流行的技术(面向对象、UML等)一样,几乎大部分人都会宣称自己“掌握、熟练掌握”,甚至“精通”,然而,真正掌握的或者精通的,实在是少之又少。
一种典型的现象是:很多人能够熟练背诵出所有的设计模式,能够快速画出各种设计模式的UML类图,也能够熟练的写出《设计模式》一书中各个模式的样例代码。但一到实际的项目设计和开发的时候,往往都会陷入迷茫:要么无从下手,不知道哪个地方要用设计模式;要么生搬硬套,胡乱使用设计模式,将方案和代码搞得一团乱麻。
这是什么原因呢,难道是设计模式不好用,或者设计模式根本就是一个噱头?
答案不在于设计模式本身是否有用,而是在于我们没有掌握正确的学习和应用设计模式的方法。
学习《设计模式》一书中的23个设计模式,只是掌握了《设计模式》的“器”,但并没有掌握设计模式的“道”。就像一个工匠,锯、钻、锤、刨样样精通,但如果不知道什么地方该用锯,什么地方该用钻,肯定是一个不合格的工匠。为了能够更好的学习和应用设计模式,我们也需要掌握设计模式的“道”。
设计模式的“道”就是用来指导我们什么时候用设计模式,为什么要用设计模式,23个设计模式只是告诉了我们how,而设计模式之道却可以告诉我们why和where!
### 【拨云见日 —— 寻找设计模式之道】
熟悉《设计模式》一书内容的同学可能会想到:《设计模式》一书中,不是每个模式都有“适用性”的说明么?这个其实就是回答了where和why的问题啊!
例如:Facade模式的“适用性”说明如下(摘自《设计模式》中文版一书):
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-20_569f5ccb1cc56.jpg)
上面这一段文字,看起来回答了where和why的问题,但实际上我个人感觉作用并不大。
首先,这段描述太长了:以上这段文字是否花了你几分钟的时间去阅读和理解?
其次,这段描述比较抽象:什么事复杂,什么叫做简单,什么叫做很大依赖性。。。。。。可能每个人理解都不一样。
再者,23个模式,所有的“适应性”条款加起来估计有几十条条,你能够背住么?即使你能够全部背住,你能够全部理解么?即使你能够全部理解,当面对一个具体问题的时候,你知道几十条里面哪一条适应你的情况么?
所以,《设计模式》一书关于“适用性”的描述,实际上还是太复杂,太多了,不具备很强的实践知道意义和可操作性。
我们需要的是一个更简单的指导思想,大道至简,最好是一两句话就能够描述!
幸运的是,答案竟然就在《设计模式》一书中,但这个答案并不是那么明显!
《设计模式》一书的内容侧重点是23个模式的详细阐述,大部分人可能都是直奔主题,逐一去研究每个模式,而对于开头部分第1章和第2章的内容并没有详细研读和思考,或者对于这两章只是简单的浏览,并未认真领会和思考,由此错过了最重要的内容。再加上GoF在这2章的内容中,既要引入一个全新的概念,又要提纲挈领的介绍各个模式,还要引入实例进行分析,以至于大量的内容将真正核心的内容反而给淹没或者冲淡了。
设计模式之道就隐藏在“2.6.2 封装实现依赖关系”的最后一段,很简单的一句话:
**对变化的概念进行封装(encapsulate the concept that varies)**
你看到这句话可能有点失望,前面分析了那么久,卖了那么多的关子,结果就这么简简单单一句话,这不是在忽悠么?
你可千万别小看了这句话,“大道至简”,设计模式之道也不例外,但“简”并不意味着没用,相反,正因为其“简”,每个人的理解才一致,也更好掌握,实践中才更好应用。正所谓:“真传一句话,假传万卷书”。
GoF在《设计模式》一书中最早提出这个原则,后来不断的有其他专家进行阐述,其中《设计模式精解》(《Design pattern explained》)一书的阐述我认为是最精辟的:**Find what varies and encapsulate it,翻译一下即“找到变化,封装变化”**。虽然含义和GoF描述的基本一致,但其更加容易理解。
正所谓:踏破铁鞋无觅处,得来全不费工夫!
### 【庖丁解牛 —— 解析设计模式之道】
现在,让我们来深入理解“找到变化,然后封装变化”的设计模式之道。
首先,“找到变化”解决了“在哪里”使用设计模式的问题,即回答了“where”的问题。
“找到变化”看起来是比较抽象的一句话,但在实践中非常好应用和操作,而且不同领域、不同行业的系统都可以完美的应用这句话。虽然不同领域、不同行业变化的因素、方式、时机等都不一样,但每个领域或者行业的需求分析人员、设计人员,对自己所处行业和领域的可能变化肯定是有比较深刻的理解的,什么会变化、会如何变化、什么时候会变化。。。。。。等等,肯定都能够自己判断,这种判断并不需要什么高深的技巧和知识水平,只需要一定的经验积累。
如果我们刚接触一个行业或者领域,经验积累并不够,那怎么办呢?是否就无法“找到变化”了?
其实也不然,有一个万能的办法,只是要花费更多的精力了。
我的这个万能办法就是“唯一不变的是变化本身”,也就是说,如果你不知道什么会变化,那么就抱着怀疑一切的想法,一切都可能是变化的。
但光有这条指导原则还不行,如果我们真的抱着“一切都是可能变化的”想法,然后封装一切变化,那么就会陷入变化的漩涡无法自拔,因为变化是会递归的,A可能变成B,B也可能继续变化,于是这样无穷无尽,系统是不可能做出来的。
所以我们需要一个终止条件,避免陷入无穷无尽的变化递归漩涡。这个终止条件就是“有限时间内可能发生的变化”。这里的“有限时间”随行业和领域的不同而变化。例如(以下时间仅供参考):
互联网行业可以说:半年内可能发生的变化。。。。。。
电信行业可以说:1年内可能发生的变化。。。。。。
金融行业可以说:2年内可能发生的变化。。。。。。
政府行业可以说:3年内可能发生的变化。。。。。。
有了这个指导原则后,你可以这样去问有经验的前辈、大虾、大牛等: XXX在1年内会发生变化么?会怎样变化?
就这样,即使你是菜鸟,通过这么一招“借花献佛”,也能够轻松发现“变化”的地方。
其次,“封装变化”解决了“为什么”使用设计模式的问题,即回答了“why”的问题。
为什么我们要用设计模式,是因为我们要封装变化!但我们为什么要封装变化呢?
答案很明显:变化不好!
当然这个“不好”不是从业务的角度来说的,而是从系统的角度来说的。从业务的角度来说,“变化”是好的,变化意味着新的机会;但从系统的角度来说,变化并不好,因为变化必然要求系统改动,改动就意味着风险!
虽然变化给系统带来风险,但我们不能因此而“拒绝变化”,因为拒绝变化就意味着失去了机会,简单来说,赚不到钱的系统,设计再优美,功能再强大,系统再稳定,也不过是一堆无用的摆设:
客户给你提了新需求,你不做,能拿到合同么。。。。。。
行业正在兴起新的流行功能,你不做,你的系统有人用么。。。。。。
一项创新带来了新的机遇,你不做,能抢占市场么。。。。。。
所以我们要“拥抱变化”,但我们又不能让变化带来太大的风险,所以就提出了“封装变化”。“封装变化”意味着将变化的影响范围控制最小,将风险降到最低。
我们来看看,变化会带来哪些问题和风险:
1)开发人员需要编码以适应变化,设计不好的方案将导致大量的编码工作量、自测工作量;
2)测试人员不单要测试因变化而新增的那部分,还要测试受影响的部分,设计不好的方案,牵一发而动全身,导致测试工作量大大增加;
3)如果为了适应某个变化而对系统做了比较大的改动,则系统的质量风险将上升,很可能导致上线失败,或者上线后出现各种问题;
因此,我们要尽量减少变化带来的工作量和风险,而减少的最有效方法就是将变化的影响范围缩小,即:将变化封装起来,使其只在有限的范围内有影响。
### 【举一反三 —— 活学活用设计模式之道】
就像一个武林高手有了深厚的内功,天下万物皆可成为手中的利器,而不必拘泥于具体的武器和招数一样,掌握了设计模式之道后,我们其实也完全可以不拘泥于只是用《设计模式》一书中的23个设计模式,可以根据需要选择最合适的方案。
例如:
不同的业务有不同的规则排列组合,规则引擎可以封装各种变化的规则。。。。。。
类之间的依赖是变化的,Spring使用XML配置文件来封装这种变化。。。。。。
每个银行的卡都不一样,银联封装了这种变化,使得不同银行可以互通。。。。。。
总之,你可以使用类和设计模式来封装变化,你也可以使用配置文件和模块来封装变化,你也可以使用一个系统来封装变化。。。。。。
(37) – 设计模式:瑞士军刀 or 锤子?
最后更新于:2022-04-01 07:30:05
## 连载:面向对象葵花宝典:思想、技巧与实践(37) - 设计模式:瑞士军刀 or 锤子?
“设计模式”这个词几乎成为了软件设计的代名词,很多人非常天真的以为掌握了设计模式就掌握了软件设计,但实际上如果只是握了设计模式,软件设计的门都还没摸到!
========================================================
谈起设计模式,那是几乎无人不知,无人不晓,大名鼎鼎的“GOF”(中文有的翻译为“四人帮”)惊世之作,真是“平生不识GOF,学尽设计也枉然!”
然而,设计模式真的是软件设计的“瑞士军刀”,切、削、锯、钻样样精通么?
读过设计模式的朋友估计不少,但真正注意过《设计模式》的副标题的估计很少,而这个副标题却是避免误解设计模式的关键。《设计模式》的副标题是:可复用面向对象软件的基础!
不要小看了这短短的一句话,如果你没有看这句话,或者只是一扫而过并没有仔细体会,那么你很可能就认为设计模式是一把“瑞士军刀”,能够解决所有的设计问题,而实际上“**设计模式只是一把锤子**”,有个谚语叫做“如果你手里有一把锤子,那么所有的问题都变成了钉子”,如果你拿着设计模式这把锤子倒出去敲,要么东西被敲坏,要么就不起作用。
为什么说设计模式只是一把锤子呢?我们还是从副标题来看。《设计模式》的副标题揭示了《设计模式》的两个主要约束:
1)设计模式解决的是“可复用”的设计问题;
2)设计模式应用的领域是“面向对象”;
相信经过我这么一提醒,大家基本上都能够明白了为什么“设计模式只是一把锤子”了:
1)设计模式只能解决“可复用”的设计问题,其它的例如性能设计、可靠性设计、安全性设计、可服务性设计等都不是设计模式能够解决的;
2)设计模式只是在面向对象的语言中应用,如果是非面向对象的语言,就不怎么好用了。当然,你可以在C语言中用上设计模式,但毕竟要折腾不少,用起来也不那么得心用手。
所以,当你遇到一个问题就想到设计模式的时候,一定要注意“设计模式只是一把锤子”,不要拿着这把锤子到处去敲!
(36) – 设计原则如何用?
最后更新于:2022-04-01 07:30:02
## 连载:面向对象葵花宝典:思想、技巧与实践(36) - 设计原则如何用?
经过前面深入的阐述,SOLID的原则我们已经基本上讲清楚了,但如果想熟练的应用SOLID原则,仅仅知道SOLID是什么(what)还不够,我们还需要知道SOLID原则在什么时候和什么场景应用(when或where)。
幸运的是,SOLID原则的5个独立原则在实际应用中基本上都是独挡一面,并不会在某个地方需要同时从可选的几个原则中挑选一个最优的原则来应用,这样大大降低了我们应用SOLID原则的难度。
SOLID原则具体的应用场景如下:
* SRP原则:用于类的设计
当我们想出一个类,或者设计出一个类的原型后,使用SRP原则核对一下类的设计是否符合SRP要求。
* OCP原则:总的指导思想
OCP原则是一个总的指导思想,在面向对象的设计中,如果能够符合LSP/ISP/DIP原则,一般情况下就能够符合OCP原则了。
除了在面向对象的软件设计中外,OCP也可以用于指导系统架构设计,例如常见的CORBA、COM协议,其实都可以认为是OCP原则的具体应用和实现。
* LSP原则:用于指导类继承的设计
当我们设计类之间的继承关系时,使用LSP原则来判断这种继承关系是否符合LSP要求。
* ISP原则:用于指导接口的设计
ISP原则可以认为是SRP原则的一个变种,本质上和SRP的思想是一样。SRP用于指导类的设计,而ISP用于指导接口的设计。
* DIP原则:用于指导类依赖的设计
当我们设计类之间的依赖关系时,可以使用DIP原则来判断这种依赖是否符合DIP原则。
DIP原则和LSP原则相辅相成:DIP原则用于指导抽象出接口或者抽象类,而LSP原则指导从接口或者抽象类派生出新的子类。
(35) – NOP原则
最后更新于:2022-04-01 07:30:00
## 连载:面向对象葵花宝典:思想、技巧与实践(35) - NOP原则
**NOP,No Overdesign Priciple,不要过度设计原则**。
这应该是你第一次看到这个原则,而且你也不用上网查了,因为这个不是大师们创造的,而是我创造的:)
之所以提出这个原则,是我自己吃过苦头,也在工作中见很多人吃过类似的苦头。
你可能也见过这样的场景:
产品提出了一个需求,设计师眼光非常长远,他甚至把5年后可能的业务变化都提出来并且加以设计了,让你不得不佩服设计师的高瞻远瞩的眼光,并且由衷的从心底赞叹:牛逼啊!
但很快你就会发现,设计师是很牛逼,但你开发的时候就很苦逼了,设计方案巨复杂,工作量巨大,即使你发扬一不怕苦二不怕累的精神,每天加班加点奋斗了三个月终于做出来了,但你苦逼完了,测试跟着苦逼了:很多东西测试都没办法测试!
怎么会出现这样的情况呢?我们做面向对象不就是为了应对变化、拥抱变化么?
要回答这个问题其实很简单,孔夫子在《论语》中已经为我们解答了:“子贡问:‘师与商也孰贤?’子曰:‘师也过,商也不及。’曰:‘然则师愈与?’子曰:‘过犹不及。’”
什么事情过头了就和没有达到是一样的效果,面向对象的设计也不例外。面向对象的初衷虽然是为了拥抱变化,但这个变化也是有一个度的,而不是预测得越长越好,原因很简单:预测越长,预测的结果正确性越低!谁能在2000年预测SUN公司的衰落?谁又能在2005年预测苹果的崛起?
除了预测时间越长准确性越差的问题外,过分设计会导致设计方案不必要的复杂、代码量庞大,投入产出不成正比,项目计划无法按时完成。。。。。。等等很多问题。
有时候过分设计比设计不足的影响和危害更大,因为如果设计不足,我们还有“重构”这个利器,也不会出现浪费大量人力物力的情况;
而如果过分设计,如果后面发现原来的设计不正确或者不合理,首先原有的投入浪费了,其次是即使重构,也需要花费更多的人力物力。
所以,在设计过程中要时刻谨记NOP原则,避免过度设计!
(34) – DIP原则
最后更新于:2022-04-01 07:29:58
## 连载:面向对象葵花宝典:思想、技巧与实践(34) - DIP原则
DIP,dependency inversion principle**,中文翻译为“依赖倒置原则”。**
DIP是大名鼎鼎的Martin大师提出来的,他在1996 5月的C++ Reporter发表“ The Dependency Inversion Principle”的文章详细阐述了DIP原则,并且在他的经典著作《 Agile Software Development, Principles, Patterns》(中文翻译为:敏捷软件开发:原则、模式与实践)、《Practices, and Agile Principles, Patterns, and Practices in C#》(中文翻译为:敏捷软件开发:原则、模式与实践(C#版))中详细解释了DIP原则。
DIP原则主要有两点含义:
1) 高层模块不应该直接依赖低层模块,两者都应该依赖抽象层;
2) 抽象不能依赖细节,细节必须依赖抽象;
虽然DIP原则的解释非常清楚,但要真正理解也不那么简单,因为有几个关键的术语都比较抽象,我们需要更详细的解析:
1)什么是模块?
英文中用到了module、component,但我们这是在讲类的设计原则,为什么要把DIP拉进来呢?
其实Martin大师只是讲一个设计原则而已,这个原则可以应用到软件系统不同的层级。
例如:站在架构层的角度,模块可以指子系统subsystem
站在子系统的角度,模块可以指module,component
站在模块的角度:模块可以指类
所以说,这里的模块应该是一个广义的概念,而不是狭义的软件系统里各个子模块。
2)什么是依赖?
这里的依赖对应到具体的面向对象领域其实包含几个内容:
高层模块“依赖”低层模块:指高层模块需要调用低层模块的方法;
高层模块依赖抽象层:指高层模块基于抽象层编程;
低层模块依赖抽象层:指低层模块继承(inheritance)或者实现(implementation)抽象层;
细节依赖抽象:其实和上一个依赖是同一个意思;
所以说,大师就是大师啊,一个简简单单的“依赖”将各种情况都概括进来了,只是苦了我们这些平凡人,要么导致无法理解,要么导致理解错误:(
我们以一个简单样例来详细解释这些依赖,样例包含一个Player类,代表玩家;ICar接口,代表汽车;Benz、Ford、Chery代表具体的汽车,详细的代码如下
【Player】
~~~
package com.oo.oop.dip;
/**
* 玩家,对应DIP中的“高层模块”
*
*/
public class Player {
/**
* 开福特
* 不好的依赖:对应DIP中的“高层模块依赖低层模块”,Player直接使用了Ford类对象作为参数,Ford类修改,Player类【需要】重新编译测试
*/
public void play(Ford car)
{
car.accelerate();
car.shift();
car.steer();
car.brake();
}
/**
* 开奔驰
* 不好的依赖:对应DIP中的“高层模块依赖低层模块”,Player直接使用了Benz类对象作为参数,Benz类修改,Player类【需要】重新编译测试
*/
public void play(Benz car)
{
car.accelerate();
car.shift();
car.steer();
car.brake();
}
/**
* 开奇瑞
* 不好的依赖:对应DIP中的“高层模块依赖低层模块”,Player直接使用了Chery类对象作为参数,Chery类修改,Player类【需要】重新编译测试
*/
public void play(Chery car)
{
car.accelerate();
car.shift();
car.steer();
car.brake();
}
/**
* 开车
* 好的依赖: 对应DIP中的“高层模块依赖抽象层”,Player依赖ICar接口,不需要知道具体的车类型,Ford、Benz、Chery类修改,Player类【不需要】重新编译测试,只有ICar修改的时候Player才需要修改
*/
public void play(ICar car)
{
car.accelerate();
car.shift();
car.steer();
car.brake();
}
}
~~~
【ICar】
~~~
package com.oo.oop.dip;
/**
* 汽车接口,对应DIP中的抽象层
*/
public interface ICar {
/**
* 加速
*/
public void accelerate();
/**
* 换挡
*/
public void shift();
/**
* 转向
*/
public void steer();
/**
* 刹车
*/
public void brake();
}
~~~
【Benz】
~~~
package com.oo.oop.dip;
/**
* 奔驰,实现了ICar接口,对应DIP中的“低层依赖抽象层”
*
*/
public class Benz implements ICar {
@Override
public void accelerate() {
//加速非常快
System.out.println("Benz accelerate: very fast !!");
}
@Override
public void shift() {
//自动挡
System.out.println("Benz shift: automatic transmission !!");
}
@Override
public void steer() {
//非常平稳
System.out.println("Benz steer: very smooth,ESP && DSC && VSC !!");
}
@Override
public void brake() {
//刹车辅助系统
System.out.println("Benz steer: ABS && EBA && BAS && BA !!");
}
}
~~~
【Ford】
~~~
package com.oo.oop.dip;
/**
* 福特,实现了ICar接口,对应DIP中的“低层依赖抽象层”
*
*/
public class Ford implements ICar {
@Override
public void accelerate() {
//加速快
System.out.println("Ford accelerate: fast !!");
}
@Override
public void shift() {
//手自一体变速器
System.out.println("Ford shift: Tiptronic transmission !!");
}
@Override
public void steer() {
//平稳
System.out.println("Ford steer: smooth,ESP !!");
}
@Override
public void brake() {
//刹车辅助系统
System.out.println("Ford steer: ABS && EBA &!!");
}
}
~~~
【Chery】
~~~
package com.oo.oop.dip;
/**
* 奇瑞,实现了ICar接口,对应DIP中的“低层依赖抽象层”
*
*/
public class Chery implements ICar {
@Override
public void accelerate() {
//加速慢
System.out.println("Chery accelerate: slow !!");
}
@Override
public void shift() {
//手动挡
System.out.println("Chery shift: manual transmission !!");
}
@Override
public void steer() {
//平稳
System.out.println("Chery steer: smooth,ESP && DSC !!");
}
@Override
public void brake() {
//刹车辅助系统
System.out.println("Chery steer: only ABS !!");
}
}
~~~
(33) – ISP原则
最后更新于:2022-04-01 07:29:56
## 连载:面向对象葵花宝典:思想、技巧与实践(33) - ISP原则
**ISP**,Interface Segregation Principle,中文翻译为“**接口隔离原则**”。
和DIP原则一样,ISP原则也是大名鼎鼎的Martin大师提出来的,他在1996年的C++ Reporter发表“ The Interface Segregation Principle”的文章详细阐述了ISP原则,并且在他的经典著作《 Agile Software Development, Principles, Patterns》(中文翻译为:敏捷软件开发:原则、模式与实践)、《Practices, and Agile Principles, Patterns, and Practices in C#》(中文翻译为:敏捷软件开发:原则、模式与实践(C#版))中详细解释了ISP原则。
ISP最原始的定义如下:
“CLIENTS SHOULD NOT BE FORCED TO DEPEND UPON INTERFACES THAT THEY DO NOT USE.”
翻译成中文就是“客户端不应该被强迫去依赖它们并不需要的接口”。
单纯从字面意思来看,ISP原则是5个原则中最好理解的一个了。但是我们深入思考一下,其实发现也没有那么简单。如果你还记得我们前面讲的那些原则,你可能会想到一个问题:既然有了SRP,为什么还要ISP?
现在我们来回想一下SRP原则,如果类满足了SRP原则,那么基于这个类提炼的接口不就自然而然的满足了ISP原则了么?为什么我们还要费神费力的又搞一个ISP原则呢?
Martin大师自然不会是吃饱了没事做,故意整个东东来折腾大家,他在ISP的论文中有这么一句话交代了ISP原则,可惜的是很多人都没有把这句话贴出来:
The ISP acknowledges that there are objects that require non-cohesive interfaces;
however it suggests that clients should not know about them as a single class. Instead, clients
should know about abstract base classes that have cohesive interfaces.
翻译一下:ISP原则承认对象需要非内聚的接口,然而ISP原则建议客户端不需要知道整个类,只需要知道具有内聚接口的抽象父类即可。
也就是说,**ISP应用的场景是某些类不满足SRP原则,但使用这些类的客户端(即调用的类)应该根据接口来使用它,而不是直接使用它**。
虽然翻译了一下,但还是比较抽象,给个例子一看就明白了,而且已经有一个很好的例子了,即SRP原则中的“一体机”。
在“一体机”的样例中,虽然“一体机”同时具备“打印、复印、扫描、传真”的功能,但我们并不会设计一个“一体机”的接口,而是设计4个接口。这样调用接口的类可以根据自己需要精确使用某个接口,而不是调用一个大而全的接口。
具体代码如下:
ICopier.java
~~~
package com.oo.java.principles.isp;
/**
* 复印机接口
*/
public interface ICopier {
/**
* 复印
* @param paper
*/
void copy(Paper paper);
}
~~~
IFaxMachine.java
~~~
package com.oo.java.principles.isp;
/**
* 传真机接口
*/
public interface IFaxMachine {
/**
* 传真
* @param msg
*/
void fax(String msg);
}
~~~
IPrinter.java
~~~
package com.oo.java.principles.isp;
/**
* 打印机接口
*/
public interface IPrinter {
/**
* 打印
* @param doc
*/
void print(Document doc);
}
~~~
IScanner.java
~~~
package com.oo.java.principles.isp;
/**
* 扫描仪接口
*/
public interface IScanner {
/**
* 扫描
* @param paper
*/
void scan(Paper paper);
}
~~~
MultiFuncPrinter.java
~~~
package com.oo.java.principles.isp;
/**
* 多功能打印机(一体机)
* 实现了IFaxMachine(传真机)、ICopier(复印机)、IPrinter(打印机)、IScanner(扫描仪)4个接口
* 而不是提供一个IMultiFuncPrinter的接口,同时提供以上接口的功能
*
*/
public class MultiFuncPrinter implements IFaxMachine, ICopier, IPrinter, IScanner {
@Override
public void scan(Paper paper) {
// TODO Auto-generated method stub
}
@Override
public void print(Document doc) {
// TODO Auto-generated method stub
}
@Override
public void copy(Paper paper) {
// TODO Auto-generated method stub
}
@Override
public void fax(String msg) {
// TODO Auto-generated method stub
}
}
~~~
People.java
~~~
package com.oo.java.principles.isp;
/**
* 人
*/
public class People {
/**
* 复印操作,copy方法依赖ICopier接口,而不是使用MutiFuncPrinter类
*/
public void copy(ICopier copier, Paper paper){
copier.copy(paper);
}
/**
* 打印操作,print方法依赖IPrinter接口,而不是使用MutiFuncPrinter类
*/
public void print(IPrinter printer, Document doc){
printer.print(doc);
}
/**
* 传真操作,fax方法依赖IFaxMachine接口,而不是使用MutiFuncPrinter类
*/
public void fax(IFaxMachine faxer, String message){
faxer.fax(message);
}
/**
* 扫描操作,scan方法依赖IScanner接口,而不是使用MutiFuncPrinter类
*/
public void scan(IScanner scanner, Paper paper){
scanner.scan(paper);
}
}
~~~
Tester.java
~~~
package com.oo.java.principles.isp;
public class Tester {
public static void mai(String args[]){
People people = new People();
MultiFuncPrinter mfp = new MultiFuncPrinter();
//如下函数都是使用mfp作为参数,但实际上是使用了MultiFuncPrinter类实现了的不同接口
people.copy(mfp, new Paper()); //使用了MultiFuncPrinter类的ICopier接口,
people.fax(mfp, "I love oo"); //使用了MultiFuncPrinter类的IFaxMachine接口,
people.print(mfp, new Document()); //使用了MultiFuncPrinter类的IPrinter接口,
people.scan(mfp, new Paper()); //使用了MultiFuncPrinter类的IScanner接口,
}
}
~~~
(32) – LSP原则
最后更新于:2022-04-01 07:29:53
## 连载:面向对象葵花宝典:思想、技巧与实践(32) - LSP原则
LSP是唯一一个以人名命名的设计原则,而且作者还是一个“女博士” ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-19_569e21abc5518.gif)
=============================================================
**LSP**,Liskov substitution principle,中文翻译为“**里氏替换原则**”。
这是面向对象原则中唯一一个以人名命名的原则,虽然Liskov在中国的知名度没有UNIX的几位巨匠(Kenneth Thompson、Dennis Ritchie)、GOF四人帮那么响亮,但查一下资料,你会发现其实Liskov也是非常牛的:2008年图灵奖获得者,历史上第一个女性计算机博士学位获得者。其详细资料可以在维基百科上查阅:[http://en.wikipedia.org/wiki/Barbara_Liskov](http://en.wikipedia.org/wiki/Barbara_Liskov)
言归正传,我们来看看LSP原则到底是怎么一回事。
LSP最原始的解释当然来源于Liskov女士了,她在1987年的OOPSLA大会上提出了LSP原则,1988年,她将文章发表在ACM的SIGPLAN Notices杂志上,其中详细解释了LSP原则:
A type hierarchy is composed of subtypes and supertypes. The intuitive idea of a subtype is one whose objects provide all the behavior of objects of another type (the supertype) plus something extra.What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
英文比较长,看起来比较累,我们简单的翻译并归纳一下:
1) **子类的对象提供了父类的所有行为**,且加上子类额外的一些东西(可以是功能,也可以是属性);
2) 当程序基于父类实现时,**如果将子类替换父类而程序不需要修改**,则说明符合LSP原则
虽然我们稍微翻译和整理了一下,但实际上还是很拗口和难以理解。
幸好还有Martin大师也觉得这个不怎么通俗易懂,Robert Martin在1996年为《C++ Reporter》写了一篇题为《The The Liskov Substitution Principle》的文章,解释如下:
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
翻译一下就是:函数使用指向父类的指针或者引用时,必须能够在不知道子类类型的情况下使用子类的对象。
Martin大师解释了一下,相对容易理解多了。但Martin大师还不满足,在2002年,Martin在他出版的《Agile Software Development Principles Patterns and Practices》一书中,又进一步简化为:
Subtypes must be substitutable for their base types。
翻译一下就是:子类必须能替换成它们的父类。
经过Martin大师的两次翻译,我相信LSP原则本身已经解释得比较容易理解了,但问题的关键是:如何满足LSP原则?或者更通俗的讲:什么情况下子类才能替换父类?
我们知道,对于调用者来说(Liskov解释中提到的P),和父类交互无非就是两部分:调用父类的方法、得到父类方法的输出,中间的处理过程,P是无法知道的。
也就是说,调用者和父类之间的联系体现在两方面:函数输入,函数输出。详细如下图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-20_569f5ccb01a50.jpg)
有了这个图之后,如何做到LSP原则就清晰了:
1) **子类必须实现或者继承父类所有的公有函数**,否则调用者调用了一个父类中有的函数,而子类中没有,运行时就会出错;
2) **子类每个函数的输入参数必须和父类一样**,否则调用父类的代码不能调用子类;
3) **子类每个函数的输出**(返回值、修改全局变量、插入数据库、发送网络数据等)必须不比父类少,否则基于父类的输出做的处理就没法完成。
有了这三条原则后,就可以很方便的判断类设计是否符合LSP原则了。需要注意的是第3条的关键是“不比父类少”,也就是说可以比父类多,即:父类的输出是子类输出的子集。
有的朋友看到这三条原则可能有点纳闷:这三条原则一出,那子类还有什么区别哦,岂不都是一样的实现了,那还会有不同的子类么?
其实如果仔细研究这三条原则,就会发现其中**只是约定了输入输出,而并没有约束中间的处理过程**。例如:同样一个写数据库的输出,A类可以是读取XML数据然后写入数据库,B类可以是从其它数据库读取数据然后本地的数据库,C类可以是通过分析业务日志得到数据然后写入数据库。这3个类的处理过程都不一样,但最后都写入数据到数据库了。
LSP原则最经典的例子就是“长方形和正方形”这个例子。从数学的角度来看,正方形是一种特殊的长方形,但从面向对象的角度来观察,正方形并不能作为长方形的一个子类。原因在于对于长方形来说,设定了宽高后,面积 = 宽 * 高;但对于正方形来说,设定高同时就设定了宽,设定宽就同时设定了高,最后的面积并不是等于我们设定的 宽 * 高,而是等于最后一次设定的宽或者高的平方。
具体代码样例如下:
Rectangle.java
~~~
package com.oo.java.principles.lsp;
/**
* 长方形
*/
public class Rectangle {
protected int _width;
protected int _height;
/**
* 设定宽
* @param width
*/
public void setWidth(int width){
this._width = width;
}
/**
* 设定高
* @param height
*/
public void setHeight(int height){
this._height = height;
}
/**
* 获取面积
* @return
*/
public int getArea(){
return this._width * this._height;
}
}
~~~
Square.java
~~~
package com.oo.java.principles.lsp;
/**
* 正方形
*/
public class Square extends Rectangle {
/**
* 设定“宽”,与长方形不同的是:设定了正方形的宽,同时就设定了正方形的高
*/
public void setWidth(int width){
this._width = width;
this._height = width;
}
/**
* 设定“高”,与长方形不同的是:设定了正方形的高,同时就设定了正方形的宽
*/
public void setHeight(int height){
this._width = height;
this._height = height;
}
}
~~~
UnitTester.java
~~~
package com.oo.java.principles.lsp;
public class UnitTester {
public static void main(String[] args){
Rectangle rectangle = new Rectangle();
rectangle.setWidth(4);
rectangle.setHeight(5);
//如下assert判断为true
assert( rectangle.getArea() == 20);
rectangle = new Square();
rectangle.setWidth(4);
rectangle.setHeight(5);
//<span style="color:#ff0000;">如下assert判断为false,断言失败,抛出java.lang.AssertionError</span>
assert( rectangle.getArea() == 20);
}
}
~~~
上面这个样例同时也给出了一个判断子类是否符合LSP的取巧的方法,即:针对父类的单元测试用例,传入子类是否也能够测试通过。如果测试能够通过,则说明符合LSP原则,否则就说明不符合LSP原则
(31) – OCP原则
最后更新于:2022-04-01 07:29:51
## 连载:面向对象葵花宝典:思想、技巧与实践(31) - OCP原则
开闭原则是一个大部分人都知道,但大部分人都不懂的设计原则!![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-20_569f5ccae167f.gif)
====================================================================
OCP,Open-Closed Principle,中文翻译为“开闭原则”。
当我第一次看到OCP原则时,我的感觉就是这原则也太抽象了吧,什么开,什么闭呢?
然后我去寻找更加详细的答案,最经典也是最常见的解释就是维基百科了:
[http://en.wikipedia.org/wiki/Open/closed_principle](http://en.wikipedia.org/wiki/Open/closed_principle)
"software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification";
翻译一下就是:对扩展开放,对修改封闭!
虽然这句解释更详细了,但其实还是很难理解,我因此去请教了一个前辈高人,他的回答更加惊世骇俗:不修改代码就可以增加新功能!!
当时我听到这句话就震惊了,这是多么神奇的事情啊,不修改代码就能够增加新功能!
但问题是:怎么做到的呢?难道这个原则是有关人工智能,又或者有什么高超的技巧,能够做到不修改代码增加新功能?
这么牛逼的原则当然要继续探索了,但怎么也没有找到“不修改代码就可以增加新功能”的独门秘籍!
于是对这个原则有了怀疑,经过继续的探索和查看各种资料,才发现原来是各位大师们在解释这个原则的时候隐藏了非常重要的“主语”,而这才是OCP原则的关键!
大师们省略的主语一个就是consumer(翻译成使用者、消费者),一个就是provider(翻译成生产者、提供着),例如A类调用了B类的方法,则A就是consumer,B就是provider。
完整的OCP原则实际上应该这样表述:open for provider extension,closed for consumer modification,翻译一下就是:对使用者修改关闭,对提供者扩展开放!
更通俗的意思就是:提供者增加新的功能,但使用者不需要修改代码!
虽然到这里我们已经基本上将OCP原则解释清楚了,但实际上细心的朋友还是会发现有问题的:提供者增加新的功能,使用者不修改代码就能用上么?
比如说:你设计一款有关车游戏,需要设计一个“car”的类,这个类原来有“加速”、“刹车”、“转向”三个功能,现在你要加一个新功能“改装”,游戏中其它类例如player,不修改代码就可以用上“改装”这个功能么?
很显然这是不可能的,我都新加了一个函数,你都不调用就能用新的功能,这也太邪乎了吧?
答案在于所谓的增加新功能,并不是增加一个全新的功能,而是原有的功能有了替代实现,这也是英文的“extension”所隐含的深意!
继续以赛车car作为例子,假设现在你设计了“卡车”、“跑车”、“家用车”三种车,现在要增加一种车“卡丁车”,只要“卡丁车”也实现了“加速”、“刹车”、“转向”,那么player不需要修改代码,就可以玩“卡丁车”了;但如果你增加了一种“改装”的功能,那么player必须修改才能使用“改装”功能。
对应到代码上来说,OCP的应用原则如下:
1) **接口不变**:包括函数名、函数参数、函数返回值等,可以应用OCP
2) **接口改变**:已有函数修改名称、参数、返回值,或者增加新的函数,OCP都不再适应
虽然OCP原则是针对类设计提出来的原则,但其思想其实适应很广,系统和系统、子系统和子系统、模块和模块之间都可以应用OCP原则,而且不同的地方应用其实都是遵循同一个原则:**通过接口交互**!例如:
1) 类之间应用OCP:使用interface进行交互;
2)模块和模块、系统和系统:使用规定好的协议,不管是私有的还是公开的,例如HTTP、SOAP
(30) – SRP原则
最后更新于:2022-04-01 07:29:49
## 连载:面向对象葵花宝典:思想、技巧与实践(30) - SRP原则
前面详细阐述了“高内聚低耦合”的总体设计原则,但如何让设计满足这个原则,并不是一件简单的事情,幸好各位前辈和大牛已经帮我们归纳总结出来了,这就是“设计原则”和“设计模式”。毫不夸张的说,**只要你吃透这些原则和模式并熟练应用,就能够做出很好的设计**。
==================================================================
**【SRP原则详解】**
SRP,single responsibility principle,中文翻译为“单一职责原则”!
这是面向对象类设计的第一个原则,也是看起来最简单的一个原则,但这实际上远远没有那么简单,很多人都不一定真正理解了!
我们随便找几个网上的解释,看看各位大师或者经典网站是如何解释的:
百度百科([http://baike.baidu.com/view/1545205.htm](http://baike.baidu.com/view/1545205.htm)):
一个类应该有且仅有一个职责。关于职责的含意,面向对象大师Robert.C.Martin有一个著名的定义:所谓一个类的职责是指引起该类变化的原因,如果一个类具有一个以上的职责,那么就会有多个不同的原因引起该类变化,其实就是耦合了多个互不相关的职责,就会降低这个类的内聚性。
说句实话,虽然是面向对象大师Martin的解释,我还是看得不甚明白:引起类变化的原因太多了,例如:
给类加一个方法是变化吧?
给类加一个属性是变化吧?
类的函数增加一个参数是变化吧?
。。。。。。
引起这些变化的原因太多了,如果每个原因都是一个职责,那SRP原则简直就没法判断了!
Wiki百科([http://en.wikipedia.org/wiki/Single_responsibility_principle](http://en.wikipedia.org/wiki/Single_responsibility_principle) )内容和百度百科基本一致,看起来百度百科像wiki百科的翻译:)
Martin defines a responsibility as a reason to change, and concludes that a class or module should have one, and only one, reason to change.
除了这些标准的解释外,还有一种说法:SRP就是指每个类只做一件事!
这个解释更通俗易懂,也更加适合中国人理解。虽然比Martin大师的解释更清楚一些,但仔细想想,还是有个地方比较难以理解:什么叫做“一件事”?
比如说一个学生信息管理类,这个类有“添加学生信息”、“查询学生信息”、“修改学生信息”、“删除学生信息”,那么这是4件事情,还是一件事情呢?
看起来好像是4个事情,但稍有经验的朋友应该都知道,这4个事情绝大部分情况下都是一个类来实现的,而不是分成4个类!
所以关键的问题在于:什么是“一件事”?是每个功能一件事情么?
其实答案就在我们自己身上,因为只要我们工作,无时不刻的承担着一定的“职责”!
现在让我们抛开面向对象,抛开软件,抛开计算机,来看看我们自己的“职责”。
比如说,我是一个程序猿,我的职责应该是“写程序”,但写程序有很多事情,例如:编码,单元测试、系统测试,bug修复,开会,写文档。。。。。。
再比如说,我的BOSS是一个管理者,他的职责是“管理程序猿”,他也有很多工作,例如:制定计划,团队建设、开会、协调资源、写文档。。。。。。
又比如说,我是一个快递员,也有很多工作:分包、快递、收款、开会。。。。。。
这些职责其实都不是我们自己定义的,而是公司或者部门或者组织给我们安排工作的时候定义的,也就是说:“职责”是站在他人的角度来定义的,而不是自己定义的,也许你想把自己定义成CEO,但你的老板会真的让你当CEO么?
经过对我们自己的职责的分析,我们可以得出两个关于职责的重要结论:
1) 职责是站在他人的角度来定义的
2) 职责不是一件事,而是很多事情,但这些事情都是和职责紧密相关的
对应到面向对象设计领域,我们可以说一个类的职责应该如下定义:
1) 类的职责是站在其它类的角度来定义的;
2) 类的职责包含多个相关功能;
因此,SRP可以翻译成“**一个类只负责一组相关的事情**”,对应到代码中就是:一个类有多个方法,这些方法是相关的。
当然,如果你再让我解释什么是“相关”,那我只能吐血了:)
有了这个定义,我们再来看“学生信息管理类”,很明显,学生管理类具有的4个功能都是和“管理”相关的,按照SRP原则,应该只设计一个“学生信息管理类”就可以了。
**【SRP原则范围】**
但现实世界往往比理想要复杂,一个最典型的例子就是“办公一体机”。
根据SRP原则,打印机可以设计成一个类,复印机也可以设计成一个类,扫描仪也可以设计成一个类,传真机还是可以设计成一个类,但偏偏就是出了个“办公一体机”,这个机器集成了“打印、复印、扫描、传真”4个职责!
如果我们要设计一个“办公一体机”的类,怎么也不可能设计出一个符合SRP原则的“办公一体机”的类来!
怎么办?是SRP不正确么,还是我们永远都不要设计“办公一体机”这样的类?
其实SRP也没有错,“办公一体机”也应该设计,但:不要用SRP来约束“办公一体机”这样的类!
也就是说,SRP其实是有适应范围的,SRP只适合那些基础类,而不适合基于基础类构建复杂的聚合类。
在“办公一体机“的样例中,“打印机”、“复印机”、“扫描仪”、“传真机”都是基础类,每个类都承担一个职责,而办公一体机是“聚合类”,同时集成了4种功能!
细心的朋友可能会继续问道:SRP不能应用于聚合类,那么如何保证聚合类的设计质量呢?
这个问题的答案在GoF的《设计模式》一书中有详细的答案,即:优先使用对象组合,而不是类继承。详细内容请参考后续“设计模式”部分的内容
(29) – 高内聚低耦合
最后更新于:2022-04-01 07:29:46
## 连载:面向对象葵花宝典:思想、技巧与实践(29) - 高内聚低耦合
**高内聚低耦合**,可以说是每个程序猿,甚至是编过程序,或者仅仅只是在大学里面学过计算机,都知道的一个简单的设计原则。
虽然如此流行和人所众知,但其实**真正理解的人并不多,很多时候都是人云亦云**。
===============================================================
要想真正理解“高内聚低耦合”,需要回答两个问题:
1)为什么要高内聚低耦合?
2)高内聚低耦合是否意味内聚越高越好,耦合越低越好?
**第一个问题:为什么要高内聚低耦合?**
经典的回答是:降低复杂性。
确实很经典,当然,其实也是废话!我相信大部分人看了后还是不懂,什么叫复杂性呢?
要回答这个问题,其实可以采用逆向思维,即:如果我们不做到这点,将会怎样?
首先来看内聚,试想一下,假如我们是低内聚,情况将会如何?
前面我们在阐述内聚的时候提到内聚的关键在于“元素的凝聚力”,如果内聚性低,则说明凝聚力低;对于一个团队来说,如果凝聚力低,则一个明显的问题是“不稳定”;对于一个模块来说,内聚性低的问题也是一样的“不稳定”。具体来说就是如果一个模块内聚性较低,则这个模块很容易变化。一旦变化,设计、编码、测试、编译、部署的工作量就上来了,而一旦一个模块变化,与之相关的模块都需要跟着改变。
举一个简单的例子,假设有这样一个设计不好的类:Person,其同时具有“学生”、“运动员”、“演员”3个职责,有另外3个类“老师”、“教练”、“导演”依赖这个类。
**Person.java**
~~~
package com.oo.cohesion.low;
/**
* “人”的类设计
*
*/
public class Person {
/**
* 学生的职责:学习
*/
public void study() {
//TODO: student's responsibility
}
/**
* 运动员的职责:运动
*/
public void play(){
//TODO: sportsman's responsibility
}
/**
* 演员的职责:扮演
*/
public void act(){
//TODO: actor's responsibity
}
}
~~~
**Teacher.java**
~~~
package com.oo.cohesion.low;
/**
* “老师”的类设计
*
*/
public class Teacher {
public void teach(Person student){
student.study(); //依赖Person类的“学生”相关的职责
}
}
~~~
**Coach.java**
~~~
package com.oo.cohesion.low;
/**
* “教练”的类设计
*
*/
public class Coach {
public void train(Person trainee){
trainee.play(); //依赖Person类的“运动员”职责
}
}
~~~
**Director.java**
~~~
package com.oo.cohesion.low;
/**
* “导演”的类设计
*
*/
public class Director {
public void direct(Person actor){
actor.act(); //依赖Person类“演员”的相关职责
}
}
~~~
在上面的样例中,Person类就是一个典型的“低内聚”的类,很容易发生改变。比如说,现在老师要求学生也要考试,则Person类需要新增一个方法:test,如下:
~~~
package com.oo.cohesion.low;
/**
* “人”的类设计
*
*/
public class Person {
/**
* 学生的职责:学习
*/
public void study() {
//TODO: student's responsibility
}
/**
* 学生的职责:考试
*/
public void test(){
//TODO: student's responsibility
}
/**
* 运动员的职责:运动
*/
public void play(){
//TODO: sportsman's responsibility
}
/**
* 演员的职责:扮演
*/
public void act(){
//TODO: actor's responsibity
}
}
~~~
由于Coach和Director类都依赖于Person类,Person类改变后,虽然这个改动和Coach、Director都没有关系,但Coach和Director类都需要重新编译测试部署(即使是PHP这样的脚本语言,至少也要测试)。
同样,Coach和Director也都可能增加其它对Person的要求,这样Person类就需要同时兼顾3个类的业务要求,且任何一个变化,Teacher、Coach、Director都需要重新编译测试部署。
对于耦合,我们采用同样的方式进行分析,**即:如果高耦合,将会怎样?**
高耦合的情况下,模块依赖了大量的其它模块,这样任何一个其它依赖的模块变化,模块本身都需要受到影响。所以,高耦合的问题其实也是“不稳定”,当然,这个不稳定和低内聚不完全一样。对于高耦合的模块,可能本身并不需要修改,但每次其它模块修改,当前模块都要编译、测试、部署,工作量同样不小。
我们同样以一个样例来说明。假设我们要设计一个Boss类,Boss是要管整个公司的,那么我们假设这是一家“麻雀虽小五脏俱全”的公司,同时有“研发、测试、技术支持、销售、会计、行政”等部门。
**Boss.java**
~~~
package com.oo.coupling.high;
/**
* “老板”类
*
*/
public class Boss {
private Tester tester;
private Developer developer;
private Supporter supporter;
private Administration admin;
private Accountant accountant;
/**
* Boss每天检查工作,看到下面的代码,是否觉得Boss很忙?
*/
public void check(){
//检查测试工作
tester.report();
//检查研发工作
developer.report();
//检查技术支持工作
supporter.report();
//检查行政工作
admin.report();
//检查财务工作
accountant.report();
}
}
~~~
**Accountant.java**
~~~
package com.oo.coupling.high;
/**
* “财务”类
*
*/
public class Accountant {
public void report(){
System.out.print("Accountant report");
}
}
~~~
**Administration.java**
~~~
package com.oo.coupling.high;
/**
* “行政”类
*
*/
public class Administration {
public void report(){
System.out.print("Administration report");
}
}
~~~
**Developer.java**
~~~
package com.oo.coupling.high;
/**
* “开发”类
* @author Administrator
*
*/
public class Developer {
public void report(){
System.out.print("Developer report");
}
}
~~~
**Supporter.java**
~~~
package com.oo.coupling.high;
/**
* “技术支持”类
*
*/
public class Supporter {
public void report(){
System.out.print("Supporter report");
}
}
~~~
**Tester.java**
~~~
package com.oo.coupling.high;
/**
* “测试”类
*
*/
public class Tester {
public void report(){
System.out.print("Tester report");
}
}
~~~
好吧,Boss很忙,我们也很钦佩,但是有一天,研发的同学觉得他们应该做更多的事情,于是他们增加了一个“研究”的工作,且这个工作也不需要报告给Boss,例如:
~~~
package com.oo.coupling.high;
/**
* “开发”类
* @author Administrator
*
*/
public class Developer {
public void report(){
System.out.print("Developer report");
}
/**
* 研发新增加一个研究的任务,这个任务也不需要向Boss汇报
*/
public void research(){
System.out.print("Developer is researching big data, hadoop :)");
}
}
~~~
虽然这个工作不需要报告给Boss,但由于Developer类修改了,那么Boss类就需要重新编译测试部署。其它几个Tester、Supporter类等也是类似,一旦改变,即使这些类和Boss类半毛钱关系都没有,Boss类还是需要重新编译测试部署,所以Boss类是很不稳定的。
所以,无论是“低内聚”,还是“高耦合”,其本质都是“不稳定”,不稳定就会带来工作量,带来风险,这当然不是我们希望看到的,所以我们应该做到“高内聚低耦合”。
回答完第一个问题后,我们来看第二个问题:高内聚低耦合是否意味着内聚越高越好,耦合越低越好?
按照我们前面的解释,内聚越高,一个类越稳定;耦合越低,一个类也很稳定,所以当然是内聚越高越好,耦合越低越好了。
但其实稍有经验的同学都会知道这个结论是错误的,并不是内聚越高越好,耦合越低越好,**真正好的设计是在高内聚和低耦合间进行平衡,也就是说高内聚和低耦合是冲突的**。
我们详细来分析一下为什么高内聚和低耦合是冲突的。
对于内聚来说,最强的内聚莫过于一个类只写一个函数,这样内聚性绝对是最高的。但这会带来一个明显的问题:类的数量急剧增多,这样就导致了其它类的耦合特别多,于是整个设计就变成了“高内聚高耦合”了。由于高耦合,整个系统变动同样非常频繁。
同理,对于耦合来说,最弱的耦合是一个类将所有的函数都包含了,这样类完全不依赖其它类,耦合性是最低的。但这样会带来一个明显的问题:内聚性很低,于是整个设计就变成了“低耦合低内聚”了。由于低内聚,整个类的变动同样非常频繁。
对于“低耦合低内聚”来说,还有另外一个明显的问题:几乎无法被其它类重用。原因很简单,类本身太庞大了,要么实现很复杂,要么数据很大,其它类无法明确该如何重用这个类。
所以,内聚和耦合的两个属性,排列组合一下,**只有“高内聚低耦合”才是最优的设计**。
因此,在实践中我们需要牢牢记住需要在高内聚和低耦合间进行平衡,而不能走极端。 具体如何平衡,且听下回分解。
(28) – 设计原则:内聚&耦合
最后更新于:2022-04-01 07:29:44
## 连载:面向对象葵花宝典:思想、技巧与实践(28) - 设计原则:内聚&耦合
前面通过实例讲解了一个一环扣一环的面向对象的开发流程:用例模型 -> 领域模型 -> 设计模型(类模型 + 动态模型),解答了面向对象如何做的问题。接下来我们就要讲“如何做好面向对象设计”的技巧了
===================================================================
**【内聚】**
参考维基百科的解释,内聚的含义如下:
cohesion refers to the degree to which the elements of a [module](http://en.wikipedia.org/wiki/Module_(programming)) belong together.
(http://en.wikipedia.org/wiki/Cohesion_(computer_science))
翻译一下即:**内聚指一个模块内部元素彼此结合的紧密程度**。
看起来很好理解,但深入思考一下,其实没有那么简单。
首先:“模块”如何理解?
你一定会说,“模块”当然就是我们所说的系统里的“XX模块”了,例如一个ERP系统的“权限”模块,一个电子商务的“支付”模块,一个论坛网站的“用户管理”模块。。。。。。等等
你说的没错,但在面向对象领域,谈到“内聚”的时候,模块的概念远远不止我们通常所理解的“系统内的某个模块”这个范围,而是可大可小,大到一个子系统,小到一个函数,你都可以理解为内聚里所说的“模块”。
所以,你可以用“内聚”来判断一个函数设计是否合理,一个类设计是否合理,一个接口设计是否合理,一个包设计是否合理,一个模块/子系统设计是否合理。
其次:“元素”究竟是什么?
有了前面对“模块”的深入研究后,元素的含义就比较容易明确了(不同语言稍有不同)。
函数:函数的元素就是“代码”
类/接口:类的元素是“函数、属性”
包:包的元素是“类、接口、全局数据”等
模块:模块的元素是“包、命名空间”等
再次:“结合”是什么?
英文的原文是“belong”,有“属于”的意思,翻译成中文“结合”,更加贴近中文的理解。但“结合”本身这个词容易引起误解。绝大部分人看到“结合”这个单词,想到的肯定是“你中有我、我中有你”这样的含义,甚至可能会联想到“美女和帅哥”的结合,抑或“青蛙王子和公主”的结合这种情况。
这样的理解本身也并没有错,但比较狭隘。
我们以类的设计为例:假如一个类里面的函数都是只依赖本类其它函数(当然不能循环调用啦),那内聚性肯定是最好的,因为“结合”得很紧密。
但如果这个类的函数并不依赖本类的函数呢?我们就一定能说这个类的内聚性不好么?
其实也不尽然,最常见的就是CRUD操作类,这几个函数相互之间没有任何结合关系(某些设计可能会先查询再修改,但这样的设计不是事务安全的),但其实这几个函数的内聚性非常高。
所以,关于内聚的结合概念,我认为不是非常恰当的描述。那么,就究竟什么才是真正的“内聚”呢?
答案就藏在显而易见的地方,翻开你的词典,仔细看看cohesion的含义,你会看到另外一个解释:凝聚力!
**“凝聚力”就是“内聚”的核心思想**,抛开面向对象不谈,我们日常工作中几乎随处可见“凝聚力”:
你可能会说,你的团队很有凝聚力。。。。。。
领导可能会说:我们要增强团队的凝聚力。。。。。。
成功学大师会说:凝聚力是一个团队成功的基石。。。。。。。
面向对象领域的“凝聚力”,和团队的“凝聚力”是一样的概念。
l 判断团队凝聚力时,我们关注团队成员是否都专注于团队的目标;判断面向对象模块的凝聚力时,我们同样关注元素是否专注于模块的目标,即:模块本身的职责!
l 判断团队凝聚力时,我们还会关注团队成员之间是否互相吸引和帮助;判断面向对象模块凝聚力时,我们同样关注元素间的结合关系;
虽然判断内聚性的时候我们会考虑元素的结合情况,**但其实是否专注模块的职责,才是内聚性的充要条件**。
当模块的元素全部都专注于模块的职责的时候,即使元素间的结合不是很紧密,也是符合内聚性的要求的,这也是CRUD设计符合内聚性的原因。
所以,判断一个模块(函数、类、包、子系统)“内聚性”的高低,最重要的是关注模块的元素是否都忠于模块的职责,简单来说就是“不要挂羊头卖狗肉”。
**【耦合】**
参考维基百科,耦合的定义如下:
coupling or dependency is the degree to which each [program module](http://en.wikipedia.org/wiki/Module_(programming)) relies on each one of the other modules
(http://en.wikipedia.org/wiki/Coupling_(computer_science))
简单翻译一下:耦合(或者称依赖)是程序模块相互之间的依赖程度。
从定义来看,耦合和内聚是相反的:内聚关注模块内部的元素结合程度,耦合关注模块之间的依赖程度。
理解耦合的关键有两点:什么是模块,什么是依赖。
什么是模块?
模块和内聚里面提到的模块一样,耦合中的模块其实也是可大可小。常见的模块有:函数、类、包、子模块、子系统等
**什么是依赖?**
依赖这个词很好理解,通俗的讲就是某个模块用到了另外一个模块的一些元素。
例如:A类使用了B类作为参数,A类的函数中使用了B类来完成某些功能。。。。。。等等
(27) – 动态模型设计
最后更新于:2022-04-01 07:29:42
## 连载:面向对象葵花宝典:思想、技巧与实践(27) - 动态模型设计
类模型指导我们如何**声明类**,动态模型指导我们如何**实现类**!
动态模型设计一般都是在类模型设计完成后才开始,因为动态模型设计的时候一般都需要用到类模型中的类。相对类模型来说,动态模型要相对简单一些,主要原因在于动态模型设计的时候没有什么设计原则和设计模式需要应用,只需要对照用例模型,根据用例模型的特点,选取一个合适的动态模型将其表述出来即可。
动态模型在实际开发过程中有非常重要的作用,简单来说,如果没有动态模型,那么你虽然完成了类设计,但还是不能编码,或者只能编写类的声明代码(类属性、方法名称),但不能写类的实现代码(方法里面的实现逻辑,即:每个方法的实现)。动态模型就是用来指导我们如何编写具体的方法的。
有的同学可能会有疑问:那些地方要进行动态模型设计呢?
还是那句老话,你觉得比较复杂你就设计,简单你就不设计,总之:**你需要你就设计**!
像我在实际开发中,基本上一个中等项目就一两个业务设计动态模型(小项目看到需求就编码了 :) ),其它业务看需求文档就能看出如何编码,这也是有经验和经验不足的差别。
参考UML标准,常见的动态模型如下:
### 【状态模型】
状态模型主要用于描述对象的生命周期的状态变化。通过状态图,我们可以了解到对象有哪些状态,状态之间如何转换,转换的触发条件等。当我们发现一个对象的状态比较复杂的时候,就需要设计对象的状态模型。
UML中使用状态图来描述状态模型
**【活动模型】**
活动模型主要用于描述一个工作流程或者计算流程。其关注点是在完成某项工作的过程中,系统中的哪些对象承担了什么样的任务、做了什么处理,以及这些对象之间的先后交互关系。当我们发现一个处理流程比较复杂的时候,就需要设计流程的活动模型。
UML中使用活动图来描述活动模型
**【序列模型】**
序列模型主要用于描述对象按照时间顺序组织的消息交互过程,其关键特征是强调按照“时间顺序”来组织对象的交互,所以序列图有时又称为“时序图”或者“顺序图”。序列模型是我们最常用的动态模型,特别适合将用例模型或者SSD转换为系统的动态模型。
UML中使用序列图来描述序列模型
**【协作模型】**
协作模型主要用于描述按照对象之间的关联来组织的消息交互过程,其关键特征是强调“对象关系”来组织对象的交互。协作模型的作用和序列模型一样,只是强调的点不同,大部分的时候我们都是选择“序列模型”,因为序列模型的时间顺序很多时候和用例模型的步骤不谋而合。
UML中使用协助图来描述协作模型
注意:以上模型并不是每个都必须有的,根据实际需要选择即可
**建模实践**
以上这些模型都可以从用例模型推导出来,活动模型、序列模型、协作模型基本上都是和用例模型一一对应的,或者对应用例中的某个分支。一般情况下不推荐一个模型中包含多个分支,因为这样会导致图比较复杂,而且主题不突出。
状态模型和其它模型相比要复杂一些,因为并不能从单个用例或者单个用例分支推导出某个对象的所有状态,而需要综合多个用例模型,从中提取出和某个对象状态相关的内容,再统一设计状态模型。
从用例模型推导出动态模型是一个“分解和分配”的过程,因为在用例模型中,系统是当做一个“黑盒”来看待的,而在动态模型中,系统不再是一个黑盒,而是分解成了一个一个的类。因此我们需要将原来笼统的划分给系统的功能和职责,进一步分解并分配给不同的类。通俗的讲,动态模型就是说:为了完成系统的XXX功能, 先需要类A做任务1,然后需要类B需要做任务2,再由类C做任务3。。。。。。依次分解下去,最终就能够实现将类串起来,相互配合,最后实现了系统的需求。
我们以POS机为例,假设我们基于买单这个用例的正常分支设计“序列模型”,则可以得到如下的“序列模型”:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-20_569f5ccac8de6.jpg)
有了上面这个“序列图”,假设我们要开始写代码,则基本可以按照如下伪码的方式实现(实际的编码肯定不会这么简单,但方法是一样):
~~~
main(){
Trade trade = new Trade();
Integer tradeId =trade.makeNewTrade(); //创建
trade.addGoods(); //增加商品
trade.cacuMoney(); //计算总额
............//省略一大段代码
Receipt receipt = new Receipt();
receipt.print(trade); //打印小票
...........//省略一大段代码
trade.finish(); //结束
}
~~~
(26) – 类模型三板斧
最后更新于:2022-04-01 07:29:40
## 连载:面向对象葵花宝典:思想、技巧与实践(26) - 类模型三板斧
**类模型设计其实就是程咬金打天下 -- 三板斧 而已 :)**
**第一斧(照猫画虎):领域类映射**
面向对象类设计首先要解决的一个问题是:类从哪里来 ?
有的人可能会认为,要发挥想象力、创造力。。。。。等各种“力”——这种方法的主要问题是:我们不是在进行纯粹的艺术创造,而是要最终满足客户需求,而不能天马行空。
有的人可能会想到,参考其它的系统吧,把类似系统拿过来改吧改吧 ——这种方法的主要问题是:如果没有其它类似系统给你参考呢 ?
还有的人干脆就说:拍脑袋吧,凭感觉吧 —— 这种方法的主要问题是:猴子能敲出莎士比亚全集么 ?
看起来以上方法都不太可行,那究竟如何才能从哪里找到我们需要的类呢?
相信绝大部分认真看书的同学都会灵光一闪:领域模型。
我们将上一章中的领域模型图拿出来,重新再看一下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-20_569f5cca458ea.jpg)
相信不用我多说,绝大部分同学一眼就能看出:哇塞,这不就是类么?
确实是这样的,**领域模型中的“领域类”,是设计模型中“软件类”最好的来源**。通过“领域类”来启发我们设计最初的“软件类”,具有如下几个明显的**优点**:
1)软件类来自领域类,领域类来自用例,用例来自客户,这样一环扣一环,软件类的正确性得到了保证,不用担心拍脑袋带来的问题;
2)领域类到软件类的转换非常简单,不需要天才的创新,或者丰富的想象力,只要掌握基本的面向对象的知识就能完成,菜鸟也能做设计;
3)不需要参考其它系统,不用担心没有参照物时无法设计的问题;
从领域类到软件类的转换操作非常简单,基本上就是一个照猫画虎的过程。
**【类筛选】**
虽然我们说从领域类到软件类是一个照猫画虎的过程,但并不意味着将领域类全盘拷贝过来即可。主要的原因在于“软件类”是软件系统内部的一个概念,而领域类是业务领域的概念,并不是每个领域类最终都会体现在软件系统中。
以POS机的领域类为例,领域类“顾客”不需要转换为软件类,因为顾客是POS机业务领域的一个重要参与者,但并不是POS机内部需要实现的一个实体,在POS机业务中,顾客甚至都不是和POS机直接交互的实体,站在POS软件系统的角度来说,顾客和POS机其实没有任何关系。
对于屏幕、键盘、扫描仪这些输入输出设备,一般情况下我们认为它们是POS机系统硬件的一部分,而并不是POS机软件系统的一部分。但假如POS机有一个需求是既支持图形界面输出,又支持字符界面输出,那么POS的软件系统就需要处理这种和屏幕相关的需求了,此时屏幕就是POS机软件系统的一部分了,需要将领域类转换为软件类。为了简单处理,接下来的分析中,输入输出设备不做转换。
经过筛选后,剩下的领域类就需要都转换为软件类,具体如下:
收银员、商品、交易、小票、支付、信用卡、会员卡、现金、购物卡。
**【名称映射】**
筛选完成后,我们开始讲领域类转换为软件类,转换的方法很简单,首先不管三七二十一,将每个领域类都用一个软件类与对应,名称都保持一样即可。
有的同学可能担心这样设计是否会不符合面向对象设计的要求,是否会导致设计质量不高。。。。。。等等,其实这种担心是多余的,因为我们后续还有很多工作要做,目前做的只是一个开始工作。
**【属性映射】**
通过名称映射的方法得到软件类后,接下来就是要设计类的属性了。由于领域类中也已经有了属性,因此我们也只需简单的照搬过来即可。
**【提炼方法】**
软件类的属性设计完成后,接下来就需要设计软件类的方法了。但这次我们就没有那么好的运气了,因为领域类中并没有方法!因此我们不能通过简单映射的方法来获取方法,必须采取其它手段。
和类的设计一样,类方法的设计同样不能采取“创造力、参考其它系统、拍脑袋”等方式来完成,为了确保正确性,类的方法设计也同样应该能够从已有的模型中推导出来。
由于已经明确领域模型中没有方法了,因此就不能从领域模型中得到软件类的方法,剩余只有一个“用例模型”了,因此我们锁定“用例模型”,看看如何从中找到我们所需要的方法。
其实方法也很简单,概括一下就是:**找动词**。
你可能不敢相信自己的眼睛,这么简单,那几乎初中生都会做设计啊,找动词谁不会呢?
然而不管你信不信,这一步确实是这么简单,当然,如果面向对象设计只是到此为止,那确实初中生也是可以做的,但实际上这只是面向对象类设计的开始步骤而已,后面的工作还多着了,所以完全不用担心初中生来抢你的饭碗。
我们以POS机为例,来看看如何通过“找动词”这种技巧来找到软件类的方法。
如下是POS机的用例,我们将相关动词都**加粗**显示:
【用例名称】
买单
【场景】
Who:顾客、收银员
Where:商店的收银台
When:营业时间
【用例描述】
1. 顾客**携带**选择好的商品到收银台;
(这一步没有异常)
2. 收银员逐一**扫描**商品条形码,系统根据条形码**查询**商品信息;
2.1 扫描仪坏了,必须支持**手工输入**条形码;
2.2 商品的条形码无法扫描,必须支持**手工输入**条形码;
2.3 条形码能够扫描,但查询不到信息,需要收银员和顾客**沟通**,放弃购买此产品
3. 扫描完毕,系统**显示**商品总额,收银员**告诉**顾客商品总额;
(这一步没有异常)
4. 顾客将钱**交给**收银员;
4.1 顾客的钱不够,顾客和收银员沟通,**删除**某商品;
4.2 顾客的钱不够,顾客和收银员沟通,**删除**某类商品中的一个或几个(例如买了5包烟,去掉两包)
4.3 顾客觉得某个商品价格太高,要求**删除**某商品;
4-A:顾客使用信用卡**支付**
4-A.1 信用卡支付流程(请读者自行思考完善,可以写在这里,如果太多,也可以另外写一个子用例)
4-B:顾客使用购物卡**支付**
4-B.1 购物卡支付流程
4-C:顾客使用会员卡积分**支付**
4-C.1 会员卡积分支付流程
5. 收银员**清点**钱数,**输入**收到的款额,系统**给出找零**的数目;
(这一步没有异常)
6. 收银员将找零的钱还给顾客,并**打印**小票;
7. 买单完成,顾客**携带**商品和小票**离开**;
【用例价值】
顾客买完单以后,就可以携带商品离开,而超市也将得到收入;
【约束和限制】
1. POS机必须符合国标XXX;
2. 键盘使用中文,因为收银员都是中国人;
3. 一次买单数额不能超过99999RMB;
4. POS机要非常稳定,至少一天内不要出现故障;
标识出所有的动词后,还需要进一步的工作:
【筛选】
并不是所有的动词都一定是软件类的方法,我们需要将这些动词识别出来并排除在后续设计范围之外。
例如:
“顾客携带选择好的商品到收银台”:这里的“携带”是顾客的一个动作,而顾客并不是我们的软件类;
“收银员告诉顾客商品总额”:这里的“告诉”确实是收银员的一个动作,而且“收银员”确实也是我们的软件类,但这里也要排除“告诉”,因为“告诉”这个动作和POS系统并没有关系,只是业务流程中的一个步骤而已。
其它需要排除的动词还有:“需要收银员和顾客沟通”、“顾客将钱交给收银员”、“收银员清点钱数”、“收银员将找零的钱还给顾客”、“顾客携带商品和小票离开”
【提炼】
筛选完不需要的动词后,剩下的就是我们需要的动词了,但此时并不能简单的将所有动词拿出来直接扔给某个软件类就行了,我们还需要进行一些加工。
继续以POS机为例:
“收银员逐一扫描商品条形码”:这里的“扫描”看起来是“收银员”的一个动作,而且“收银员”确实也是我们的软件类,但其实深究一下,“扫描”这个动词并不能分配给“收银员”这个软件类,因为真正执行“扫描”功能的是“扫描仪”,收银员只是拿着扫描仪扫描商品,并不是收银员自己去读取商品条形码;类似的动词还有“必须支持手工输入条形码”,也不能算作“收银员”的功能。
那我们为什么不排除这两个动词呢?秘密就在于我们要从这两个动词提炼出软件类的方法。稍作分析,我们就可以发现,无论是“扫描条形码”,还是“手工输入条形码”,其实最终的目的都是“添加本次交易的商品”,因此我们可以提炼出“增加交易商品”的动词。
还有一种提炼的方法需要从已有的动词中推断出来,例如:“扫描完毕,系统显示商品总额”,这里只提到了“显示”这个动词,但相信大部分人都能一眼看出,“显示”之前肯定要“计算”,不然显示出来的值从哪里来呢?
有的朋友可能会疑惑,为什么不在用例的时候就写清楚呢?例如:扫描完毕,系统计算商品总额,然后系统显示商品总额。这样不就一目了然的看出来了么?
理想情况下这种想法当然没错,但现实往往没有那么美好,写用例的产品人员可能经验不足,也可能表达能力有限,还有可能比较马虎,或者遗漏了。。。。。。总之会有很多异常情况,因此设计人员必须具备这样的推断和判断能力。
经过这一步骤后,我们获得的动词如下:
* 增加商品
* 计算商品总额
* 显示商品总额
* 删除商品
* 现金支付
* 信用卡支付
* 购物卡支付
* 会员卡积分支付
* 打印小票
当然,以上列出来的动词并不是就一定是100%的标准答案,不同的人来进行分析和设计,可能略有不同,但总体应该比较相似,毕竟业务是一样的,而业务需求就是设计最强的约束。
【分配】
识别出有效的动词后,最有一步就是分配了,即:将从用例中提炼出来的动词,分配给已经有了属性的软件类。这种分配操作很多时候都是按图索骥,特别是对于有领域经验的人来说,基本上凭直觉就能基本分配正确。
当然,如果你的经验并不是很丰富,那么还是老老实实的一个一个来分析吧。
以POS机为例:
* 增加商品:很明显应该分配给“交易”类
* 计算商品总额:分配给“交易”类
* 显示商品总额:分配给“交易”类
* 删除商品:分配给“交易”类
* 现金支付:分配给“现金”类
* 信用卡支付:分配给“信用卡”类
* 购物卡支付:分配给“购物卡”类
* 会员卡积分支付:分配给“会员卡”类
* 打印小票:这个动词的分配存在一定的灵活性,有的人可能认为应该分配给“交易”类,因为打印小票可以认为是“交易”流程中的一个步骤;有的人可能认为应该分配给“小票”类,因为打印小票可以认为是“小票”类的一个基本功能。其实两者都有一定道理,如果没有其它更有力的选择因素,我建议根据个人经验选择一个即可,这里我们选择分配给“小票”。
分配完成后,我们可以看到“交易”、“小票”、“信用卡”、“购物卡”、“会员卡”、“现金”都已经有方法了。
当然,对于有经验的人来说,以上步骤完全可以在脑海中就迅速完成了,而并不会这样一步一步的演示给别人看,所以看起来就像变戏法一样,不知怎么就设计出来了很多的软件类。
经过上面的处理步骤后,我们得到如下的类图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-20_569f5cca68a19.jpg)
与领域模型相比,部分领域类被剔除了,留下来的领域类映射成软件类后,又增加了方法。虽然还不完善,但软件类的是越来越有型,越来越清晰了。
**第二斧(精雕细琢):应用设计原则和设计模式**
完成了从领域类到软件类的映射后,类出来了,属性也出来了,方法也有了,看起来设计已经大功告成了。
事实上也确实有很多人基本上做到这一步就开始动手编码了,而且经过一番拼搏,最后发布的系统也能用。
但相信很多人都会有这个疑问:这样做就够了么,这样设计是否是好的设计呢?
要回答这个问题,我们首先要明确:什么叫做“好”的设计呢 ?
到目前为止,我们已经有了一个类的设计模型,而且如果按照这个模型去实现的话,最终应该也是能够满足用户的需求,毕竟我们这个类模型是按照“需求模型 -> 领域模型 -> 类模型”这样一路推导过来的,不会出现大的偏差。
那么,满足了用户需求的设计就是好的设计么?
相信有经验的朋友都会知道答案:“满足用户需求”只是设计的一个最基本要求,而不是一个“好设计”的评判标准。
既然如此,那么到底什么才是好的设计呢,是否有明确的标准来进行评价呢?
幸运的是,面向对象领域经过几十年的发展,确实已经发展出了很多成熟的指导思想和方法,用于评价和指导如何才能做好面向对象的设计。其中最具代表性的就是**“设计原则”和“设计模式”**。
**【设计原则】**
当我们谈到面向对象领域的设计原则的时候,我们其实都是在谈论罗伯特.C.马丁(Robert C. Martin ,又叫Bob大叔)的SOLID原则。
这也难怪,Bod大叔实在是太牛了,面向对象领域的设计原则几乎被他全部包揽了,加上他在他的畅销书《敏捷软件开发:原则、模式与实践》中详细的将这些原则集中一 一阐述,面向对象领域设计原则的权威非他莫属。毫不夸张的说,Bob大叔的威名和在面向对象领域中的地位,和设计模式的“四人帮”是不相上下的。
虽然很多资料都将SOLID原则和敏捷开发、测试驱动开发等方法绑定在一起,但我觉得只要是面向对象设计,不管是瀑布流程、、CMM流程、RUP流程、还是敏捷开发流程,都应该应用设计原则以提高设计质量。
参考wiki百科,SOLID设计原则简单介绍如下:
SOLID实际上是取5个设计原则的首字母拼起来的一个助记单词。具体的设计原则如下(详细的设计原则,我们会在后面详细阐述,这里不再详细展开):
| 首字母| 英文简写| 英文名称| 中文名称| 说明|
|--|--|--|--|--|
| S| SRP| Single Responsibility Principle| 单一职责原则|对象应该只具备单一职责|
| O| OCP| Open/Close Principle| 开放/封闭原则|认为“软件体应该是对于扩展开放的,但是对于修改封闭的”的概念。|
| L| LSP| Liskov Substitution Principle| Liskov替换原则|认为“程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的”的概念|
| I| ISP| Interface Segregation Principle|[接口隔离原则](http://zh.wikipedia.org/w/index.php?title=%E6%8E%A5%E5%8F%A3%E9%9A%94%E7%A6%BB%E5%8E%9F%E5%88%99&action=edit&redlink=1)|多个特定客户端接口要好于一个宽泛用途的接口|
| D| DIP| Dependency Inversion Principle|[依赖反转原则](http://zh.wikipedia.org/wiki/%E4%BE%9D%E8%B5%96%E5%8F%8D%E8%BD%AC%E5%8E%9F%E5%88%99)| 依赖于抽象而不是一个实例|
(wiki百科链接如下:
http://zh.wikipedia.org/wiki/SOLID_(%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%AE%BE%E8%AE%A1))
前面我们简单的八卦了一下,现在回归正题:设计原则有什么用?
其实和所有的原则一样,设计原则也是一个判断标准,说通俗点,设计原则就像是木匠手中的尺子,尺子是用来衡量木材的长短的,而设计原则就是衡量类设计的“尺子”:量一量,看长了还是短了,还是正好,长了就裁短一些,短了就加长一些。经过如此衡量并调整,最终就能够得到我们希望的设计作品。
当然,和木匠的尺子稍有不同,木匠不用尺子就做不出能用的家具,但我们不用设计原则的话,其实还是能够做出满足需求的系统的。
既然这样,我们为什么一定要用设计原则呢?ARTHUR J.RIEL在《OOD启思录》一书中针对这个问题给出了非常形象的解释:
你不必严格遵守这些原则,违背它也不会被处以宗教刑罚。但你应当把这些原则看做警铃,若违背了其中的一条,那么警铃就会响起。”-------ARTHUR J.RIEL,《OOD启思录》
也就是说,如果违背了这些设计原则,就可能有危险,但究竟是什么危险呢,警铃要警告我们什么呢,是火灾、水灾、地震、陷阱、还是有狮子、老虎。。。。。?
要回答这个问题,还需要回到面向对象的本源:我们在第一章解释为什么要面向对象的时候提到了面向对象的核心思想是“可扩展性”,这其实就是我们应用设计原则的根本目的:保证可扩展性。如果我们不遵守这些设计原则,警铃就会响起,提醒我们:你的设计可扩展性会有问题!
除了设计原则外,后面要讲到的设计模式,其本质也是为了提高可扩展性。这也是为什么我们通过领域类映射得到了很多软件类之后,还需要不辞辛劳的继续应用设计原则和设计模式的主要原因,本质上都是为了提高设计的可扩展性。
SOLID设计原则的各个子原则详细介绍会在后面详细介绍,这里我们简单的以POS机为例,看看如何应用设计原则。
仔细观察我们通过领域类映射得到的软件类,可以发现一个很明显不符合SOLID原则中的DIP原则的地方,即:“交易类”直接依赖“会员卡”、“购物卡”、“信用卡”、“现金”4个子类,这样的实现不符合DIP原则,当需要增加新的支付方式时,“交易类”也需要跟着修改。
既然不满足DIP设计原则,那么我们就按照DIP原则的要求,提取出一个支付的父类来,即:“交易类”依赖“支付类”,“会员卡”、“购物卡”、“信用卡”、“现金”都继承“支付”类。具体实现如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-20_569f5cca808d6.jpg)
可以看到,应用DIP设计原则之后,我们又多出了一个“支付”的类,这个类原来在领域模型中是没有的,而是我们在设计阶段“创造”出来的。
对于其它各个类,我们都可以依次使用设计原则进行判断,当发现不符合设计原则的设计时,就采取增加、删除、合并、拆分等手段,使我们的设计逐步改进,最终达到符合设计原则的目的。
**【设计模式】**
相比设计原则来说,设计模式更加普及和流行,当我们谈到设计方法的时候,大部分人肯定都会想到设计模式,设计模式如此深入人心,几乎到了言必谈设计模式的地步。
和设计原则类似,当我们谈论设计模式的时候,我们其实都是在谈论GOF(Gang of Four,中文翻译为“四人帮”)在经典名作《设计模式 --可复用面向对象软件的基础》一书中提到的设计模式。
通俗的讲,设计模式是用于指导我们如何做出更好的设计方案,而前面提到的设计原则,其作用也是这样的。那么,设计原则和设计模式,我们该如何选择?
有的朋友可能会以为这两个是二选一的关系,要么用设计原则,要么用设计模式。这种理解是错误的,设计原则和设计模式并不是竞争关系,正好相反,它们是互补的关系。
设计原则和设计模式互补体现在:**设计原则主要用于指导“类的定义”的设计,而设计模式主要用于指导“类的行为”的设计**,更通俗一点的讲:设计原则是类的静态设计原则,设计模式是类的动态设计原则。
一般情况下,我们是采用“先设计原则,后设计模式”的方法来操作的。
设计模式的相关内容会在后文详细介绍,这里我们以POS机为例,看看如何应用设计模式来优化我们的设计。
通过分析应用设计原则优化后的类,我们发现“信用卡”这个类存在优化的空间,因为国际上存在不同的信用卡,最常见的有中国银联(UnionPay)、Visa、MasterCard这几种,每种信用卡在支付的时候需要接入不同的机构,其接入方式和协议肯定都是有一定差异的。为了封装这种差异以支持后续更好的扩展,我们应用设计模式的Bridge模式,提取出“信用卡处理”这个类,这个类的主要处理“连接、认证、扣款”这样的职责。UnionPay、Visa、MasterCard都继承“信用卡处理”这个类。具体如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-20_569f5cca977e0.jpg)
**第三斧(照本宣科):拆分辅助类**
经过前面的设计步骤之后,面向对象类的设计工作已经完成,我们输出了完整的类模型,看起来已经可以开始动手编码了,你是否舒了一口气,看着自己的设计作品,不由得产生了一种自豪感呢?
确实值得自豪,毕竟我们一步一个脚印,从最初仅仅存在于客户脑袋中的需求,逐步的推导、演变、设计出了能够付诸实施的类模型了。但在最终实施之前,还有一点小小的动作要完成,这就是我们的拆分辅助类操作。
拆分辅助类的主要目的是为了使我们的类在编码的时候能够满足一些框架或者规范的要求。比如说常见的MVC模式,将一个业务拆分成Control、Model、View三个元素;J2EE模式中,将对象分为PO、BO、VO、DTO等众多对象。
之所以说这是一点小小的动作,是因为这个动作确实很简单,只要将我们设计出来的类,按照规范要求,一 一对应分拆即可。
以POS机为例,假如我们的框架要求提供DAO对象,负责数据库的相关操作,则“购物卡”类就应该拆分为两个:“购物卡”、“购物卡DAO”,其中“购物卡”用于负责提供“支付”功能给“交易”类调用,“购物卡DAO”用于负责从数据库读取购物卡信息,修改数据库中购物卡余额等操作。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-20_569f5ccab4b18.jpg)
需要注意的是,拆分设计辅助类仅仅是为了满足框架或者规范的要求,**本身并不是一个设计的步骤,而是实施的一个步骤**,所以我们一般都不需要将拆分的辅助类体现在类模型中,仅仅在编码的时候拆分即可。
(25) – 类模型
最后更新于:2022-04-01 07:29:37
## 连载:面向对象葵花宝典:思想、技巧与实践(25) - 类模型
面向对象设计和弹吉他差不多,有很多成熟的理论和技巧,学会弹吉他并不难,你只需要应用这些理论和技巧即可!
**【师傅领进门,修行在个人】**
“类模型”是整个面向对象设计模型的核心,是面向对象设计阶段的主要输出,也是设计师们最能够发挥自己才能的地方。
虽然“类模型”如此重要,但面向对象设计技术经过几十年的发展后,目前已经形成了很成熟的一套体系,因此真正在进行“类模型”设计的时候,其实难度并不高,这也是多亏了众多前辈们的无私贡献,才能使得我们这些芸芸众生也能轻松掌握这些原本带有艺术色彩的技能。
不过话又说回来了,入门容易精通难,“类模型”的设计也是如此,虽然有前辈们各种各样的思想结晶指导着我们,但毕竟如何理解、如何应用这些思想结晶,还是要看个人的领悟力和把握力。就像同样的《葵花宝典》,岳不群看了创立了气宗,蔡子峰看了创立了剑宗,渡元禅师听了后悟出了辟邪剑法,东方不败看了竟然练出了绣花针绝技!所以接下来的内容,也只是“师父领进门,修行在个人”,如果希望做出优秀的设计,更多还是依靠个人的领悟和实践!
**【设计的魔法崇拜】**
面向对象类的设计很多时候都蒙上了一层神秘的面纱。一个常见的场景是:“设计师”拿到需求后,经过一段时间的设计,感觉就像变戏法一样,然后就拿出了一个类模型。普通的开发人员看到这样的类模型,很难想象如何从需求才能够得到了这些类,由于大部分公司的设计人员确实也都是公司里面的牛人,因此大家就自然而然地认为这是水平和创造力的表现,心底不由得产生由衷的赞叹:“牛逼啊!”
我称这种现象为设计的“魔法崇拜”,具体表现就是认为设计是一种魔法,做设计的人是魔法师,只有牛逼的人才能够做设计。
但这种认识并不准确,就像我们前面提到的,面向对象领域已经发展了几十年,各种思想、理论都已经基本成熟,绝大部分人在做设计的时候,都不可能有什么天才的创新或者天才的灵感,而只是这些已经成熟的思想和理论的应用而已。
之所以现在还会出现这种现象,主要原因还是在于各种思想、理论、方法都是针对具体问题的分析或者总结,但并没有谁明确的将这些东东形成一套完整的面向对象设计体系。
可能很多人都有这种感觉:面向对象我也懂,设计模式我也知道,设计原则我也明白,但真的要进行设计的时候,就不知道如何下手了,比如说:
对象从哪里来?
什么时候用设计模式?
如何判断设计是否正确?
什么样的设计才是优秀的设计?
。。。。。。。。。。。
正因为存在这样的原因,我们自然会对那些能够设计出完整的类模型的设计师们刮目相看了。
其实面向对象设计并不是什么高深的技术,也不需要天才的创新,更不需要变魔法,而是有章可循的,只要我们按照一定的步骤,一步一个脚印,不断精益求精,就能够完成面向对象的设计。
但正如前面提到的一样,面向对象设计更多的时候是一门艺术。虽然我们按照一定的步骤能够完成面向对象的设计,但在这些步骤实施的过程中,如何应用相关技术,如何做出设计选择等,更多时候是带有艺术色彩的。就像很多人都会弹吉他,但真正的吉他大师弹出来的感觉,肯定和一个普通人弹的不一样。
接下来我们将会分几个小节讲述如何进行面向对象的类设计:
第一步(照猫画虎):领域类映射 --- 告诉你类从哪里来
第二步(精雕细琢):应用设计原则和设计模式 ---告诉你如何设计“好”类
第三步(照本宣科):拆分辅助类 ---告诉你如何和你的开发框架结合起来
(24) – 设计模型
最后更新于:2022-04-01 07:29:35
## 连载:面向对象葵花宝典:思想、技巧与实践(24) - 设计模型
完成领域类到软件类的转换,这就是面向对象领域设计阶段的主要任务。
经过领域模型的分析后,面向对象已经初具雏形,但领域类并不能指导我们进行编码工作,因为领域类只是从用例模型中提炼出来的反应业务领域的概念,而并不是真正意义上的软件类。
“革命尚未成功,同志还需努力”,我们需要再进一步,完成**领域类到软件类的转换**,这就是面向对象领域设计阶段的主要任务。
设计阶段是整个面向对象分析和设计的高潮阶段。在设计阶段中,我们将要输出设计模型,并且需要综合各种方法、技巧,运用十八般武艺,使出浑身解数,以求能够设计出满足各种需要的设计方案。
这也是最考验设计师功力的时候,因为设计并没有一个量化的标准,也没有一个标准答案,更多的时候需要设计师综合知识、技能、经验、灵感等因素,综合权衡而得出一个方案。世界上找不到两片相同的叶子,同样,你也找不到两个完全一样的设计方案。毫不夸张的说,面向对象的设计更多时候是一项艺术。
虽然我们说面向对象设计是一门艺术,但这并不意味着只有天才才能进行面向对象设计,面向对象设计也是有一定的规律和方法可寻的,我们将在接下来的章节逐一介绍。
## 【设计模型总览】
设计模型主要包含2部分内容:**静态模型、动态模型**,任何一个模型的缺失或者不完善,都将导致最终的设计质量不高,甚至可能导致最终的系统没有实现业务需求。
静态模型又可以称为“类模型”,主要关注系统的“静态”结构,描述了系统包含的类, 以及类的名称、职责、属性、方法,类与类之间的关系。
动态模型关注系统的“动态”行为,描述类本身的一些动作或者状态变化,以及类之间如何配合以完成最终的业务功能。只有结合静态模型和动态模型,我们才能够真正的将一个系统描述清楚。
静态模型和动态模型对于后续的编码也具有不同的指导意义。静态模型主要用于指导类的声明,包括类名称,属性名,方法名;而动态模型主要用于指导类的实现,主要就是每个方法内部的具体实现。
(23) – 领域建模三字经
最后更新于:2022-04-01 07:29:33
## 连载:面向对象葵花宝典:思想、技巧与实践(23) - 领域建模三字经
看起来有点不可思议,需求阶段“白纸黑字”的用例文档,经过我们一步一步的操作,逐步就得到了“图形化”的领域模型,面向对象初具雏形。
领域建模的三字经方法:**找名词、加属性、连关系**。
我们接下来以一个样例看看领域模型具体如何建模。
## 1.1. 找名词
我们以POS机买单的用例来看看具体如何建领域模型。
首先,将用例中所有的名词挑选出来(如下用例文档中**蓝色加粗**的词组):
【用例名称】
买单
【场景】
Who:**顾客、收银员**
Where:商店的**收银台**
When:营业时间
【用例描述】
1. **顾客**携带选择好的**商品**到收银台;
(这一步没有异常)
2. 收银员逐一扫描商品**条形码**,系统根据条形码查询商品信息;
2.1 **扫描仪**坏了,必须支持手工输入条形码;
2.2 商品的**条形码**无法扫描,必须支持手工输入条形码;
2.3 条形码能够扫描,但查询不到信息,需要收银员和顾客沟通,放弃购买此产品
3. 扫描完毕,系统显示商品总额,收银员告诉顾客商品总额;
(这一步没有异常)
4. 顾客将**钱**交给收银员;
4.1 顾客的钱不够,顾客和收银员沟通,删除某商品;
4.2 顾客的钱不够,顾客和收银员沟通,删除某类商品中的一个或几个(例如买了**5包烟**,去掉两包)
4.3 顾客觉得某个商品价格太高,要求删除某商品;
4-A:顾客使用**信用卡**支付
4-A.1 信用卡支付流程(请读者自行思考完善,可以写在这里,如果太多,也可以另外写一个子用例)
4-B:顾客使用**购物卡**支付
4-B.1 购物卡支付流程
4-C:顾客使用**会员卡**积分支付
4-C.1 会员卡积分支付流程
5. 收银员清点钱数,输入收到的款额,系统给出找零的数目;
(这一步没有异常)
6. 收银员将找零的钱还给顾客,并打印**小票**;
7. **买单**完成,顾客携带**商品**和**小票**离开;
【用例价值】
顾客买完单以后,就可以携带商品离开,而超市也将得到收入;
【约束和限制】
1. POS机必须符合国标XXX;
2. 键盘和屏幕使用**中文**,因为收银员都是**中国人**;
3. 一次买单数额不能超过99999RMB;
4. POS机要非常稳定,至少一天内不要出现故障;
名词列表:
顾客、收银员、收银台、商品、条形码、扫描仪、钱、5包烟、信用卡、会员卡、小票、买单、键盘、屏幕、中文、中国人
通过这种简单的方法,我们很轻松的就识别出了领域中的各种概念,但是还不能高兴的太早,识别领域概念的工作还没有结束,接下来我们还需要提炼。
有了前面步骤识别的名词列表后,提炼的工作就相对很简单了,只需要删除不是领域对象的名词即可。
但具体应该删除什么名词,是和不同的业务领域强相关的,并没有完全统一的标准,此时分析师的行业和领域经验起决定作用,而这也正是菜鸟和专家的区别。
以我们的收银机为例,提炼的过程如下:
1)删除“收银台”:收银台只是一个物理设备,且这个设备与我们的POS机也没有任何交互,所以不能算作领域模型中的一个概念;
2)删除“5包烟”:5包烟只是用例中举例时的一个实例,是一个具体的商品,已经包含在“商品”中了;
3)删除“中文”:“中文”只是“键盘”和“屏幕”的一个属性,并不是一个独立的领域概念;
4)删除“中国人”:“中国人”只是“收银员”的一个属性,并不是一个独立的领域概念;
5)删除“条形码”:“条形码”只是“商品”的一个属性,并不是一个独立的领域概念;
经过上面的提炼步骤后,就得到了真正的POS机领域类,详细如下:
**顾客、收银员、商品、扫描仪、钱、信用卡、会员卡、小票、买单、键盘、屏幕**
## 1.2. 加属性
找出领域模型的名词后,接下来一个重要工作就是将这些名词相关的属性找出来,使其更加准确。
但加属性和前面找名词有一点点差别:有的属性并没有在用例中明确给出,需要分析人员和设计人员额外添加,此时也是分析师的行业和领域经验起决定作用。
| 名词| 属性| 备注|
|--|--|--|
| 顾客| NA| 对于POS机来说,并不需要识别顾客的相关信息,因此在领域模型中,顾客是没有属性的|
| 收银员| 国籍、编号| “国籍”由找名词步骤中的“中国人”提炼|
| 商品| 条形码、名称、价格|名称和价格并没有在用例中体现,但毫无疑问这是商品最基本的属性|
| 扫描仪| NA| 扫描仪是POS机的一个输入设备,POS机不需要识别扫描仪的相关信息,因此在领域模型中,扫描仪也是没有属性的|
| 钱(现金)| 数量,币别| 从领域分析的角度来讲,“现金”更专业一些|
| 信用卡|卡号| NA|
| 会员卡| 会员号、积分、有效期| NA|
| 小票| 交易信息、POS机信息、收银员信息|小票的属性在用例中并没有详细体现,但有经验的分析师能够很容易识别出来|
| 买单(交易)| 商品列表、日期时间、总额、支付信息|这里的属性看起来和“小票”一样,是因为“小票”本质上是给客户的一个交易记录。这里为了更加符合软件系统的属于习惯,可以将“买单“改为“交易”。|
| 键盘| NA| 和扫描仪类似,POS机不需要识别键盘信息|
| 屏幕| NA | 和扫描仪类似,POS机不需要识别屏幕信息|
## 1.3. 连关系
有了类,也有了属性,接下来自然就是找出它们的关系了。
有了前面的工作,看起来连关系自然也是睡到渠成的事情,但不要忘了我们的这个例子是非常简单的,在一些复杂的系统中,领域模型之间的关系并不那么明显,菜鸟可能就只能看到最显而易见的一些联系,而系统分析师和设计师可以凭着丰富的经验、良好的技巧识别出来,这也是系统分析师和设计师的价值所在。
POS机的领域类关系如下(仅供参考,并不要求每个分析师和设计师都一定是这么理解,但总体来说应该相似):
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-20_569f5cca458ea.jpg)
看起来有点不可思议,需求阶段白纸黑字的用例文档,经过我们一步一步的操作,最后得到了图形化的领域模型。
只要曾经画过甚至只是看过UML类图的同学都应该很容易发现,领域模型和设计类图非常相似,面向对象终于有了雏形了
(22) – 领域模型
最后更新于:2022-04-01 07:29:30
## 连载:面向对象葵花宝典:思想、技巧与实践(22) - 领域模型
领域模型是面向对象分析和设计的第一步!!
完成了需求分析之后,我们已经有了一个良好的开端,但我们的主角“面向对象”还不见踪影。
前面我们提到,需求分析和面向对象是没有直接关系的,需求分析阶段是不区分是面向对象还是面向过程,那么什么时候才真正开始面向对象的工作呢?
答案就在本章:**领域建模**。
从领域模型开始,我们就开始了面向对象的分析和设计过程,可以说,领域模型是完成从需求分析到面向对象设计的一座桥梁。
领域模型,顾名思义,就是需求所涉及的领域的一个建模,更通俗的讲法是业务模型。
参考百度百科(http://baike.baidu.cn/view/757895.htm ),领域模型定义如下
领域模型是对领域内的概念类或现实世界中对象的可视化表示,又称概念模型、领域对象模型、分析对象模型。它专注于分析问题领域本身,发掘重要的业务领域概念,并建立业务领域概念之间的关系。
从这个定义我们可以看出,领域模型有两个主要的作用:
1)发掘重要的业务领域概念
2)建立业务领域概念之间的关系
## 【领域建模三字经】
领域模型如此重要,很多同学可能会认为领域建模很复杂,需要很高的技巧。然而事实上领域建模非常简单,简单得有点难以让人相信,领域建模的方法概括一下就是“找名词”!
许多同学看到这个方法后估计都会笑出来:太假了吧,这么简单,找个初中生都会啊,那我们公司那些分析师和设计师还有什么用哦?
分析师和设计师当然有用,后面我们会看到,即使是简单的找名词这样的操作,也涉及到分析和提炼,而不是简单的摘取出来就可,这种情况下分析师和设计师的经验和技能就能够派上用场了。但领域模型分析也确实相对简单,即使没有丰富的经验和高超的技巧,至少也能完成一个能用的领域模型。
虽然我们说“找名词”很简单,但一个关键的问题还没有说明:从哪里找?
如果你还记得领域模型是“需求到面向对象的桥梁”,那么你肯定一下子就能想到:从需求模型中找,具体来说就是从用例中找。
归纳一下域建模的方法就是“从用例中找名词”。
当然,找到名词后,为了能够更加符合面向对象的要求和特点,我们还需要对这些名词进一步完善,这就是接下来的步骤:加属性,连关系!
最后我们总结出领域建模的三字经方法:**找名词、加属性、连关系**。
欲知具体如何操作,请看下回分解