敏捷软件开发(2)--- 设计原则

之前讲设计模式系列的时候,也提过这些原则:

http://www.cnblogs.com/deman/category/634503.html

现在在根据敏捷一书,学习下。

我们已经有23种设计模式,是不是每一个类,功能都要用到设计模式?怎么选用合适的设计模式?

是不是开始开发了一个类,或者使用一个类以后,就不能修改这部分代码了吗?

其实每一次选择都是根据具体的情况而定,没有标准。

它依赖于设计者的经验,开发团队的能力以及协作度,时间周期,以及可以预见的扩展性。

它一定是一个基于,时间成本,人力成本,技术水平壁垒,以及产品开发周期等各种因素的一个综合结果。

在熟练掌握设计模式的基础上如何根据具体情况选择,这就是设计模式六大原则。

这些原则不是“某个人”提出来的,是根据几十年的软件开发项目的经验总结,是面向对象的“内功心法”。

敏捷讨论的是,怎么适应变化。

所有的设计模式讨论的都是拥抱变化,不是一种“万能型”的模式。

就像独孤九剑,没有固定的招式,但有明确的目的,“破招”。

敏捷开发的精髓就是,它依赖于人,视实际的情况而定,有时候不使用设计模式,也是一种模式!

1.单一职责原则

什么是职责?

职责就是变化的原因。也就是一个可以需要改动类的原因。

假设一个Modem的类,它有四个方法

package com.joyfulmath.agileexample.singletheroy;

/**
 * @author deman.lu
 * @version on 2016-05-25 13:36
 */
public interface Modem {
    public void dial();
    public void handleup();
    public void send(char c);
    public void recv();
}

这是一个Modem的类,它有2个职能,一个是连接,一个是数据收发。这2个职责一定要分开吗?

不一定,看具体情况。如果连接的部分进行修改,并不会引起数据部分的变化,这样send 和recv的代码需要从新编译,

然而他们并没有变化,这就是职责不单一的后果。但是如果这2块,会同时变化,如果把他们分开,就会有过度复杂化的问题。

还有一个推论,就是变化的轴线只有当变化实际应用的时候,才具有意义。

已这个例子为例,可以很好的阐释 敏捷到底是什么?

1)一开始开发Modem类的时候,连接和数据是绑定在一起的,我们并不需要分开他们。

所以当我们开发第一版的时候,我们并不知道后面会设计成什么样,可能会有那些变化,我们没有能力,也不应该去过度的考虑后续的扩展性,以及其他方面。

2)需求发生了变化,连接发生改变的时候,数据这块确没有任何变化。这时候我们就需要重构,需要把职责分开。

package com.joyfulmath.agileexample.singletheroy;

/**
 * @author deman.lu
 * @version on 2016-05-25 14:16
 */
public interface IConnect {
    void dial(String a);
    void handleup(int id);
}

package com.joyfulmath.agileexample.singletheroy;

/**
 * @author deman.lu
 * @version on 2016-05-25 14:17
 */
public interface IDataChannel {
    void send(char c);
    void recv();
}

package com.joyfulmath.agileexample.singletheroy;

/**
 * @author deman.lu
 * @version on 2016-05-25 14:17
 */
public interface NewModem extends IConnect,IDataChannel{
    
}

通过接口的设计,连接和数据职能被分开了。

在这个需求变更的时候,我们对代码进行了重构,以满足单一原则,或者其他什么的。

这就是敏捷的精髓,我只在需要的时候进行下“小步修改”,而不是在某个节点,进行大规模的代码重构之类的。

敏捷发生在每一个时候,每一天,每一小时。敏捷只做必要的修改,过度的设计,也许永远不会发生。

2.开发封闭原则

对扩展开发,对更改封闭。

一个被经常用到的例子:

package com.joyfulmath.agileexample.oop;

/**
 * @author deman.lu
 * @version on 2016-05-25 14:57
 */
public abstract class Shape {
    abstract void draw();
}
public class Cycle extends Shape {
    @Override
    void draw() {

    }
}
public class Supare extends Shape {
    @Override
    void draw() {

    }
}
public class DrawShape {

    public void drawShapes(Vector<Shape> shapes)
    {
        for(Shape shape:shapes)
        {
            shape.draw();
        }
    }
}

我们可以添加一个类型,比如rectanle,但是上述的代码,我们都不不需要做任何修改,这就是对扩展开发,对修改封闭。

关键就是使用抽象的概念。

这是一个近乎完美的例子,但是就想一个厨师不可能作出满足所有人口味的美食一样,没有一个抽象或者其他技术可以解决所有的变化。

我们无法预测到需求的所有变化,只能是最可能的变化,这需要开发人员的经验。

but,如果要求所有的圆都比正方形先画。

这个变化,需要重构代码,已满足这个条件。

3.Liskov 替换原则

子类型必须能够替换掉它们的基本类型。

为什么有这么奇怪的名字,Liskov是一个人名,我们来看看这位计算机界的名人:

Barbara Liskov

上面是百度百科的介绍,具体你可以google一下她的经历。美国第一个获得计算机科学博士学位的女性(1968年,斯坦福大学)。

那时候知道计算机是什么东西的人,估计都不多。

这个原则表面上看和OCP是有些矛盾的。

我们先举例,然后在分析:

package com.joyfulmath.agileexample.lsp;

/**
 * File Description:
 *
 * @auther deman
 * Created on 2016/5/29.
 */
public class Rectangle {
    private double width;
    private double height;
    Point topLeft;

    public double getWidth() {
        return width;
    }

    public void setWidth(double width) {
        this.width = width;
    }

    public double getHeight() {
        return height;
    }

    public void setHeight(double height) {
        this.height = height;
    }
}

这是一个矩形的定义,假设它运行良好,但是某天用户想要一个正方形。

正方形是一种特殊的矩形,把正方形从矩形中继承,符合一般的意义。

从上面的结构来说,正方形并不需要长和宽,它只要一条边的长度就可以。

如果我们不考虑内存的浪费,从Rectangle派生出Square 也会有问题。

如果把Square定义如下:

package com.joyfulmath.agileexample.lsp;

/**
 * File Description:
 *
 * @auther deman
 * Created on 2016/5/29.
 */
public class Square extends Rectangle {
    @Override
    public void setWidth(double width) {
        super.setWidth(width);
        super.setHeight(width);
    }

    @Override
    public void setHeight(double height) {
        super.setWidth(height);
        super.setHeight(height);
    }
}

这样看起来可以满足正方形的要求。

无论Square怎么设置,它都是正真意义上的正方形。而且Square是Rectangle 的子类。

但是问题来了,看如下的代码?

public class ShapeTest {
    
    public void g(Rectangle r)
    {
        r.setHeight(4);
        r.setWidth(5);
        if(r.getHeight()*r.getWidth() == 20)
        {
            
        }
    }
}

当传入是正方形的时候,这个if条件就不会满足。这是一个不易发现的情况。就是正方形的行为与长方形不一致。

或者说这里需要区分正方形做特殊处理。这样就符合LSP,或者我们的常识。

所以我们发现正方形作为长方形的子类,这件事情是很能实现的,或者说会违反我们的常识!

从数学上来讲,正方形就是长方形的子类,但是从行为方式角度考虑,正方形和长方形的行为是严格区分的。

所以LSP所讲的一致性,就是指行为方式的一致。

如果某个基类的方法会抛出一个excption,而他的子类会抛出一个全新的excption,那么行为就是不一致,我们需要

对这个子类做特殊的处理,这就违反了LSP,也失去了多肽的意义。

要解决这个难题,就是把基类A和子类B公共的部分提取出来,成为 一个新的抽象类C,A & B 同时继承自C就可以解决不一致性。

现在来说说OCP & LSP的关联。

OCP所说的扩张,不是指对原有的子类 的方法赋予新的行为方式,而是创建新的派生类,对功能的扩展。

这样既满足OCP,也满足LSP。

如果原有的类结构无法满足OCP 或者LSP,甚至其他各原则。

敏捷就是,任何软件无法再任何时刻满足各种原则,和各种需求。

当需求改变时,我们就需要综合考虑各种成本,来实现和重构软件。

 4.依赖倒置原则(DIP)

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

抽象不应该依赖于细节,细节应该依赖于抽象。

如图,PolicyLayer 的实现需要依赖于UtilityLayer,但是他们根本不打交道。所有这是很奇怪的设计。

PolicyLayer应该依赖于它的接口,也就是PolicyLayour是业务逻辑的模块。就像FM一项,我用那家公司的芯片都可以,

但是我需要明确我上层需要那些服务,然后由底层实现它。

如图 PolicyLayer只要由Policy Service Interface提高的服务就可以。至于Mechanism Layer,

如果通过PolicyServiceManager来管理,则PolicyLayer根本不需要知道Mechanism Layer。

换句话说,把Mechanism Layer换掉,不用修改PolicyLayer的任何一行代码,这样Mechanism Layer & PolicyLayer

就完全解耦。

根据依赖倒置规则我们可以推导3个小规则。

1)任何变量都不应该持有指向具体类的引用。

2)任何类都不应该从具体类派生

3)任何方法都不应该覆写它的基类中已经实现的方法。

这些规则应该在最复杂,不稳定的类或模块中使用。

因为如果你每个类都准寻这个方式,必将产生60%以上几本不变的类,缺写的过于复杂。

这就是代码过于复杂的味道。

你不能保证每个类都无比强大,应为这么做没有必要,而且效率底下。

就是一个国家的部队,不可能全是特种兵,国家也养不起这么多特种兵,只有合适的兵种搭配,才是强大的军队。

所以一个高效的设计,肯定是基于产品,开发团队,时间周期,产品质量等各方面因素综合的一个产物。

5.接口分离原则

先看一个Door的例子:

/**
 * @author deman.lu
 * @version on 2016-05-31 16:48
 */
public interface Door {
    void lock();
    void unlock();
    boolean isDoorOpen();
}
/**
 * @author deman.lu
 * @version on 2016-05-31 16:57
 */
public class Timer {
    public void Register(int timeOut,TimerClient client){

    }
}
/**
 * @author deman.lu
 * @version on 2016-05-31 16:58
 */
public interface TimerClient {
    void TimeOut();
}

如果门长期开着,就发出alarm。所以我们需要一个TimerClient来发现TimeOut。如果一个对象希望获得time out的通知,那就让Time来注册这个对象。

我们看看如下的方案:


 
这样TimerDoor 就可以注册到Timer上面。但是这样写有个问题,Door其实跟TimerClient没有关系。
这就是接口胖的味道。
如何分离TimeClient & Door ,可以有很多选择,这里我们使用多重继承的方式。

 
 

参考:

《敏捷软件开发》 Robert C. Martin 

原文地址:https://www.cnblogs.com/deman/p/5523830.html