0

面向对象五大原则

Posted in PHP at 三月 21st, 2008 / No Comments »

在写设计模式读书笔记之前,我想先总结一下自己对于面向对象设计五大原则的理解,他们分别是:
SRP——单一职责原则;
OCP——开放封闭原则;
LSP——Liskov替换原则;
DIP——依赖倒置原则;
ISP——接口隔离原则。
1.   单一职责原则
    在《敏捷软件开发》中,把“职责”定义为“变化的原因”,也就是说,就一个类而言,应该只有一个引起它变化的原因。
    在《UML与模式应用》一书中又提到,“职责”可以定义为“一个类或者类型的契约或者义务”,并把职责分成“知道”型职责和“做”型职责。
    其中“做”型职责指的是一个对象自己完成某种动作或者和其他对象协同完成某个动作;“知道”型职责指的是一个对象需要了解哪些信息。如果按照这种方式来定义“职责”的话,就与《敏》中对单一职责原则的定义不太相符了,所以还是理解为“变化的原因”比较恰当。
    这个原则很好理解,但是既然谈到了职责,就不妨再来看看GRASP——通用职责分配软件模式(选自《UML与模式应用》)。按照我自己的看法来讲,在下面这些职责分配模式中所涉及到的设计问题,是建立在现实世界抽象层次上的设计,从这个层次上进一步细化,才到了设计模式所说的针对接口编程和优先使用组合的两大原则。
    在这个层次上的抽象,一定要按照现实生活中的思维方法来进行,从我们人类考虑问题的角度出发,把解决现实问题的思维方式逐渐转化成程序能够理解的思维方式,绝不允许在这一步考虑程序代码如何实现,那样子的架构就是基于程序实现逻辑,而不是从解决问题的角度出发来实现业务逻辑(参考“面向对象的思维方法”)。

    1)   专家模式。
    在一个系统中可能存在成千上万个职责,在面向对象的设计中,定义对象的交互时,就要做出如何将职责分配给类的设计选择。
    专家模式的解决方案就是:把一个职责分配给信息专家——掌握了为履行职责所必需的信息的类。
    按照专家模式可以得到:一个对象所执行的操作通常是这个对象在现实世界中所代表的事物所执行的操作——这恰恰印证了我上面中的说法。
    不过使用专家模式的时候,一定要仔细判断什么样的职责是应该只由一个类完成,什么样的职责应该由不同的类协作完成。举一个小小的反例吧,在“思维方法”一文中,提供了一个收发邮件的例子用以说明作者的观点,源码如下所示: 复制内容到剪贴板
代码:
public class JunkMail {
        private String head;
        private String body;
        private String address;
        public JunkMain() { // 默认的类构造器
        this.head=…;
        this.body=…;
        }
        public static boolean sendMail(String address) {
        // 调用qmail,发送email
        }
        public static Collection listAllMail() {
        // 访问数据库,返回一个邮件地址集合
        }
}作者在这里就犯了一个职责分配的错误:上面的head、body和address都是属于邮件自身的属性,但是这个类却有一个叫做sendMail的方法,错误就在这个方法这里。在现实生活中,我们发送邮件的时候,是通过邮递员来进行的,绝对没有一封信会长上翅膀自己飞到收信人的手中,在程序中也是一样,一封邮件绝不可能自己把自己发送出去,应该通过某个MailController之类的类来完成这个功能(之所以不命名为MailSender,是因为后面可能还要添加receiveMail等功能)。
   2)    创建者模式
   如果下列条件满足的话,就把创建类A的实例的职责分配给类B的实例:
        a)        B聚集了A对象
        b)        B包含了A对象
        c)        B记录了A对象的实例
        d)        B要经常使用A对象
        e)        当A的实例被创建时,B具有要传递给A的初始化数据(也就是说B是创建A的信息专家)
    如果以上条件中不止一条满足的话,那么最好让B聚集或者包含A
    创建者模式用于指导对象实例创建任务的分配,基本目的就是找到一个与被创建对象有关联关系的创建者。

    3)    低耦合度

    4)    高聚合度

    以上两个模式都是在设计过程中要记住的原则,是时时刻刻需要注意的隐含实现目标。
    剩下的几种模式要么和以后的话题关系不大,要么在23种模式中有着体现,不再一一赘述。

2.       开放封闭原则
Software entity should be open for extension,but closed for modification 
依据这个原则,我们在设计的时候,就要考虑设计中什么会发生变化,并且不让这变化影响到设计,当然在这里就会遇到“过度设计”的问题了,而且预测变化对经验的要求比较高,创建抽象所付出的代价也很大,所以只能限定在可能的变化上,仅仅对程序中呈现出频繁变化的部分进行抽象。
我们至少应当做到的是:一种可变性不应当散落在代码的很多角落里,而应当被封装在一个对象里面,更不应该和另外一种可变性混在一起。

3       Liskov替换原则
子类必须能够替换掉它们的父类。
另外还有一句话就是:子类具有扩展父类的责任,而不是重写的责任。
这个原则在Thinking in Java(the 2nd editon)和Effective Java中均有提及,当时想到的是因为父类是一组子类公共特征的抽象,所以子类理所当然应该可以替换掉自己的父类。后来看了《敏捷软件开发》中对于这个原则的阐释,才明白这里所说的抽象,其真正意义应该是对对象行为方式的抽象。也就是说,IS-A关系是就行为方式而言的,即使是在现实生活中看上去具有IS-A关系的两个对象,如果在程序中它们的行为方式不相同,那么就不应当具有IS-A关系。
比如说,正方形和矩形,按照数学概念,正方形应该属于矩形——Square is a Rectangle。但是,如果在程序中有一个方法,只改变矩形的长,但是不改变矩形的宽,这时候正方形和矩形的行为方式就不相同了,所以在这种情况下,Square isn’t a Rectangle。
获取系统行为的基础是用例分析(详见《UML和模式应用》),各个用例之间的交互过程就是系统行为的触发过程。我们通过用例契约来定义用例实现的目标,描述一个操作执行前后系统的状态变化,并声明该操作的前置条件和后置条件,这样就可以清楚地得到系统行为。
上面说的是分析阶段的工作,同样在设计阶段,我们也应该为每一个类制定契约,定义它的行为方式,声明每一个方法的前置条件和后置条件,从而得到对象的行为方式。

4.       依赖倒置原则
高层模块不应该依赖于低层模块。二者都应该依赖于抽象。
抽象不应该依赖于细节,细节应该依赖于抽象。
抽象层次包含的是应用系统的商务逻辑和宏观的战略性决定,而具体层次含有的是和实现有关的算法与逻辑,具体层次的代码会经常发生变动,不能避免出现错误。所以便应该是高层的策略设置模块影响低层的细节实现模块,而不是相反。比如说,先制定业务逻辑的规则,再实现数据库操作等等细节。
比较合适的模型是:每个较高的层次都为它需要的服务声明一个抽象接口,较低的层次实现这个接口,每个高层类都通过该抽象接口使用下一层。换句话来说,由客户类声明它们需要的服务接口,仅当客户需要时才对接口进行改变,所以改变实现细节的类就不会影响到客户。
拿《敏》中P118中Button和Lamp对象的例子来说,作者举了一个不成熟的设计,如下: 复制内容到剪贴板
代码:
public class Button{
       private Lamp itsLamp;
       public void poll(){
              if(/* some condition*/){
                     itsLamp.turnOn();
              }
       }
}作者说,这个例子背后的抽象是检测用户的开/关指令并将指令传给目标对象,而检测用户指令的机制是无关紧要的,目标对象同样也是无关紧要的。我按照作者的分析写了一段程序,如下所示: 复制内容到剪贴板
代码:
public interface SwitchableDevice {
       public void turnOn();
       public void turnOff();
}

public class Lamp implements SwitchableDevice{
       public void turnOn(){
              //do something
       }
       public void turnOff(){
              //do something
       }
}

public class Button {
       public void poll(SwitchableDevice device){
              device.turnOff();
       }
       public void push(SwitchableDevice device){
              device.turnOn();
       }
}这样子,Button类就可以控制所有实现了SwitchableDevice接口的对象,而lamp也变成了依赖于SwitchableDevice接口,而不必在乎谁来控制它。
5.       接口隔离原则
不应该强迫客户依赖于他们不用的方法。
使用多个专门的接口比使用单一的总接口要好,一个类对另外一个类的依赖性应当建立在最小的接口上。

Published in PHP

No Responses to “面向对象五大原则”

Leave a Reply

请输入算式结果(看不清请点击图片)
(必须)