设计原则SOLID

一、开闭原则

OCP - Open Close Principle

  • 定义:可扩展(extension),不可修改(modification)。把容易改变或增减的功能抽象成接口或方法
  • 例子:不同vip等级的服务不同,把服务方法抽象成接口。(如果不抽象,那么每次增删改的时候就需要用if-else来判断,就修改了代码,与开闭原则不符)
  • 符合开闭原则最典型的设计模式是装饰者模式

❌下面是一种不好的实现方式,每次改变vip等级都需要修改if-else代码逻辑

 struct Vip {
     func service(user: String) {
         if user == "vip1" {
             print("服务vip1: \(user)")
         } else if user == "vip2" {
             print("服务vip2: \(user)")
         }
     }
 }

✅下面是符合开闭原则的实现方式,这样每次增删改vip只会处理对应的vip(也可以通过创建基类,子类根据需求不同实现)

 protocol ServiceProvider {
     func service(user: String)
 }

 struct Vip1: ServiceProvider {
     func service(user: String) {
         print("服务vip1: \(user)")
     }
 }

 struct Vip2: ServiceProvider {
     func service(user: String) {
         print("服务vip2: \(user)")
     }
 }

二、单一职责原则

SRP - Single Responsibility Principle

  • 定义:一个类只负责一件事,当这个类需要做过多事情的时候,就需要分解这个类
  • 不仅仅是类,函数(方法)也要遵循单一职责原则,即:一个函数(方法)只做一件事情。如果发现一个函数(方法)里面有不同的任务,则需要将不同的任务以另一个函数(方法)的形式分离出去。
  • 例子:A中有两种类型的职责:work1和work2,如果后面需要修改work1,有可能会不小心改坏work2,所以最好把不同类的职责work2分离出去。

三、里氏替换原则

LSP - Liskov Substitution Principle

  • 所有引用基类的地方必须能透明地使用其子类的对象,也就是说子类对象可以替换其父类对象,而程序执行效果不变。
  • 在继承体系中,子类中可以增加自己特有的方法,也可以实现父类的抽象方法,但是不能重写父类的非抽象方法,否则该继承关系就不是一个正确的继承关系。
  • 使用继承前仔细考虑继承关系是否正确

四、迪米特法则(最少知道原则)

LoD - Law of Demeter ( Least Knowledge Principle)

  • 定义:迪米特法则也叫做最少知道原则(Least Know Principle),一个对象应该对尽可能少的对象有接触,也就是只接触那些真正需要接触的对象。
  • 对象与对象之间交互的设计时,应该极力避免引出中间对象的情况(需要导入其他对象的类):需要什么对象直接返回即可,降低类之间的耦合度。
  • 例如:方法的作用是返回机器的名字,那么我们直接就返回一个机器名的String,而不是返回一个带有机器名的对象Machine
  • 符合迪米特法则最典型的设计模式是中介者模式

五、接口分离原则

ISP - Interface Segregation Principle

  • 不应该强迫客户依赖于它们不用的方法
  • 使用多个专门的接口比使用单一的总接口要好
  • 应尽量细化接口,接口中的方法应该尽量少
  • 避免同一个接口里面包含不同类职责的方法,接口责任划分更加明确,符合高内聚低耦合的思想
  • 例如:UITableView的UITableViewDelegate和UITableViewDataSource都是创建tableview的接口,但是设计成了两个接口,就很好的遵循了接口分离原则
  • Swift中可以使用符号&来组合两个接口(协议),能够很方便的应用接口分离
protocol Named {
    var name: String { get }
}

protocol Aged {
    var age: Int { get }
}

// 协议组合
struct Man: Named & Aged {
    var name: String
    var age: Int
}

// 也可以重新定义一个名字
typealias Person = Named & Aged

struct Woman: Person {
    var name: String
    var age: Int
}

六、依赖倒置原则

DIP - Dependency Inversion Principle

  • 高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象
  • 高层模块不应该直接依赖于底层模块的具体实现,而应该依赖于底层的抽象。换言之,模块间的依赖是通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的
  • 公司作为一个需要把自己的商品分销到全国各地,但是发现,不同的分销渠道有不同的玩法,于是派出了各种销售代表玩不同的玩法,随着渠道越来越多,发现,每增加一个渠道就要新增一批人和一个新的流程,严重耦合并依赖各渠道商的玩法。实在受不了了,于是制定业务标准(抽象接口),开发分销信息化系统,只有符合这个标准的渠道商才能成为分销商(依赖协议的低层模块)。让各个渠道商反过来依赖自己标准。反转了控制,倒置了依赖。
  • 例:高层模块A要依赖并使用低层模块B。那么就让B依赖并实现实现接口I,那么在A中使用的就是实现了接口I的B,这样A也就依赖的是接口I了,并且可以通过接口I的方法使用B。

❌不好的实现方式:高层Mother直接依赖了低层Book,所以只能读书,却不能读报纸了,如果再出现杂志等也是不能读

class Book {
    func getContent() -> String {
        return "很久很久以前有一个故事……";
    }
}

class Newspaper {
    func getContent() -> String {
        return "有一个新闻……";
    }
}

class Mother {
    // 直接依赖低层模块Book,导致无法读报纸Newspaper
    func narrate(book: Book) {
        print("妈妈开始讲故事")
        print(book.getContent())
    }
}

class Client {
    static func run() {
        let mother = Mother();
        mother.narrate(book: Book());
    }
}

✅好的实现:创建接口Contentable,低层依赖接口并实现接口方法,高层Mother通过接口调用低层,很好的实践了依赖倒置,解耦。加入后面再出现杂志,网页等信息,只要添加低层的实现,高层不用变化。

// 创建抽象接口,让低层模块和高层模块都依赖它(面向协议)
protocol Contentable {
    func getContent() -> String
}

// 低层模块也依赖抽象接口Contentable
class Book: Contentable {
    func getContent() -> String {
        return "很久很久以前有一个故事……";
    }
}

class Newspaper: Contentable {
    func getContent() -> String {
        return "有一个新闻……";
    }
}

class Mother {
    // 高层模块Mother依赖抽象接口Contentable,这样如果低层做了修改,高层也是不用改变的
    func narrate(content: Contentable) {
        print("妈妈开始读:")
        print(content.getContent())
    }
}

class Client {
    static func run() {
        let mother = Mother();
        mother.narrate(content: Book());
        mother.narrate(content: Newspaper());
    }
}

参考: