什么是面向对象技术
面向对象技术是一种以对象为基础,以事件或消息来驱动对象执行处理的程序设计技术
面向对象有哪些特性
面向对象技术具有抽象性、封装性、继承性、多态性
抽象
把众多的事物进行归纳、分类 。是人们在认识客观世界时经常采用的思维方法,“物以类聚,人以群分”就是分类的意思,分类所依据的原则是抽象。抽就是忽略事物中与当前目标无关的非本质特征,更充分地注意与当前目标有关的本质特征。从而找出事物的共性,并把具有共性的事物划为一类,得到一个抽象的概念。
封装
就是把对象的属性和行为结合成一个独立的单位,并尽可能隐蔽对象的内部细节。
封装有两个含义:一是把对象的全部属性和行为结合在一起,形成一个不可分割的独立单位。对象的属性值(除了公有的属性值)只能由这个对象的行为来读取和修改;
二是尽可能隐蔽对象的内部细节,对外形成一道屏障,与外部的联系只能通过外部接口实现。将功能封装成一个个独立的单元,减小耦合,避免牵一发而动全身,方便对程序的修改
继承
客观事物既有共性,也有特性。如果只考虑事物的共性,而不考虑事物的特性,就不能反映出客观世界中事物之间的层次关系,不能完整地、正确地对客观世界进行抽象描述。运用抽象的原则就是舍弃对象的特性,提取其共性,从而得到适合一个对象集的类。如果在这个类的基础上,再考虑抽象过程中各对象被舍弃的那部分特性,则可形成一个新的类,这个类具有前一个类的全部特征,是前一个类的子集,形成一种层次结构,即继承结构。 代码重用,减少编码量,间接减少维护成本。
多态
面向对象设计借鉴了客观世界的多态性,体现在不同的对象收到相同的消息时产生多种不同的行为方式。
总结
面向对象技术强调在软件开发过程中面向客观世界或问题域中的事物,采用人类在认识客观世界的过程中普遍运用的思维方法,直观、自然地描述客观世界中的有关事物。
对象、类、抽象类、接口的意义
对象
对象是现实世界中的物理实体在计算机逻辑中的映射和体现
类
类是同种对象的集合与抽象
抽象类
抽象类是对类的抽象
接口
接口是对行为的抽象
什么是面向过程
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了,它其实是最为实际的一种思考方式,就算是面向对象的方法也是含有面向过程的思想.可以说面向过程是一种基础的方法.它考虑的是实际地实现.一般的面向过程是从上往下步步求精.所以面向过程最重要的是模块化的思想方法.
对比面向过程,面向对象的方法主要是把事物给对象化,对象包括属性与行为.当程序规模不是很大时,
面向过程的方法还会体现出一种优势,因为程序的流程很清楚,按着模块与函数的方法可以很好的组织.
五子棋游戏设计思路
面向过程设计思路
1、开始游戏
2、黑子先走
3、绘制画面
4、判断输赢
5、轮到白子
6、绘制画面
7、判断输赢
8、返回步骤2
9、输出最后结果。
面向对象设计思路
1、黑白双方,这两方的行为是一模一样的
2、棋盘系统,负责绘制画面,
3、规则系统,负责判定诸如犯规、输赢等。
加入悔棋功能的结果
现在要加入悔棋的功能,如果要改动面向过程的设计,那么从输入到判断到显示这一连串的步骤都要改动,甚至步骤之间的循序都要进行大规模调整。
如果是面向对象的话,只用改动棋盘对象就行了,棋盘系统保存了黑白双方的棋谱,简单回溯就可以了,而显示和规则判断则不用顾及,同时整个对对象功能的调用顺序都没有变化,改动只是局部的。由此可以看出面向对象更易于扩展。
面向对象和面向过程的不同之处
首先,我们使用面向过程想的是什么呀?先想的是游戏的步骤,怎么一步步的走下去对吧。
那使用面向对象呢? 我们先想的是有哪些设计的对象,然后这些对象都做了些什么,具有什么行为,最后再按照面向过程的思想把行为一步步组织起来是吧。面向过程中重的是函数,这个函数做什么,那个做什么,对于数据是和函数分开的。面向对象是把数据和函数封装在一起。所以我们可以总结一下简单区别如下:
1.面向过程采用函数(或过程)来描述对数据的操作,但将函数与其操作的数据分离开;面向对象将数据和对数据的操作封装在一起
2.面向过程以功能为中心设计功能模块,难维护;面向对象以数据为中心来描述系统,数据相对于功能而言具有较强的稳定性,易维护。
我们分析了面向对象和面向过程,并通过五子棋的设计简单阐述了二者的区别。接下来我们看看如何软件设计有什么需要注意的。
什么是软件设计
软件设计是从软件需求规格说明书出发,根据需求分析阶段确定的功能设计软件系统的整体结构、划分功能模块、确定每个模块的实现算法以及编写具体的代码,形成软件的具体设计方案。
什么是好的设计
好的设计是容易扩展的(软件产品需求变更和扩充经常的事情,在变化来领的时候,我们的软件结构如何能从容不迫的面对这些变化,决定了你设计的高度)
好的设计是容易维护的(项目结束后依然要维护,修复使用过程中发现的bug以及人员变换等等,都对软件的可维护性要求较高)
好的设计是容易移植的(市面上类似的软件很多很多,当你对不同客户开发一款类似的软件,那么所有的东西再做一遍么? 那成本多高,很多时候,我们会考虑复用项目中的功能模块和代码,所以容易移植和复用也是衡量一个软件设计好坏与否的重要指标)
7大类与接口的设计原则
单一职责原则
一个类只负责一项职责.
两种常见的情况是:
一种是职责发散
当某一功能发生修改的时,结果修改了很多类。修改越多的类,造成的风险越大,因为类之间的功能模块互相影响。
第二种是承担太多职责
一个类有功能F1,结果因为某种情况增加了F2.F!和F2中可能有共同使用的数据。对F1进行修改的时候有可能影响F2的功能。
这样可以添加一个新类,把F2移到这个新类当中。
单一职责的优势是:
降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;
提高类的可读性,提高系统的可维护性;
变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。
迪米特原则
一个对象应该对其他对象保持最少的了解。
问题由来
类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
解决方案
尽量降低类与类之间的耦合。
自从我们接触编程开始,就知道了软件编程的总的原则:低耦合,高内聚。无论是面向过程编程还是面向对象编程,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率。低耦合的优点不言而喻,但是怎么样编程才能做到低耦合呢?那正是迪米特法则要去完成的。
迪米特法则又叫最少知道原则,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。迪米特法则还有一个更简单的定义:只与直接的朋友通信。首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。
里氏替换原则
所有引用基类的地方必须能透明地使用其子类的对象。
定义1
如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
定义2
所有引用基类的地方必须能透明地使用其子类的对象。
问题由来
有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。
解决方案
当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。
继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。
继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加了对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能会产生故障。
里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:
1.子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
2.子类中可以增加自己特有的方法。
3.当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
4.当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
合成组合原则
尽量使用对象组合,而不是继承来达到复用的目的。
核心思想
尽量使用对象组合,而不是继承来达到复用的目的。该原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分:新的对象通过向这些对象的委派达到复用已有功能的目的。
术语:
(1)聚合(Aggregation):聚合用来表示“拥有”关系或者整体与部分的关系;
(2)合成(Composition):合成则用来表示一种强得 多的“拥有”关系。在一个合成关系里面,部分和整体的生命周期是一样的。
复用的种类:
继承
优点: 新的实现较为容易,因为基类的大部分功能可以通过继承关系自动进入派生类; 修改或扩展继承而来的实现较为容易
缺点: 继承复用破坏包装,因为继承将基类的实现细节暴露给派生类,这种复用也称为白箱复用; 如果基类的实现发生改变,那么派生类的实现也不得不发生改变; 从基类继承而来的实现是静态的,不可能在运行时发生改变,不够灵活。
合成聚合
优点: 新对象存取成分对象的唯一方法是通过成分对象的接口; 这种复用是黑箱复用,因为成分对象的内部细节是新对象所看不见的; 这种复用支持包装; 这种复用所需的依赖较少; 每一个新的类可以将焦点集中在一个任务上; 这种复用可以在运行时动态进行,新对象可以使用合成/聚合关系将新的责任委派到合适的对象。
缺点: 通过这种方式复用建造的系统会有较多的对象需要管理。
如何选择继承或合成聚合
在复用时应优先考虑使用合成聚合而不是继承,而判定的判断为以下四个Coad条件:
1.派生类是基类的一个特殊种类,而不是基类的一个角色,即要分清"Has-A"和"Is-A"的区别;
2.永远不会出现需要将派生类换成另一个类的派生类的情况;
3.派生类具有扩展基类的责任,而不是具有置换或者注销掉基类的责任;
4.只有在分类学角度有意义时,才可以使用继承。
接口隔离原则
定义
客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
问题由来
类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。
解决方案
将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。
接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
开闭原则
定义
一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
问题由来
在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。
解决方案
当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统。开闭原则可能是设计模式几项原则中定义最模糊的一个了,它只告诉我们对扩展开放,对修改关闭,可是到底如何才能做到对扩展开放,对修改关闭,并没有明确的告诉我们。以前,如果有人告诉我“你进行设计的时候一定要遵守开闭原则”,我会觉的他什么都没说,但貌似又什么都说了。因为开闭原则真的太虚了。
在仔细思考以及仔细阅读很多设计模式的文章后,终于对开闭原则有了一点认识。其实,我们遵循设计模式前面几大原则,以及使用23种设计模式的目的就是遵循开闭原则。也就是说,只要我们对前面5项原则遵守的好了,设计出的软件自然是符合开闭原则的,这个开闭原则更像是前面五项原则遵守程度的“平均得分”,前面几项原则遵守的好,平均分自然就高,说明软件设计开闭原则遵守的好;如果前面5项原则遵守的不好,则说明开闭原则遵守的不好。
说到这里,再回想一下前面说的几项原则,恰恰是告诉我们用抽象构建框架,用实现扩展细节的注意事项而已:单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。
6大包设计原则
重用发布原则
重用的粒度就是发布的粒度:一个包中的软件要么都是可重用的,要么都是不可重用的。
代码可以看作为可重用的代码,当且仅当:
- 它的使用者(下称用户)无需看它的源代码
- 用户只需联结静态库或包含动态库
- 当库发生改变(错误纠正,功能增强)时,用户只需要得到一个新的版本便能集成到原有的系统
怎么做到重用呢?
一个组件要做到能够重用,它必须有一个得到良好设计的结构,它所包含所有元素必须也是可以重用的。
因为如果一个为重用而设计的发布单位里,包含了不可重用的元素,当不可重用的元素发生改变时,用户也不得不改变原有系统以适应新的版本。这显然违反了重用的定义规则。
也就是说,一个为重用目的而设计的发布单位里,不能包含不可重用的元素;如果包含了不可重用的元素,它将变得不可重用。
共同重用原则
一个包中所有类应该是共同重用的。如果重用了包中的一个类,那么就重用包中的所有类。
这个原则可以帮助我们决定哪些类应该放在同一个包中。它规定了趋向共同重用的类应该属于同一个包。
类很少会孤立地重用。一般来说,可重用的类需要与作为该可重用抽象一部分的其他类协作。CRP规定了这些类应该属于同一个包。在这样的一个包中,我们会看到类之间有很多的互相依赖。
一个简单的例子是容器类以及与它关联的迭代器类。这些类彼此之间紧密耦合在一起,因此必须共同重用。所以它们应该在同一个包中
但是,CRP告诉我们的不仅仅是什么类应该共同放入一个包中。它还告诉我们什么类不应该放入同一个包中。当一个包使用了另一个包时,它们之间会存在一个 依赖关系。也许一个包仅仅使用了另外一个包中的一个类。然而,那根本不会削弱这两个包之间的依赖关系。使用者包依然依赖于被使用的包。每当被使用的包发布 时,使用者包必须进行重新验证和重新发布。即使发布的原因仅仅改变了一个使用者包根本不关心的类,也必须要这样做。
此 外,包也经常以共享库、DLL、JAR等物理表示的形式出现。如果被使用的包以JAR的形式发布,那么使用这个包的代码就依赖于整个JAR。对JAR的任 何修改——即使所修改的是与用户代码无关的类,也会造成这个JAR的一个新版本发布。这个新JAR也要重新发行,并且,使用这个JAR的代码也要进行重新 验证。
共同封闭原则
这是单一职责原则(SRP)对于包的重新规定。正如SRP规定的一个类不应该包含多个引起变化的原因那样,这个原则规定了一个包不应该包含多个引起变化的原因。
在大多数的应用中,可维护性的重要性是超过可重用性的。如果一个应用中的代码必须更改,那么我们宁愿更改都集中在一个包中,而不是分布在多个包中。如果 更改集中在一个单一的包中,那么我们仅仅需要发布那一个更改了的包。不依赖于那个更改了的包的其他包则不需要重新验证或重新发布。
CCP鼓励我们把可能由于同样的原因而改变的所有类共同聚集在同一个地方。如果两个类之间有非常紧密的绑定关系,不管是物理上的还是概念上的,那么它们总会一同进行变化,因而它们应该属于同一个包中,这样做会减少软件的发布、重新验证、重新发行的工作量。
这个原则和开放-封闭原则(OCP)密切相关。本原则中“封闭”这个词和OCP中的具有同样的含义。OCP规定了类对于修改应该是封闭的,对于扩展应该是开放的。但是,正如我们所学到的,100%的封闭是不可能的。应当进行有策略的封闭。我们所设计的系统应该对于我们经历过的最常见的变化做到封闭。
CCP通过把对于一些确定的变化类型开放的类共同组织到同一个包中,从而增强了上述内容。因而,当需求中的一个变化到来时,那个变化就会很有可能被限制在最小数量的包中。
无环依赖原则
在包的依赖图中,不允许存在环。
稳定依赖原则
朝着稳定的方向进行依赖
稳定抽象原则
包的抽象程度应该和其稳定程度一致
该原则把包的稳定性和抽象性联系起来。它规定,一个稳定的包应该也是抽象的,这样它的稳定性就不会使其无法扩展。另一方面,它规定,一个不稳定的包应该是具体的,因为它的不稳定性使得其内部具体代码易于更改。
因此,如果一个包是稳定的,那么它应该也要包含一些抽象类,这样就可以对它进行扩展。可扩展的稳定包是灵活的,并且不会过分限制设计。
SAP和SDP结合在一起形成了针对包的DIP原则。这样说是准确的,因为SDP规定依赖应该朝着稳定的方向进行,而SAP则规定稳定性意味着抽象性。因此,依赖应该朝着抽象的方向进行。
然而,DIP是一个处理类的原则。类没有灰度(the shades of grey)的概念。一个类要么是抽象的,要么不是。SDP和SAP的结合是处理包的,并且允许一个包是部分抽象、部分稳定的。