摘录自某PPT
设计模式 | 菜鸟教程
软件设计模式概述 | C语言中文网
(Bass,Clements,and Kazman 2003)
封装(Encapsulation)封装也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据。
封装的意义:如果我们对类中属性的访问不做限制,那任何代码都可以访问、修改类中的属性,虽然这样看起来更加灵活,但从另一方面来说,过度灵活也意味着不可控,属性可以随意被以各种奇葩的方式修改,而且修改逻辑可能散落在代码中的各个角落,势必影响代码的可读性、可维护性。除此之外,类仅仅通过有限的方法暴露必要的操作,也能提高类的易用性。如果我们把类属性都暴露给类的调用者,调用者想要正确地操作这些属性,就势必要对业务细节有足够的了解。而这对于调用者来说也是一种负担。
抽象(Abstraction)隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的。我们常借助编程语言提供的接口类(interface)或者抽象类(abstract)这两种语法机制,来实现抽象这一特性。
抽象的意义:抽象及其前面讲到的封装都是人类处理复杂性的有效手段。在面对复杂系统的时候,人脑能承受的信息复杂程度是有限的,所以我们必须忽略掉一些非关键性的实现细节。很多设计原则都体现了抽象这种设计思想,比如基于接口而非实现编程、开闭原则、代码解耦等。我们在定义(或者叫命名)类的方法的时候,也要有抽象思维,不要在方法定义中,暴露太多的实现细节,以保证在某个时间点需要改变方法的实现逻辑的时候,不用去修改其定义。
继承(Inheritance)继承是用来表示类之间的 is-a 关系,从继承关系上来讲,继承可以分为两种模式,单继承和多继承。单继承表示一个子类只继承一个父类,多继承表示一个子类可以继承多个父类,比如猫既是哺乳动物,又是爬行动物。但在Java和.Net等只能单继承,更提倡面向接口编程。
继承的意义:继承最大的一个好处就是代码复用。假如两个类有一些相同的属性和方法,我们就可以将这些相同的部分,抽取到父类中,让两个子类继承父类。通过继承来关联两个类,反应真实世界中的这种关系,非常符合人类的认知,而且,从设计的角度来说,也有一种结构美感。不过,过度使用继承,继承层次过深过复杂,就会导致代码可读性、可维护性变差。
多态(Polymorphism)顾名思义,一个接口,多种形态。子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。同样是一个绘图(draw)的方法,如果以正方形调用,则绘制出一个正方形;如果以圆形调用,则画出的是圆形。
多态的意义:只使用封装和继承的编程方式,称之为基于对象(Object Based)编程,而只有把多态加进来,才能称之为面向对象(Object Oriented)编程。多态特性能提高代码的可扩展性和复用性。除此之外,多态也是很多设计模式、设计原则、编程技巧的代码实现基础,比如策略模式、基于接口而非实现编程、依赖倒置原则、里式替换原则、利用多态去掉冗长的 if-else 语句等等。
面向对象和面向过程两种编程风格并不是非黑即白、完全对立的。在用面向对象编程语言开发的软件中,面向过程风格的代码并不少见,甚至在一些标准的开发库(比如 JDK、Apache Commons、Google Guava)中,也有很多面向过程风格的代码。
不管使用面向过程还是面向对象哪种风格来写代码,我们最终的目的还是写出易维护、易读、易复用、易扩展的高质量代码。只要我们能避免面向过程编程风格的一些弊端,控制好它的副作用,在掌控范围内为我们所用,我们就大可不用避讳在面向对象编程中写面向过程风格的代码。
面向对象分析就是要搞清楚做什么,面向对象设计就是要搞清楚怎么做,面向对象编程就是将分析和设计的的结果翻译成代码的过程。
需求分析的过程实际上是一个不断迭代优化的过程。我们不要试图一下就给出一个完美的解决方案,而是先给出一个粗糙的、基础的方案,有一个迭代的基础,然后再慢慢优化。
面向对象设计和实现要做的事情就是把合适的代码放到合适的类中。至于到底选择哪种划分方法,判定的标准是让代码尽量地满足“松耦合、高内聚”、单一职责、对扩展开放对修改关闭等我们之前讲到的各种设计原则和思想,尽量地做到代码可复用、易读、易扩展、易维护。
面向对象分析的产出是详细的需求描述。
面向对象设计的产出是类。
划分职责进而识别出有哪些类 根据需求描述,我们把其中涉及的功能点,一个一个罗列出来,然后再去看哪些功能点职责相近,操作同样的属性,可否归为同一个类。
定义类及其属性和方法 我们识别出需求描述中的动词,作为候选的方法,再进一步过滤筛选出真正的方法,把功能点中涉及的名词,作为候选属性,然后同样再进行过滤筛选。
定义类与类之间的交互关系 UML 统一建模语言中定义了六种类之间的关系。它们分别是:泛化、实现、关联、聚合、组合、依赖。我们从更加贴近编程的角度,对类与类之间的关系做了调整,保留了四个关系:泛化、实现、组合、依赖。
将类组装起来并提供执行入口 我们要将所有的类组装在一起,提供一个执行入口。这个入口可能是一个 main() 函数,也可能是一组给外部用的 API 接口。通过这个入口,我们能触发整个代码跑起来。
抽象类是对成员变量和方法的抽象,是一种 is-a 关系,是为了解决代码复用问题。
接口仅仅是对方法的抽象,是一种 has-a 关系,表示具有某一组行为特性,是为了解决解耦问题,隔离接口和具体的实现,提高代码的扩展性。
什么时候该用抽象类?什么时候该用接口?实际上,判断的标准很简单。如果要表示一种 is-a 的关系,并且是为了解决代码复用问题,我们就用抽象类;如果要表示一种 has-a 关系,并且是为了解决抽象而非代码复用问题,那我们就用接口。
在一个面向对象的系统中,系统的各种功能是由许许多多的不同对象协作完成的。在这种情况下,各个对象内部是如何实现自己的,对系统设计人员来讲就不那么重要了;而各个对象之间的协作关系则成为系统设计的关键。小到不同类之间的通信,大到各模块之间的交互,在系统设计之初都是要着重考虑的,这也是系统设计的主要工作内容。面向接口编程就是指按照这种思想来编程。
应用这条原则,可以将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样当实现发生变化的时候,上游系统的代码基本上不需要做改动,以此来降低耦合性,提高扩展性。
实际上,“基于接口而非实现编程”这条原则的另一个表述方式是,“基于抽象而非实现编程”。后者的表述方式其实更能体现这条原则的设计初衷。在软件开发中,最大的挑战之一就是需求的不断变化,这也是考验代码设计好坏的一个标准。
越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。好的代码设计,不仅能应对当下的需求,而且在将来需求发生变化的时候,仍然能够在不破坏原有代码设计的情况下灵活应对。而抽象就是提高代码扩展性、灵活性、可维护性最有效的手段之一。
继承是面向对象的四大特性之一,用来表示类之间的 is-a 关系,可以解决代码复用的问题。虽然继承有诸多作用,但继承层次过深、过复杂,也会影响到代码的可维护性。在这种情况下,我们应该尽量少用,甚至不用继承。
继承主要有三个作用:表示 is-a 关系、支持多态特性、代码复用。而这三个作用都可以通过组合、接口、委托三个技术手段来达成。除此之外,利用组合还能解决层次过深、过复杂的继承关系影响代码可维护性的问题。
尽管我们鼓励多用组合少用继承,但组合也并不是完美的,继承也并非一无是处。如果类之间的继承结构稳定,层次比较浅,关系不复杂,我们就可以大胆地使用继承。反之,我们就尽量使用组合来替代继承。除此之外,还有一些设计模式、特殊的应用场景,会固定使用继承或者组合。
很多Web 项目的业务开发,大部分都是基于贫血模型的 MVC 三层架构,我们称为传统的开发模式。之所以称之为“传统”,是相对于新兴的基于充血模型的 DDD 开发模式来说的。基于贫血模型的传统开发模式,是典型的面向过程的编程风格。相反,基于充血模型的 DDD 开发模式,是典型的面向对象的编程风格。
不过,DDD 也并非银弹。对于业务不复杂的系统开发来说,基于贫血模型的传统开发模式简单够用,基于充血模型的 DDD 开发模式有点大材小用,无法发挥作用。相反,对于业务复杂的系统开发来说,基于充血模型的 DDD 开发模式,因为前期需要在设计上投入更多时间和精力,来提高代码的复用性和可维护性,所以相比基于贫血模型的开发模式,更加有优势。
SOLID 原则 -SRP(Single Responsibility Principle) 单一职责原则
SOLID 原则 -OCP(Open Closed Principle) 开闭原则
SOLID 原则 -LSP(Liskov Substitution Principle) 里式替换原则
SOLID 原则 -ISP(Interface Segregation Principle) 接口隔离原则
SOLID 原则 -DIP(Dependence Inversion Principle) 依赖倒置原则
DRY原则(Don’t Repeat Yourself)
KISS原则 (Keep It Simple,Stupid)
YAGNI原则 (You Ain’t Gonna Need It)
LOD法则(Law of Demeter)
设计模式 | 菜鸟教程
软件设计模式概述 | C语言中文网
编程规范主要解决的是代码的可读性问题。
编码规范相对于设计原则、设计模式,更加具体、更加偏重代码细节。
即便你可能对设计原则不熟悉、对设计模式不了解,但你最起码要掌握基本的编码规范:
《重构》《代码大全》《代码整洁之道》
在软件开发中,只要软件在不停地迭代,就没有一劳永逸的设计。随着需求的变化,代码的不停堆砌,原有的设计必定会存在这样那样的问题。针对这些问题,我们就需要进行代码重构。
重构是软件开发中非常重要的一个环节。持续重构是保持代码质量不下降的有效手段,能有效避免代码腐化到无可救药的地步。
而重构的工具就是我们前面罗列的那些面向对象设计思想、设计原则、设计模式、编码规范。实际上,设计思想、设计原则、设计模式一个最重要的应用场景就是在重构的时候。虽然使用设计模式可以提高代码的可扩展性,但过度不恰当地使用,也会增加代码的复杂度,影响代码的可读性。
在开发初期,除非特别必须,我们一定不要过度设计,应用复杂的设计模式。而是当代码出现问题的时候,我们再针对问题,应用原则和模式进行重构。这样就能有效避免前期的过度设计。
静态建模展示的是问题的静态结构视图,它不随时间的变化而变化。一个静态模型描述了被建模系统的静态结构,相比系统的功能,这些静态结构被认为不太会改变。特别地,静态模型定义了系统中的类、类的属性、类的操作及类之间的关系。
类之间的关系,有三种类型:
关联定义了两个或多个类之间的关系,指明了类之间的一种静态的、结构化的关系。例如,“雇员”工作于“部门”,这里“雇员”和“部门”是类,“工作于”是一个关联。关联本身是双向的。关联的名称取其正向:“雇员”工作于“部门”。关联也有一个隐含的相反的方向:“部门”雇佣“雇员”。关联大部分是二元的,即描述两个类之间的关系。
组合和聚合层次都是讨论一个类由其他类构成的情况。组合和聚合都是关系的特殊形式:类通过整体/部分关联连接起来。在这两种情况下,部分和整体间的关系是(是……的一部分)关系。
组合是一种比聚合更强的关系,聚合是一种比关联更强的关系。
组合关系是一种在部分和整体关联较强的关系,也是实例之间的关系。部分对象的创建、存在和消亡都是和整体一起的。
聚合层次是整体/部分关系较弱的形式。在一个聚合里,部分实例能够添加到聚合整体中,也能从聚合整体中移除。
有一些类相似但不相同,它们有些共同的属性,也有其他不同的属性。在泛化/特化层次中,共同属性被抽象到一个泛化类,称作超类或父类。不同的属性是特化类的特质,特化类被称作子类。在子类和超类之间由一个Is-a关系。
每一个子类继承了超类的性质,但是也对这些性质以不同的方式进行了扩展。一个类的性质是其属性或操作。继承允许对父类进行适配,来形成子类。子类从超类继承了属性和操作。子类还可以增加属性、增加操作或者重定义操作。每一个子类自身也可以成为超类,进一步特化形成其他子类。
UML2.5教程
一个对象是现实世界中物理的或概念的实体,它提供了对现实世界的理解。一个现实世界的对象可具有物理属性(它们能被看到或被触摸到),例如一扇门、一辆汽车或一盏灯。一个概念对象是更抽象的概念,例如一个账户或一次交易。
面向对象的应用由对象组成。从设计的视角来说,一个对象涵盖了数据(data)以及作用于数据之上的过程(procedure),这些过程通常被称为操作(operation)或者方法(method)。
一个操作的签名(signature)代表该操作的名字、参数以及返回值。一个对象的接口(interface)是它提供的操作的集合,这些操作通过签名定义。
属性(attribute)是由类中的对象所持有的一个数据值,每一个对象的属性都有一个特定的值。
操作是由一个对象所执行的一项功能的规约。一个对象可拥有一个或多个操作。操作对象所包含的属性进行操控。操作可具有输入和输出参数。在同一个类中的所有对象拥有相同的操作。
对象创建包含申请内存、初始化变量、堆栈引用等一系列操作,需要分配资源且消耗时间。常见的对象创建方式如下,
对象构成组件,组件构成模块,模块构成系统。一个系统包含多少对象,每个对象何时创建、何时回收等,需要进行统一的管理,对象不应该游离于系统之外。如果控制权交给用户,一方面用户需要对系统深入了解,增加使用负担,另一方面也会导致对象创建过程不可控。从现实世界的角度,任何一个对象都不是凭空出现的,都有其被创建的过程和动机。
对象创建的管理
降低对象间的耦合和依赖,贯穿整个软件开发周期,也是很多设计模式和设计原则要达到的目标。
架构名词起源于建筑。架构就是指人们根据自己对世界的认识,为解决某个问题,主动地、有目的地去识别问题,并进行分解、合并,解决这个问题的实践活动。架构的产出物,自然就是对问题的分析,以及解决问题的方案:包括拆分的原则以及理由,沟通合并的原则以及理由,以及拆分,拆分出来的各个部分和合并所对应的角色和所需要的核心能力等。架构的一般步骤:
用分层来隔离领域
层中的任何元素都仅依赖于本层其他元素,或其下层元素。
高内聚,低耦合
领域层应重点放在如何表达领域模型上,而不需要考虑自己的显示和存储问题
用户界面层:向用户显示信息,解释用户命令
应用层:尽量简单,不包含业务规则或知识,只为下层的领域对象分配任务,使他们协作
领域层:负责表达业务概念,业务状态信息和业务规则
基础设施层:上面各层提供通用的技术。比如持久
Bounded context(界限上下文)