总结下软件设计的六大原则

摘要:不管你用什么语言,做什么开发。只要是攻城狮,都应该知道软件设计的一些基本原则。将这些原则应用到你的项目中。你写的项目将不会令自己讨厌。,这里我用Swift简单总结下软件设计的6大原则。

1、依赖倒置原则

原文链接

1、高层模块(稳定)不应该依赖低层模块(变化),二者都依赖抽象(稳定)。
2、抽象(稳定)不应该依赖于实现细节(变化),实现细节应该依赖于抽象(稳定)

下面为代码示例:

class DrawingBoard{//绘画板,代表高层模块
        var lineArray:Array?
        var rectArray:Array?
        func onPaint(){
            for lineInstance in lineArray{
                 event.Graphics.DrawLine(Pens.Red,
                  lineInstance.leftUp,
                  lineInstance.width,
                  lineInstance.height)
             }
            for rectInstance in rectArray{
                 event.Graphics.DrawRect(Pens.Red,
                  rectInstance.leftUp,
                  rectInstance.width,
                  rectInstance.height)
             }
         }
       
   }
  class Line{//底层模块(代表容易变化的模块)
       func Draw(){ ... }
   }
  class Rect{//底层模块(代表容易变化的模块)
        func Draw(){ ... }
   }

而这个设计原则告诉我们应该像这样去思考:

class DrawingBoard{//绘画板,代表高层模块
        var shapeArray:Array?
        func onPaint(){
             for shape in shapeArray{
                  shape.Draw();
             }
         }
   }
  protocol Shape{//抽象接口,同时也是一种稳定的模块(高层和低层都依赖抽象类)
        func Draw(){  }
   }
  class Line:Shape{//底层模块(代表容易变化的模块)
        override func Draw() {  }//实现细节应该依赖于抽象
   }
  class Rect:Shape{//底层模块(代表容易变化的模块)
        override func Draw() {  }//实现细节应该依赖于抽象
   }

代码修改完,结构就变成了下面这样。

回过头来再次验证是否符合“依赖倒置原则”


image

2、开放封闭原则

原文链接

1、对扩展开放,对更改封闭.


2、类模块应该是可扩展的,但是不可修改.

假如我们来一个新的需求时,如果不使用设计模式,我们经常会在原有代码结构上进行更改。根据这个原则,我们应该避免这种更改,而选择去扩展。
因为更改的代价往往是十分大的,

class DrawingBoard{
    var lineArray:Array?
    var rectArray:Array?
    
    //新的改变需求
    var circleArray:Array?

    func onPaint(event:PaintEventArgs){
        //旧代码      
        for lineInstance in lineArray{
           //同下
        }    
        for rectInstance in rectArray{
            //同下
        }    
        //新代码
        for circleInstance in circleArray{
             event.Graphics.DrawCircle(Pens.Red,
                      circleInstance.leftUp,
                      circleInstance.width,
                      circleInstance.height)
        }        
    }
   
}
class Line{//底层模块(代表容易变化的模块)
    //...
}
class Rect{//底层模块(代表容易变化的模块)
    //...
}
class Circle{
}

这种代码就违反了开放封闭原则,它是在改变代码,这就意味着这块代码需要重新编译、重新测试、重新部署,改变的代价十分高昂。
我们依旧像之前那样,重新修改代码:

class DrawingBoard{//绘画板,代表高层模块
        var shapeArray:Array?
        func onPaint(){
             for shape in shapeArray{
                  shape.Draw();
             }
         }
   }
  protocol Shape{//抽象接口,同时也是一种稳定的模块(高层和低层都依赖抽象类)
        func Draw(){  }
   }
  class Line:Shape{//底层模块(代表容易变化的模块)
        override func Draw() {  }//实现细节应该依赖于抽象
   }
  class Rect:Shape{//底层模块(代表容易变化的模块)
        override func Draw() {  }//实现细节应该依赖于抽象
   }  
  class Circle:Shape{//底层模块(代表容易变化的模块)
        override func Draw() {  }//实现细节应该依赖于抽象
   }

3、接口隔离原则(ISP)

原文链接

1、客户端不应该依赖它不需要的接口。

2、一个类对另一个类的依赖应该建立在最小的接口上。

问题由来:

类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。

code
protocol I {
    func m1() -> Void
    func m2() -> Void
    func m3() -> Void
    func m4() -> Void
    func m5() -> Void
}

class B: I {
    func m1() -> Void {}
    func m2() -> Void {}
    func m3() -> Void {}
    // 实现多余的方法
    func m4() -> Void {}
    func m5() -> Void {}
}

class A: B {
    func m1(i: I) -> Void {
        i.m1()
    }
    func m2(i: I) -> Void {
        i.m2()
    }
    func m3(i: I) -> Void {
        i.m3()
    }
}

类D、类C的实现和上面相似,这里就不写代码了 
解决方案

将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。


image
code
protocol I {
    func m1() -> Void
}

protocol I2 {
    func m2() -> Void
    func m3() -> Void
}

protocol I3 {
    func m4() -> Void
    func m5() -> Void
}

class B: I,I2 {
    func m1() -> Void {}
    func m2() -> Void {}
    func m3() -> Void {}
}

class A: B {
    func m1(i: I) -> Void {
        i.m1()
    }
    func m2(i: I2) -> Void {
        i.m2()
    }
    func m3(i: I2) -> Void {
        i.m3()
    }
}
优点

建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。

思考
  1. 你是否觉得ISP跟之前的单一职责原则很相似?
    其实不然。其一,单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。其二,单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口接口,主要针对抽象,针对程序整体框架的构建。
注意点

接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。

4、单一职责原则(SRP:Single responsibility principle)

原文链接

定义

一个类应该只有一个发生变化的原因,即一个类只负责一项职责。
如果一个类有多个职责,这些职责就耦合在了一起。当一个职责发生变化时,可能会影响其它的职责。另外,多个职责耦合在一起会影响复用性。
此原则的核心是解耦和增强内聚性。

由来

类A负责两个职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类A时,有可能会导致原本运行正常的职责P2功能发生故障。

解决方案

遵循SRP。分别建立两个类A1、A2,使A1完成职责P1,A2完成职责P2。这样,当修改类A1时,不会影响到职责A2;同理,当修改A2时,也不会影响到职责P1。

优点

降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多。
提高类的可读性,提高系统的可维护性。
变更引起的风险降低,变更是必然的,如果SRP遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。

小结

这种在类中新加一个方法的修改方式,虽然也违背了SRP,但在方法级别上却是符合SRP的,因为它并没有动原来方法的代码。
这三种方式各有优缺点,在开发中,需要根据实际情况来确定。需要注意的是:只有逻辑足够简单,才可以在代码级别上违反SRP;只有类中方法数量足够少,才可以在方法级别上违反SRP;

5、迪米特法则

定义

迪米特法则,也叫最少知识原则:如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。

要义

1、从被依赖者的角度来说:只暴露应该暴露的方法或者属性,即在编写相关的类的时候确定方法/属性的权限

2、从依赖者的角度来说,只依赖应该依赖的对象

先说说第一点:

当我们按下计算机的关机按钮的时候,计算机会执行一些列的动作会被执行:比如保存当前未完成的任务,然后是关闭相关的服务,接着是关闭显示器,最后是关闭电源,这一系列的操作以此完成后,计算机才会正式被关闭。

现在,我们来用简单的代码表示这个过程,在不考虑迪米特法则情况下,我们可能写出以下代码

//计算机类
public class Computer {
    
    public func saveCurrentTask(){
    //do something
    }
    public func closeService(){
    //do something
    }
    public func closeScreen(){
    //do something
    }
    
    public func closePower(){
    //do something
    }
    
    public func close(){
    saveCurrentTask()
    closeService()
    closeScreen()
    closePower()
    }
}

//人
public class Person {
    private var c = Computer()
    
    public func clickCloseButton(){
    //现在你要开始关闭计算机了,正常来说你只需要调用close()方法即可,
    //但是你发现Computer所有的方法都是公开的,该怎么关闭呢?于是你写下了以下关闭的流程:
    c.saveCurrentTask();
    c.closePower();
    c.close();
    
    //亦或是以下的操作
    c.closePower();
    
    //还可能是以下的操作
    c.close();
    c.closePower();
    }
}

发现上面的代码中的问题了没?
我们观察clickCloseButton()方法,我们发现这个方法无法编写:c是一个完全暴露的对象,其方法是完全公开的,那么对于Person来说,当他想要执行关闭的时候,却发现不知道该怎么操作:该调用什么方法?靠运气猜么?如果Person的对象是个不按常理出牌的,那这个Computer的对象岂不是要被搞坏么?

对应迪米特法则的第一要义:代码应该改成如下:

//计算机类
public class Computer {
    
    private func saveCurrentTask(){
    //do something
    }
    private func closeService(){
    //do something
    }
    private func closeScreen(){
    //do something
    }
    
    private func closePower(){
    //do something
    }
    
    public func close(){
    saveCurrentTask()
    closeService()
    closeScreen()
    closePower()
    }
}

//人
public class Person {
    private var c = Computer()
    
    public func clickCloseButton(){
    //现在你要开始关闭计算机了,正常来说你只需要调用close()方法即可,
    c.close();
    }
}

6、里式替换原则

定义

子类可以替换父类的位置.并且程序的功能不受影响.

通俗来讲:子类可以扩展父类的功能,但不能改变父类原有的功能,它包含以下2层含义:

1、子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法;

2、子类中可以增加自己特有的方法。

具体表现在:
  1. 子类必须实现父类所有非私有的属性和方法或子类的所有非私有属性和方法必须在父类中声明。即,子类可以有自己的“个性”,这也就是说,里氏代换原则可以正着用,不能反着用(在项目中,采用里氏替换原则时,尽量避免子类的“个性”,一旦子类有“个性”,这个子类和父类之间的关系就很难调和了)。根据里氏代换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。

  2. 尽量把父类设计为抽象类或者接口。让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。

你可能感兴趣的:(总结下软件设计的六大原则)