设计模式:在软件开发中会面临许多不断重复发生的问题,这些问题可能是代码冗余、反复修改旧代码、重写以前的代码、在旧代码上不断堆新的代码(俗称屎山)等难以扩展、不好维护的问题。因此1990年有四位大佬(GoF组合)合作出了一本书,叫做《设计模式:可复用面向对象软件的基础》,它比较完美地解决了在软件工程当中所遇到的上述问题,即给出了如果遇到上述问题的标准答案!这本书提出了23种设计模式供后人学习,正确使用设计模式有如下优点:
- 程序设计标准化,工程化,开发效率大大提高,大家遵守统一规范。
- 设计模式的代码可用性高、可读性强(读者掌握设计模式的基础上)、可靠性好、可维护性强、灵活性好等特点。
设计模式的分类
创建者模式
这个设计模式主要思考如何创建一个对象,如何将对象的创建与使用分离。一般初级程序员都是new一个对象,然后紧接着使用这个对象,在某些场景中这样子是有问题的,需要使用创建者模式替代的(例如使用单例模式)。设计模式中提供了五种创建模型,分别是单例、原型、工厂方法、抽象工厂方法、创建者五种模式。
结构型模式
这个设计模式主要思考的是如何将对象进行合理的布局来组成一个更大的功能体或者结构体,这个现在讲有点抽象,用大白话讲就是利用现有的对象进行组合或者配合,使得组合后的这个系统更加好。好是相对于不使用设计模式,按照自己的堆屎山的逻辑堆成一个冗余的系统。结构型模式包括:代理模式、适配器模式、桥接模式、装饰模式、外观模式、享元模式、组合模式七种设计模式。
行为型设计模式
这个设计模式主要思考的是如何分配对象的职责和将对象之间相互协作完成单个对象无法完成的任务,这个与结构型模式有点像,结构型可以理解为静态的组合,例如将不同的组件拼起来成为一个更大的组件;而行为型更是一种动态或者具有某个动作触发的事件,具有一定行为的设计模式。现在不清楚没关系,学完23种设计模式再回头看就能理解了。行为型模式包括:模板方法模式、策略模式、命令模式、职责链模式、状态模式、观察者模式、中介者模式、迭代器模式、访问者模式、备忘录模式、解释器模式共11种。
先搞懂UML中的类图
统一建模语言(Unified Modeling Language,UML)是用来设计软件的可视化建模语言。它的特点是简单、统一、图形化、能表达软件设计中的动态与静态信息。UML 从目标系统的不同角度出发,定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等 9 种图。这里不需要管这么多,只需要懂得类图即可。类图(Class diagram)是显示了模型的静态结构,特别是模型中存在的类、类的内部结构以及它们与其他类的关系等。类图不显示暂时性的信息。类图是面向对象建模的主要组成部分。
在UML类图中,类使用包含类名、属性(field) 和方法(method) 且带有分割线的矩形来表示,比如下图表示一个Employee类,它包含name,age和address这3个属性,以及work()方法。
属性/方法名称前加的加号和减号表示了这个属性/方法的可见性,UML类图中表示可见性的符号有三种:
- +:表示public
- -:表示private
- #:表示protected
类图之间关系的表示方式
关联关系
关联关系是对象之间的一种引用关系,用于表示一类对象与另一类对象之间的联系,即一个类属性对象的引用关系。在Java中可以理解为依赖关系或者引用关系。
-
单向关联
使用单向黑箭头表示,被指向的类表示被引用了,如下地址类被顾客类所引用,通过让Customer类持有一个类型为Address的成员变量类实现:
-
双向关联
使用无方向线条表示,两个相连接的类可以互为对方的成员变量,下图中在Customer类中维护一个List<Product>,表示一个顾客可以购买多个商品;在Product类中维护一个Customer类型的成员变量表示这个产品被哪个顾客所购买:
-
自关联
使用单向黑箭头表示,下图的意思就是Node类包含类型为Node的成员变量,也就是"自己包含自己":
-
聚合关系
聚合关系可以用带空心菱形的实线来表示,菱形指向整体,聚合关系是关联关系的一种,是强关联关系,是整体和部分之间的关系。聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。
注意:这里很容易理解为单向关联关系,区别如下:从整体与部分来理解,单向关联关系是类与类之间的联接,它使一个类知道另一个类的属性和方法。它是依赖关系相对聚合关系更加弱的一种关系。而聚合关系更加强调整体与部分,如下教师知识一个学校的一部分,一个学校不可以没有教师,但是没有学校教师依旧可以存在。而顾客可以有地址,也可以不填地址。单向关联关系是一种相对较弱的关系,其中一个类知道另一个类的存在;而聚合关系则表示更强的整体与部分之间的关系,其中被包含的类是整体的一部分。总之聚合关系依赖关系更加强,强调整体与部分!
-
组合关系
组合关系使用黑色菱形箭头表示,菱形指向整体。组合关系非常好理解,他和聚合关系的明显区别就是被包含的类不可用脱离整体类而存在。聚合关系中的教师类可以脱离学校而存在,就像鼠标类可以脱离电脑类而单独存在,但是汽车方向盘类不可以脱离汽车类而存在。就像下图所示是头和嘴的关系图:
依赖关系
依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类。依赖关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。在代码中,某个类的方法通过局部变量、方法的参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责。如下图所示,司机和汽车的关系图,汽车类只是司机类的一个参数|局部变量:
继承关系
泛化关系用带空心三角箭头的实线来表示,箭头从子类指向父类。继承关系是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系,是一种继承关系。在代码实现时,使用面向对象的继承机制来实现泛化关系。例如,Student 类和 Teacher 类都是 Person 类的子类,其类图如下图所示:
实现关系
实现关系使用带空心三角箭头的虚线来表示,箭头从实现类指向接口。实现关系是接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作。例如,汽车和船实现了交通工具,其类图如图 所示。
软件设计原则
在软件开发中,为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,程序员要尽量根据6条原则来开发程序,从而提高软件开发效率、节约软件开发成本和维护成本。
开闭原则
开闭原则的意思是对扩展开放、对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类。但是尽量不要使用抽象类,而实现接口类!组合优于继承,假设我们将燕子、老鹰抽象为一个鸟类,那么这个类就有鸟的一些共性,例如都有飞的方法。到目前为止,这个系统能够安稳地运行。但是现在出现了另外一个动物鸵鸟,现在开发者非常纠结到底要不要继承鸟这个类。这个列子再开发中更加常见,有很多本身就比较抽象的对象,继承本来就是写死了一些东西。如果使用接口,就无需烦恼了,需要什么实现什么,就像搭积木一样组合起来就行。例如鸵鸟对象,我们可以让它实现飞的接口、地上跑的接口等,组合起来就行。因此,开闭原则总结就是如果要升级某个功能或者重写某个需求,先看看能不能通过实现接口重写,而不是直接修改代码,这个在设计模式中有很多地方都体现了这一点。
里氏代换原则
子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类已经实现的方法。如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。
依赖倒转原则
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。例子如下:
假设电脑仅由CPU、硬盘、内存卡三个设备组成,上图是错误的,因为抽象不应该依赖细节,细节应该依赖抽象。即三个属性应该都是抽象的,也就是多态,而不应该写死(具体化),应该改成下面的,具体的CPU、硬盘、内存卡知识对接口的实现,Computer而不直接依赖具体实现:
接口隔离原则
实现类不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上。典型例子就是组合大于继承的列子,使用继承违背了接口隔离原则,而合理使用接口不违背,我们需要创建一个黑马品牌的安全门,该安全门具有防火、防水、防盗的功能。可以将防火,防水,防盗功能提取成一个接口,形成一套规范。类图如下:
上面的设计我们发现了它存在的问题,黑马品牌的安全门具有防盗,防水,防火的功能。现在如果我们还需要再创建一个传智品牌的安全门,而该安全门只具有防盗、防水功能呢?很显然如果实现SafetyDoor接口就违背了接口隔离原则,那么我们如何进行修改呢?看如下类图:
迪米特法则
迪米特法则又叫最少知识原则。只和你的直接朋友交谈,不跟"陌生人"说话(Talk only to your immediate friends and not to strangers)。
其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。迪米特法则中的"朋友"是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。否则应该使用第三方对象负责调用。下面看一个例子来理解迪米特法则。
【例】明星与经纪人的关系实例
明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如和粉丝的见面会,和媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则。
类图如下:
合成复用原则
合成复用原则是指:尽量先使用组合或者聚合等关联关系来实现(实现接口得到组合对象),其次才考虑使用继承关系来实现。通常类的复用分为继承复用和合成复用两种。
继承复用虽然有简单和易实现的优点,但它也存在以下缺点:
- 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为"白箱"复用。
- 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
- 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:
- 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为"黑箱"复用。
- 对象间的耦合度低。可以在类的成员位置声明抽象。
- 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
下面看一个例子来理解合成复用原则
【例】汽车分类管理程序
汽车按"动力源"划分可分为汽油汽车、电动汽车等;按"颜色"划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,其组合就很多。类图如下:
从上面类图我们可以看到使用继承复用产生了很多子类,如果现在又有新的动力源或者新的颜色的话,就需要再定义新的类。我们试着将继承复用改为聚合复用看一下。
参考自黑马设计模式学习资料