设计模式-软件设计的7个原则

设计模式-软件设计的7个原则

概述

在软件开发时为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,通常要遵守一定的设计原则:

  1. 开闭原则
  2. 里式替换原则
  3. 依赖倒置原则
  4. 单一职责原则
  5. 接口隔离原则
  6. 迪米特原则
  7. 合成复用原则

1. 开闭原则

软件实体应当对扩展开放,对修改关闭。
开闭原则是软件设计的终极目标,对扩展开放可以使软件具有一定的灵活性,同时对修改关闭又可以保证软件的稳定性。使用开闭原则设计的软件有如下优势:

  • 测试方便。由于开闭原则对修改关闭,因此软件实体是拥有稳定性的,测试时只需要对扩展代码进行测试即可;
  • 更好地提高代码复用性。开闭原则通常采用抽象接口的方式来组织代码结构,抽象的编程本身就是对代码的复用性提高有很大的帮助;
  • 提高软件的维护性和扩展性。由于开闭原则对扩展开放,因此当软件需要升级时,可以很容易地通过扩展来实现新功能,开发效率更高,代码也更易于维护。

在面向对象开发中,实现开闭原则可以通过继承父类和实现接口两种方式。
在开闭原则中,一个类只应该因为错误而修改,新加入的功能都不应该修改原始代码。

重构前的代码

enum Color: String {
    case unknown
    case black
    case white
    case gray
    case blue
    case red
}
class Style {
    var backgroudColor = Color.black
    var textColor = Color.white
    func apply() {
        print("皮肤 - 背景色: \(self.backgroudColor), 文字颜色: \(self.textColor)")
    }
}
let baseStyle = Style()
baseStyle.apply()

此时,如果有一个新需求增加一个背景色为灰色,文字颜色为蓝并且按钮颜色为红的主题,如何修改?并且需要遵守开闭原则

继承

class Custom1Style: Style {
    var buttonColor = Color.red
    override init() {
        super.init()
        backgroudColor = .gray
        textColor = .blue
    }
    override func apply() {
        print("皮肤 - 背景色: \(self.backgroudColor), 文字颜色: \(self.textColor), 按钮颜色: \(self.buttonColor)")
    }
}
let custom1Style = Custom1Style()
custom1Style.apply()

通过继承方式实现开闭原则并不彻底,通过接口可以更好的实现开闭原则。

接口

protocol StyleInterface {
    var backgroudColor: Color { get }
    var textColor: Color { get }
    var buttonColor: Color { get }
    func apply()
}
extension StyleInterface {
    var buttonColor: Color {
        get {
            return .unknown
        }
    }
}
class BaseStyle: StyleInterface {
    var backgroudColor: Color = .black
    var textColor: Color = .white
    func apply() {
        print("皮肤 - 背景色: \(self.backgroudColor), 文字颜色: \(self.textColor)")
    }
}
class Custom2Style: StyleInterface {
    var backgroudColor: Color = .gray
    var textColor: Color = .blue
    var buttonColor: Color = .red
    func apply() {
        print("皮肤 - 背景色: \(self.backgroudColor), 文字颜色: \(self.textColor), 按钮颜色: \(self.buttonColor)")
    }
}
let baseStyle2 = BaseStyle()
let custom2Style = Custom2Style()
baseStyle2.apply()
custom2Style.apply()

StyleInterface 协议定义了与主题相关的属性和方法,方法需要扩展多个主题时,需要对接口进行不同的实现即可。

2. 里式替换原则

继承必须保证超类所拥有的性质在子类中依然成立。即:在进行类的继承时,要保证子类不对父类的属性或方法进行重写,只是扩展父类的功能。
如果在设计时发现子类不得不重写父类的方法,则表明类的组织结构有问题,需要重新设计类的继承关系,比如将被重写的方法从父类抽离,仅在需要的子类声明。

3. 单一职责原则

一个类只应该承担一项责任,在实际设计中,可以以是否只有一个引起类变化的原因作为准则如果不止一个原因会引起类的变化,则需要对类重新进行拆分。
如果一个类或对象承担了太多的责任,则其中一个责任的变化可以带来对其他责任的影响,且不利于代码的复用性,容易造成代码的冗余,遵守单一职责设计的程序有以下几个特点:

  • 降低类的复杂度,一个类承担单一的职责,逻辑清晰,提高内聚,降低耦合
  • 提高代码可读性和可复用性
  • 增强代码可维护性和可扩展性
  • 类的变更是必然的,功能的增加必然会产生类的变更,单一职责可以使变更带来的影响最小。

4. 接口隔离原则

将庞大的接口定义拆分为更小的和更具体的接口,其“隔离”的主要是指对接口依赖的隔离。例如 UITableViewUITableViewDataSourceUITableViewDelegate。定义的各个接口各司其职,尽量少耦合其他业务逻辑。

5. 依赖倒置原则

高层模块不应该依赖底层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。与 开闭原则 的核心思路相同,都是要尽量减少对已有代码的修改,同时又易于进行扩展。优势:

  • 由于都对接口进行依赖,减少了类之间的耦合
  • 封闭了对类实现的修改,增强了程序的稳定性
  • 核心是面向接口开发,减少了并行开发的依赖于风险
  • 提高代码可读性和可维护性

重构前的代码:
如下代码即上层依赖下层:

class FoodStore {
    func sell(count: Int) {
        print("食品商店卖了\(count)食物")
    }
}
class Customer {
    func buy(store: FoodStore, count: Int) {
        print("购物--")
        store.sell(count: count)
    }
}
let customer = Customer()
customer.buy(store: FoodStore(), count: 4)

当有新的商店出现时就需要更改上层的 Customer 类。
使用依赖倒置的原则进行重构,使 Customer 只对抽象的接口进行依赖。

protocol Store {
    func sell(count: Int)
}
class FoodStore: Store {
    func sell(count: Int) {
        print("食品商店卖了\(count)食物")
    }
}
class ClothStore: Store {
    func sell(count: Int) {
        print("服装商店卖了\(count)服装")
    }
}
class Customer {
    func buy(store: Store, count: Int) {
        print("购物--")
        store.sell(count: count)
    }
}
let customer = Customer()
customer.buy(store: FoodStore(), count: 4)
customer.buy(store: ClothStore(), count: 2)

重构后的 Customer 不再依赖具体的 Store ,扩展也不需要更改其内部实现。

6. 迪米特原则

又叫 “最小知识原则”。核心为一个类或对象尽可能少地与其他实体发生交互作用。通常,我们不会对单独的类使用迪米特原则,这样做的解耦效果并不明显,但是如果是模块之间的交互通过一个中介类来统一处理,那就可以大大减少模块间的耦合程度,例如在iOS组件化开发中的路由器,可以将模块之间的耦合通过路由进行隔离,降低模块间的耦合。

7. 合成复用原则

在设计类的复用时,要尽量先使用组合或聚合的方式设计,尽量少使用继承。合成复用原则通过组合和聚合的方式实现复用,实现上通常使用属性、参数的方式引入其他实体进行通信。
重构前的代码:

class Teacher {
    var name: String
    init(_ name: String) {
        self.name = name
    }
    func teach() {
        print("讲课")
    }
}
class MathTeacher: Teacher {
    override func teach() {
        print("\(name)讲数学课")
    }
}
class EnglishTeacher: Teacher {
    override func teach() {
        print("\(name)讲英语课")
    }
}
let james = MathTeacher("james")
james.teach()
let davis = EnglishTeacher("davis")
davis.teach()

根据合成复用原则,不使用继承,把学科封装为Teacher的一个属性。

class Suject {
    var name: String
    init(_ name: String) {
        self.name = name
    }
}
class Teacher {
    var name: String
    var subject: Suject
    init(_ name: String, subject: String) {
        self.name = name
        self.subject = Suject(subject)
    }
    func teach() {
        print("\(name)讲\(subject.name)课")
    }
}
let james = Teacher("james", subject: "数学")
james.teach()
let davis = Teacher("davis", subject: "英语")
davis.teach()

总结

  • 开闭原则是核心,在设计软件时保持扩展的开放性和修改的封闭性
  • 里式替换原则要求在继承时不要破坏父类的实现
  • 单一职责原则要求类的功能要单一
  • 接口隔离原则要求接口的设计要精简
  • 依赖倒置原则要求面向抽象编程,即面向接口编程
  • 迪米特原则提供一种降低系统耦合性的方式
  • 合成复用原则要求组织类的关系时谨慎使用继承

7种基本设计原则

  • 开闭原则:软件设计的终极目标,对扩展开放,对修改关闭
  • 里式替换原则:子类可以扩展父类方法,但不能修改父类原有方法本身行为
  • 依赖倒置原则:面向协议编程,尽量依赖抽象
  • 单一职责原则:降低类的复杂度,一个类只负责一种职责
  • 接口隔离原则:精简接口,一个接口只负责一类职责
  • 迪米特原则:简化类之间的交互,使用中介者统一处理
  • 合成复用原则:使用组合或聚合代理继承