`
xwood
  • 浏览: 100342 次
  • 性别: Icon_minigender_1
  • 来自: 成都
社区版块
存档分类
最新评论

OO原则的一些想法

oo 
阅读更多
一、OO特性

1.抽象(Interface and AbstractClass)

        抽象其实就是一种建模,抽象过程就是一个认识事务关系的过程,而抽象的结果就是我们对于事务关系认识的结果,也就是我们对于事务关系的建模结果。而在建模过程中需要考虑到的东西,这个根据设计人员自身的经验和思维以及采用的建模方式不同而有一定的差异。以下是我个人在OO的抽象建模过程一般采用的思维方式:

   
引用
        1>.建立业务流程关系,深刻的认识业务流程,按一般的过程解决方案设置数据结构及相关的处理方法。
        2>.根据数据结构整理实体关系,建立初步的实体关系模型,并考虑相关处理方法的实现。
        3>.考虑变化,包括业务中已经提到的变化和将来可能产生的变化,根据变化重新整理实体关系。
        4>.建立抽象模型。在这个过程可以强制的考虑一下设计模式,提高自己的设计水平,当然,当设计能力到一定水平之后,模式早就在设计之中了。


2.继承(抽象,而非实现)

        继承一般说来是为了解决代码的复用,及实体的多态而存在的;另外,它也是抽象模型实现的关键,因为很多时候模型过于抽象,需要借助继承才能落地,也就是与现实接轨。

        首先来说一下继承解决的代码复用。如果我们有一个类叫做Vehicle,这是对所有车辆的一个统称,他包含车辆的所有基本属性和方法,如name, type, drive(), turnLeft(), turnRight()...而现在一个汽车公司新出产了一种机动车,叫EP11,他同样包含name, type属性,而且这部分是不变的,那么这个时候使用继承就可以解决这个问题。

        但是,EP11这个新车型在drive的时候与其他车车型不太一样,以前都是4个轮子由后面两个轮子驱动,现在是前后一起驱动,而且他还是采用太阳能的。所以这个drive方法就不能用了,那么现在我们就只能override Vehicle中的drive方法了。这其实也就是继承在解决类型多态时的表现。

        从这个问题可以看得出,drive这个方法对于Vehicle来说是没有多大意义的,除非所有车型都采用相同的驱动方式。而现实情况是,每一个车型几乎都会有自己的发动机型号,采用自己的驱动方式。所以drive这个方法应当被设计成abstract,而Vehicel这个类同样也应当是abstract类型的。当然,如果有一个方法,如stop对于所有的车型都是一样的,那么这个方法其实也就是Vehicle的方法,因为它代表了所有的车辆,这个方法就应该在Vehicle中被实现,而由其子类去继承。

        接下来,我们在看另外一们问题。它涉及到继承的冗余。如,Vehicle有一个自属性trunk(后备箱),这个属性并非所有车型都会有,现在我们假设一个没胡trunk的车型对Vehicle进行了继承,那么他还得继承这个对他没用的trunk属性,这样其类型也就产生了冗余。另个,对于方法也存在同样的问题,也就是继承会产生冗余。

        更为糟糕的是,设计之初并没有好的模型,我们只是为了一个方法或者一个属性的复用便使用继承,然后又随意的修改其他方法,添加新的方法,那这个类就会变得乱七八糟。

        所以,抽象是继承的基础,继承的是抽象的模型类型而非某一类型的具体实现。而如果可以使用组合解决问题就不要使用继承,这样就以避免冗余(当然,这个是有场景约束的,这个只是泛化的建议)。

3.多态(面向方法抽象及类型抽象)

        在OO设计中我们常会提到一个短语,叫封装变化,下面我们就来看一个这方面的例子。

  
 
    public abstract class Vehicle{
         public abstract void drive();
    }
    public class Motorbike extends Vehicle{
          public void drive(){
                 //TODO
          }
    } 
    public class Car extends Vehicle{
          public void drive(){
                 //TODO
          }
    }
    public class Client {
           public void testVehicle(Vehicle v){
                  v.drive();
           }
           public static void main(String[] args){
                  //TODO
           }
    }


        这是一个针对类型的多态,它展示出了Motorbike及Car针对同一消息drive所采取的不同反应。在testVehicle方法中实现了对于Vehicle类型变化的封装。

        另外,还有一种针对方法的多态,如:

   
    public abstract void drive();
    public abstract void drive(boolean auto);


        多态是OO语言对于变化的一种关键反应机制,在各种设计模式中几乎都有多态的存在,因为他们都需要适应变化。不过,说到底,多态其实是一个分派问题。


二、OO原则(基本原则)

        其实,对于OO的特性有比较深入的理解后,下面的三个原则也就比较好理解了,毕竟OO的特性也是因为有了这些原则才出现,而它们又相互佐证,似乎有点互为因果的味道。

1.封装变化(Encapsulate what varies).

        坚持这个原则,注意适可而止,控制变化范围,不然很可能将问题无限放大。将不需要考虑的问题无限的牵扯进来,如果你是在做项目,那么恭喜你,如果你耐性十足,这辈子你都会有事情做了。

2.面向接口编程而不是面向实现(Code to an interface rather than to an implementation).

        其实说得恰当点应当是面向变化编程,就好比之前的 testVehicle(Vehicle v) 方法,需要面向的是Vehicle而非Motorbike或者是Car. 这样代码对于变化的反应能力才能更强,可扩展性也才能更高,当然,将来的可维护性也会更强。  

3.优先使用组合而非继承(Favor Composition Over Inheritance)

        使用组合,除了上面提到的可以消除冗余之外,还有一个优点就是灵活。继承的方式只有一种,但组合的方案却可以很多。这样,对于代码的灵活性,解决问题的可选方案的范围都有郫益。


以下是OO编程的六大原则,其实际上也就是对上面的原则与特性的一种详细解释。


SRP: The single responsibility principle 单一职责

        系统中的每一个类都应该只有一个单独的职责,而所有类所关注的就是自身职责的完成。

引用
        Every object in your system should have a single responsibility ,and all the object's services should  be focused on carrying out that single responsibility .


  
引用
        1.每一个职责都是一个设计的变因,需求变化的时候,需求变化反映为类职责的变化。当你系统里面的对象都只有一个变化的原因的时候,你就已经很好的遵循了SRP原则。
        2.如果一个类承担的职责过多,就等于把这些职责耦合在了一起。一个职责的变化就可能削弱或者抑制这个类其它职责的能力。这种设计会导致脆弱的设计。当变化发生的时候,设计会遭到意想不到的破坏。
        3.SRP 让这个系统更容易管理维护,因为不是所有的问题都搅在一起。

        内聚Cohesion 其实是SRP原则的另外一个名字.你写了高内聚的软件其实就是说你很好的应用了SRP原则。
        怎么判断一个职责是不是一个对象的呢?你试着让这个对象自己来完成这个职责,比如:“书自己阅读内容”,阅读的职责显然不是书自己的。
仅当变化发生时,变化的轴线才具有实际的意义,如果没有征兆,那么应用SRP或者任何其它的原则都是不明智的。
        SRP其实就是针关于内聚的一条原则,而内聚按其类型又分为7种,包括偶然内聚、逻辑内聚、时间内聚、过程内聚、通信内聚、顺序内聚、功能内聚、信息内聚,而对于一般的程序设计而言,最好能够达到功能内聚及信息内聚的层次。

【功能内聚】

        小到一个方法,大到一个系统,我们都可以说是完成了某一个功能,只不过这个功能的粒度有大有小。而所有的代码都是为了完成这个功能而聚集到了一起,我们就说这是功能内聚。自然,在设计一个模块,一个类,一个方法的时候,其功能的粒度是个需要考量的问题,总体而言,粒度越小越好。这样就类似于使用组合而非继承一样,可以有更大的灵活性,且代码的复用程度而更高。但很多时候我们也应当考虑细化粒度的成本问题。

【信息内聚】

        模块完成多个功能,各个功能都在同一数据结构上操作,每一项功能有一个唯一的入口点。这个模块将根据不同的要求,确定该模块执行哪一个功能。由于这个模块的所有功能都是基于同一个数据结构(符号表),因此,它是一个信息内聚的模块。

        信息内聚其实只比功能内聚多一个约束,即一个模块内的所有功能都操作相同的数据结构,当然,在实际的软件工程中,要在一个模块中只操作单一的数据结构,那么模块的粒度势必会被控制得很小,而且还不一定能成功,但是核心数据结构唯一还是比较好控制的,而这类内聚我们就可以称之为信息内聚。 

        至于判断一个功能是否应当添加到一个类,或者一个模块中,我们只需要判断一下【类.功能】这个语句是否符合逻辑就可以了。如Human.run()。人是主语,跑是谓语,人跑步,自然是可以的。但是Vehicle.drive()这个方法就值得商榷了,因为车辆只能做为驾驶的宾语。但是如果用Vehicle.drivedByPerson(Person p)这个方法,同样也是可以的。所以方法的取舍重要的符合逻辑,而逻辑正是我们抽象建模的基础。

----------------------------------------------------------------------------------



DRY : Don"t repeat yourself Principle

        通过抽取公共部分放置在一个地方避免代码重复.
引用
        Avoid duplicate code by abstracting out things that are common and placing those thing in a single location .


   
引用
        1.DRY 很简单,但却是确保我们代码容易维护和复用的关键。
        2.你尽力避免重复代码候实际上在做一件什么事情呢?是在确保每一个需求和功能在你的系统中只实现一次,否则就存在浪费!系统用例不存在交集,所以我们的代码更不应该重复,从这个角度看DRY可就不只是在说代码了。
        3.DRY 关注的是系统内的信息和行为都放在一个单一的,明显的位置。就像你可以猜到正则表达式在.net中的位置一样,因为合理所以可以猜到。
        4.DRY 原则:如何对系统职能进行良好的分割!职责清晰的界限一定程度上保证了代码的单一性。


        理解DRY最简单的方法就是回忆一下我们在项目设计中所见过的common包,还有apache的common包,这些包在项目架构中一般都是放在什么位置,起到一个什么作用,什么时候被调用。当然,对于一个项目来说有application.common,有module.common有的时候甚至还会加一个kits或者utils,这些都是DRY的直接体现。
        但其实,所有的抽象模型都可以视作DRY设计,因为它们一直都在被复用。
----------------------------------------------------------------------------------

OCP : Open-Close Principle开闭原则

        类应该对修改关闭,对扩展打开;

引用
        Classes should be open for extension ,and closed  for modification .


   
引用
        1.OCP 关注的是灵活性,改动是通过增加代码进行的,而不是改动现有的代码;
        2.OCP的应用限定在可能会发生的变化上,通过创建抽象来隔离以后发生的同类变化
OCP 原则传递出来这样一个思想:一旦你写出来了可以工作的代码,就要努力保证这段代码一直可以工作。这可以说是一个底线。稍微提高一点要求,一旦我们的代码质量到了一个水平,我们要尽最大努力保证代码质量不回退。这样的要求使我们面对一个问题的时候不会使用凑活的方法来解决,或者说是放任自流的方式来解决一个 问题;比如代码添加了无数对特定数据的处理,特化的代码越来越多,代码意图开始含混不清,开始退化。
        3.OCP 背后的机制:封装和抽象;封闭是建立在抽象基础上的,使用抽象获得显示的封闭;继承是OCP最简单的例子。除了子类化和方法重载我们还有一些更优雅的方法来实现比如组合;怎样在不改变源代码(关闭修改)的情况下更改它的行为呢?答案就是抽象,OCP背后的机制就是抽象和多态
        4.没有一个可以适应所有情况的贴切的模型!一定会有变化,不可能完全封闭.对程序中的每一个部分都肆意的抽象不是一个好主意,正确的做法是开发人员仅仅对频繁变化的部分做出抽象。拒绝不成熟的抽象和抽象本身一样重要。
        5.OCP是OOD很多说法的核心,如果这个原则有效应用,我们就可以获更强的可维护性 可重用 灵活性 健壮性 LSP是OCP成为可能的主要原则之一


        其实上面已经说的很详细了,这里我就再啰嗦两句,也算是我的理解吧。写代码就要保证你的代码可以好好的工作,以保证别人不会乱动你的代码,当然也不能让他动,不然整个软件设计体系都可能被打破。但是当你的代码不能满足别人的需求时,也就是出现了现行代码不能满足的变化,那么你必须为这种变化提供一个可以解决的接口,也就是可扩展的设计。这里面有一个成本问题,所以是不能什么变化都提供可扩展接口的。
----------------------------------------------------------------------------------


LSP: The Liskov substitution principle

        子类必须能够替换基类。

引用
        Subtypes must be substitutable  for their base types.


引用
        1.LSP关注的是怎样良好的使用继承.
        2.必须要清楚是使用一个Method还是要扩展它,但是绝对不是改变它。
        3.LSP清晰的指出,OOD的IS-A关系是就行为方式而言,行为方式是可以进行合理假设的,是客户程序所依赖的。
        4.LSP让我们得出一个重要的结论:一个模型如果孤立的看,并不具有真正意义的有效性。模型的有效性只能通过它的客户程序来表现。必须根据设计的使用者做出的合理假设来审视它。而假设是难以预测的,直到设计臭味出现的时候才处理它们。
        5.对于LSP的违反也潜在的违反了OCP


        LSP其实就是强制的要求你必须满足类型的多态,实现面向抽象的设计而非面向具体实现的设计。这与之前讨论类型的多态比较类似,可以做个参考。所以我们在进行可扩展接口设计的时候,应当考虑的是抽象的模型,而非模型的具体实现类,这样这样接口才具体通用性,这样任何这个模型的子类才能在这里代替它们的父类实现功能的扩展。
----------------------------------------------------------------------------------


DIP:依赖倒置原则

引用
        1.高层模块不应该依赖于底层模块 二者都应该依赖于抽象
        2.抽象不应该依赖于细节 细节应该依赖于抽象
        3.什么是高层模块?高层模块包含了应用程序中重要的策略选择和业务模型。这些高层模块使其所在的应用程序区别于其它。
        4.如果高层模块依赖于底层模块,那么在不同的上下文中重用高层模块就会变得十分困难。然而,如果高层模块独立于底层模块,那么高层模块就可以非常容易的被重用。该原则就是框架设计的核心原则。
        5.这里的倒置不仅仅是依赖关系的倒置也是接口所有权的倒置。应用了DIP我们会发现往往是客户拥有抽象的接口,而服务者从这些抽象接口派生。
        6.这就是著名的Hollywood原则:"Don"t call us we"ll call you."底层模块实现了在高层模块声明并被高层模块调用的接口。
        7.通过倒置我们创建了更灵活 更持久更容易改变的结构
        8.DIP的简单的启发规则:依赖于抽象;这是一个简单的陈述,该规则建议不应该依赖于具体的类,也就是说程序汇总所有的依赖都应该种植于抽象类或者接口。
        9.如果一个类很稳定,那么依赖于它不会造成伤害。然而我们自己的具体类大多是不稳定的,通过把他们隐藏在抽象接口后面可以隔离不稳定性。
        10.依赖倒置可以应用于任何存在一个类向另一个类发送消息的地方
        11.依赖倒置原则是实现许多面向对象技术所宣称的好处的基本底层机制,是面向对象的标志所在。


        所谓高层模块就是抽象的上层模型,而底层模块就是上层抽象模型的具体实现,或为了粘合这些实现而创建的附加类。而在整个软件设计过程中,为了适应变化,提倡并应当使用抽象模型来进行消息传递,这在之前讨论的几个原则中基本上都有体现。
----------------------------------------------------------------------------------


ISP:接口隔离原则

        不应该强迫客户程序依赖它们不需要的使用的方法。

引用
        1.接口不是高内聚的,一个接口可以分成N组方法,那么这个接口就需要使用ISP处理一下。
        2.接口的划分是由使用它的客户程序决定的,客户程序是分离的接口也应该是分离的。
        3.一个接口中包含太多行为时候,导致它们的客户程序之间产生不正常的依赖关系,我们要做的就是分离接口,实现解耦。
        4.应用了ISP之后,客户程序看到的是多个内聚的接口。


        ISP原则的提出其实与优先使用组合而非继承的原因一样,因为接口只能实现,而每次实现都需要实现其所有方法,这导致可能产生不需要冗余,如我根本不需要这个方法,但是为了使用接口里的其他方法,我必须对其做一次实现,哪怕是一个空方法。
        所以在设计模式里有一种针对接口的处理方法是让一个abstract类去实现一个接口。将其中的一些方法做初步实现,然后子类只需要复写或实现与其相关的方法即可。这样至少在客户写自己的代码的时候,他不需要与他不相关的方法打交道。
----------------------------------------------------------------------------------
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics