【设计模式】第⼀篇:概述、耦合、UML、七⼤原则,详细分析
总结(基于Java)
迷茫了⼀周,⼀段时间重复的 CRUD ,着实让我有点烦闷,最近打算将这些技术栈系列的⽂章先暂时搁置⼀下,开启⼀个新的篇章《设计模式》,毕竟前⾯写了不少 “武功招式” 的⽂章,也该提升⼀下内功了
⼀设计模式概述
(⼀) 什么是设计模式
设计模式,即Design Patterns,是指在软件设计中,被反复使⽤的⼀种代码设计经验。使⽤设计模式的⽬的是为了可重⽤代码,提⾼代码的可扩展性和可维护性
1995年,GoF(Gang of Four,四⼈组/四⼈帮)合作出版了《设计模式:可复⽤⾯向对象软
件的基础》⼀书,收录了23种设计模式,从此树⽴了软件设计模式领域的⾥程碑,【GoF设计模式】
(⼆) 为什么学习设计模式
前⾯我们学习了 N 种不同的技术,但是归根结底,也只是 CRUD 与调⽤之间的堆砌,或许这个创意亦或
是业务很完善、很强⼤,其中也巧妙运⽤了各种⾼效的算法,但是说⽩了,这也只是为了实现或者说解决某个问题⽽做的
还有时候,两个⼈同时开发⼀款相同的产品,均满⾜了预期的需求,但是 A 的程序,不仅代码健壮性强,同时后期维护扩展更是便捷(这种感觉,我们会在后⾯具体的设计模式中愈发的感觉到)⽽ B 的代码却是⼀⾔难尽啊
有⼀句话总结的⾮常好:
设计模式的本质是⾯向对象设计原则的实际运⽤,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解
也就是说,毕竟像例如Java这样⾯向对象的语⾔中,如何实现⼀个可维护,可维护的代码,那必然就是要降低代码耦合度,适当复⽤代码,⽽要实现这⼀切,就需要充分的利⽤ OOP 编程的特性和思想
注:下⾯第⼆⼤点补充【耦合】的相关概念,若不需要跳转第三四⼤点【UML类图及类图间的关系】/【设计模式七⼤原则】
在之前我写 Spring依赖注⼊的时候【万字长⽂】 Spring框架层层递进轻松⼊门(0C和D),就是从传统开发,讲到了如何通过⼯⼚模式,以及多例到单例的改进,来⼀步步实现解耦,有兴趣的朋友可以看⼀下
⼆什么是耦合?(⾼/低)
作为⼀篇新⼿都能看懂的⽂章,开始就⼀堆 IOC AOP等专业名词扔出去,好像是不太礼貌,我得把需要铺垫的知识给⼤家尽量说⼀说,如果对这块⽐较明⽩的⼤佬,直接略过就OK了
耦合,就是模块间关联的程度,每个模块之间的联系越多,也就是其耦合性越强,那么独⽴性也就越差了,所以我们在软件设计中,应该尽量做到低耦合,⾼内聚
⽣活中的例⼦:家⾥有⼀条串灯,上⾯有很多灯泡,如果灯坏了,你需要将整个灯带都换掉,这就是⾼耦合的表现,因为灯和灯带之间是紧密相连,不可分割的,但是如果灯泡可以随意拆卸,并不影响整个灯带,那么这就叫做低耦合
代码中的例⼦:来看⼀个多态的调⽤,前提是 B 继承 A,引⽤了很多次
A a = new B();
如果你想要把B变成C,就需要修改所有new B()的地⽅为new C()这也就是⾼耦合
如果如果使⽤我们今天要说的 spring框架就可以⼤⼤的降低耦合
A a = BeanFactory().getBean(B名称);
这个时候,我们只需要将B名称改为C,同时将配置⽂件中的B改为C就可以了
常见的耦合有这些分类:
(⼀) 内容耦合
当⼀个模块直接修改或操作另⼀个模块的数据,或者直接转⼊另⼀个模块时,就发⽣了内容耦合。此时,被修改的模块完全依赖于修改它的模
块。这种耦合性是很⾼的,最好避免
public class A {
public int numA = 1;
}
public class B {
public static A a = new A();
public static void method(){
a.numA += 1;
}
public static void main(String[] args) {
method();
System.out.println(a.numA);
}
}
(⼆) 公共耦合
两个以上的模块共同引⽤⼀个全局数据项就称为公共耦合。⼤量的公共耦合结构中,会让你很难确定是哪个模块给全局变量赋了⼀个特定的值
(三) 外部耦合
⼀组模块都访问同⼀全局简单变量,⽽且不通过参数表传递该全局变量的信息,则称之为外部耦合从定义和图中也可以看出,公共耦合和外部耦合的区别就在于前者是全局数据结构,后者是全局简单变量
(四) 控制耦合
控制耦合。⼀个模块通过接⼝向另⼀个模块传递⼀个控制信号,接受信号的模块根据信号值⽽进⾏适当的动作,这种耦合被称为控制耦合,也就是说,模块之间传递的不是数据,⽽是⼀些标志,开关量等等
(五) 标记耦合
标记耦合指两个模块之间传递的是数据机构,如⾼级语⾔的数组名、记录名、⽂件名等这些名字即为标记,其实传递的是这个数据结构的地址
(六) 数据耦合
模块之间通过参数来传递数据,那么被称为数据耦合。数据耦合是最低的⼀种耦合形式,系统中⼀般都存在这种类型的耦合,因为为了完成⼀些有意义的功能,往往需要将某些模块的输出数据作为另⼀些模块的输⼊数据
(七) ⾮直接耦合
两个模块之间没有直接关系,它们之间的联系完全是通过主模块的控制和调⽤来实现的
三 UML 类图及类图之间的关系
在⼀个相对完善的软件系统中,每个类都有其责任,类与类之间,类与接⼝之间同时也存在着各种关系,UML(统⼀建模语⾔)从不同的⾓度定义了多种图,在软件建模时⾮常常⽤,下⾯我们说⼀下在设计模式中涉及相对较多的类图,因为在后⾯单个设计模式的讲解中,我们会涉及到,也算是⼀个基础铺垫。
(⼀) 类
类是⼀组相关的属性和⾏为的集合,是⼀个抽象的概念,在UML中,⼀般⽤⼀个分为三层的矩形框来代表类
第⼀层:类名称,是⼀个字符串,例如 Student
第⼆层:类属性(字段、成员变量)格式如下:
[可见性]属性名:类型[=默认值]
例如:-name:String
第三层:类操作(⽅法、⾏为),格式如下:
[可见性]名称(参数列表)[:返回类型]
例如:+ display():void
(⼆) 接⼝
接⼝,是⼀种特殊⽽⼜常⽤的类,不可被实例化,定义了⼀些抽象的操作(⽅法),但不包含属性其实能见到接⼝ UML 描述的有三种形式:
第⼀种:使⽤⼀个带有名称的⼩圆圈来表⽰,上⾯的Dog是接⼝名,下⾯是接⼝定义的⽅法
第⼆种:使⽤⼀个“框”来表⽰,和类很像,但是在最上⾯特别标注了<<interface>>
(三) 关系
(1) 依赖关系
定义:如果⼀个元素 A 的变化影响到另⼀个元素 B,但是反之却不成⽴,那么这两个元素 B 和 A 就可以称为 B 依赖 A
例如:开门的⼈想要执⾏开门这个动作,就必须借助于钥匙,这⾥也就可以说,这个开门的⼈,依赖于钥匙,如果钥匙发⽣了什么变化就会影响到开门的⼈,但是开门的⼈变化却不会影响到钥匙开门java技术栈图
例如:动物⽣活需要氧⽓、⽔分、⾷物,这就是⼀个很字⾯的依赖关系
依赖关系作为对象之间耦合度最低的⼀种临时性关联⽅式
在代码中,某个类的⽅法通过局部变量、⽅法的参数或者对静态⽅法的调⽤来访问另⼀个类(被依赖类)中的某些⽅法来完成⼀些职责。
(2) 关联关系
关联就是类(准确的说是实例化后的对象)之间的关系,也就是说,如果两个对象需要在⼀定时间内保持⼀定的关系,那么就可以称为关联关系。
例如:学⽣(Student)在学校(School)学习知识(Knowledge)那么这三者之间就存⼀个某种联系,可以建⽴关联关系
例如:⼤雁(WildGoose)年年南下迁徙,因为它知道⽓候(climate)规律
关联关系的双⽅是可以互相通讯的,也就是说,“⼀个类知道另⼀个类”
这种关联是可以双向的,也可以是单向的
双向的关联可以⽤带两个箭头或者没有箭头的实线来表⽰
单向的关联⽤带⼀个箭头的实线来表⽰,箭头从使⽤类指向被关联的类
也可以在关联线的两端标注⾓⾊名,代表两种不同的⾓⾊
在代码中通常将⼀个类的对象作为另⼀个类的成员变量来实现关联关系
下图是⼀个教师和学⽣的双向关联关系
(3) 聚合关系
聚合关系也称为聚集关系,它是⼀种特殊的较强关联关系。表⽰类(准确的说是实例化后的对象)之间整体与部分的关系,是⼀种 has-a 的关系
例如:汽车(Car)有轮胎(Wheel),Car has a Wheel,这就是⼀个聚合关系,但是轮胎(Wheel)独⽴于汽车也可以单独存在,轮胎还是轮胎
聚合关系可以⽤带空⼼菱形的实线箭头来表⽰,菱形指向整体
(4) 组合关系
组合是⼀种⽐聚合更强的关联关系,其也表⽰类整体和部分之间的关系。但是整体对象可以控制部分对象的⽣命周期,⼀旦整体对象消失,部分也就⾃然消失了,即部分不能独⽴存在
聚合关系可以⽤带实⼼菱形的实线箭头来表⽰,菱形指向整体
(5) 泛化关系
泛化描述⼀般与特殊(类图中“⼀般”称为超类或⽗类,“特殊”称为⼦类)的关系,是⽗类和⼦类之间的关系,是⼀种继承关系,描述了⼀种 is a kind of 的关系,特别要说明的是,泛化关系式对象之间耦合度最⼤的⼀种关系
Java 中 extend 关键字就代表着这种关系,通常抽象类作为⽗类,具体类作为⼦类
例如:交通⼯具为抽象⽗类,汽车,飞机等就位具体的⼦类
泛化关系⽤带空⼼三⾓箭头的实线来表⽰,箭头从⼦类指向⽗类
(6) 实现关系
实现关系就是接⼝和实现类之间的关系,实现类中实现了接⼝中定义的抽象操作
实现关系使⽤带空⼼三⾓箭头的虚线来表⽰,箭头从实现类指向接⼝
四设计模式七⼤原则
(⼀) 开闭原则
定义:软件实体应当对扩展开放,对修改关闭
我们在开发任何产品的时候,别指望需求是⼀定不变的,当你不得不更改的你的代码的时候,⼀个⾼质量的程序就体现出其价值了,它只需要在原来的基础上增加⼀些扩展,⽽不⾄于去修改原先的代码,因为这样的做法常常会牵⼀发⽽动全⾝。
也就是说,开闭原则要求我们在开发⼀个软件(模块)的时候,要保证可以在不修改原有代码的模块的基础上,然后能扩展其功能
我们下⾯来具体谈谈
(1) 对修改关闭
对修改关闭,即不允许在原来的模块或者代码上进⾏修改。
A:抽象层次
例如定义⼀个接⼝,不同的定义处理思路,会有怎样的差别呢
定义⼀
boolean connectServer(String ip, int port, String user, String pwd)
定义⼆
boolean connectServer(FTP ftp)
public class FTP{
private String ip;
private int port;
private String user;
private String pwd;
...... 省略 get set
}
两种⽅式看似都是差不多的,也都能实现要求,但是如果我们想要在其基础上增加⼀个新的参数
如果以定义⼀的做法,⼀旦接⼝被修改,所有调⽤ connectServer ⽅法的位置都会出现问题
如果以定义⼆的做法,我们只需要修改 FTP 这个实体类,添加⼀个属性即可
这种情况下没有⽤到这个新参数的调⽤处就不会出现问题,即使需要调⽤这个参数,我们也可以在 FTP 类的构造函数中,对其进⾏⼀个默认的赋值处理
B:具体层次
对原有的具体层次的代码进⾏修改,也是不太好的,虽然带来的变化可能不如抽象层次的⼤,或者碰巧也没问题,但是这种问题有时候是不可预料的,或许⼀些不经意的修改会带了和预期完全不⼀致的结果
(2) 对扩展开放
对扩展开放,也就是我们不需要在原代码上进⾏修改,因为我们定义的抽象层已经⾜够的合理,⾜够的包容,我们只需要根据需求重新派⽣⼀个实现类来扩展就可以了
(3) 开发时如何处理
⽆论模块是多么“封闭”,都会存在⼀些⽆法对之封闭的变化。既然不可能完全封闭,设计⼈员必须对他设计的模块应该对那种变化封闭做出选择,他必须先猜测出最有可能发现的变化种类,然后构造抽象来隔离那些变化 ——《⼤话设计模式》
预先猜测程序的变化,实际上是有很⼤难度,或许不完善,亦或者完全是错误的,所以为了规避这⼀点,我们可以选择在刚开始写代码的时候,假设不会有任何变化出现,但当变化发⽣的时候,我们就要⽴即采取⾏动,通过 “抽象约束,封装变化” 的⽅式,创建抽象来隔离发⽣的同类变化
举例:
例如写⼀个加法程序,很容易就可以写的出来,这个时候变化还没有发⽣
如果这个时候让你增加⼀个减法或者乘除等的功能,你就发现,你就需要在原来的类上⾯修改,这显然违背了 “开闭原则”,所以变化⼀旦发⽣,我们就⽴即采取⾏动,决定重构代码,⾸先创建⼀个抽象类的运算类,通过继承多态等隔离代码,以后还想添加什么类型的运算⽅式,只需要增加⼀个新的⼦类就可以了,也就是说,对程序的改动,是通过新代码进⾏的,⽽不是更改现有代码
⼩结:
我们希望开发刚开始就知道可能发⽣的变化,因为等待发现变化的时间越长,要抽象代码的代价就越⼤
不要刻意的去抽象,拒绝不成熟的抽象和抽象本⾝⼀样重要
(⼆) ⾥⽒替换原则
(1) 详细说明
定义:继承必须确保超类所拥有的性质在⼦类中仍然成⽴
⾥⽒替换原则,主要说明了关于继承的内容,明确了何时使⽤继承,亦或使⽤继承的⼀些规定,是对于开闭原则中抽象化的⼀种补充
这⾥我们主要谈⼀下,继承带来的问题:
继承是侵⼊性的,⼦类继承了⽗类,就必须拥有⽗类的所有属性和⽅法,降低了代码灵活度
耦合度变⾼,⼀旦⽗类的属性和⽅法被修改,就需要考虑⼦类的修改,或许会造成⼤量代码重构
⾥⽒替换原则说简单⼀点就是:它认为,只有当⼦类可以替换⽗类,同时程序功能不受到影响,这个⽗类才算真正被复⽤
其核⼼主要有这么四点内容:
①⼦类可以实现⽗类的抽象⽅法,但不能覆盖⽗类的⾮抽象⽅法
②⼦类中可以增加⾃⼰特有的⽅法
③当⼦类的⽅法重载⽗类的⽅法时,⼦类⽅法的前置条件(即⽅法的输⼊参数)要⽐⽗类的⽅法更宽松
④当⼦类的⽅法实现⽗类的⽅法时(重写/重载或实现抽象⽅法),⽅法的后置条件(即⽅法的的输出/返回值)要⽐⽗类的⽅法更严格
或相等
对照简单的代码来看⼀下,就⼀⽬了然了
①⼦类可以实现⽗类的抽象⽅法,但不能覆盖⽗类的⾮抽象⽅法
前半句很好理解,如果不实现⽗类的抽象⽅法,会编译报错
后半句是这⾥的重点,⽗类中但凡实现好的⽅法,其实就是在设定整个继承体系中的⼀系列规范和默认的契约,例如鸟类 Bird
中,getFlyingSpeed(double speed) ⽤来获取鸟的飞⾏速度,但⼏维鸟作为⼀种特殊的鸟类,其实是不能飞⾏的,所以需要重写继承的⼦类⽅法 getFlyingSpeed(double speed) 将速度置为 0 ,但是会对整个继承体系造成破坏
虽然我们平常经常会通过重写⽗类⽅法来完成⼀些功能,同样这样也很简单,但是⼀种潜在的继承复⽤体系就被打乱了,如果在不适当的地⽅调⽤重写后的⽅法,或多次运⽤多态,还可能会造成报错
我们看下⾯的例⼦:
⽗类 Father
public class Father {
public void speaking(String content){
System.out.println("⽗类: " + content);
}
}
⼦类 Son
public class Son extends Father {
@Override

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。