精华内容
下载资源
问答
  • 面向对象设计的七大设计原则详解

    万次阅读 多人点赞 2018-10-03 12:32:21
    面向对象的七大设计原则 文章目录面向对象的七大设计原则简述七大原则之间的关系一、开闭原则(The Open-Closed Principle ,OCP)概念理解系统设计需要遵循开闭原则的原因开闭原则的实现方法一个符合开闭原则的...

    面向对象的七大设计原则


    简述

    类的设计原则有七个,包括:开闭原则里氏代换原则迪米特原则(最少知道原则)单一职责原则接口分隔原则依赖倒置原则组合/聚合复用原则

    七大原则之间的关系


    七大原则之间并不是相互孤立的,彼此间存在着一定关联,一个可以是另一个原则的加强或是基础。违反其中的某一个,可能同时违反了其余的原则。

    开闭原则是面向对象的可复用设计的基石。其他设计原则是实现开闭原则的手段和工具。

    一般地,可以把这七个原则分成了以下两个部分:

    设计目标:开闭原则、里氏代换原则、迪米特原则
    设计方法:单一职责原则、接口分隔原则、依赖倒置原则、组合/聚合复用原则

    一、开闭原则(The Open-Closed Principle ,OCP)


    软件实体(模块,类,方法等)应该对扩展开放,对修改关闭。

    概念理解


    开闭原则是指在进行面向对象设计中,设计类或其他程序单位时,应该遵循:

    • 对扩展开放(open)
    • 对修改关闭(closed) 的设计原则。

    开闭原则是判断面向对象设计是否正确的最基本的原理之一。

    根据开闭原则,在设计一个软件系统模块(类,方法)的时候,应该可以在不修改原有的模块(修改关闭)的基础上,能扩展其功能(扩展开放)。

    • 扩展开放:某模块的功能是可扩展的,则该模块是扩展开放的。软件系统的功能上的可扩展性要求模块是扩展开放的。
    • 修改关闭:某模块被其他模块调用,如果该模块的源代码不允许修改,则该模块修改关闭的。软件系统的功能上的稳定性,持续性要求模块是修改关闭的。

    通过下边的例子理解什么是扩展开放和修改关闭:

    在这里插入图片描述

    左边的设计是直接依赖实际的类,不是对扩展开放的。

    右边的设计是良好的设计:

    • Client对于Server提供的接口是封闭的;
    • Client对于Server的新的接口实现方法的扩展是开放的。

    系统设计需要遵循开闭原则的原因


    1. 稳定性。开闭原则要求扩展功能不修改原来的代码,这可以让软件系统在变化中保持稳定。
    2. 扩展性。开闭原则要求对扩展开放,通过扩展提供新的或改变原有的功能,让软件系统具有灵活的可扩展性。
      遵循开闭原则的系统设计,可以让软件系统可复用,并且易于维护。

    开闭原则的实现方法


    为了满足开闭原则的对修改关闭原则以及扩展开放原则,应该对软件系统中的不变的部分加以抽象,在面向对象的设计中,

    • 可以把这些不变的部分加以抽象成不变的接口,这些不变的接口可以应对未来的扩展;
    • 接口的最小功能设计原则。根据这个原则,原有的接口要么可以应对未来的扩展;不足的部分可以通过定义新的接口来实现;
    • 模块之间的调用通过抽象接口进行,这样即使实现层发生变化,也无需修改调用方的代码。

    接口可以被复用,但接口的实现却不一定能被复用。
    接口是稳定的,关闭的,但接口的实现是可变的,开放的。
    可以通过对接口的不同实现以及类的继承行为等为系统增加新的或改变系统原来的功能,实现软件系统的柔性扩展。

    好处:提高系统的可复用性和可维护性。

    简单地说,软件系统是否有良好的接口(抽象)设计是判断软件系统是否满足开闭原则的一种重要的判断基准。现在多把开闭原则等同于面向接口的软件设计。

    一个符合开闭原则的设计


    需求:创建一系列多边形。
    首先,下面是不满足开闭原则的设计方法:

    Shape.h

    enumShapeType{ isCircle, isSquare};
    typedef struct Shape {
    	enumShapeType type
    } shape;
    

    Circle.h

    typedef struct Circle {
    	enumShapeType type;
    	double radius;
    	Point center;
    } circle;
    void drawCircle( circle* );
    

    Square.h

    typedef struct Square {
    	enumShapeType type;
    	double side;
    	Point topleft;
    } square;
    void drawSquare( square* );
    

    drawShapes.cpp

    #include "Shape.h"
    #include "Circle.h"
    #include "Square.h"
    void drawShapes( shape* list[], intn ) {
    	int i;
    	for( int i=0; i<n; i++ ) {
    		shape* s= list[i];
    		switch( s->type ) {
    		case isSquare:
    			drawSquare( (square*)s );
    			break;
    		case isCircle:
    			drawCircle( (circle*)s );
    			break;
    		}
    	}
    }
    

    该设计不是对扩展开放的,当增加一个新的图形时:

    • Shape不是扩展的,需要修改源码来增加枚举类型
    • drawShapes不是封闭的,当其被其他模块调用时,如果要增加一个新的图形需要修改switch/case

    此外,该设计逻辑复杂,总的来说是一个僵化的、脆弱的、具有很高的牢固性的设计。

    用开闭原则重构该设计如下图:

    在这里插入图片描述

    此时,在该设计中,新增一个图形只需要实现Shape接口,满足对扩展开放;也不需要修改drawShapes()方法,对修改关闭。

    开闭原则的相对性


    软件系统的构建是一个需要不断重构的过程,在这个过程中,模块的功能抽象,模块与模块间的关系,都不会从一开始就非常清晰明了,所以构建100%满足开闭原则的软件系统是相当困难的,这就是开闭原则的相对性。

    但在设计过程中,通过对模块功能的抽象(接口定义),模块之间的关系的抽象(通过接口调用),抽象与实现的分离(面向接口的程序设计)等,可以尽量接近满足开闭原则。

    二、 里氏替换原则(Liskov Substitution Principle ,LSP)


    所有引用基类的地方必须能透明地使用其派生类的对象。

    概念理解


    也就是说,只有满足以下2个条件的OO设计才可被认为是满足了LSP原则:

    • 不应该在代码中出现if/else之类对派生类类型进行判断的条件。

    • 派生类应当可以替换基类并出现在基类能够出现的任何地方,或者说如果我们把代码中使用基类的地方用它的派生类所代替,代码还能正常工作。

    以下代码就违反了LSP定义。

    if (obj typeof Class1) {
        do something
    } else if (obj typeof Class2) {
        do something else
    }
    

    里氏替换原则(LSP)是使代码符合开闭原则的一个重要保证。

    同时LSP体现了:

    • 类的继承原则:如果一个派生类的对象可能会在基类出现的地方出现运行错误,则该派生类不应该从该基类继承,或者说,应该重新设计它们之间的关系。

    • 动作正确性保证:从另一个侧面上保证了符合LSP设计原则的类的扩展不会给已有的系统引入新的错误。
      示例:

    里式替换原则为我们是否应该使用继承提供了判断的依据,不再是简单地根据两者之间是否有相同之处来说使用继承。

    里式替换原则的引申意义:子类可以扩展父类的功能,但不能改变父类原有的功能。

    具体来说:

    • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
    • 子类中可以增加自己特有的方法。
    • 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松。
    • 当子类的方法实现父类的方法时(重载/重写或实现抽象方法)的后置条件(即方法的输出/返回值)要比父类更严格或相等。

    下面举几个例子帮助更进一步理解LSP:
    例:1:
    在这里插入图片描述

    Rectangle是矩形,Square是正方形,Square继承于Rectangle,这样一看似乎没有问题。

    假如已有的系统中存在以下既有的业务逻辑代码:

    void g(Rectangle r)
    {
    r.SetWidth(5);
    r.SetHeight(4);
    assert(r.GetWidth() * r.GetHeight()) == 20);
    }
    

    则对应于扩展类Square,在调用既有业务逻辑时:

        Rectangle square = new Square();
        g(square);
    

    时会抛出一个异常。这显然违反了LSP原则。说明这样的继承关系在这种业务逻辑下不应该使用。

    例2:鲸鱼和鱼,应该属于什么关系?从生物学的角度看,鲸鱼应该属于哺乳动物,而不是鱼类。没错,在程序世界中我们可以得出同样的结论。如果让鲸鱼类去继承鱼类,就完全违背了Liskov替换原则。因为鱼作为基类,很多特性是鲸鱼所不具备的,例如通过腮呼吸,以及卵生繁殖。那么,二者是否具有共性呢? 有,那就是它们都可以在水中"游泳",从程序设计的角度来说,它们都共同实现了一个支持"游泳"行为的接口。

    例:3:运动员和自行车例子,每个运动员都有一辆自行车,如果按照下面设计,很显然违反了LSP原则。

    class Bike {
    public:
           void Move( );
           void Stop( );
           void Repair( );
    protected:
           int    ChangeColor(int );
    private:
           int    mColor;
    };
    
    
    class Player : private Bike
    {
    public:
          void  StartRace( );
          void  EndRace( ); 
    protected:
           int    CurStrength ( ); 
    private:
            int   mMaxStrength;
            int   mAge;
    };
    

    里式替换原则的优点


    • 约束继承泛滥,是开闭原则的一种体现。
    • 加强程序的健壮性,同时变更时也可以做到非常好地提高程序的维护性、扩展性。降低需求变更时引入的风险。

    重构违反LSP的设计

    如果两个具体的类A,B之间的关系违反了LSP 的设计,(假设是从B到A的继承关系),那么根据具体的情况可以在下面的两种重构方案中选择一种:

    • 创建一个新的抽象类C,作为两个具体类的基类,将A,B的共同行为移动到C中来解决问题。

    • 从B到A的继承关系改为关联关系。

    对于矩形和正方形例子,可以构造一个抽象的四边形类,把矩形和正方形共同的行为放到这个四边形类里面,让矩形和正方形都是它的派生类,问题就OK了。对于矩形和正方形,取width 和height 是它们共同的行为,但是给width 和height 赋值,两者行为不同,因此,这个抽象的四边形的类只有取值方法,没有赋值方法。

    对于运动员和自行车例子,可以采用关联关系来重构:

    class Player 
    {
    public:
          void  StartRace( );
          void  EndRace( ); 
    protected:
           int    CurStrength ( ); 
    private:
            int   mMaxStrength;
            int   mAge;
    Bike * abike;
    };
    

    在进行设计的时候,我们尽量从抽象类继承,而不是从具体类继承。

    如果从继承等级树来看,所有叶子节点应当是具体类,而所有的树枝节点应当是抽象类或者接口。当然这只是一个一般性的指导原则,使用的时候还要具体情况具体分析。

    在很多情况下,在设计初期我们类之间的关系不是很明确,LSP则给了我们一个判断和设计类之间关系的基准:需不需要继承,以及怎样设计继承关系。

    三、 迪米特原则(最少知道原则)(Law of Demeter ,LoD)


    迪米特原则(Law of Demeter)又叫最少知道原则(Least Knowledge Principle),可以简单说成:talk only to your immediate friends,只与你直接的朋友们通信,不要跟“陌生人”说话。

    概念理解


    对于面向OOD来说,又被解释为下面两种方式:

    1)一个软件实体应当尽可能少地与其他实体发生相互作用。

    2)每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。

    朋友圈的确定
    “朋友”条件:

    1. 当前对象本身(this)
    2. 以参量形式传入到当前对象方法中的对象
    3. 当前对象的实例变量直接引用的对象
    4. 当前对象的实例变量如果是一个聚集,那么聚集中的元素也都是朋友
    5. 当前对象所创建的对象

    任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”,否则就是“陌生人”。

    迪米特原则的优缺点


    迪米特原则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。

    迪米特原则不希望类直接建立直接的接触。如果真的有需要建立联系,也希望能通过它的友元类来转达。因此,应用迪米特原则有可能造成的一个后果就是:系统中存在大量的中介类,这些类之所以存在完全是为了传递类之间的相互调用关系,这在一定程度上增加了系统的复杂度

    例如,购房者要购买楼盘A、B、C中的楼,他不必直接到楼盘去买楼,而是可以通过一个售楼处去了解情况,这样就减少了购房者与楼盘之间的耦合,如图所示。

    违反迪米特原则的设计与重构


    下面的代码在方法体内部依赖了其他类,这严重违反迪米特原则

    class Teacher { 
    public: 
     void command(GroupLeader groupLeader) { 
    	   list<Student> listStudents = new list<Student>; 
    	   for (int i = 0; i < 20; i++) { 
    	        listStudents.add(new Student()); 
    	   } 
    	   groupLeader.countStudents(listStudents); 
    } 
    }
    

    方法是类的一个行为,类竟然不知道自己的行为与其他类产生了依赖关系(Teacher类中依赖了Student类,然而Student类并不在Teacher类的朋友圈中,一旦Student类被修改了,Teacher类是根本不知道的),这是不允许的。

    正确的做法是:

    class Teacher { 
    public:
    void command(GroupLeader groupLeader) { 
    	        groupLeader.countStudents(); 
      } 
    }
    
    class GroupLeader { 
    private:
    list<Student> listStudents; 
    public:
    GroupLeader(list<Student> _listStudents) { 
    	this.listStudents = _listStudents; 
    } 
    void countStudents() { 
    	cout<<"女生数量是:" <<listStudents.size() <<endl; 
       } 
    }
    

    使用迪米特原则时要考虑的


    • 朋友间也是有距离的

    一个类公开的public属性或方法越多,修改时涉及的面也就越大,变更引起的风险扩散也就越大。因此,为了保持朋友类间的距离,在设计时需要反复衡量:是否还可以再减少public方法和属性,是否可以修改为private等。

    注意: 迪米特原则要求类“羞涩”一点,尽量不要对外公布太多的public方法和非静态的public变量,尽量内敛,多使用private、protected等访问权限。

    • 是自己的就是自己的

    如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,就放置在本类中。

    四、单一职责原则


    永远不要让一个类存在多个改变的理由。

    换句话说,如果一个类需要改变,改变它的理由永远只有一个。如果存在多个改变它的理由,就需要重新设计该类。

    单一职责原则原则的核心含意是:只能让一个类/接口/方法有且仅有一个职责。

    为什么一个类不能有多于一个以上的职责?


    如果一个类具有一个以上的职责,那么就会有多个不同的原因引起该类变化,而这种变化将影响到该类不同职责的使用者(不同用户):

    • 一方面,如果一个职责使用了外部类库,则使用另外一个职责的用户却也不得不包含这个未被使用的外部类库。
    • 另一方面,某个用户由于某个原因需要修改其中一个职责,另外一个职责的用户也将受到影响,他将不得不重新编译和配置。
      违反了设计的开闭原则,也不是我们所期望的。

    职责的划分


    既然一个类不能有多个职责,那么怎么划分职责呢?

    Robert.C Martin给出了一个著名的定义:所谓一个类的一个职责是指引起该类变化的一个原因。

    如果你能想到一个类存在多个使其改变的原因,那么这个类就存在多个职责。

    SRP违反例:

    class Modem {
    		   void dial(String pno);    //拨号
               void hangup();        //挂断
               void send(char c);    //发送数据
               char recv();        //接收数据
    };
    

    乍一看,这是一个没有任何问题的接口设计。
    但事实上,这个接口包含了2个职责:第一个是连接管理(dial,hangup);另一个是数据通信(send,recv)。
    很多情况下,这2个职责没有任何共通的部分,它们因为不同的理由而改变,被不同部分的程序调用。所以它违反了SRP原则。

    下面的类图将它的2个不同职责分成2个不同的接口,这样至少可以让客户端应用程序使用具有单一职责的接口:
    在这里插入图片描述
    让 ModemImplementation实现这两个接口。我们注意到,ModemImplementation又组合了2个职责,这不是我们希望的,但有时这又是必须的。通常由于某些原因,迫使我们不得不绑定多个职责到一个类中,但我们至少可以通过接口的分割来分离应用程序关心的概念。

    事实上,这个例子一个更好的设计应该是这样的,如图:
    在这里插入图片描述
    例如,考虑下图的设计。

    Retangle类具有两个方法,如图。一个方法把矩形绘制在屏幕上,另一个方法计算矩形的面积。
    在这里插入图片描述
    有两个不同的Application使用Rectangle类,如上图。一个是计算几何面积的,Rectangle类会在几何形状计算方面给予它帮助。另一Application实质上是绘制一个在舞台上显示的矩形。

    这一设计违反了单一职责原则。Rectangle类具有了两个职责,第一个职责是提供一个矩形形状几何数据模型;第二个职责是把矩形显示在屏幕上。

    对于SRP的违反导致了一些严重的问题。首先,我们必须在计算几何应用程序中包含核心显示对象的模块。其次,如果绘制矩形Application发生改变,也可能导致计算矩形面积Application发生改变,导致不必要的重新编译,和不可预测的失败。

    一个较好的设计是把这两个职责分离到下图所示的两个完全不同的类中。这个设计把Rectangle类中进行计算的部分移到GeometryRectangle类中。现在矩形绘制方式的改变不会对计算矩形面积的应用产生影响了。
    在这里插入图片描述

    使用单一职责原则的理由


    单一职责原则从职责(改变理由)的侧面上为我们对类(接口)的抽象的颗粒度建立了判断基准:在为系统设计类(接口)的时候应该保证它们的单一职责性。

    降低了类的复杂度、提高类的可读性,提高系统的可维护性、降低变更引起的风险

    五、 接口分隔原则(Interface Segregation Principle ,ISP)


    不能强迫用户去依赖那些他们不使用的接口。

    概念理解


    换句话说,使用多个专门的接口比使用单一的总接口总要好。

    它包含了2层意思:

    • 接口的设计原则:接口的设计应该遵循最小接口原则,不要把用户不使用的方法塞进同一个接口里。如果一个接口的方法没有被使用到,则说明该接口过胖,应该将其分割成几个功能专一的接口。

    • 接口的依赖(继承)原则:如果一个接口a继承另一个接口b,则接口a相当于继承了接口b的方法,那么继承了接口b后的接口a也应该遵循上述原则:不应该包含用户不使用的方法。 反之,则说明接口a被b给污染了,应该重新设计它们的关系。

    如果用户被迫依赖他们不使用的接口,当接口发生改变时,他们也不得不跟着改变。换而言之,一个用户依赖了未使用但被其他用户使用的接口,当其他用户修改该接口时,依赖该接口的所有用户都将受到影响。这显然违反了开闭原则,也不是我们所期望的。

    总而言之,接口分隔原则指导我们:

    1. 一个类对一个类的依赖应该建立在最小的接口上

    2. 建立单一接口,不要建立庞大臃肿的接口

    3. 尽量细化接口,接口中的方法尽量少

    违反ISP原则的设计与重构


    下面我们举例说明怎么设计接口或类之间的关系,使其不违反ISP原则。

    假如有一个Door,有lock,unlock功能,另外,可以在Door上安装一个Alarm而使其具有报警功能。用户可以选择一般的Door,也可以选择具有报警功能的Door。

    有以下几种设计方法:

    ISP原则的违反例一:在Door接口里定义所有的方法。
    在这里插入图片描述
    但这样一来,依赖Door接口的CommonDoor却不得不实现未使用的alarm()方法。违反了ISP原则。

    ISP原则的违反例二:在Alarm接口定义alarm方法,在Door接口定义lock,unlock方法,Door接口继承Alarm接口。
    在这里插入图片描述
    跟方法一一样,依赖Door接口的CommonDoor却不得不实现未使用的alarm()方法。违反了ISP原则。

    遵循ISP原则的例一:通过多重继承实现

    在这里插入图片描述
    在Alarm接口定义alarm方法,在Door接口定义lock,unlock方法。接口之间无继承关系。CommonDoor实现Door接口,AlarmDoor有2种实现方案:

    1)同时实现Door和Alarm接口。

    2)继承CommonDoor,并实现Alarm接口。

    第2)种方案更具有实用性。

    这样的设计遵循了ISP设计原则。

    遵循ISP原则的例二:通过关联实现
    在这里插入图片描述
    在这种方法里,AlarmDoor实现了Alarm接口,同时把功能lock和unlock委让给CommonDoor对象完成。

    这种设计遵循了ISP设计原则。

    接口分隔原则的优点和适度原则


    • 接口分隔原则从对接口的使用上为我们对接口抽象的颗粒度建立了判断基准:在为系统设计接口的时候,使用多个专门的接口代替单一的胖接口。

    • 符合高内聚低耦合的设计思想,从而使得类具有很好的可读性、可扩展性和可维护性。

    • 注意适度原则,接口分隔要适度,避免产生大量的细小接口。

    单一职责原则和接口分隔原则的区别


    单一职责强调的是接口、类、方法的职责是单一的,强调职责,方法可以多,针对程序中实现的细节;

    接口分隔原则主要是约束接口,针对抽象、整体框架。

    六、 依赖倒置原则(Dependency Inversion Principle ,DIP)


    A. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象
    B. 抽象不应该依赖于细节,细节应该依赖于抽象 C.针对接口编程,不要针对实现编程。

    概念理解


    依赖:在程序设计中,如果一个模块a使用/调用了另一个模块b,我们称模块a依赖模块b。

    高层模块与低层模块:往往在一个应用程序中,我们有一些低层次的类,这些类实现了一些基本的或初级的操作,我们称之为低层模块;另外有一些高层次的类,这些类封装了某些复杂的逻辑,并且依赖于低层次的类,这些类我们称之为高层模块。

    依赖倒置(Dependency Inversion)
    面向对象程序设计相对于面向过程(结构化)程序设计而言,依赖关系被倒置了。因为传统的结构化程序设计中,高层模块总是依赖于低层模块。
    在这里插入图片描述
    问题的提出:
    Robert C. Martin氏在原文中给出了“Bad Design”的定义:

    1. 系统很难改变,因为每个改变都会影响其他很多部分。

    2. 当你对某地方做一修改,系统的看似无关的其他部分都不工作了。

    3. 系统很难被另外一个应用重用,因为很难将要重用的部分从系统中分离开来。

    导致“Bad Design”的很大原因是“高层模块”过分依赖“低层模块”。

    一个良好的设计应该是系统的每一部分都是可替换的。如果“高层模块”过分依赖“低层模块”,一方面一旦“低层模块”需要替换或者修改,“高层模块”将受到影响;另一方面,高层模块很难可以重用。

    问题的解决:

    为了解决上述问题,Robert C. Martin氏提出了OO设计的Dependency Inversion Principle (DIP) 原则。

    DIP给出了一个解决方案:在高层模块与低层模块之间,引入一个抽象接口层。
    在这里插入图片描述

    High Level Classes(高层模块) --> Abstraction Layer(抽象接口层) --> Low Level Classes(低层模块)

    抽象接口是对低层模块的抽象,低层模块继承或实现该抽象接口。

    这样,高层模块不直接依赖低层模块,而是依赖抽象接口层。抽象接口也不依赖低层模块的实现细节,而是低层模块依赖(继承或实现)抽象接口。

    类与类之间都通过抽象接口层来建立关系。

    依赖倒置原则的违反例和重构


    示例:考虑一个控制熔炉调节器的软件。该软件从一个IO通道中读取当前的温度,并通过向另一个IO通道发送命令来指示熔炉的开或者关。

    温度调节器的简单算法:

      const byte THERMONETER=0x86;
      const byte FURNACE=0x87;
      const byte ENGAGE=1;
      const byte DISENGAGE=0;
    
      void Regulate(double minTemp,double maxTemp)
      {
         for(;;)
         {
            while (in(THERMONETER) > minTemp)
               wait(1);
            out(FURNACE,ENGAGE);
            
            while (in(THERMONETER) < maxTemp)
               wait(1);
            out(FURNACE,DISENGAGE);
         }
      }
    

    算法的高层意图是清楚的,但是实现代码中却夹杂着许多低层细节。这段代码根本不能重用于不同的控制硬件。

    由于代码很少,所以这样做不会造成太大的损害。但是,即使是这样,使算法失去重用性也是可惜的。我们更愿意倒置这种依赖关系。
    在这里插入图片描述
    图中显示了 Regulate 函数接受了两个接口参数。Thermometer 接口可以读取,而 Heater 接口可以启动和停止。Regulate 算法需要的就是这些。这就倒置了依赖关系,使得高层的调节策略不再依赖于任何温度计或者熔炉的特定细节。该算法具有很好的可重用性。

    通用的调节器算法:

      void Regulate(Thermometer t, Heater h, double minTemp,
         double maxTemp)
      {
        for(;;)
        {
           while (t.Read() > minTemp)
              wait(1);
           h.Engate();
    
           while (t.Read() < maxTemp)
              wait(1);
           h.Disengage();
        }
      }
    

    怎么使用依赖倒置原则


    1. 依赖于抽象

    • 任何变量都不应该持有一个指向具体类的指针或引用。

    如:

    class class1{
    class2* cls2 = new class2();
    }
    class class2{
    .......
    }
    
    • 任何类都不应该从具体类派生。

    2. 设计接口而非设计实现

    • 使用继承避免对类的直接绑定
      在这里插入图片描述

    • 抽象类/接口: 倾向于较少的变化;抽象是关键点,它易于修改和扩展;不要强制修改那些抽象接口/类

    例外:

    有些类不可能变化,在可以直接使用具体类的情况下,不需要插入抽象层,如:字符串类

    3. 避免传递依赖

    • 避免高层依赖于低层
      在这里插入图片描述
    • 使用继承和抽象类来有效地消除传递依赖
      在这里插入图片描述

    依赖倒置原则的优点


    可以减少类间的耦合性、提高系统稳定性,提高代码可读性和可维护性,可降低修改程序所造成的风险。

    七、 组合/聚合复用原则(Composite/Aggregate Reuse Principle ,CARP)


    尽量使用组合/聚合,不要使用类继承。

    概念理解


    即在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分,新对象通过向这些对象的委派达到复用已有功能的目的。就是说要尽量的使用合成和聚合,而不是继承关系达到复用的目的。

    组合和聚合都是关联的特殊种类。

    聚合表示整体和部分的关系,表示“拥有”。组合则是一种更强的“拥有”,部分和整体的生命周期一样。

    组合的新的对象完全支配其组成部分,包括它们的创建和湮灭等。一个组合关系的成分对象是不能与另一个组合关系共享的。

    组合是值的聚合(Aggregation by Value),而一般说的聚合是引用的聚合(Aggregation by Reference)。

    在面向对象设计中,有两种基本的办法可以实现复用:第一种是通过组合/聚合,第二种就是通过继承。

    什么时候才应该使用继承


    只有当以下的条件全部被满足时,才应当使用继承关系:

    • 1)派生类是基类的一个特殊种类,而不是基类的一个角色,也就是区分"Has-A"和"Is-A"。只有"Is-A"关系才符合继承关系,"Has-A"关系应当用聚合来描述。

    • 2)永远不会出现需要将派生类换成另外一个类的派生类的情况。如果不能肯定将来是否会变成另外一个派生类的话,就不要使用继承。

    • 3)派生类具有扩展基类的责任,而不是具有置换掉(override)或注销掉(Nullify)基类的责任。如果一个派生类需要大量的置换掉基类的行为,那么这个类就不应该是这个基类的派生类。

    • 4)只有在分类学角度上有意义时,才可以使用继承。

    总的来说:

    如果语义上存在着明确的"Is-A"关系,并且这种关系是稳定的、不变的,则考虑使用继承;如果没有"Is-A"关系,或者这种关系是可变的,使用组合。另外一个就是只有两个类满足里氏替换原则的时候,才可能是"Is-A" 关系。也就是说,如果两个类是"Has-A"关系,但是设计成了继承,那么肯定违反里氏替换原则。

    错误的使用继承而不是组合/聚合的一个常见原因是错误的把"Has-A"当成了"Is-A" 。"Is-A"代表一个类是另外一个类的一种;"Has-A"代表一个类是另外一个类的一个角色,而不是另外一个类的特殊种类。

    看一个例子:

    如果我们把“人”当成一个类,然后把“雇员”,“经理”,“学生”当成是“人”的派生类。这个的错误在于把 “角色” 的等级结构和 “人” 的等级结构混淆了。“经理”,“雇员”,“学生”是一个人的角色,一个人可以同时拥有上述角色。如果按继承来设计,那么如果一个人是雇员的话,就不可能是学生,这显然不合理。
    在这里插入图片描述
    正确的设计是有个抽象类 “角色”,“人”可以拥有多个“角色”(聚合),“雇员”,“经理”,“学生”是“角色”的派生类。
    在这里插入图片描述

    通过组合/聚合复用的优缺点


    优点:

    • 1.新对象存取子对象的唯一方法是通过子对象的接口。
    • 2.这种复用是黑箱复用,因为子对象的内部细节是新对象所看不见的。
    • 3.这种复用更好地支持封装性。
    • 4.这种复用实现上的相互依赖性比较小。
    • 5.每一个新的类可以将焦点集中在一个任务上。
    • 6.这种复用可以在运行时间内动态进行,新对象可以动态的引用与子对象类型相同的对象。
    • 7.作为复用手段可以应用到几乎任何环境中去。

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

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


    优点:

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

    缺点:

    • 继承复用破坏封装性,因为继承将基类的实现细节暴露给派生类。由于基类的内部细节常常是对于派生类透明的,所以这种复用是透明的复用,又称“白箱”复用。

    • 如果基类发生改变,那么派生类的实现也不得不发生改变。

    • 从基类继承而来的实现是静态的,不可能在运行时间内发生改变,没有足够的灵活性。

    展开全文
  • 设计原则硬核干货

    千次阅读 热门讨论 2021-05-21 22:39:44
    一篇文章帮你拿下设计模式的核心:设计原则,万字长文 以下所有的原则,都不能脱离应用场景!! 很多人都过编写代码时候无所适从,无法写出十分优雅,高效的代码。 也有些人在翻阅设计模式的时候摸不透各种奇奇怪...

    文章有点长,推荐先进行收藏!
    精心编写,在迷茫的时候可以反复观看!

    一篇文章帮你拿下设计模式的核心:设计原则,万字长文

    以下所有的原则,都不能脱离应用场景!!

    不管是应用设计原则还是设计模式,最终的目的还是提高代码的可读性、可扩展性、复用性、可维护性等。

    一、SOLID五大原则

    1、单一职责原则(SRP - Single Resposibility Princple)

    简单清晰的定义:一个类只负责完成一个职责或者功能。也就是说,不要设计大而全的类,要设计粒度小、功能单一的类。

    如何判断一个类的职责是否足够单一?

    举个例子:

    public class UserInfo {
        private long userId;
        private String username;
        private String email;
        private String telephone;
        private long createTime;
        private long lastLoginTime;
        private String avatarUrl;
        private String provinceOfAddress; // 省
        private String cityOfAddress; // 市
        private String regionOfAddress; // 区
        private String detailedAddress; // 详细地址
        // ... 省略其他属性和方法...
    }
    

    对于这个类的设计,有两种观点:

    • 第一种:如果我们从“用户”这个业务层面来看,UserInfo 包含 的信息都属于用户,满足职责单一原则。

    • 第二种:如果我们从更加细分的“用户展示信息”“地址信 息”“登录认证信息”等等这些更细粒度的业务层面来看,那 UserInfo 就应该继续拆分。

    综上所述,评价一个类的职责是否足够单一,我们并没有一个非常明确的、可以量化的标准,可以说,这是件非常主观、仁者见仁智者见智的事情。实际上,在真正的软件开发中, 我们也没必要过于未雨绸缪,过度设计。所以,我们可以先写一个粗粒度的类,满足业务需 求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度类。这就是所谓的持续重构(文章后面部分将会具体提及)

    对于单一职责原则的定义,我们不好判断是否满足条件。

    所以我们可以从以下几条具体的指导原则来进行设计:

    • 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分;
    • 类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分;
    • 私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性;
    • 比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰;
    • 类中大量的方法都是集中操作类中的某几个属性,比如,在 UserInfo 例子中,如果一半的方法都是在操作 address 信息,那就可以考虑将这几个属性和对应的方法拆分出来。

    实际上,不管是应用设计原则还是设计模式,最终的目的还是提高代码的可读性、可扩展性、复用性、可维护性等。我们在考虑应用某一个设计原则是否合理的时候,也可以以此作为最终的考量标准。

    总结 + 提问

    1. 如何理解单一职责原则(SRP)?

    一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。

    2. 如何判断类的职责是否足够单一?

    不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。实际上,一些侧面的判断指标更具有指导意义和可执行性,比如,出现下面这些情况就有可能说明这类的设计不满足单一职责原则:

    • 类中的代码行数、函数或者属性过多;
    • 类依赖的其他类过多,或者依赖类的其他类过多;
    • 私有方法过多;
    • 比较难给类起一个合适的名字;
    • 类中大量的方法都是集中操作类中的某几个属性。

    3. 类的职责是否设计得越单一越好?

    单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。

    2、开闭原则(OCP - Open Closed Principle)

    简单清晰的定义:对扩展开放,对修改关闭

    稍微详细一点的解读:添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。

    修改代码就意味着违背开闭原则吗?

    我们再一块回忆一下开闭原则的定义:软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。从定义中,我们可以看出,开闭原则可以应用在不同粒度的代码中,可以是模块,也可以类,还可以是方法(及其属性)。同样一个代码改动,在粗代码粒度下,被认定为“修改”,在细代码粒度下,又可以被认定为“扩展”。比如,在一个类中添加属性和方法相当于修改类,在类这个层面,这个代码改动可以被认定为“修改”;但这个代码改动并没有修改已有的属性和方法,所以在方法(及其属性)这一层面,它又可以被认定为“扩展”。

    如何做到对扩展开放、修改关闭?

    实际上,开闭原则讲的就是代码的扩展性问题,是判断一段代码是否易扩展的“金标准”。如果某段代码在应对未来需求变化的时候,能够做到“对扩展开放、对修改关闭”,那就说明这段代码的扩展性比较好。所以,问如何才能做到“对扩展开放、对修改关闭”,也就粗略地等同于在问,如何才能写出扩展性好的代码。

    这里要先明确一个指导思想:

    • 在写代码的时候后,我们要多花点时间往前多思考一下,这段代码未来可能有哪些需求变更、如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,不需要改动代码整体结构、做到最小代码改动的情况下,新的代码能够很灵活地插入到扩展点上,做到“对扩展开放、对修改关闭”。
    • 其次,在识别出代码可变部分和不可变部分之后,我们要将可变部分封装起来,隔离变化,提供抽象化的不可变接口,给上层系统使用。当具体的实现发生变化的时候,我们只需要基于相同的抽象接口,扩展一个新的实现,替换掉老的实现即可,上游系统的代码几乎不需要修改。

    在众多的设计原则、思想、模式中,最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态等)。

    如何在项目中灵活应用开闭原则?

    前面我们提到,写出支持“对扩展开放、对修改关闭”的代码的关键是预留扩展点。那问题是如何才能识别出所有可能的扩展点呢?

    • 如果你开发的是一个业务导向的系统,比如金融系统、电商系统、物流系统等,要想识别出尽可能多的扩展点,就要对业务有足够的了解,能够知道当下以及未来可能要支持的业务需求。
    • 如果你开发的是跟业务无关的、通用的、偏底层的系统,比如,框架、组件、类库,你需要了解“它们会被如何使用?今后你打算添加哪些功能?使用者未来会有哪些更多的功能需求?”等问题。

    有一句话说得好,“唯一不变的只有变化本身”。即便我们对业务、对系统有足够的了解,那也不可能识别出所有的扩展点,即便你能识别出所有的扩展点,为这些地方都预留扩展点,这样做的成本也是不可接受的。我们没必要为一些遥远的、不一定发生的需求去提前买单,做过度设计。

    针对这个情况我们要在合理的控制范围能进行一定的设计,并根据上面提到过的观点,针对后续的业务需求变更之后,再对代码的设计进行持续重构

    总结 + 问题

    1. 如何理解“对扩展开放、对修改关闭”?

    添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。关于定义,我们有两点要注意。第一点是,开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。第二点是,同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”。
    

    2. 如何做到“对扩展开放、修改关闭”?

    我们要时刻具备扩展意识、抽象意识、封装意识。在写代码的时候,我们要多花点时间思考一下,这段代码未来可能有哪些需求变更,如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,在不改动代码整体结构、做到最小代码改动的情况下,将新的代码灵活地插入到扩展点上。
    
    很多设计原则、设计思想、设计模式,都是以提高代码的扩展性为最终目的的。特别是 23 种经典设计模式,大部分都是为了解决代码的扩展性问题而总结出来的,都是以开闭原则为 指导原则的。最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编 程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态)。
    

    3、里氏替换原则 (LSP - Liskov Substitution Principle)

    简单清晰的定义:子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。

    核心思维:子类完美继承父类的设计初衷,并做了增强

    举个简单的例子来说明一下:

    public class LiskovSubstitutionPrinciple {
        public static void main(String[] args) throws Exception {
            // Human student = new Student();
            Human human = new Human();
            Student student = new Student();
            human.eat("bread");
            human.eat("d");
            student.eat("d");
        }
    }
    
    class Human {
        public void eat(String things) throws Exception {
            System.out.println("I am eating " + things);
        }
    }
    
    class Student extends Human{
        @Override
        public void eat(String things) throws Exception {
            if (things.length() < 2) {
                throw new Exception("You are eating shit ?");
            }
            System.out.println("I am eating " + things);
        }
    }
    

    运行结果十分清晰明了:

    I am eating bread
    I am eating d
    Exception in thread "main" java.lang.Exception: You are eating shit ?
    	at designpattern.solid.Student.eat(LiskovSubstitutionPrinciple.java:23)
    	at designpattern.solid.LiskovSubstitutionPrinciple.main(LiskovSubstitutionPrinciple.java:9)
    

    上述现象是完全不满足里氏替换原则的,同一个函数在父类和子类声明的对象中调用获得了我们不想得到的结果。

    更细致的解释一下:子类在设计的时候,应当遵守父类的行为约定(或者叫协议)。父类定义了函数的行为约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。**这里的行为约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。**实际上,定义中父类和子类之间的关系,也可以替换成接口和实现类之间的关系。

    总结

    里式替换原则是用来指导,继承关系中子类该如何设计的一个原则。理解里式替换原则,最核心的就是理解“design by contract,按照协议来设计”这几个字。父类定义了函数 的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至 包括注释中所罗列的任何特殊说明。

    理解这个原则,我们还要弄明白里式替换原则跟多态的区别。虽然从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,用来指导继承关系中子类该如何设计,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑及不破坏原有程序的正确性。

    4、接口隔离原则(ISP - Interface Segregation Principle)

    接口隔离中对接口的三种理解:一组 API 接口集合、单个 API 接口、函数 OOP 中的接口概念。下面对这三种理解分别进行详细的阐述。

    • 一组 API 接口集合

    举个例子:

    class User {
        private int id;
        private String username;
        private String password;
        private String phone;
    }
    
    interface UserService {
        void login(String username, String password);
        void register(User user);
        User getUserById(int id);
        User deleteUserById(int id);
        User deleteUserByPhone(String phone);
    }
    

    根据代码,我们可以看出来,UserService接口中罗列了几个看起来没什么问题的方法。不过在实际生产需求之中,接口中的deleteUserById方法和deleteUserByPhone方法如果暴露给所有实现类,可能会因为接口全部暴露而导致误操作,而产生不必要的麻烦,这个时候我们根据接口隔离原则可以设计成如下的方式:

    interface UserService {
        void login(String username, String password);
        void register(User user);
        User getUserById(int id);
    }
    
    interface RestrictUserService {
        User deleteUserById(int id);
        User deleteUserByPhone(String phone);
    }
    

    这样,就很明朗了。讲不同级别的接口进行隔离开来,在需要实现的时候,再进行分别的引入。这样就可以使调用者可以根据需要进行引入,而不必强迫的实现不需要使用到的接口。

    • 单个 API 接口或函数
    class DataInfo {
        private long sum;
        private long avg;
        private long max;
        private long min;
    		// 省略 getter,setter
    }
    
    class DataHandler {
        
        public DataInfo DataCalc() {
            DataInfo dataInfo = new DataInfo();
            // 省略庞大的计算逻辑
            dataInfo.setMax(...);
            dataInfo.setMin(...);
            dataInfo.setSum(...);
            dataInfo.setAvg(...);
            return dataInfo;
        }
    }
    

    此时我们如果发现如果这个数据计算类这么设计,会使得整个方法十分的臃肿;如果我们讲各参数的计算分开设计,如下

    interface DataCalc {
        public long calcSum();
        public long calcAvg();
        public long calcMax();
        public long calcMin();
    }
    
    class DataHandler implements DataCalc{
        
        public DataInfo DataCalc() {
            DataInfo dataInfo = new DataInfo();
    	      dataInfo.setSum(calcSum());
          	// 省略其他参数的计算
            return dataInfo;
        }
    		// 分别重写实现方法,将具体业务逻辑分离开
        @Override
        public long calcSum() {
          	// 计算 calcSum 的逻辑代码
            return 0;
        }
    		// 这里省略其他的重写实现
    }
    

    不难发现,我们的代码优美了不少,而且今后在遇到业务逻辑需要变更的时候,可以更加清晰的对代码进行重构

    • OOP 中的接口概念

    假设我们的项目中用到了三个外部系统:Redis、MySQL、Kafka。每个系统都对应一系列 配置信息,比如地址、端口、访问超时时间等。为了在内存中存储这些配置信息,供项目中 的其他模块来使用,我们分别设计实现了三个 Configuration 类:RedisConfig、MysqlConfig、KafkaConfig。每个类都有一些共有的一些功能比如update等,不过他们的逻辑实现分别不同。这个时候我们可以将他们共有的方法进行向上抽取出来,因为是OOP中的接口,我们之前也有提及,这里就不再进行过多的阐述了。

    问题

    接口隔离原则与单一职责原则的区别

    单一职责原则针对的是模块、类、接口的设计。接口隔离原则相对于单一职责原则,一方面更侧重于接口的设计,另一方面它的思考角度也是不同的。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。

    5、依赖倒置原则(DIP - Dependence Inversion Principle)

    原文定义:高层模块不要依赖低层模块。高层模块和低层模块应该通过抽象来互相依赖。除此之外,抽象不要依赖具体实现细节,具体实现细节依赖抽象。

    百度定义;依赖倒置原则(Dependence Inversion Principle)是程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

    依赖倒置的核心思想是面向接口编程

    依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在java中,抽象指的是接口或抽象类,细节就是具体的实现类。
    使用接口或抽象类的目的是:制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。

    举一个生动形象的例子:

    // 上层模块依赖下层实现
    class UploadFile {   
        public void uploadFileToBaiduyun() {
            // TODO 上传至百度云实现逻辑
        }
    }
    

    大家应该都写过这样的案例,但是这样的实现方式对吗?这样的实现方式不是错误的,但是如果现在业务需求变更了,我们将上传至百度云更换为上传至阿里云了,这样子我们将要修改uploadfile类内的方法,如下:

    // 上层模块依赖下层实现
    class UploadFile {   
    //    public void uploadFileToBaiduyun() {
            // TODO 上传至百度云实现逻辑
    //    }
      
        public void uploadFileToAliYun() {
            // TODO 上传至阿里云实现逻辑
        }
    }
       
    

    根据我们上面描述道的开闭原则:对扩展开放,对修改关闭。显然是有一些冲突的。因此我们根据依赖倒置原则,可以将代码进行一定的优化,使其变得更有扩展性和维护性,如下:

    interface UploadFile {
        public void uploadFile();
    }
    
    class UploadFileToBaiduYun implements UploadFile {
        @Override
        public void uploadFile() {
            // TODO 上传至百度云实现逻辑
        }
    }
    
    class UploadFileToAliYun implements UploadFile {
        @Override
        public void uploadFile() {
            // TODO 上传至阿里云实现逻辑
        }
    }
    

    这样后来需求增加上传什么云服务器的时候,我们都可以以扩展的方式加进来,而不用去修改以前的上传方式,这样随着产品的迭代,面对业务需求的变更时,我们就可以游刃有余的对项目进行扩展,而不是改来改去。

    二、KISS、YAGNI、DRY

    在除了设计模式的SOLID五大设计原则还有这么三个经典的设计原则,比较偏向具体编码细节

    KISS:Keep It Stupid and Simple

    保持简单,曾经读到的一本书有看过这么一句话: 在编写代码的时候大家往往喜欢追求“高大上”的代码,以为写出让人们很难看懂的代码,才是厉害的代码。恰恰相反,高手都是将最复杂的思想用最简单的代码清晰直观的表现出来。

    上面这句话深深点醒了我,不知道大家编写代码的时候有没有这样的经历:

    • 这个地方,我可以用位运算操作一手~ 效率杠杠的!
    • 这个地方,我可以手写一个轮子,让项目更高效!
    • 这个地方,我可以用正则进行匹配,代码简练清晰!
    • 这个地方,我可以 … …

    不是说这是错误的想法,恰恰相反在有些业务效率达到瓶颈的时候,这样细节部分的优化是有必要的。不过这样的代码,可读性就有一点令人堪忧了。所以如何去平衡效率与可读性呢。 在业务开发的时候,应该遵循这么几个思想:

    • 这个地方可以用提供的工具类进行操作。
    • 这个地方可以拆分成几个简单的逻辑类接口,还可以进行复用。
    • 不要使用大家不懂的技术来实现代码
    • 不要过度优化,不要使用奇淫技巧

    其实这是一个十分主观的思想,因为不同的人对代码的理解能力是有差异的,很多人对位运算,正则,lambda等觉得十分清晰简单,而有的人却没有过深刻的了解,就导致不同人对代码可读性的评判是有区别的。因此我们要尽量在不牺牲可读性的前提下,对代码进行优化,提高团队开发效率。

    YAGNI:You Ain’t Gonna Need It

    直译就是:你不会需要它。这个原则在提醒我们在设计开发的时候不要过度设计,不仅提高了时间成本,而且降低了开发效率。这样子是十分不必要的,我们可以在相应的地方留出一些扩展点即可,在业务需求迭代的时候,在进行加入适当的逻辑即可。

    DRY:Don’t Repeat Yourself

    简单粗暴的理解:不要重复!

    可以分为三个方向:逻辑实现重复、语义功能重复、代码执行重复。

    • 逻辑实现重复:
    // 简化版判断注册校验参数逻辑
        public boolean validateRegisterParameter(String username, String password) {
            if (validateUserName(username) && validatePassword(password)) return true;
            return false;
        }
        public boolean validateUserName (String username) {
            if (username == null) return false;
            // TODO 对username 格式进行判断
            // TODO 判断username是否满足6-18位
            // TODO 判断username是否包含字母大小写
            // TODO 判断username是否含有非法字符
            return true;
        }
    
        public boolean validatePassword (String password) {
            if (password == null) return false;
            // TODO 对username 格式进行判断
            // TODO 判断username是否满足6-18位
            // TODO 判断username是否包含字母大小写
            // TODO 判断username是否含有非法字符
            return true;
        }
    

    看着是不是十分头大,一样的逻辑 分了两个不同的方法,这个时候,我们可以将相同部分以抽象的方式抽取出一个新的方法:

    public boolean validateUserName (String username) {
      	if (!validateStringPattern(username)) return false;
      	// ...
      	return true;
    }
    
    public boolean validatePassword (String password) {
      	if (!validateStringPattern(password)) return false;
      	return true;
    }
    public boolean validateStringPattern(String s) {
        if (s == null) return false;
        // TODO 对s 格式进行判断
        // TODO 判断s是否满足6-18位
        // TODO 判断s是否包含字母大小写
        // TODO 判断s是否含有非法字符
        return true;
    }
    
    • 语义功能重复:
    public void isValidData(Data data) {
      // TODO 对data进行验证
    }
    
    public void checkIfDataValid(Data data) {
      // TODO 对data进行验证
    }
    

    上面代码中,同一语义对方法名,会引起歧义,如果在其他业务中分别调用了不同的方法,在业务进行更迭重构的时候,会使不清楚的编程人员不明白这两个函数有什么区别,付出不必要的额外时间成本。其次,若今后对其中一个验证方法进行了重构,而忘记了对另一个方法进行重构,则会引起比较难以发现的bug。

    • 代码执行重复:
    class UserService {
        private UserRepo userRepo = new UserRepo();
        boolean login(User user) {
            String phone = user.getPhone();
          	// 这里执行了一次 checkUserByPhone()
            if(userRepo.checkUserByPhone(phone)) {
                userRepo.register(user);
                return true;
            }
            return false;
        }
    }
    
    class UserRepo {
        private UserDao userDao;
        
        boolean checkUserByPhone(String phone) {
            if (userdao.getUserByPhone(phone) == null) return true;
            return false;
        }
        
        void register(User user) {
          	// 这里又执行了一个 checkUserByPhone()
            if (checkUserByPhone(user.phone)) {
                // TODO 注册逻辑
            }
            return;
        }
    }
    

    上述代码就很明显的反应了代码执行重复这么一种情况,重复执行一段代码是很没有必要的,而且例子中的代码还是对数据库进行操作,更是极大的影响了程序执行的效率。

    三、迪米特法则(LOD - Law of Demeter)

    在介绍完经典的SOLID五大原则和KISS、YAGNI、DRY几个实用的原则后,我们来聊一聊这个超级优美的一个法则:迪米特法则。

    至于为什么说迪米特法则十分的优美。我们来看一看迪米特法则要实现什么。

    定义:迪米特法则(Law of Demeter)又叫作最少知识原则(The Least Knowledge Principle),一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话。

    核心思想:实现高内聚,低耦合

    "高内聚、低耦合"是一个非常重要的设计思想,能够有效地提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。实际上,在前面的章节中,我们已经多次提到过这个设计思想。很多设计原则都以实现代码的“高内聚、松耦合”为目的,比如单一职责原则、基于接口而非实现编程等。
    

    我们通过前面讲述的所有原则,基本上都可以直接或间接的通向这么一个道路 -> 实现高内聚,低耦合。

    什么是“高内聚”?

    所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。 相近的功能往往会被同时修改,放到同一个类中,修改会比较集中,代码容易维护。实际 上,我们前面讲过的单一职责原则是实现代码高内聚非常有效的设计原则。

    什么是“低耦合”?

    所谓低耦合,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的代码改动。实际上,我们前面讲的依赖注入、接口隔离、基于接口而非实现编程,以及今天讲的迪米特法则,都是为了实现代码的松耦合。

    我们结合具体的代码进行分析:

    class Serialization {
        public String serialize(Object object) {
            String serializeResult = "";
            // TODO 计算值操作...
            return serializeResult;
        }
    
        public Object deserialize(String str) {
            Object deserializeResult = null;
            // TODO 计算值操作...
            return deserializeResult;
        }
    }
    

    上述代码实现了序列化与反序列化的操作,乍一看,这个代码是没有任何问题的,不过站在高内聚和低耦合的角度上分析,还是有一定的优化空间的,根据最小知识原则:

    在调用序列化的时候是完全不需要知道反序列化的操作是什么样子的,因此我们可能会做出如下设计:

    class Serialize {
        public String serialize(Object object) {
            String serializeResult = "";
            // TODO 计算值操作...
            return serializeResult;
        }
    }
    
    class Deserialize {
        public Object deserialize(String str) {
            Object deserializeResult = null;
            // TODO 计算值操作...
            return deserializeResult;
        }
    }
    

    虽然满足的迪米特法则的最小知识原则了,但是不符合高内聚的这个思想。高内聚要求相近的功能要放到同一个类中,这样可以方便功能修改的时候,修改的 地方不至于过于分散。对于刚刚这个例子来说,如果我们修改了序列化的实现方式,比如从 JSON 换成了 XML,那反序列化的实现逻辑也需要一并修改。在未拆分的情况下,我们只需要修改一个类即可。在拆分之后,我们需要修改两个类。显然,这种设计思路的代码改动范围变大了。那么我们该如何改进呢?

    为了既能实现高内聚,又能实现迪米特的最小知识原则。我们只需引入两个接口:

    interface Serialize {
        public String serialize(Object object);
    }
    
    interface Deserialize {
        public Object deserialize(String str);
    }
    
    class Serialization implements serialize, deserialize{
    
        @Override
        public String serialize(Object object) {
            // TODO 序列化
            return null;
        }
    
        @Override
        public Object deserialize(String str) {
            // TODO 反序列化
            return null;
        }
    }
    

    是不是豁然开朗!!!

    一切都是那么的清晰明了,在调用的时候仅仅需要:

    Serialize serializeObject = new Serialization();	// 多态声明
    serializeObject.serialize();
    

    既隔离了接口,又内聚了模块。简直完美,这种高内聚,低耦合的思想在其他原则中也都有隐约的体现。

    如果觉得文章对你有帮助,点个赞支持一下噻~~

    展开全文
  • 尼尔森十大交互设计原则

    千次阅读 2019-04-21 12:16:30
    Jakob Nielsen(雅各布·尼尔森)的十大交互设计原则。它们被称为“启发式”,因为它们是广泛的经验法则,而不是特定的可用性指导原则。因此,我们不能把它上升为一种标准,而是应该当做一种经验来学习,然后跟现实...

    前言

    Jakob Nielsen(雅各布·尼尔森)的十大交互设计原则。它们被称为“启发式”,因为它们是广泛的经验法则,而不是特定的可用性指导原则。因此,我们不能把它上升为一种标准,而是应该当做一种经验来学习,然后跟现实中的设计结合来使用。接下来,作者通过一些具体的实例来跟大家深度解析尼尔森十大交互设计原则在设计中的用法~

    作者简介

    雅各布·尼尔森(Jakob Nielsen)是毕业于哥本哈根的丹麦技术大学的人机交互博士 , 他拥有79项美国专利,专利主要涉及让互联网更容易使用的方法。尼尔森在2000年6月,入选了斯堪的纳维亚互动媒体名人堂,2006年4月,并被纳入美国计算机学会人机交互学院,被赋予人机交互实践的终身成就奖 。他还被纽约时报称为“Web 易用性大师”,被 Internet Magazine 称为 “易用之王”

    原则一 状态可感知

    系统应该让用户时刻清楚当前发生了什么事情,也就是快速的让用户了解自己处于何种状态、对过去发生、当前目标、以及对未来去向有所了解,一般的方法是在合适的时间给用户适当的反馈,防止用户使用出现错误。

    自检: 

    1. 菜单tab记录是否明显?菜单Tab可以让用户知道自己当前在哪个位置,否则他们就会在我们的产品里迷路。
    2.  标题栏是否明确?文章的标题是一篇文章的招牌,而标题栏就是一个页面的招牌,一个页面如果连标题栏都没有,或者标题栏和内容不符,那用户体验也是极差的。
    3. 数据加载是否有进度提醒?

    案例一:比如今日头条的下拉刷新功能:头条页面的刷新功能使用的是下拉刷新的交互方式,当用户下拉页面时,页面状态栏跟内容区中间会出现“新年快乐”的提示,当我松开页面中间会出现“推荐中”的动态提示,加载完毕之后中间出现一条“今日头条推荐引擎有8条更新”的文字提示;这一系列的提示就是我们所说的动态可见原则,如下图:

    案例二:比如安心记加班中关注和取消圈子功能:当用户点击关注按钮之后,页面中间会出现一个“关注成功”的提示,停留2S之后消失;类似这种,操作之后的提示也是状态可见原则的一中,如下图:

    原则二 环境贴切原则

    软件系统应该使用用户熟悉的语言、文字、语句,或者其他用户熟悉的概念,而非系统语言。软件中的信息应该尽量贴近真实世界,让信息更自然,逻辑上也更容易被用户理解。

    自检:

    1.  所有的图标或者术语是否都在用户的理解范围内?
    2. 有没有容易产生歧义的图标?
    3. 除了图标之外,所有的文字,用户是不是都能读懂?有没有技术性语言是贴近技术而不是贴近用户的?

    案例一:比如计算器的软件界面设计:现在我们手机中的计算器软件设计界面,基本上跟我们现实中使用的计算器的样式差不多,下图左一是我们现实中是使用的计算器,左二、三依次为锤子手机和苹果X自带计算器软件的界面,真的是很相似,这样设计能让用户很快上手,易于操作,因为现实生活中用户已经很熟悉计算器的使用方法了,这就是环境贴切原则:

    案例二:比如新浪微博安卓的中文版和国际版:微博的中文版和国际版的logo和内部页面风格、语言、结构布局包括交互方式也不一样;考虑到国外用户的使用,软件的语言默认为英文,当然还支持各种语言版本,可以根据所需在设置中调整,另外国际版界面的布局使用的设计风格完全遵守谷歌的设计规范,这就是环境贴切原则,具体看下图:

    原则三 用户可控原则

    用户常常会误触到某些功能,我们应该让用户可以方便的退出。这种情况下,我们应该把“紧急出口”按钮做的明显一点,而且不要在退出时弹出额外的对话框。很多用户发送一条消息、总会有他忽然意识到自己不对的地方,这个叫做临界效应;所以最好支持撤销/重做功能。

    自检:

    1. 是否支持撤销或者重做?

    案例一:比如微信聊天中的撤回功能:两个人在微信中聊天的时候,我发了一条消息或者表情,突然觉得不合适,我可以在长按这条消息或者表情,在出现的选择框中选择撤回,然后重新编辑发送,来避免一时没想好而错发消息可能给对方或者自己造成困扰,这就是用户可控原则。

    案例二:比如谷歌相册删除照片之后的撤销功能:在使用谷歌相册的时候,我们会对照片做一些操作,比如照片的删除,当我在谷歌相册中删除一张照片的时候,它会在底部出现一条提示框,框内后边就会出现撤销的提示,这也是用户可控原则的体现。

    原则四 一致性原则

    对于用户来说,同样的文字、状态、按钮,都应该触发相同的事情,遵从通用的平台惯例;也就是,同一用语、功能、操作保持一致。软件产品的一致性包括以下五个方面:

    • 1. 结构一致性:保持一种类似的结构,新的结构变化会让用户思考,规则的排列顺序能减轻用户的思考负担;

    案例:例如微信每个模块的条目布局:微信中每个模块的条目都有统一的“图标+文字信息”的结构样式,能让用户快速了解朋友圈、扫一扫、摇一摇、看一看、搜一搜、附近的人、漂流瓶、购物、游戏及小程序等功能都是作什么的,这就是结构一致性的体现。

    • 2. 色彩一致性:产品所使用的主要色调应该是统一的,而不是换一个页面颜色就不同;

    案例:例如网易云音乐的颜色:网易云音乐的图标颜色与界面的主色均为红色,也包括其中一些标签和强调的文字颜色都是红色,整个界面除了图片的有效信息外,都通过灰、白、红色来呈现,界面保持了很好的一致性,这就是色彩一致性原则。

    • 3. 操作一致性:能让产品更新换代时仍然让用户保持对原产品的认知,减小用户的学习成本;

    案例:比如安卓版微信、支付宝和钉钉APP中左上角的返回操作:它们三个安卓版的应用内返回上一级操作,都是通过顶部左侧的返回按钮进行的,当然也可以通过安卓的物理返回键,这就是操作一致性的体现。

    • 4. 反馈一致性:用户在操作按钮或者条目的时候,点击的反馈效果应该是一致的;

    案例:比如安卓版手机QQ信息列表的打开方式:它的信息都是列表式结构,不管你点击那一行条目,下一级界面都是由右往左滑出,点击顶部左上角的返回按钮会从左往右滑回,体验相当一致;这就是反馈一致性的体现。

    • 5. 文字一致性:产品中呈现给用户阅读的文字大小、样式、颜色、布局等都应该是一致的;

    案例:例如微信几个关键界面的字体:下图我用红色框框起来的条目部分的文字,三个主界面不尽相同,但是,字体大小、颜色、布局的样式都一样,这样让整个APP视觉上看起来很舒服,这就是字体一致性,因此,我们在做视觉设计的时候尽量使用同意风格的文字。

     

    自检:

    1. 同一个含义的功能是否是同一个称呼或者同一个图标?
    2. 返回和前进按钮的位置是否都保持不变?有没有出现有的页面有而有的页面没有的情况?

    原则五 防错原则

    比一个优秀错误提醒弹窗更好的设计方式,是在这个错误发生之前就避免它。可以帮助用户排除一些容易出错的情况,或在用户提交之前给他一个确认的选项。在此,特别要注意在用户操作具有毁灭性效果的功能时要有提示,防止用户犯不可挽回的错误。

    自检:

    1. 在容易犯错的重要操作是否有二次确认?比如取消订单,比如删除记录等等。
    2. 在做出删除等不可恢复的操作之前,有没有文字提示?

    案例一:比如知乎安卓版本的登录操作:当用户在知乎中登录时,在没有填写完手机号码和密码前,底部的登录按钮是置灰不可点击的,只有两项都填写完整底部的登录按钮才会变为可点击状态,也就会蓝色的,这就是为了防止用户犯更多错误,也是防错原则的一种体现。

    案例二:比如安卓版微信发朋圈动态时,点击返回按钮出现的提示弹窗:弹出框方式会增加不可逆操作的难度,当用户发一条动态一半的时候,因为误操作或者其它退出当前状态的时候,使用弹窗是个不错的选择,因为用户这个操作会让之前辛苦编辑的内容删除找不回,想要再发只能从头开始,对用户造成损失比较大;这就是防错原则的另外一种体现。

    原则六 易取原则

    通过把组件、按钮及选项可见化,来降低用户的记忆负荷。用户不需要记住各个对话框中的信息。软件的使用指南应该是可见的,且在合适的时候可以再次查看。

    自检:

    1. 已填写的内容,是否在最终确认时向用户展示以示确认?
    2. 已选择的商品和数量在最终下单前有没有和用户再次确认过?

    案例一:比如谷歌相册中的删除照片操作:用一个类似垃圾桶的“图标”标识删除功能,对于用户来讲是有一定的认知负荷的,且点击“删除”之后用户对于造成的后果及影响也不清楚,因此,删除之后出现弹窗提示很有必要,此弹窗清除的写明了删除之后的影响、后续的帮助说明以及操作的选项,弹出框的出现很好的减少了用户前后的记忆负荷,这就是易取原则的体现。

    案例二:比如安卓版爱奇艺更新后的新功能引导:更新完APP之后,当用户触发到这些功能时,会出现下图类型的遮罩类的提示,这些提示告诉用户功能所在的地方以及功能的作用;这种做法在很多APP中都会出现,这也是易取原则的一种体现,看下图:

    原则七 灵活高效原则

    汽车油门—新手用户常常看不见,而且对于高手来说可以通过它快速与汽车互动。这样的系统可以同时满足有经验和无经验的用户。允许用户定制常用功能

    自检:

    1. 加载速度够不够快?
    2. 用起来是不是能快速上手?
    3. 功能有没有过于复杂不好理解?
    4. 所有能点击的地方,指向性是否明确?有没有让用户迟疑的点击事件?
    5. 最重要的是,你的中级用户都明白如何使用吗?

    案例一:比如安卓版本支付宝中的编辑应用功能:支付宝首页的应用是可以根据自身喜好自定义的,包括定义常用应用、排序、删除、新增等等;这样用户可以根据自己的个人兴趣定制自己适合的应用分布方式,这就叫做用户定制常用功能,也就是灵活高效原则的一种体现,如下图:

    案例二:比如安卓版QQ聊天常用表情模块:安卓版本的QQ聊天界面表情弹窗中会有一个“常用表情”的模块,它把个人平时使用频率或者次数最多的表情进行归类,当用户使用的时候能很快的找到自己喜欢或者常用的表情,提高了聊天效率,体验很好,这也是灵活高效原则的体现。

    原则八 优美且简约原则

    对话中的内容应该去除不相关的信息或几乎不需要的信息。任何不相关的信息都会让原本重要的信息更难被用户察觉。

    自检: 

    1. 重要的功能/内容是否突出?
    2.  版面划分是否清楚?
    3. 不重要的功能是否隐藏或转移或弱化?

    案例一:苹果手机中自带的软件(IOS11设计规范):在新版本的苹果手机中自带的软件中标题都属于字体放大,界面简洁的设计风格;还有苹果自带的音乐软件中,段落中的标题和正文区别是很明显的,标题明显很大,而正文部分相对较小,这就是优美简约原则的体现。

    案例二:例如安卓版网易云音乐及QQ音乐播放页面:网易云音乐和QQ音乐APP音乐播放界面,从视觉及功能布局上面做的相当不错,美观简约、功能主次分明、用户体验不错;也是优美且简约原则的一种体现,如下图:

    原则九 容错原则

    错误信息应该使用简洁的文字(不要用代码),指出错误是什么,并给出解决建议。也就是在用户出错时如何为出错的用户提供及时正确的帮助?即要帮助用户识别出错误,分析出错误的原因再帮助用户回到正确的道路上。如果真的不能帮助用户从错误中恢复,也要尽量为用户提供帮助让用户损失降到最低。

    自检:

    1. 如果出现了错误,是否有人性化的提示告诉用户应该去哪儿而不是只有404?
    2.  会不会出现异常操作导致应用崩溃的情况发生?

    案例一:比如网易邮箱PC端的注册界面:用户在网易163电脑端注册邮箱时,在输入出错时不但会出现错误的提示,还会给出相应的建议,帮助用户进行正确的抉择,这样就避免用户出现更大的失误并且提高了注册的效率,这是一种相当好的用户体验,也是容错原则的一种体现,如下图:

    案例二:例如Twitter注册页面的错误提示:用户在注册Twitter账号时,第一步要输入名字和手机号码,当用户输入正确的时候,输入框后边会有绿色的对勾圆圈,提示用户输入正确,可以进入下一步操作了,而当用户输入错误的时候,输入框会变为红色并且在下方出现红色字的错误提示,这样让用户很清楚的知道用户输入错误以及错误的原因,这样用户就知道怎么修改了,这也是容错原则的一种体现,如下图:

    原则十 人性化帮助原则

    即使系统不适用帮助文档是最好的,但我们也应该提供一份帮助文档。任何帮助信息都应该可以方便地搜索到,以用户的任务为核心,列出相应的步骤,但文字不要太多。

    自检:

    1. 简单的功能是否不需要文档就能看懂?
    2. 陌生的功能是否有新手引导?
    3. 困难的共是否有帮助中心?
    4. 复杂的问是否有帮助文档?
    5. 客服电话贴上了吗?

    案例一:例如淘宝APP和知乎APP登录页面的帮助入口:在比较重要的功能入口处有必要提供相应的帮助入口,来解决用户在操作功能过程中遇到的问题或者反馈问题的入口,不要让用户在出现问题时手足无措,不知道怎么办,具体看下图:

    案例二:比如mac上一些常用的大型软件:原型制作工具Axure RP 8软件、图像编辑软件Photshop CC以及mac上的Safari浏览器,在顶部状态栏上都有有一个“帮助”的入口,也体现了帮助文档的必要性,所以,不管是什么样的产品都要给用户提供一个帮助的入口,用来解决用户操作过程中遇到的问题。

    展开全文
  • 软件设计的七大设计原则

    千次阅读 2019-01-05 00:02:35
    七大设计原则是23种设计模式的基础,体现了软件设计的思想,但并不是所有设计模式都遵循这七大设计原则,有些设计模式只遵循一部分设计原则,是对一些实际情况做的一些取舍。在我们项目中也并不一定完全遵循所有设计...

    一、前言

          七大设计原则是23种设计模式的基础,体现了软件设计的思想,但并不是所有设计模式都遵循这七大设计原则,有些设计模式只遵循一部分设计原则,是对一些实际情况做的一些取舍。在我们项目中也并不一定完全遵循所有设计模式,因为受一些因素如时间、人力、成本等,如果一开始将扩展性做的很完美,那么成本就上来了。所以遵循设计模式不要过度,一定要适度。
          本文将讲解每一种设计原则,从定义开始进行分析,理解,然后使用代码示例和UML图,分析存在的问题,逐步演进,力求将设计原则讲透,使读者理解。后续的设计模式文章也将采用这种方式。

    二、开闭原则

          开闭原则的定义是:一个软件实体(如类、模块、函数)应该对扩展开放、对修改关闭。生活中也有很多开闭原则的体现,比如公司的8小时弹性上班制度,对8小时的上班时间是的修改是关闭的,但是什么时候来什么时候走是开放的。
          开闭原则的含义其实是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现。在项目中需求变更是非常常见的,如果我们频繁修改原有的代码会增加系统的复杂度,增加项目的风险。使用开闭原则可以提高系统的可复用性及可维护性,具体做法就是用抽象构建框架,用实现扩展细节。
          下面用一个例子来讲解开闭原则,有一个在线书店卖书的场景,每本书有价格和名称,我们新建一个书的接口IBook,和该接口的一个实现类JavaBook。

    public interface IBook {
        String getName();
        double getPrice();
    }
    public class JavaBook implements IBook {
        @Override
        public String getName() {
            return "Java入门到精通";
        }
        @Override
        public double getPrice() {
            return 68.99;
        }
    }
    

          此时的UML图是这样的(图有点错误,IDEA生成UML时get开头的方法它会认为是属性)
    在这里插入图片描述
          现在有一个需求,就是有些书需要打折销售,我们需要获取打折后的价格,一种思路是修改IBook,新增一个打折价格的方法,但是这种改动影响很大,实现了该接口的类都要实现这个方法;另一种思路就是将JavaBook中的getPrice() 方法修改成打折后的价格,但是这样就获取不到原价了;还一种思路是在JavaBook中新增一个获取打折后价格的方法,这样既能获取原价又能获取打折后的价格,但是这三种种做法都违背了开闭原则,也就是对JavaBook进行了修改来实现变化,正确的做法是新增一个JavaBook的打折类,继承自JavaBook,新增一个获取打折后价格的方法。

    public class JavaDiscountBook extends JavaBook {
        double getDiscountPrice(){
            return super.getPrice() * 0.8;
        }
    }
    

          此时的UML图是:
    在这里插入图片描述
          这样在不修改原来代码的基础上,实现了需求变更,其实实现开闭原则的核心是面向抽象编程,后面一些设计原则也是如此。

    三、依赖倒置原则

          依赖倒置原则定义:高层模块不应该依赖底层模块,二者都应该依赖其抽象。也就是说针对接口编程,不要针对实现编程,针对接口编程包括使用接口或抽象类,这样可以使得各个模块彼此独立,降低模块间的耦合性。而且在实现类中尽量不发生直接的依赖关系,依赖关系通过接口或抽象类产生。
          有一个场景,司机可以开车,我们新建一个Driver类和一个Benz类,司机可以开奔驰车,代码如下:

    public class Driver {
        public void driver(Benz benz){
            benz.run();
        }
    }
    public class Benz {
        public void run(){
            System.out.println("奔驰车可以跑!");
        }
    }
    

    此时的UML图是这样的
    在这里插入图片描述
          Driver类就依赖Benz类,显然违反了依赖倒置原则,如果我们司机想开其他车,就必须修改Driver类,我们进一步修改,增加IDriver的接口和ICar接口,IDriver的driver类参数是ICar,这样使得依赖关系发生在这两个接口上,不同的司机实现IDriver接口,不同的车实现ICar接口就可以了,代码如下:

    public interface IDriver {
        void driver(ICar car);
    }
    public interface ICar {
        void run();
    }
    public class Benz implements ICar{
        @Override
        public void run(){
            System.out.println("奔驰车可以跑!");
        }
    }
    public class Driver implements IDriver{
        @Override
        public void driver(ICar car){
            car.run();
        }
    }
    

          此时的UML图是这样的
    在这里插入图片描述
          这样不论什么类型的车都可以传入Driver的driver()方法里面,进行调用。

    四、 单一职责原则

          单一职责原则的定义:不要存在多于一个导致类变更的原因。如果我们一个类有两个职责:职责1和职责2,当我们需求变更的时候,职责1需要改变,变更的时候很可能会导致原本正常的职责2出问题。所以一个类、接口方法只负责一项职责,这样能降低类的复杂度,提高类的可读性,提高可维护性,降低修改带来的风险。在实际项目中,很多类不遵循单一职责原则,但是接口和方法要做到单一职责。单一职责原则还有一个很重要的点就是职责的划分,有些需求正常情况下有多个职责,但是某些特殊情况下又是一个职责,职责划分也需要视实际情况而定。

    五、接口隔离原则

          接口隔离原则定义:用多个专门的接口而不使用单一的总接口,客户端不应该依赖它不需要的接口。也就是说一个类对另一个类的依赖应该建立在最小的接口上,尽量细化接口,减少接口中的方法,但是一定要注意适度的原则,过分细化接口会带来复杂度。
          比如我们有个接口IAnimalAction,描述动物的行为,代码如下:

    public interface IAnimalAction {
        void eat();
        void fly();
        void swim();
    }
    

          这个接口中,我们定义了三个行为,后面需要的动物类实现这个接口,但是这就存在一个问题,如果某些动物不具备接口里面的三个行为中的某一个,但是它必须要实现那个方法,这就违背了接口隔离原则,正确的做法是将接口中的三个方法隔离开,分成三个接口,这样有具体行为的动物实现具体的接口,减少了耦合,将代码演进:

    public interface IEatAnimal {
        void eat();
    }
    public interface IFlyAnimal {
        void fly();
    }
    public interface ISwimAnimal {
        void swim();
    }
    

          接口隔离原则强调的是接口依赖隔离,单一职责原则强调的是职责单一。单一职责是对实现的约束,接口隔离原则是对抽象的约束。

    六、迪米特原则

          迪米特原则又叫最少知道原则,定义是:一个对象应该对其他对象保持最少的了解。简单讲就是只和朋友交流,不和陌生人说话,朋友指的是出现在成员变量、方法输入、方法输出中的类,但是出现在方法内部的类不属于朋友。不应该和这样的类发生关系。使用迪米特原则可以降低类与类之间的耦合,提高类的复用率,但是还是要强调适度的原则,过分使用迪米特原则会产生大量的中介类,使系统变复杂。
          现在有一个场景,学校里面有多个班级,班级里面有多个学生,我们现在要打印所有班级的所有学生,一种实现方式如下(这里将成员变量设置成public省略了get、set方法):

    public class School {
        public int id;
        public String schoolName;
        public List<Class> classes;
        public void print(){
            for(Class c : classes){
                for(Student s : c.students){
                    System.out.println(s.studentName);
                }
            }
        }
    }
    class Class{
        public int id;
        public String className;
        public List<Student> students;
    }
    class Student{
        public int id;
        public String studentName;
    }
    

          在School类中,Class类是它的属性,也就是它的朋友,但是Student类既不是成员变量,方法入参,也不是方法返回值,它不是School类的朋友,不应爱出现在方法内部,应该让Class类打印本班级的学生,代码演进如下:

    public class School {
        public int id;
        public String schoolName;
        public List<Class> classes;
        public void print(){
            for(Class c : classes){
                c.print();
            }
        }
    }
    class Class{
        public int id;
        public String className;
        public List<Student> students;
        public void print(){
            for(Student s : students){
                System.out.println(s.studentName);
            }
        }
    }
    class Student{
        public int id;
        public String studentName;
    }
    

          这样School类和Student就没有耦合了,逻辑也很清晰,符合迪米特原则。

    七、里氏替换原则

          里氏替换原则的定义是:对于每一个类型为T1的对象O1,都有类型为T2的对象O2,使得以T1定义的所有程序P在所有对象O1替换为O2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。也就是子类替换父类,程序逻辑不变。里氏替换原则约束了继承,继承在程序设计中能够复用代码但是对程序是有入侵的,因为子类默认就拥有父类的行为,而且增加了耦合。
          在继承中如何遵守里氏替换原则?首先子类可以扩展父类的功能,但不能改变原有的功能,子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法,如果重写了,那么用子类替换父类时,程序会调用子类的方法(否则重写就没有意义了),也就导致程序的行为发生变化。还有就是子类重载(不是重写)父类方法时,方法的前置条件(方法入参)要比父类更宽松比如父类的参数是HashMap,子类的话参数可以是Map,更宽松的话替换或不替换程序都会调用父类的方法,程序的行为也就不会改变,符合里氏替换原则。同理,方法的后置(方法返回值)条件要比父类更严格。使用里氏替换原则,可以避免子类重写父类的方法,降低代码出错的可能性。

    八、合成复用原则

          合成复用原则的定义是:尽量使用对象的组合/聚合,而不是继承关系达到软件复用的目的。
          组合是contains-A的关系,比如一个人的手、脚就是组合关系,这是一种强关系,其中一部分不存在了,所有的都不存在了,聚合是has-A的关系,是一种弱的关系,人群中的人就是聚合关系,其中一个人离开了,人群还是存在的,通过组合聚合也可以达到复用的目的,但是这种复用是黑箱复用,不需要知道细节,继承的复用是白箱复用,父类的细节会暴露给子类。尽量使用组合/聚合来实现软件复用并不是说抛弃继承,如果两个实体是is-A的关系时,可以使用继承。
          现在有个场景就是数据访问层要操作数据,需要先获得数据库连接,我们新建两个类:

    public class DBConnection {
        public String getConnection() {
            return "获得数据库连接";
        }
    }
    public class ProductDao extends DBConnection {
        private void addProduct(){
            getConnection();
            System.out.println("操作数据库!");
        }
    }
    

          通过继承我们实现了获取数据库连接,但是如果我们要更换一种数据库,那么就要修改DBConnection类,违背了开闭原则,现在我们可以通过抽象加组合的方式来实现变更后的需求,代码演进如下:

    public abstract class DBConnection {
        public abstract String getConnection();
    }
    public class MySQLConnection extends DBConnection {
        @Override
        public String getConnection() {
            return "MySQL的数据库连接";
        }
    }
    public class OracleConnection extends DBConnection {
        @Override
        public String getConnection() {
            return "Oracle数据库连接";
        }
    }
    public class ProductDao{
    
        private DBConnection dbConnection;
    
        ProductDao(DBConnection dbConnection){
            this.dbConnection = dbConnection;
        }
        public void addProduct(){
            String con = dbConnection.getConnection();
            System.out.println("使用"+con + "增加一个产品");
        }
    }
    

          这样,当我们新增一种数据库连接的时候,只要继承DBConnection这个抽象类就可以了,按我们实际传入的类型,ProductDao会调用对应的数据库连接来操作数据库,此时的UML是这样的。
    在这里插入图片描述

    展开全文
  • 设计模式原则SOLID

    千次阅读 2015-11-16 14:50:03
    设计模式五大原则: 单一职责原则SRP告诉我们实现类要职责单一; 里氏替换原则LSP告诉我们不要破坏继承体系; 依赖倒置原则DIP告诉我们要面向接口编程; 接口隔离原则ISP告诉我们在设计接口的时候要精简单一; 开闭...
  • 数据库设计原则和优化

    千次阅读 2018-12-12 20:37:57
    数据库设计原则:   1. 原始单据与实体之间的关系   可以是一对一、一对多、多对多的关系。在一般情况下,它们是一对一的关系:即一张原始单据对应且只对应一个实体。  在特殊情况下,它们可能是一对多或多对...
  • 面向对象七大设计原则

    千次阅读 2020-06-15 23:49:37
    面向对象设计原则概述 可维护性:指软件能够被理解、改正、适应及扩展的难易程度。 可复用性:指软件能够被重复使用的难易程度。 面向对象设计的目标之一在于支持可维护性复用,一方面需要实现设计方案或者源代码的...
  • java设计六大原则

    千次阅读 2018-09-28 09:15:00
    类的设计原则 依赖倒置原则-Dependency Inversion Principle (DIP)  里氏替换原则-Liskov Substitution Principle (LSP)  接口分隔原则-Interface Segregation Principle (ISP)  单一职责原则-Single ...
  • 设计模式7大原则

    千次阅读 2018-12-11 11:21:37
    文章目录面向对象设计原则概述1.单一职责原则单一职责定义单一职责举例分析2.开闭原则开闭原则简介开闭原则的优势案例3.里氏代换原则里氏替换原则简介里氏替换原则约束里氏替换原则实战4.依赖倒转原则依赖倒转原则...
  • 六大设计原则--开闭原则

    千次阅读 多人点赞 2015-09-08 08:44:52
    开闭原则【Open Close Principle】定义 software entities like classes, modules and functions should be open for extension but closed for modifications. 一个软件实体应该对扩展开放,对修改关闭。 什么...
  • 七大设计原则

    千次阅读 2018-11-17 10:44:10
    七大原则是程序员架构之路上躲...设计原则的出现是为了提高系统的可维护性和可复用性,提高系统的高内聚和低耦合! 每种设计原则的组合和使用都是在业务场景和需求量中进行取舍! =================================...
  • 界面设计原则

    千次阅读 2010-04-18 16:52:00
    1.设计原则 (1)用户原则。人机界面设计首先要确立用户类型。划分类型可以从不同的角度,视实际情况而定。确定类型后要针对其特点预测他们对不同界面的反应。这就要从多方面设计分析。(2)信息最小量原则。人机...
  • 23种设计模式分类+SOLID设计原则+从设计模式角度看MVC框架
  • 设计模式六大原则

    千次阅读 2017-10-12 09:56:00
    软件设计最大的难题就是应对需求的变化,往往我们对这些变化不知所措。我们会遇到系统修改难或者扩展难、代码过分复杂而且重复代码多、公共代码...今天首先介绍设计模式需要遵循的六大原则。 第一:单一职责原则(SPR)
  • 设计模式笔记---6大设计原则

    千次阅读 2016-01-04 15:29:32
    设计模式笔记
  • 分别就是Java设计模式六大原则和常用的23种设计模式了。本篇是对六大原则的整理。(最后一种是哈姆雷特)1.开闭原则(Open Close Principle)定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。 开放-...
  • 接口与设计原则

    千次阅读 2016-07-28 20:22:17
    11种设计原则 类原则  1.单一职责原则 - Single Responsibility Principle(SRP) 就一个类而言,应该仅一个引起它变化的原因。 职责即为“变化的原因”。 2.开放-封闭原则 - Open Close Principle(OCP) ...
  • 六个设计原则

    千次阅读 2018-05-29 16:40:03
    六个设计原则 一、单一职责原则 ​ (Single Responsibility Principle) There should never be more than one reason for a class to change. ​ 意思说:一个类,应当只有一个引起它变化的原因;即一个...
  • 软件系统设计基本原则

    万次阅读 2018-03-02 17:29:31
    一、抽象抽象是一种设计技术,说明一个实体的本质,而...在进行模块化设计时也可以多个抽象层次,最高抽象层次的模块用概括的方式叙述问题的解法,较低抽象层次的模块是对较高抽象层次模块对问题解法描述的细化...
  • PCB设计的一般原则

    千次阅读 2018-08-31 14:16:38
    印制电路板(PCB)是电子产品中电路元件和器件的支撑件.它提供电路元件和器件之间的电气连接。随着电于技术的飞速发展,PCB的密度越来越高。PCB设计的好坏对抗干扰...应遵循以下一般原则:  1.布局  首先,要考虑...
  • Python函数设计原则

    千次阅读 2015-01-01 05:45:10
    高内聚低耦合则是任何语言函数设计的总体原则。 如何将任务分解成更针对性的函数从而导致了聚合性 函数间将要如何通信则又涉及到耦合性 而将函数设计成了合适的大小又可以加强其聚合性及降低其耦
  • 算法及其设计原则

    千次阅读 2018-10-23 10:54:56
    (1) 穷性 对于任意一组合法的输入值,在执行穷步骤之后一定能结束。 (2) 确定性 对于每种情况下所应执行的操作,在算法中都确切的规定,使算法的执行者或阅读者都能明确其含义及如何执行。并且在任何条件...
  • Java设计模式6大原则

    千次阅读 2019-03-01 18:50:45
    设计模式的6大原则,单一职责原则,开放封闭原则,里式替换原则,依赖导致原则,迪米特原则和接口隔离原则
  • 测试用例的设计基本原则

    千次阅读 2017-04-06 11:57:20
    1、测试用例的代表性:能够代表并覆盖各种合理的和不合理、合法的和非法的、边界的和越界的、以及极限的输入...不管是从个人角度还是从公司角度,根据我这几年的经验我觉得case的设计应该符合以下几点:1、一个case一个
  • 用户界面设计原则

    千次阅读 2008-12-22 14:46:00
    (一)软件产品界面设计原则1.设计原则 (1)用户原则。人机界面设计首先要确立用户类型。划分类型可以从不同的角度,视实际情况而定。确定类型后要针对其特点预测他们对不同界面的反应。这就要从多方面设计分析。...
  • 交互设计原则

    千次阅读 2013-11-18 11:21:12
    交互设计原则:基于定性数据研究支持的模型,一旦了稳定的设计想法,要判断你所做出设计选择是否有效,可用性测试是个有效的工具,而定性研究能帮助你们在设计中这一艰难的过程中一个好的开始。 交互设计...
  • 软件界面设计原则

    千次阅读 2008-04-30 08:57:00
    1.设计原则 (1)用户原则。人机界面设计首先要确立用户类型。划分类型可以从不同的角度,视实际情况而定。确定类型后要针对其特点预测他们对不同界面的反应。这就要从多方面设计分析。(2)信息最小量原则。人机...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 109,538
精华内容 43,815
关键字:

以下属于输入设计原则的有