设计模式
设计模式(Design pattern) 是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。
使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。
毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现在中都有相应的原理来与之对应,每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是它能被广泛应用的原因。
简单说:
设计模式是对软件设计普遍存在的问题,所提出的解决方案。
如果这个软件就只有一小部分人用,并且功能非常简单,在未来可预期的时间内,不会做任何大的修改和添加,即可以不使用设计模式。但是这种的太少了,所以设计模式还是非常重要的。
为什么要用设计模式
写代码不能仅仅只考虑眼前的苟且 需求,还有诗和远方。为了方便以后程序代码的开发,可以更高效的完成系统的维护开发,持续的支持业务的发展,而不会成为业务发展的障碍,提升代码重用性、代码可读性和代码可扩展性是必经之路。
- 重用性:相同功能的代码,不多多次编写
- 可读性:编程规范性,便于其他程序员阅读
- 可扩展性:当增加新的功能后,对原来的功能没有影响
使用设计模式的最终目的是实现:“高内聚,低耦合”。
内聚
故名思议,表示模块内部间聚集、关联的长度,那么高内聚就是指要高度的聚集和关联。
高内聚是指类与类之间的关系而定,高,意思是他们之间的关系要简单,明了,不要有很强的关系,不然,运行起来就会出问题。一个类的运行影响到其他的类。由于高内聚具备可靠性,可重用性,可读性等优点,模块设计推荐采用高内聚。
内聚有如下的种类,它们之间的内聚度由弱到强排列如下:
-
偶然内聚:一个模块内的各处理元素之间没有任何联系,只是偶然地被凑到一起。这种模块也称为巧合内聚,内聚程度最低。
-
逻辑内聚:这种模块把几种相关的功能组合在一起, 每次被调用时,由传送给模块参数来确定该模块应完成哪一种功能。
-
时间内聚:把需要同时执行的动作组合在一起形成的模块称为时间内聚模块。
-
过程内聚:构件或者操作的组合方式是,允许在调用前面的构件或操作之后,马上调用后面的构件或操作,即使两者之间没有数据进行传递。简单的说就是如果一个模块内的处理元素是相关的,而且必须以特定次序执行则称为过程内聚。
- 例如某要完成登录的功能,前一个功能判断网络状态,后一个执行登录操作,显然是按照特定次序执行的。
-
通信内聚:指模块内所有处理元素都在同一个数据结构上操作或所有处理功能都通过公用数据而发生关联(有时称之为信息内聚)。即指模块内各个组成部分都使用相同的数据结构或产生相同的数据结构。
-
顺序内聚:一个模块中各个处理元素和同一个功能密切相关,而且这些处理必须顺序执行,通常前一个处理元素的输出时后一个处理元素的输入。
- 例如某要完成获取订单信息的功能,前一个功能获取用户信息,后一个执行计算均价操作,显然该模块内两部分紧密关联。
顺序内聚的内聚度比较高,但缺点是不如功能内聚易于维护。
- 例如某要完成获取订单信息的功能,前一个功能获取用户信息,后一个执行计算均价操作,显然该模块内两部分紧密关联。
-
功能内聚:模块内所有元素的各个组成部分全部都为完成同一个功能而存在,共同完成一个单一的功能,模块已不可再分。即模块仅包括为完成某个功能所必须的所有成分,这些成分紧密联系、缺一不可。
- 功能内聚是最强的内聚,其优点是它的功能明确。判断一个模块是否功能内聚,一般从模块名称就能看出。如果模块名称只有一个动词和一个特定的目标(单数名词),一般来说就是功能内聚,如:“计算水费”、“计算产值”等模块。功能内聚一般出现在软件结构图的较低层次上。
- 功能内聚模块的一个重要特点是:他是一个“暗盒”,对于该模块的调用者来说,只需要知道这个模块能做什么,而不需要知道这个模块是如何做的。
耦合
耦合是对模块之间关联程度的度量。
耦合的强弱取决与模块间接口的复杂性、调用模块的方式以及通过界面传送数据的多少。
模块间的耦合度是指模块之间的依赖关系,包括控制关系、调用关系、数据传递关系。模块间联系越多,其耦合性越强,同时表明其独立性越差。
降低模块间的耦合度能减少模块间的影响,防止对某一模块修改所引起的“牵一发动全身”的水波效应,保证系统设计顺利进行。
耦合可以分为以下几种,它们之间的耦合度由高到低排列如下:
-
内容耦合:一个模块直接访问另一模块的内容,则称这两个模块为内容耦合。
若在程序中出现下列情况之一,则说明两个模块之间发生了内容耦合:- 一个模块直接访问另一个模块的内部数据。
- 一个模块不通过正常入口而直接转入到另一个模块的内部。
- 两个模块有一部分代码重叠(该部分代码具有一定的独立功能)。
- 一个模块有多个入口。
内容耦合可能在汇编语言中出现。大多数高级语言都已设计成不允许出现内容耦合。这种耦合的耦合性最强,模块独立性最弱。
-
公共耦合:一组模块都访问同一个全局数据结构,则称之为公共耦合。公共数据环境可以是全局数据结构、共享的通信区、内存的公共覆盖区等。如果模块只是向公共数据环境输入数据,或是只从公共数据环境取出数据,这属于比较松散的公共耦合;如果模块既向公共数据环境输入数据又从公共数据环境取出数据,这属于较紧密的公共耦合。
- 公共耦合会引起以下问题:
- 无法控制各个模块对公共数据的存取,严重影响了软件模块的可靠性和适应性。
- 使软件的可维护性变差。若一个模块修改了公共数据,则会影响相关模块。
- 降低了软件的可理解性。不容易清楚知道哪些数据被哪些模块所共享,排错困难。
- 一般地,仅当模块间共享的数据很多且通过参数传递很不方便时,才使用公共耦合。
- 公共耦合会引起以下问题:
-
外部耦合:一组模块都访问同一全局简单变量,而且不通过参数表传递该全局变量的信息,则称之为外部耦合。
-
控制耦合:模块之间传递的不是数据信息,而是控制信息例如标志、开关量等,一个模块控制了另一个模块的功能。
-
标记耦合:调用模块和被调用模块之间传递数据结构而不是简单数据,同时也称作特征耦合。表就和的模块间传递的不是简单变量,而是像高级语言中的数据名、记录名和文件名等数据结果,这些名字即为标记,其实传递的是地址。
-
数据耦合:调用模块和被调用模块之间只传递简单的数据项参数。相当于高级语言中的值传递。
-
非直接耦合:两个模块之间没有直接关系,它们之间的联系完全是通过主模块的控制和调用来实现的。耦合度最弱,模块独立性最强。
高内聚与低耦合的平衡
并不是内聚越高越好,耦合越低越好,真正好的设计是在高内聚和低耦合间进行平衡,也就是说高内聚和低耦合是冲突的。
- 最强的内聚莫过于一个类只写一个函数,这样内聚性绝对是最高的。但这会带来一个
明显的问题:类的数量急剧增多,这样就导致了其它类的耦合特别多,于是整个设计就变成了“高内聚高耦合”了。由于高耦合,整个系统变动同样非常频繁。 - 最弱的耦合是一个类将所有的函数都包含了,这样类完全不依赖其它类,耦合性是最低的。但这样会带来一个明显的问题:内聚性很低,于是整个设计就变成了“低耦合低内聚”了。由于低内聚,整个类的变动同样非常频繁。
真正做到高内聚、低耦合是很难的,很多时候未必一定要这样,更多的时候“最适合”的才是最好的,不过、审时度势、融会贯通、人尽其才、物尽其用,才是设计的王道。
下面就是介绍做好高内聚低耦合而要遵循的七大原则。
七大原则
七大原则不仅是设计模式的依据,也是我们平常编程中应该遵守的原则。
开闭原则
开闭原则是编程汇总最基础,最重要的设计原则。
核心:
对扩展开放、对修改封闭
体现:
在设计的时候,时刻要考虑让这个类是足够好,写好了就不要去修改。
对于有新需求,只需要增加一些类就可以完成,原来的代码能不动就不动
举例:
未使用开闭原则
/**
* 接口 transport
*/
public interface transport {
public void run();
}
/**
* Bus 具体类
*/
public class Bus implements transport {
@Override
public void run() {
System.out.println("大巴在公路上跑");
}
}
当我们修改需求,让大巴也能有在水里开的属性,我们可以对Bus类添加一个方法即可。但是这个已经违背了开闭原则,如果业务复杂,这样子的修改很容易出问题的。
使用开闭原则:
我们可以新增一个类,实现transport接口,并继承Bus类,写自己的需求即可。
/**
* 实现接口并继承原先Bus类,重写父类方法
*/
public class universalBus extends Bus implements transport {
@Override
public void run() {
System.out.println("大巴既然在公路上开,又能在水里开");
}
}
开闭原则需要其他四个原则做铺垫:
- 接口隔离原则
- 单一职责原则
- 依赖倒置原则
- 里氏替换原则
接口隔离原则
核心:
建立单一接口,不要建立庞大的接口,尽量细化接口,接口中的方法尽量少
体现:
为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用,因为类不应该依赖它不需要的接口,依赖几个专用的接口要比依赖一个综合的接口更灵活
举例:
public interface People {
void exam();
void teach();
}
/*
* 被迫实现了接口所不必要的接口
*/
public class Student implements People {
@Override
public void exam() {
System.out.println("学生考试");
}
@Override
public void teach() {
}
}
public class Teacher implements People{
@Override
public void exam() {
}
@Override
public void teach() {
System.out.println("教师教书");
}
}
虽然这么做代码不会报错,但是看得出来十分冗余且奇怪,造成使用者的混淆。
使用开闭原则:
我们将People接口的两个方法拆分开,分为两个接口LearnPeople和TeachPeople,并且让Sudent实现People1接口,Teacher实现People2接口,使其互不干扰
/**
* 只负责学习的一类人
*/
public interface LearnPeople {
void exam();
}
/**
* 只负责教育的一类人
*/
public interface TeachPeople {
void teach();
}
public class Student implements People1 {
@Override
public void exam() {
System.out.println("学生考试");
}
}
public class Teacher implements People2 {
@Override
public void teach() {
System.out.println("教师教书");
}
}
组合/聚合复用原则
该原则是针对实体对象类的
核心:
尽量使用对象组合,而不是继承来达到复用的目的。
体现:
在一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分。
通过继承来进行复用的主要问题在于继承复用会破坏系统的封装性,因为继承会将基类的实现细节暴露给子类,由于基类的内部细节通常对子类来说是可见的,所以这种复用又称“白箱”复用。
如果基类发生改变,那么子类的实现也不得不发生改变;从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性;而且继承只能在有限的环境中使用(如类没有声明为不能被继承)。
组合/聚合可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少。
举例:
里氏替换原则
里氏替换原则是1988年麻省理工姓李的女士提出,它是阐述了对继承的一些看法。
继承通常会使类之间的耦合度增强,如果拓展父类可能会使子类受到影响,如果子类重写父类的方法,父类中的方法将会显得多余,假如父类中有了具体的方法实现,那么子类请不要重写这个方法。
里氏替换的原则告诉我们当我们在使用继承的时候,子类最好的做法是不要继承父类已经实现的方法,继承容易增强类之间的的耦合性,最好的做法是使用类之间的依赖,组合,聚和来解决问题。
核心:
子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。从而使子类对象能够替换父类对象,而程序逻辑不变。
体现:
里氏替换原则有至少以下两种含义:
-
里氏替换原则是针对继承而言的,如果继承是为了实现代码重用,也就是为了共享方法,那么共享的父类方法就应该保持不变,不能被子类重新定义。子类只能通过新添加方法来扩展功能,父类和子类都可以实例化,而子类继承的方法和父类是一样的,父类调用方法的地方,子类也可以调用同一个继承得来的,逻辑和父类一致的方法,这时用子类对象将父类对象替换掉时,当然逻辑一致,相安无事。
-
如果继承的目的是为了多态,而多态的前提就是子类覆盖并重新定义父类的方法,为了符合里氏替换原则,我们应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法,当父类是抽象类时,父类就是不能实例化,所以也不存在可实例化的父类对象在程序里。也就不存在子类替换父类实例(根本不存在父类实例了)时逻辑不一致的可能。
举例:
public class LspDemo {
public static void main(String[] args) {
B b = new B();
// 其实是想调用的父类 A 中的减方法,但是 B 不小心重写了 A 中的方法
int num = b.test1(7, 8);
int num2 = b.test2(6, 3);
System.out.println(num);
System.out.println(num2);
}
}
class A {
int test1(int a, int b) {
return a - b;
}
}
class B extends A {
int test1(int a, int b) {
return a + b;
}
int test2(int a, int b) {
return a / b;
}
}
上面的这个例子,B 想使用父类 A 中的两数相减的方法是做不到了,因为重写了父类中的方法
//改例子很好的解决了上述重写的问题,通过增加一个接口 Base ,接口中定义标准,A 和 B 去做具体的实现
//这里 B 通过引入 A 的实例,间接地使用了 A 中两个数相减的方法
public class LspDemo {
public static void main(String[] args) {
Base base = new A();
B b = new B(base);
//使用的仍然是 B 类中的加方法
int num = b.test1(9, 7);
int num2 = b.test2(10,2);
//通过 B 调用 A 中的减方法
int num3 = b.test3(9, 7);
System.out.println(num);
System.out.println(num2);
System.out.println(num3);
}
}
interface Base {
int test1(int a, int b);
}
class A implements Base {
public int test1(int a, int b) {
return a - b;
}
}
class B implements Base{
private Base base;
B(Base base) {
this.base = base;
}
public int test1(int a, int b) {
return a + b;
}
int test2(int a, int b) {
return a / b;
}
int test3(int a, int b) {
return base.test1(a,b);
}
}
上面的这个例子做了一个很好的改进,为了使用 A 中的方法,B 不在继承 A ,B中引入了 A , 这里面还使用了依赖倒转原则,没有直接在 B 中引入 A 的实例,而是通过在 B 中引入 A 的接口类 Base ,在 main 方法中具体使用的时候才注入 A 的实例对象,从而达到了使用 A 中方法的目的。
最少知识原则(迪米特法则)
迪米特法则讲的是类之间的耦合关系,如果两个类之间建立耦合关系以后,最好不要暴露第三方的类,就像 A 依赖 B ,B 依赖 C ,那么请不要把 C 暴露在 A 当中。
核心:
一个类对自已依赖的类知道的越少就越好,也就是说被依赖的类不管多么的复杂,都把逻辑封装在类的内部,对外只需要暴露自己的 public 方法就好了
体现:
不是直接耦合依赖的类最好不要作为局部变量的形式出现在类的内部
在一个类里能少用多少其他类就少用多少,尤其是局部变量的依赖类,能省略尽量省略
举例:
/*
* 这是一个相当简单的 demo,总共就三个类 A ,B,C
* 在这里 A 类中出现了一个问题: A 中的 testA 的方法中出现类一个间接的朋友 C
* 这个例子违反了迪米特原则,A 中只需要用到 B 就可以了,不可以出现 C
*
* */
public class LodDemo {
public static void main(String[] args) {
A a = new A();
B b = new B();
a.testA(b);
}
}
class A {
//关键性代码如下:
void testA(B b) {
C c = b.testB();
c.testC();
}
}
class B{
C testB() {
return new C();
}
}
class C{
void testC() {
System.out.println("c的方法运行了");
}
}
上面的这个例子 A 中有一个 C ,这个 C 的存在违背了迪米特法则。
/*
* 分析一下这个改进的 demo ,同上一个 demo 的效果相同
* 但是这个例子中 A 中只有一个 B 而已
* A 中的 C 交给了 C 的直接朋友 B 去处理了
* B 暴露给 A 的只是一个可以调用的方法而已
*/
public class LodDemo {
public static void main(String[] args) {
A a = new A();
B b = new B();
a.testA(b);
}
}
class A {
void testA(B b) {
//改进的代码
b.testB();
}
}
class B{
// B 做了一个改进
private C c;
void testB() {
c = new C();
this.addTest(c);
}
// 新添加的调用 C 的方法 testC
private void addTest(C c ) {
c.testC();
}
}
class C{
void testC() {
System.out.println("c的方法运行了");
}
}
这个改进的 demo 中 A 和 B 是直接的朋友关系,B 和 C是直接的朋友关系,两个类之间没有第三者的存在。
单一职责原则
核心:
每一个类应该专注于做一种事情,一个方法只做一件事
体现:
降低类的复杂度,当修改一个功能时,可以显著降低对其他功能的影响。
举例:
public class SrpDemo {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle();
vehicle.run("巴士");
vehicle.run("摩托");
vehicle.run("飞机");
}
}
class Vehicle {
void run(String vehicle){
System.out.println(vehicle +"在公路上运行");
}
}
该 demo 就存在了一个问题: 飞机多是在空中飞行的,都集中在 vehicle 类中就显得不合适了
使用单一职责原则
/**
* 该案例把 vehicle 的职责进行一个拆分,飞机飞的功能单独定义
*/
public class SrpDemo {
public static void main(String[] args) {
Vehicvle vehicvle = new Vehicvle();
vehicvle.run("巴士");
vehicvle.run("摩托");
AirPlane airPlane = new AirPlane();
airPlane.run("飞机");
}
}
class Vehicvle{
void run(String vehicle) {
System.out.println(vehicle+"在公路上运行");
}
}
//增加一个飞机类,提供飞的功能
class AirPlane{
void run(String plane) {
System.out.println(plane +"在天空中飞着");
}
}
再进一步优化,通过方法实现单一职责原则
// 有时候可以通过通过添加方法进行单一职责的实现,可以有效的避免类过多,如果添加的方法过多,建议添加类比较好
public class SrpDemo2 {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle();
vehicle.run("巴士");
vehicle.run("摩托");
vehicle.fly("飞机");
}
}
class Vehicle{
void run(String vehicle) {
System.out.println(vehicle +"在公路上跑着");
}
void fly(String plane) {
System.out.println(plane + "在天空中飞着");
}
}
依赖倒置原则
核心:
抽象不应该依赖具体,具体应该依赖抽象,上层依赖下层
体现:
面向接口编程
举例:
public class DIPDemo {
public static void main(String[] args) {
Car car = new Car("bus");
Traveling traveling = new Traveling();
traveling.travel(car);
}
}
class Traveling {
void travel(Car car) {
System.out.println("我的旅游工具是 "+ car.getName());
}
}
class Car {
private String name;
Car(String name) {
this.name = name;
}
String getName() {
return this.name;
}
}
分析上述代码可以看到一个问题,如果旅游的方式是飞机怎么办呢?这样拓展起来就比较麻烦了。因此,我们做一个改进:
public class DIPDemo {
public static void main(String[] args) {
Car car = new Car("bus");
Plane plane = new Plane("飞机");
Traveling traveling = new Traveling();
traveling.travel(plane);
traveling.travel(car);
}
}
class Traveling {
void travel(Tool tool) {
System.out.println("我的旅游工具是 "+ tool.getName());
}
}
interface Tool{
String getName();
}
class Car implements Tool {
private String name;
Car(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
class Plane implements Tool{
private String name;
Plane(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
上述例子我们可以看到旅游类里面我们传入的是一个 Tool ,各种交通工具继承 Tool 接口,这样就方便我们更好的拓展各种交通工具类了。