你的位置:首页 > Java教程

[Java教程]面向对阿象设计原则


如何衡量软件设计质量1

首要的标准

  满足软件的功能需求

  满足软件功能需求的设计并不一定就是好的设计。

好的设计

  可读性:软件的设计文档是否轻易被其他程序员理解。可读性差的设计会给大型软件的开发和维护过程带来严重的危害。

  可复用性:软件系统的架构、类、组件等单元能否很容易被本项目的其它部分或者其它项目复用。

  可扩展性:软件面对需求变化时,功能或性能扩展的难易程度。

  可维护性:软件维护(主要是指软件错误的修改、遗漏功能的添加等)的难易程度。

上面四个标准太抽象了,无法考量

   内聚度

   耦合度


 

什么是内聚度

定义:表示一个应用程序的单个单元所负责的任务数量和多样性。内聚与单个类或者单个方法单元相关。

好的软件设计应该做到高内聚。

   理想状态下,一个代码单元应该负责一个内聚的任务,也就是说一个任务可以看作是一个逻辑单元。一个方法应该实现一个逻辑操作,一个类应该代表一种类型的实体。

   内聚原则背后的主要原因是重用。遵循该规则的另一个优点是,当一个应用程序的某些方面需要做出改变时,我们能够在相同单元中找到所有相关的部分。

如果一个系统单元只负责一件事情,就说明这个系统单元有很高的内聚度;如果一个系统单元负责了很多不相关的事情,则说明这个系统单元是内聚度很低。

内聚度的简单判断方法

  如果一个方法可以用简单的“动词+名词”的形式来命名,或者如果一个类可以用准确的名词来命名,那么这样的类或者方法就是内聚度较高的系统单元;反之,如果类或者方法的名字必须包含“和”、“或”等字样才能准确反映其功能特性的话,这些类或方法的内聚度就一定不高。


 

什么是耦合度

耦合度表示类之间关系的紧密程度。

耦合度决定了变更一个应用程序的容易程度。在紧密耦合的类结构中,更改一个类会导致其它的类也随之需要做出修改。显然,这是我们在类设计时应该避免的,因为微小的修改会迅速波动影响到整个应用程序。此外,找到需要修改的所有的地方是必须的,实际上就使得修改变得困难并且耗费时间。而在松散耦合的系统中,我们可以更改一个类,不需要修改其它类,而应用程序仍然能够正常工作。


 

设计原则

“高内聚、低耦合”是所有优秀软件的共同特征。

如何做到? 在设计时遵循一定的设计原则。


 

一)单一职责

Single Responsibility Principle,SRP

定义:所有的对象都应该有单一的职责,它提供的所有的服务也都仅围绕着这个职责。

换句话说就是:一个类而言,应该仅有一个引起它变化的原因,永远不要让一个类存在多个改变的理由。

 

单一职责的理解

   要理解单一职责原则,首先我们要理解什么是类的职责。类的职责是由该类的对象在系统中的角色所决定的。举例来讲,教学管理系统中,老师就代表着一种角色,这个角色决定老师的职责就是教学。而要完成教学的职责,老师需要讲课、批改作业,而讲课、批改作业的行为就相当于我们在程序中类的方法,类的方法和属性就是为了完成这个职责而设置的。

   类的单一职责是说一个类应该只做一件事情。如果类中某个方法或属性与它所要完成的职责无关,或是为了完成另外的职责,那么这样的设计就不符合类的单一职责原则。而这样的设计的缺点是降低了类的内聚性,增强了类的耦合性。由此带来的问题是当我们使用这个类时,会把原本不需要的功能也带到了代码中,从而造成冗余代码或代码的浪费。

 

单一职责原则的思考

 单一职责原则提出了对对象职责的一种理想期望。对象不应该承担太多职责,正如人不应该一心分为二用。唯有专注,才能保证对象的高内聚;唯有单一,才能保证对象的细粒度。对象的高内聚与细粒度有利于对象的重用。一个庞大的对象承担了太多的职责,当客户端需要该对象的某一个职责时,就不得不将所有的职责都包含进来,从而造成冗余代码或代码的浪费。

 单一职责原则还有利于对象的稳定。对象的职责总是要提供给其他对象调用,从而形成对象与对象的协作,由此产生对象之间的依赖关系。对象的职责越少,则对象之间的依赖关系就越少,耦合度减弱,受其他对象的约束与牵制就越少,从而保证了系统的可扩展性。

 单一职责原则并不是极端地要求我们只能为对象定义一个职责,而是利用极端的表述方式重点强调:在定义对象职责时,必须考虑职责与对象之间的所属关系。职责必须恰如其分地表现对象的行为,而不至于破坏和谐与平衡的美感,甚至格格不入。换言之,该原则描述的单一职责指的是公开在外的与该对象紧密相关的一组职责。


 

二)开闭原则

 开闭原则(Open-Close Principle,简称OCP)是指一个软件实体(类、模块、方法等)应该对扩展开放,对修改关闭。

 遵循开闭原则设计出来的模块具有两个基本特征:

    对于扩展是开放的(Open for extension):模块的行为可以扩展,当应用的需求改变时,可以对模块进行扩展,以满足新的需求。

    对于更改是封闭的(Closed for modification):对模块行为扩展时,不必改动模块的源代码或二进制代码。

看起来相互矛盾

 

如何实现开闭原则?

关键在于抽象化

到底该抽象化什么?或者到底该将什么东西抽象为抽象类或者接口?

抽象化分为两种情况:

    针对多个领域类的抽象化

    针对单个领域类的抽象化

 

多个领域类的抽象化

一组对象的共同行为抽象到抽象类或者接口中,而将不同行为的实现封装在子类或者实现类中。接口或抽象类是不能实例化的,因此对修改就是关闭的;而添加新功能只要实现接口或者继承抽象类,从而实现对扩展开放。

   使用抽象类。在设计类时,对于拥有共同功能的相似类进行抽象化处理,将公用的功能部分放到抽象类中,而将不同的行为封装在子类中。这样,在需要对系统进行功能扩展时,只需要依据抽象类实现新的子类即可。在扩展子类时,不仅可以拥有抽象类的共有属性和共有方法,还可以拥有自定义的属性和方法。

   使用接口。与抽象类不同,接口只定义实现类应该实现的接口方法,而不实现公有的功能。在现在大多数的软件开发中,都会为实现类定义接口,这样在扩展子类时必须实现该接口。如果要改换原有的实现,只需要改换一个实现类即可。

 

单个领域类

将单个领域类中可能会发生变化的行为进行封装,也就是找出类中可能需要变化之处,把它们封装成抽象类或者接口,从而将变化点与不需要变化的代码分离。

如果每次新的需求一来,都会使一个领域类的某个行为的代码发生变化,那么我们就可以确定,这部分的代码需要被抽象出来,和其它稳定的代码有所区分。把会变化的部分取出并封装出抽象类或接口,以便以后可以轻易地改动或扩充此部分,而不会影响不需要变化的其它部分。封装变化点的好处在于,将类中经常变化的部分和稳定的部分隔离,有助于增加复用性,并降低系统耦合度。


 

开闭原则是核心

开闭原则是面向对象设计的核心所在。遵循这个原则可以带来灵活性、可重用性和可维护性。其它设计原则(里氏替换原则、依赖倒转原则、组合/聚合复用原则、迪米特法则、接口隔离原则)是实现开闭原则的手段和工具。


 

三)里氏替换原则

里氏替换原则(The Liskov Substitution Principle,LSP)的定义:在一个软件系统中,子类应该能够完全替换任何父类能够出现的地方,并且经过替换后,不会让调用父类的客户程序从行为上有任何改变。

里氏替换原则的意义

里氏替换原则是使代码符合开闭原则的一个重要的保证,同时,它体现了:

   类的继承原则:里氏替换原则常用来检查两个类是否为继承关系。在符合里氏替换原则的继承关系中,使用父类代码的地方,用子类代码替换后,能够正确的执行动作处理。换句话说,如果子类替换了父类后,不能够正确执行动作,那么他们的继承关系就是不正确的,应该重新设计它们之间的关系。

   动作正确性保证:里氏替换原则对子类进行了约束,所以在为已存在的类进行扩展,来创建一个新的子类时,符合里氏替换原则的扩展不会给已有的系统引入新的错误。

里氏代换原则给我们的启示 

类的继承原则:如果一个继承类的对象可能会在基类出现的地方出现运行错误,则该子类不应该从该基类继承,或者说,应该重新设计它们之间的关系。

动作正确性保证:符合里氏代换原则的类扩展不会给已有的系统引入新的错误。


 

四)依赖倒转原则 

依赖倒转原则(Dependency Inversion Principle,简称DIP)是指将两个模块之间的依赖关系倒置为依赖抽象类或接口。具体有两层含义:

    高层模块不应该依赖于低层模块,二者都应该依赖于抽象;

    抽象不应该依赖于细节,细节(子类,实现类)应该依赖于抽象。

依赖倒转原则给我们的启示

要针对接口编程,不要针对实现编程。(Program to an interface. not an implementation)


 

五)组合/聚合复用原则

组合/聚合复用原则(Composite/Aggregation Reuse Principle,CARP)是指要尽量使用组合/聚合而非继承来达到复用目的。另一种解释是在一个新的对象中使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象委托功能达到复用这些对象的目的。

在面向对象的设计中,有两种方法可以实现对已有对象重用的目的,即通过组合/聚合,或者通过继承。那么,这两种不同的复用方式在可维护性方面有什么区别呢?

  1)组合/聚合复用

  2)继承复用

组合/聚合复用的好处:

   1)新对象存取成分对象的唯一方法是通过成分对象的接口。

   2)这种对象的复用是黑箱复用,因为成分对象的内部实现细节对于新的对象是看不见的。

   3)这种复用所用的依赖更少。

   4)新对象可以在运行的时候动态的引用于成分对象类型相同的对象。

继承复用的优点:

    1)新的实现较为容易,因为超类的大部分功能可以通过继承关系自动进入子类。

    2)修改或者扩展继承而来的实现比较容易。

继承复用的缺点:

    1)继承复用破坏包装。将超类的实现细节暴露给子类。超类的内部细节常常对子类是透明的,白箱复用。

    2)超类的实现发生了改变,子类的实现也不得不改变。

    3)超类继承而来的是静态的,不可能在运行时间内发生改变。因此没有足够的灵活性。


 

六)接口隔离原则 

接口隔离原则(Interface Segregation Principle,简称ISP)是指客户不应该依赖它们用不到的方法,只给每个客户它所需要的接口。换句话说,就是不能强迫用户去依赖那些他们不使用的接口。

接口隔离原则两层意思(1)

接口的设计原则:接口的设计应该遵循最小接口原则,不要把用户不使用的方法塞进同一个接口里。如果一个接口的方法没有被使用到,则说明该接口过胖,应该将其分割成几个功能专一的接口,使用多个专门的接口比使用单一的总接口要好。

示例:手机

接口隔离原则两层意思(2)

接口的继承原则:如果一个接口A继承另一个接口B,则接口A相当于继承了接口B的方法,那么继承了接口B后的接口A也应该遵循上述原则:不应该包含用户不使用的方法。反之,则说明接口A被B给污染了,应该重新设计它们的关系。

示例:门


 

七)迪米特法则(解耦) 

迪米特法则(Law of Demeter,简称LOD),又称为“最少知识原则”,它的定义为:一个软件实体应当尽可能少的与其他实体发生相互作用。

妈妈说:不要与陌生人说话!

示例:GUI的设计