java面向对象设计的六大原则

开闭原则是设计原则基础的基础,其它原则均围绕开闭原则进行展开。开闭原则也就是一个软件实体应当对扩展开放,但对修改关闭。满足了开闭原则的设计,我们的系统将达到在设计稳定的基础上,方便的对软件进行扩展,插入新的功能模块的目的。
    怎么样做的开闭原则呢?抽象化是关键,也是我们经常听到的“面象接口编程”,具体一点就是声明的变量的类型、函数的参数类型、函数的返回类型等要尽量使用抽象类和接口。开闭原则实际也是“对可变性的封装原则”,那就是“考虑你的设计中什么可能发生变化,找到一个系统的可变因素,将它封装起来”。把变化封闭起来也就是我们经常用到的继承,编写一个抽象类用子类来继承他,或者编写一个接口让子类去实现他。继承固然很好,既封装了变化又实现了复用,但却不能滥用,也就是说应当把继承仅仅当做封装变化的方法,而不应当被认为是从一般的对象生成特殊的对象。就如我们为了复用一个类里面的函数,便用另一个毫不相干的类来继承此类,这样的继承真可谓不伦不类。更糟糕一些那便是为复用而复用,编写一个抽象类去继承一个毫不相干的另一个抽象类(这样做可能是想复用此类中封装的变化),这样便把多于一种的可变性混合在了一起,混合的结果便是最终让我们思维混乱。
    开头我们说开闭原则是设计原则基础的基础,因为其它的设计原则可以作为实现开闭原则的手段和工具。
    1、里氏代换原则:任何基类可以出现的地方,子类一定可以出现。
    我们知道,实现开闭原则的关键是抽象化,而里氏代换原则中的基类和子类的继承关系正是抽象化的具体体现,所以里氏代换原则是对实现抽象化的具体步骤的规范。违反里氏代换原则一个最经典的例子便是把正方形设计成长方形的子类。
    2、依赖倒转原则:要依赖于抽象,不要依赖于实现。说的白一点就是要依赖于抽象类和接口不要依赖具体类,具体类也就是我们可以用new关键字实例化的类。依赖倒转原则是实现开闭原则的一个手段。其实说白了就是我们通常所说的面向接口编程。
    "开-闭"原则是我们OOD的目标,达到这一目标的主要机制就是"依赖倒转原则".这个原则的内容是:要依赖于抽象,不要依赖于具体.
    对于抽象层次来说,它是一个系统的本质的概括,是系统的商务逻辑和宏观的,战略性的决定,是必然性的体现;具体的层次则是与实现有关的算法和逻辑,一些战术性的决定,带有相当大的偶然性.传统的过程性系统设计办法倾向于使高层次的模块依赖于低层次的模块;抽象层次依赖于具体层次.这实际上就是微观决定宏观,战术决定战略,偶然决定必然.依赖倒转原则就是要把这种错误的依赖关系倒转过来.
    许多的建构设计模型,例如COM,CORBA,JavaBean,EJB等,它们背后的基本原则就是DIP.
    对于软件设计的两个目标,复用和可维护性来说,传统的设计侧重于具体层次模块的复用和可维护,比如算法,数据结构,函数库等等.但是,对系统的抽象是比较稳定的,它的复用是很重要的,同时,抽象层次的可维护性也应当是一个重点.就是说DIP也导致复用和可维护性的"倒转".
    我们现在来看看依赖有几种,依赖也就是耦合,分为下面三种
    1.零耦合(Nil Coupling)关系,两个类没有依赖关系,那就是零耦合.
    2.具体耦合(Concrete Coupling)关系,两个具体的类之间有依赖关系,那么就是具体耦合关系,如果一个具   体类直接引用另外一个具体类,就会发生这种关系.
    3.抽象耦合(Abstract Coupling)关系.这种关系发生在一个具体类和一个抽象类之间,这样就使必须发生关系的类之间保持最大的灵活性.
    DIP要求客户端依赖于抽象耦合,抽象不应当依赖于细节,细节应当依赖于抽象(Abstractions should not depend upon details. Details should depend upon abstractions),这个原则的另外一个表述就是"四人团"强调的那个:要针对接口编程,不要对实现编程.(Program to an interface, not an implementation),程序在需要引用一个对象时,应当尽可能的使用抽象类型作为变量的静态类型,这就是针对接口编程的含义. DIP是达到"开-闭"原则的途径.
    要做到DIP,用抽象方式耦合是关键.由于一个抽象耦合总要涉及具体类从抽象类继承.并且需要保证在任何引用到某类的地方都可以改换成其子类,因此,LSP是DIP的基础.DIP是OOD的核心原则,设计模式的研究和应用都是用它作为指导原则的.DIP虽然强大,但是也很难实现.另外,DIP是假定所有的具体类都会变化,这也不是全对,有些具体类就相当稳定.使用这个类的客户端就完全可以依赖这个具体类而不用再弄一个抽象类.
    3、合成/聚合复用原则:要尽量使用合成/聚合,而不是继承关系达到复用的目的。就如我们前面说的,如果为了复用,便使用继承的方式将两个不相干的类联系在一起,这样的方式是违反合成/聚合复用原则的,更进一步的后果那便是违反里氏代换原则。合成/聚合复用和里氏代换原则相辅相成,合成/聚合复用原则要求我们在复用时首先考虑合成/聚合关系,而里氏代换原则是要求我们在使用继承时,必须满足一定的条件。
    合成(Composition)和聚合(Aggregation)都是关联(Association)的特殊种类。聚合表示整体和部分的关系,表示 “拥有 ”;合成则是一种更强的“拥有”,部分和整体的生命周期一样。合成的新的对象完全支配其组成部分,包括它们的创建和湮灭等。一个合成关系的成分对象是不能与另一个合成关系共享的。
      换句话说,合成是值的聚合(Aggregation by Value),而一般说的聚合是引用的聚合(Aggregation by Reference)。
      简短的说,合成-聚合复用原则(CARP)是指,尽量使用合成/聚合,而不是使用继承。
      在OOD中,有两种基本的办法可以实现复用,一种是通过合成/聚合,另外一种就是通过继承。通过合成/聚合的好处是:

    * 新对象存取成分对象的唯一方法是通过成分对象的接口。
    * 这种复用是黑箱复用,因为成分对象的内部细节是新对象所看不见的。
    * 这种复用支持包装。
    * 这种复用所需的依赖较少。
    * 每一个新的类可以将焦点集中在一个任务上。
    * 这种复用可以在运行时间内动态进行,新对象可以动态的引用与成分对象类型相同的对象。
    * 作为复用手段可以应用到几乎任何环境中去。

      它的缺点就是系统中会有较多的对象需要管理。

      通过继承来进行复用的优点是:

    * 新的实现较为容易,因为超类的大部分功能可以通过继承的关系自动进入子类。
    * 修改和扩展继承而来的实现较为容易。

      缺点是:

    * 继承复用破坏包装,因为继承将超类的实现细节暴露给子类。由于超类的内部细节常常是对于子类透明的,所以这种复用是透明的复用,又称“白箱”复用。
    * 如果超类发生改变,那么子类的实现也不得不发生改变。
    * 从超类继承而来的实现是静态的,不可能在运行时间内发生改变,没有足够的灵活性。
    * 继承只能在有限的环境中使用。

      要正确的选择合成/复用和继承,必须透彻的理解里氏代换原则和Coad法则。里氏代换原则前面学习过,Coad法则由Peter Coad提出,总结了一些什么时候使用继承作为复用工具的条件。只有当以下的Coad条件全部被满足时,才应当使用继承关系:

    * 子类是超类的一个特殊种类,而不是超类的一个角色,也就是区分“Has-A”和“Is-A”。只有“Is-A”关系才符合继承关系,“Has-A”关系应当用聚合来描述。
    * 永远不会出现需要将子类换成另外一个类的子类的情况。如果不能肯定将来是否会变成另外一个子类的话,就不要使用继承。
    * 子类具有扩展超类的责任,而不是具有置换调(override)或注销掉(Nullify)超类的责任。如果一个子类需要大量的置换掉超类的行为,那么这个类就不应该是这个超类的子类。
    * 只有在分类学角度上有意义时,才可以使用继承。不要从工具类继承。

      错误的使用继承而不是合成/聚合的一个常见原因是错误的把“Has-A”当成了“Is-A”。“Is-A”代表一个类是另外一个类的一种;“Has-A”代表一个类是另外一个类的一个角色,而不是另外一个类的特殊种类。
      我们看一个例子。如果我们把“人”当成一个类,然后把“雇员”,“经理”,“学生”当成是“人”的子类。这个的错误在于把“角色”的等级结构和“人”的等级结构混淆了。“经理”,“雇员”,“学生”是一个人的角色,一个人可以同时拥有上述角色。如果按继承来设计,那么如果一个人是雇员的话,就不可能是经理,也不可能是学生,这显然不合理。正确的设计是有个抽象类“角色”,“人”可以拥有多个“角色”(聚合),“雇员”,“经理”,“学生”是“角色”的子类。
      另外一个就是只有两个类满足里氏代换原则的时候,才可能是“Is-A”关系。也就是说,如果两个类是“Has-A”关系,但是设计成了继承,那么肯定违反里氏代换原则。

    4、接口隔离原则:应当为客户端提供尽可能小的单独接口,而不要提供大的总接口。
        一个类对另一个类的依赖应该表现成依赖尽可能小的接口。

        这个原则是用来处理胖接口的缺陷,避免接口承担太多的责任。比如说一个接口内的方法可以被分成好几组,分别为不同的客户程序服务,说明这个接口太胖了。当然,确实也有一些类不需要内聚的接口,但这些类不应该做为单独的类被客户程序直接看到,而应该通过抽象基类或接口来关联访问。

        接口污染
        所谓接口污染就是为接口添加了不必要的职责。在接口中加一个新方法只是为了给实现类带来好处,以减少类的数目。持续这样做,接口就被不断污染,变胖。实际上,类的数目根本不是什么问题,接口污染会带来维护和重用方面的问题。最常见的问题是我们为了重用被污染的接口,被迫实现并维护不必要的方法。

        分离客户程序就是分离接口。如果客户程序是分离的,那么相应的接口也应该是分离的,因为客户程序对它们使用的接口有反作用力。通常接口发生了变化,我们就要考虑所有使用接口的客户程序该如何变化以适应接口的变化。如果客户程序发生了变化呢?这时也要考虑接口是否需要发生变化,这就是反作用力。有时业务规则的变化不是那么直接的,而是通过客户程序的变化引发的,这时我们就需要改变接口以满足客户程序的需要。

      分离接口的方式一般分为两种,委托和多继承。前者把请求委托给别的接口的实现类来完成需要的职责,后者则是通过实现多个接口来完成需要的职责。两种方式各有优缺点,通常我们应该先考虑后一个方案,如果涉及到类型转换时则选择前一个方案。

      胖接口会导致客户程序之间产生不必要的耦合关系,牵一发而动全身。分解胖接口,使客户程序只依赖它需要的方法,从设计上讲,简单易维护,重用度也高。
    5、迪米特法则:一个软件实体应当尽可能少的与其他实体发生相互作用。
        迪米特法则最初是用来作为面向对象的系统设计风格的一种法则,于1987年秋天由lanholland在美国东北大学为一个叫做迪米特的项目设计提出的。被UML的创始者之一Booch等普及。后来,因为在经典著作《 The PragmaticProgrammer》阐述而广为人知。
        狭义的迪米特法则是指:如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一类的某一个方法的话,可以通过第三者转发这个调用。
        广义的迪米特法则是指:一个模块设计的好坏的一个重要标志就是该模块在多大程度上讲自己的内部数据与实现的有关细节隐藏起来。
        一个软件实体应当尽可能少的与其他实体发生相互作用。
        每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
        迪米特法则的目的在于降低类与类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,是的相互间存在尽可能少的依赖关系。
      在运用迪米特法则到系统的设计中时,要注意以下几点:
      第一:在类的划分上,应当创建弱耦合的类,类与类之间的耦合越弱,就越有利于实现可复用的目标。
      第二:在类的结构设计上,每个类都应该降低成员的访问权限。
      第三:在类的设计上,只要有可能,一个类应当设计成不变的类。
      第四:在对其他类的应用上,一个对象对其他类的对象的应用应该降到最低。
      第五:尽量限制局部变量的有效范围。
      但是过度使用迪米特法则,也会造成系统的不同模块之间的通信效率降低,使系统的不同模块之间不容易协调等缺点。同时,因为迪米特法则要求类与类之间尽量不直接通信,如果类之间需要通信就通过第三方转发的方式,这就直接导致了系统中存在大量的中介类,这些类存在的唯一原因是为了传递类与类之间的相互调用关系,这就毫无疑问的增加了系统的复杂度。解决这个问题的方式是:使用依赖倒转原则(通俗的讲就是要针对接口编程,不要针对具体编程),这要就可以是调用方和被调用方之间有了一个抽象层,被调用方在遵循抽象层的前提下就可以自由的变化,此时抽象层成了调用方的朋友。
      迪米特法则是一种面向对象系统设计风格的一种法则,尤其适合做大型复杂系统设计指导原则。但是也会造成系统的不同模块之间的通信效率降低,使系统的不同模块之间不容易协调等缺点。同时,因为迪米特法则要求类与类之间尽量不直接通信,如果类之间需要通信就通过第三方转发的方式,这就直接导致了系统中存在大量的中介类,这些类存在的唯一原因是为了传递类与类之间的相互调用关系,这就毫无疑问的增加了系统的复杂度。解决这个问题的方式是:使用依赖倒转原则(通俗的讲就是要针对接口编程,不要针对具体编程),这要就可以是调用方和被调用方之间有了一个抽象层,被调用方在遵循抽象层的前提下就可以自由的变化,此时抽象层成了调用方的朋友。
    一个和其它实体关系很少的实体,在系统扩展时受的影响就会很小。当然接口隔离原则也是同样的道理,小的单独的接口,在系统扩展时受到的影响也会很小,所不同的是,迪米特法则(广义迪米特)限制的是相互作用的深度和广度,而接口隔离原则限制的只是相互作用的广度
    在网上看到“板桥里人”的一篇文章《你还在用ifelse吗?》他在最后总结里说:“将ifelse用在小地方还可以,如简单的数值判断。但是如果按照你的传统习惯思维,在实现业务功能时也使用ifelse,那么说明你的思维可能需要重塑,你的编程经验越丰富,传统过程思维模式就容易根深蒂固,想靠自己改变很困难;建议接受专业头脑风暴训练。”此文通篇是如何用设计模式来替换ifelse语句的说词,不知道他这种极端的为了设计模式而设计模式的做法,是不是被他所谓的“头脑风暴”给刮疯癫所导致的。我也说一下我极端的想法,如果一个极端到没有任何可扩展性要求的系统,用面向对象的语言开发面向过程的程序,并无不可,面向对象里面的类只不过是我们分类存放函数的一个容器,其他别无用处,如果这时你还要写一大堆的接口和抽象类来显摆你所了解的设计模式,这简直就是强奸了设计模式,因为设计模式是为方便的扩展系统而生的,并不是让你拿来显摆的。关于各种对“将条件转移语句改写成为多态性”的代码重构做法的看法,我更倾向阎宏的的看法。
    上而讲的“将条件转移语句改写成为多态性”的代码重构的做法有两大缺点。
    第一大缺点,任何语言都提供条件转移功能,如果抛弃语言本身的一些功能,那么语言存在的价值又能体现在哪里呢?难道一个面向对象的语言,面向对象的特性就是这个语言的全部吗?条件转移本身并不是错误的,也不是什么罪恶,他更不是某些人眼里面向过程语言向面向对象语言过渡时一个权宜的保留。如果需要,设计师完全可以选择使用条件转移ifelse,并且不需要去接受什么头脑风暴的摧残。
    第二大缺点,使用多态性代替条件转移意味着大量的类被创建出来。比如一个类如果有三个方法,每个方法都有一个三段的条件转移语句,如果将它们都用多态性代替的话,就会造出九个不同的类。很难想象设计师怎么能明白这九种组合成员之间的关系。
    那么何时需要用多态性取代条件转移语句呢?
    应当从“开闭”原则出发来做判断。如果一个条件转移语句确实封装了某种业务逻辑的可变性,那么将此种可变性封装起来就符合“开闭”原则的设计思想。但是,如果一个条件转移语句没有涉及重要的业务逻辑,或者不会随着时间的变化而变化,也不意味着任何的可扩展性,那么它就没有涉及任何有意义的可变性。这时候将这个条件转移语句改写成为多态性就是一种没有意义的浪费。阎宏将这种对多态性的滥用叫做“多态性污染”。如果再滥用一些设计模式,估计就应该叫“设计模式污染”了。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值