面向对象之设计原则简记

面向对象的三大特性:

封装:隐藏内部实现,对外暴露接口,侧重于对象的描述;

继承:复用模块或代码;

多态:一个对象具有多种状态行为,改变对象的状态;

三大特性之间的关系,继承依赖封装,多态依赖继承,即 封装<继承<多态;继承和多态侧重于对象之间的关系。


我们为什么要使用面向对象?怎样使用三大特性实现好的“面向对象”?应该遵循怎样的规则实现面向对象?

隔离变化:宏观,能更适应软件的变化,将变化所带来的影响化为最小;

各司其职:微观,强调各个类的责任,新增的类不会影响原来的类的改变;如:内存坏了,主板、CPU等不用更换;

耦合关系决定着软件面对变化时的行为:
 模块间的紧耦合,模块间关系紧密,依赖程度高,一个变化,其它的也跟随变化;
 模块间的松耦合,模块间关系松散,一个变化,其它不变,容易更改或替换某一模块而其它的不作改变;模块或系统间使用接口来联系;如:内存坏了,主板、CPU等不用更换。

针对接口编程,不是针对实现编程,方法返回类型的接口,不需要返回某个具体的类型对象,只需知道对象拥用所期望的接口即可。

类继承:称为“白箱复用”,对象组合:称为“黑箱复用”;继承关系使得父子类的耦合度高,破坏了一定的封装性,而对象被组合的对象具有良好定义的接口,满足接口即可,耦合度低。

设计模式是“封装变化”方法的最佳阐释,无论创建型模式、结构型模式还是行为模式,最终的目的是寻找软件中存在的变化,然后利用抽象的方式对变化进行封装,由于抽象没有具体实现,就代表着无限可能,使扩展成了可能;封装变化最重要的是发现或寻找变化。


单一职责原则(Single Responsibility Principle,SRP):重要性:4星。

定义或描述:

类的职责要单一,不能将太多的职责放在一个类中;对应的职责完整的封装在一个类中, 简单理解,其核心思想是:一个类仅有一个引起它变化的原因,一个类最好只做一件事。

举例分析:

T类负责两个不同的职责:P1和P2职责,由于职责P1需求发生变化而需要修改类T时,可能会导致原本正常运行的职责P2功能发生故障。

一个类(大到模块,小到方法)承担的职责越多,复用的可能性就越小,多职责耦合到一起,其中一个发生变化,可能会影响其它职责的运行。

类的职责包括:数据职责和行为职责,数据职责使用属性来体现,行为职责使用方法来体现。

作用:

单一职责原则是实现高内聚、低耦合的指导方针,是最简单但又最难运用的原则,需要设计人员发现类的不同职责并加以分离,需要设计人员具有较强的分析能力和重构经验。
遵循单一职责原则时,上述的P1和P2会分离成两个类,P1或P2需求发生变化时,两者的修改相互之间没有影响。

将职责定义为引起变化的原因,以提高内聚性减少引起变化的原因,职责过多,可能引起变化的原因就越多,这就会导致职责依赖,相互之间产生影响,从而极大的损伤其内聚性和耦合性;

单一职责通常意味着单一的功能,因此不要为类实现过多的功能点,以保证实体只有一个引起化变化的原因。

类的复杂性降低,可读性、可维护性提高,变更引起的风险降低,易维护、易扩展、易复用且灵活。


开闭原则(Open-Closed Principle,OCP):重要性:5星。

定义或描述:

软件实体对扩展是开放的,但对修改是关闭的,即在不修改一个软件实体的基础上去扩展其功能。

不允许更改系统的抽象层,只能更改系统的实现层;一个软件实体应对扩展开放,对修改关闭;在设计一个模块时,应当使这个模块可以在不修改源代码的前提下被扩展,改变这个模块的行为。软件实体完成后,后期所有新增加的功能不应在已有的代码基础上进行修改。

软件实体是一个软件、一个模块或一个类、或多个类组成的局部结构;

抽象化是开闭原则的关键,开闭原则是对“对可变性封装原则”的进一步描述。对可变性封装原则是要求找到系统的可变因素并封装起来。

作用:

可以在不修改一个软件实体的基础上去扩展其功能。开闭原则是面向对象的核心所在,遵循这个原则可以带来面向对象技术所声称的巨大好处:可维护、可扩展、可复用,灵活性好,应仅对程序中出现频繁变化的部分进行抽象。

注意:过度(程序每一部分或不需要抽象的部分)抽象不是一个好主意,拒绝不成熟的抽象和抽象一样重要,尽量抽象并非好事。应在工作开发不久就知道可能发生的变化,查明可能发生变化所等待的时间越长,抽象就越困难。


依赖倒置原则(Dependency Inversion Principle):重要性:5星

定义或描述:

针对抽象编程,而不是针对具体类实现编程;抽象(接口和抽象类)不依赖于细节(实现),细节依赖于抽象。高层模块不应依赖于低层模块,它们都应该赖于抽象。

举例分析:

如:类A依赖于类B,现要将类A改为依赖于类C,则必须修改类A的代码来实现,这种场景下,类A是高层模块,负责复杂的业务逻辑操作;类B和类C是低层模块,负责基本的原子操作;修改类A会为程序带来风险。但将类A改为依赖接口,类B和类C各自实现接口,类A通过接口与类B或类C发生联系,这种方式会大大降低修改类A的几率。

依赖倒置的中心思想是面向接口编程。

作用:

依赖倒置原则基于这样的一个事实:相对细节的多变性,抽象东西要稳定的多,以抽象为基础搭建的架构比以细节为基础搭建的构架要稳定的多。只要抽象(接口和抽象类)稳定,那么任何一个更改都不用担心其它受到影响,高层模块与低层模块都可以容易复用;实现开闭原则的关键是抽象,并且从抽象化导出具体化实现,开闭原则是面向对象设计的目标,依赖倒置是面向对象设计的手段。

为什么依赖了抽象的接口或抽象类,就不怕更改了呢?这是里氏替换原则解决的问题。

类之间的耦合关系:零耦合、具体(实现)耦合和抽象耦合;依赖倒置的原则要求客户端依赖于抽象耦合,以抽象方式耦合是依赖倒置原则的关键。


里氏替换原则(Liskov Substitution Principle,LSP):重要性:4星

定义或描述:

在软件系统中,一个可以接受基类(父类)对象的地方必然可以接受子类对象;可以通俗表述为:在软件中如果能够使用基类对象,那么就一定能使用其子类对象。把基类对象都替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么不一定能使用基类来代替。

举例分析:

如:有两个类,一个类为基类BaseClass,一个是SubClass类,是BaseClass的子类,那么一个方法如果可以接受一个BaseClass类型的基类对象,那么它必然可以接受一个BaseClass的子类SubClass类型的对象,而且方法同样能够运行,反过来是不成立的,如果一个方法接受SubClass类型的对象,BaseClass类型的对象传给方法是不合法的,除非有其它重载方法实现。

作用:

里氏替换原则是实现开闭原则的重要方式之一,因此程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类替换父类对象。当满足继承的时候,父类肯定存在非私有成员,子类肯定也得到了父类的这些非私有成员,那么父类对象也就可以在子类对象中调用这些非私有成员,所以,子类对象可以替换父类对象的位置。

只有当子类可以替换父类,软件单位的功能不受影响时,父类才能真正的被复用,而子类也能够在父类的基础上增加新的行为。

由于有了里氏替换原则,才使得开闭成为了可能,由于子类型替换性才使得使用父类型的模块在无需修改的情况下就可以扩展;否则就不能扩展开放,修改关闭;而依赖倒置原则,高层模块不依赖于低层模块,两者都应该依赖抽象;依赖倒置其实就是谁也不依赖谁,除了约定接口,可以灵活自如。

如果编程时考虑的都是如何抽象编程而不是针对细节编程,即程序中所有的依赖关系都是抽象于抽象类或接口,那是面向对象的设计,反之是面向过程设计。所以纯C语言是面向过程开发,不灵活,维护成本过高。直接面向对象的设计和编程是程序开发效率的重要保障。

继承与里氏替换原则的问题

继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了一些弊端,它增加了对象间的耦合性。因此在系统设计时,遵循里氏替换原则,尽量避免子类重写父类的方法,可以有效降低码出错的可能性。

class A
{
   public virtual void Inc(int a,int b)
   {
     return a+b;
   }
}

class B:A
{
   public override void Inc(int a,int b)
   {
     return a-b;   //违反了里氏替换原则,直接重写了父类的虚方法Inc,引用父类的地方并不能透明的使用子类对象。
   }
}


接口隔离原则(Interface Segregation Principle,ISP):重要性:2星

定义或描述:

使用多个专门的单一功能接口来取代一个统一(多个功能组合)的接口,客户端不应该依赖它不需要的接口;换言之,一个类对另一个类的依赖性应当是建立在最小接口上的,过于臃肿的接口会对接口产生污染,不应该强迫客户端依赖于它们不需要的方法。一旦一个接口太大,则需要将它分割成一些更细小的接口,使用该接口的客户端仅需要知道与之相关的方法即可。

作用:

接口隔离原则是指使用多个专门的接口(抽象类也是接口),而不是使用单一的总接口(多个功能方法)。每个接口承担一种相对独立的角色,不多不少,不干不该干的事,该干的事都要干。

一个接口就只代表一个角色,每个角色都有它特定的一个接口,此时这个原则叫“角色隔离原则”。

接口仅仅提供客户端需要的行为,即所需的方法,客户端不需要的行为则隐藏起来,应当为客户端提供尽可能小的单独的接口,而不要提供大接口。

采用接口隔离原则对接口进行约束时,要注意以下几点:

接口尽量小,但要有限度;对接口进行细化可以提高程序设计的灵活性是肯定的,但是如果接口过小,则会造成接口数量过多,使设计复杂化,所以要适度。

为依赖接口的类定制服务,只暴露给调用的类需要的方法,它不需要的方法则隐藏起来,只有专注的为一个模块提供定制服务,才能建立最小的依赖关系;提高内聚性,减少对外交互,使接口用最少的方法完成最多的事情。运用接口隔离原则,一定要适度,接口设计的过大或过小都不好;设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。

当客户端被强迫依赖那些它们不需要的接口时,则这些客户类不得不受制于这些接口,这无意间就导致了所有客户类之间耦合;换言之,如果一个客户类依赖了一个类,这个类包含了客户类不需要的接口,但这些接口是其它客户类所需要的,那么当其它客户类要求修改这个类时,这个修改也将影响这个客户端,通常我们都是在尽可能的避免这种耦合,所以我们需要竭尽全力地分离这些接口。

接口隔离原则的含义是:

建立单一接口,不要建立庞立臃肿的接口,尽量细化接口,接口中的方法尽量少,也就是说,我们要为各个类建立专用接口,而不要去建立一个很庞大的接口供所有依赖它的类去使用;在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活,接口是设计时对外部设定的“契约”,通过定义多个接口,可以预防外部变更的扩散,提高系统的灵活性和可维护性。

单一职原则和接口隔离原则的比较:

其一,单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离;
其二,单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象,针对程序整体框架的构建。
 


合成复用原则(Composite Reuse Principle,CRP):重要性:4星

定义或描述:

在系统中应尽可能多的使用对象组合(合成)和聚合关联关系,尽量少使用或不使用对象的继承(父子类耦合程度高)来达到复用目的;就是在一个新的对象里使用一些已有的对象,使之成为新对象的一部分,新对象通过向这些对象委派调用达到复用已有功能的目的。

对象的继承关系是在编译时就定义好了的,所以无法在运行时改变从父类继承的实现;子类的实现与它的父类的有非常紧密的依赖关系,以至于父类实现中任何变化必然导致子类发生变化;当你要复用子类时,如果继承下来的实现不适合解决新的问题,则父类必须重写或被其它更适合的类替换;这种依赖关系限制了灵活性并最终限制了复用性。

合成和聚合都是关联关系的两种类别,但都是关联的特殊类,聚合表示一种弱的“拥有关系”,体现的是A对象可以包含B对象,但B对象不是A对象的一部分。合成则是一种强的“拥用”关系,体现了严格的部分和整体的关系,部分和整体的生命周期一样。如:大雁和自己的翅膀是强拥有关系,一只大雁与一群大雁的关系是弱拥有关系。

盲目使用继承会影响程序的扩展与修改,本质原因是继承是一种强耦合的结构,父类变,子类也要跟着变;使用继承时要在“is-a”的关系时再考虑使用,而不是任何时候都去使用。

作用:

合成和聚合可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其它类造成的影响较少,因此一般首选使用合成聚合实现复用,其次才考虑继承,在使用继承时,需要严格遵循里氏替换原则,有效的使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。要避免在系统设计中出现一个类的继承层次超过3次。

合成聚合复用:称为“黑箱复用”,耦合度相对较低,选择性的调用成员对象操作;可以在运行时动态进行。

Has-A与Is-A

Has-A:某一角色具有某一项责任,是其它依赖类的一个组成部分,不可或缺,是合成关系;

Is-A:一个类是另一个类的其中一种,一个对象是一类该,是聚合关系;

继承、合成和聚合的区别

继承:父子类之间的关系;合成:是整体与部分的关系;聚合:集体与个体间的关系。

继承复用:称为“白箱复用”,实现简单,易于扩展,但破坏系统的封装性,从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性,只能在有限环境中使用。


迪米特法则(Law of Demeter,LoD):重要性:3星

定义或描述:

一个软件实体对其它软件实体的引用越少越好,或者两个类不需要直接通信,那么这两个类就不应当发生直接的相互作用,而是通过引入一个第三方者发生间接交互;也就是说,一个对象应当对其它对象有尽可能少的了解,只与你直接的朋友们通信,不要和陌生人说话,每一个软件单位对其它的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。

迪米特法则相关的设计模式:Facade(外观)模式和Mediator(中介)者模式;

强调的是在类的结构设计上,每个类都应当尽量降低成员的访问权限,即一个类包装好自己的private状态,不需要让别的类知道的字段或行为就不要公开,需要公开的字段通常用属性来体现;

面向对象的设计原则与面向对象的三大特性根本就不是矛盾的,迪米特法则其根本思想,是强调了在类之间的松耦合;程序设计时,类之间的耦合越弱,越利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成影响,即信息的隐藏促进了软件代码的复用。

一个对象的朋友有以下几种:
  当前对象本身;
  以参数形式传入到当前对象方法中的对象;
  当前对象的成员对象;
  如果当前对象的成员对象是一个集合,那么这个集合中元素也都是朋友;
  当前对象创建的对象;


任何一个对象,如果满足上面的条件之一,就是当前对象的朋友,否则就是“陌生人”

狭义迪米特法则:
 
  可以降低类之间的耦合,但是会在系统中增加大量的小方法,并散落在系统的各个角落,它可以使用一个系统的局部设计简化,因为每一个局部都不会和远距离的对象有直接的关联,但是也会造成系统的不同模块之间通信效率降低,使得系统的不同模块之间不容易协调,在该法则中,如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,如果其中一个类需要调用另一个类的某一方法的话,可以通过第三者转发这个调用。

广义迪米特法则:

  指对之间的信息流量、流向以及信息影响的控制,主要是对信息隐藏的控制,信息的隐藏可以使各个子系统之间脱耦,从而允许它们独立的被开发、优化、使用和修改,同时可以促进软件的复用,由于每一个模块都不依赖于其它模块而存在,困此每一个模块都可以独立的在其它地方使用,一个系统规模越大,信息的隐藏就越重要,而信息隐藏的重要性也就越明显。

作用:
迪米特法则主要用途于控制信息的过载,在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个好处在于松耦合中的类一旦被修改,不会对关联的类造成太大的波及,在类的结构设计上,每一个类都应当尽量降低其成员变量和成员方法的访问权限;在类设计上,只要有可能,一个类型应当设计不可变类;在对其它类的引用上,一个对象对其它对象的引用应当降到最低。

 

设计原则总结:用抽象构建框架,用实现扩展细节

   因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定,而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了,当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。

   单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一,迪米特法则告诉我们要降低耦合;而开闭原则是总纲,告诉我们要对扩展开放,对修改开闭。

  
设计原则本身是思想层面上进行指导,本身是调试概括和原则性,只是一个设计上的大体方向,其具体实现并不是只有设计模式这一种,理论上来说,可以在相同的原则之下,做出很多不同的实现来。
 
  每一种设计模式并不是单一的体现某一个设计原则,事实上,很多设计模式都是融合了很多个设计原则的思想,并不好特别强调设计模式是对某个或者是某些设计原则的体现;而且每个设计模式在应用的时候也会有很多的考量,不同使用场景下,突出体现的设计原则也可能是不一样的;这些原则只是设计指导,事实上在实际开发中,很少做到完全遵守,总是在有意无意的违反一些或者是部分设计原则。设计工作本来就是一个不断权衡的工作,有句话说的好:设计是一种危险的平衡艺术;设计原则只是一个指导,有些时候,还得综合考虑业务功能,实现的难度,系统性能,时间等很多方面的问题。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值