zoukankan      html  css  js  c++  java
  • 敏捷软件开发(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 

  • 相关阅读:
    浏览器缓存机制
    关于CSRF的攻击
    关于CGI、FastCGI和PHP-FPM的关系
    PHP-FPM进程数的设定
    一个论坛引发的血案
    Django中的权限系统
    Django中使用ModelForm实现Admin功能
    Django中使用Bootstrap
    Django的用户认证
    vtkMapper
  • 原文地址:https://www.cnblogs.com/deman/p/5523830.html
Copyright © 2011-2022 走看看