浅谈面向对象五大原则 S.O.L.I.D
Single Responsibility Principle(SRP) 单一职责原则
A class should have one and only one reason to change, meaning that a class should have only one job.
定义
- 一个类应该有且只有一个去改变它的理由
- 这意味着一个类应该只有一项工作(Do one thing)
理解
1.什么是职责
在SRP,我们把职责定义为“变化的原因“(a reason for change),如果你能想到多于一个动机去改变一个类,那么这个类就多于一个职责。
所以,SRP中,引起类变化的原因只有一个!!!
首先,我们举个简单的例子:
假设我们有一些形状shape,现在我们的需求是,想要所有shape的面积的和
好,首先我们写了个基类Shape,子类Circle,Square, 和计算面积的类AreaCalculator
class Shape {
}
class Circle: Shape {
var radius: Double
init(radius: Double) {
self.radius = radius
}
}
class Square: Shape {
var length: Double
init(length: Double) {
self.length = length
}
}
class AreaCalculator {
var shapes: [Shape]
init(shapes: [Shape]) {
self.shapes = shapes
}
func areaSum() -> Double{
// 求每个图形的面积和
}
func printSum() {
print("Sum of the areas of shapes: \(areaSum())")
}
}
let shapes = [Circle(radius: 3), Circle(radius: 6), Square(length: 5)]
let areas = AreaCalculator(shapes: shapes)
areas.printSum()//Sum of the areas of shapes: 123.3
嗯,好像没什么问题。
然后,第二天,需求变了,要求你输出的数据,换种形式,可能是Json格式,也可能是HTML格式,好,于是你修改了代码
class AreaCalculator {
var shapes: [Shape]
init(shapes: [Shape]) {
self.shapes = shapes
}
func areaSum() -> String{
// 求每个图形的面积和
}
func printSum() {
print("Sum of the areas of shapes: \(areaSum())")
}
func printSumByHTML() {
}
func printSumByJson() {
}
}
如果第三天,需求想要打印的不是所有形状的面积和,而是把每个形状面积都打印出来呢?
有点奇怪吧,这个类改变的原因不仅一个,不仅可能改变打印方式,还有可能是改变输出面积的逻辑方式。
数据的逻辑处理和打印方式是两个职责,AreaCalculator 类应该只对提供的Shape进行面积求和,它不该关系怎么打印,因此,我们可以创建一个AreaPrint类,使用这个来提供对面积的展示方式
class AreaPrint { var calculator: AreaCalculator init(calculator: AreaCalculator) { self.calculator = calculator } func HTML() { } func json() { } } let shapes = [Circle(radius: 3), Circle(radius: 6), Square(length: 5)] let areas = AreaCalculator(shapes: shapes) let print = AreaPrint(calculator: areas) print.HTML() print.json()
2.为什么要用SRP
即为何要将两个职责分离到单独的类?
因为每个职责都是变化的一个轴线,当需求变化,该变化会反映为类的职责变化,如果一个类承担多个职责,那么引起它变化的原因就有多个。
如果一个类承担的职责过多,就等于把职责耦合在一起,一个职责的变化可能会削弱或抑制这个类完成其他职责的能力。
比如
所以,单一职责可以降低耦合性,增强代码的可复用性,降低某个类的复杂度。
3.怎么划分职责
我们需要根据对业务和需求的理解,去划分一个类,一个函数的职责。也就是说,一个类只有一个任务,如果有一个原因导致类中其中一个职责变化,其他不变化,那么需要将这个职责分离;如果这个类的所有职责同时改变,那么可以不分离。
但是,一个职责的划分是跟着需求变化而变化的,刚说了每个职责都是变化的一个轴线,但变化的轴线仅当变化实际发生时才具有真正的意义,否则去应用任何原则都是不明智的。
小结
- 单一职责:一个类有且只有一个改变它的理由,即每个类只做一件事。
- SRP是所有原则中最简单,也是最难正确运用的职责,我们会自然地把职责结合在一起,软件设计真正要做的许多内容,就是发现职责并把那些职责相互分离。
- SRP最重要的是职责的划分,职责划分的程度取决于需求。
Open Closed Principle(OCP) 单一职责原则
Objects or entities should be open for extension, but closed for modification.
定义
软件实体(类、模块、函数等)应该是可以扩展的,但是不可修改的,即对扩展开放,对修改封闭。
即一个类应该无需修改类本身但却容易扩展。
理解
1. 对什么开放
对扩展开放
当需求改变,我们可以对模块进行扩展,使其具有满足新的改变的行为,换句话说,我们可改变模块的功能。
2. 对什么封闭
对修改封闭
对模块进行扩展时,不必改动模块的源代码或二进制代码。
3.为什么要用OCP
如果违反OCP,那么如果程序中的一处改动会产生连锁反应,就会导致一系列相关模块的改动,那么设计就太过僵化。
OCP建议我们应对系统进行重构,那么以后再进行同样改动,只需添加新代码而不必改动已正常运行的代码。
在很多方面,OCP都是面向对象设计的核心所在,可增强灵活性、可重用性、可维护性等。
4.怎么用OCP
抽象是关键,其背后的主要机制是抽象和多态。
模块可以依赖抽象。由于模块依赖于一个固定的抽象体,所以它对于更改可以是关闭的,同时,通过从这个抽象体派生,也可以扩展此模块的行为。
例子
之类的例子,AreaCalculator 根据不同Shape,来计算其面积,如下
Class AreaCalculator {
func sum(shape: [Shape]) -> Double {
for s in shape {
if s is Circle {
// 计算圆形面积
}
else if s is Square {
// 计算方形面积
}
}
}
}
那么,当添加新的Shape,则需要增加更多的 if-else块,这是违背OCP的,如果使用抽象编程,如何做呢?
我们可以将计算每个shape的面积的逻辑从sum方法中抽取,将它附加各自的类上。
class Circle {
func area() -> Double {
//计算圆形面积
}
}
class Square {
func area() -> Double {
//计算方形面积
}
}
那么计算所有shape的面积的和就变得简单了,只需要遍历shape数组,对每个shape都调用area()
函数,当增加其他图形类的时候,并不会破坏AreaCalculator
类的代码,但是,我们怎么知道传递到AreaCalculator
类上的对象有一个area()
方法呢,我们可以创建一个接口(Swift的protocol相当于接口)
protocol Shape {
func area() -> Double
}
class Circle: Shape {
func area() -> Double {
//计算圆形面积
}
}
class Square: Shape {
func area() -> Double {
//计算方形面积
}
}
func sum(shape: [Shape]) -> Double {
var areas: Double = 0
for s in shape {
let area = s.area()
areas += area
}
return areas
}
注意
1.遵循OCP的代价是昂贵的
- 创造正确的抽象花费开放时间和精力
- 抽象增加软件设计的复杂性
- 开发人员有能力处理抽象的数量是有限的
2.OCP无法做到完全封闭
一般而言,无论模块是多么的“封闭“,都会存在一些无法对之封闭的变化,没有对于所有的情况都贴切的模型。所以,必须有策略地对待这个问题。
设计人员必须对他所设计的模块应该对哪种变化封闭做出选择,必须先猜测出最有可能发生的变化种类,然后构造抽象来隔离那些变化。但大多数情况,猜测都是错误的。
3.隔离变化
我们会在认为可能发生的地方抽象,然而,通常我们的猜测是错误的,后续即使不使用这些抽象也必须去支持和维护它们,这不是一件好事,所以,通常我们会一直等到确实需要那些抽象时再去进行抽象。
-
只受一次愚弄
在我们最初编写代码时,假设变化不会发生,当变化发生时,再创建抽象来隔离以后发生的同类变化。
-
刺激变化
a.编写测试
b.使用很短的迭代周期进行开发
c.加入基础结构前就开发特性,并经常展示给涉众
d.首先开发最重要的特性
e.尽早地、经常性地发布软件
小结
- OCP:软件实体(类、模块、函数等)应该是可以扩展的,但是不可修改的。
- 实现OCP的核心思想就是对抽象编程。
- 出现新的需求时,尽量(注意是尽量)不要修改原有代码,而是对原有代码进行扩展。
- 不能对每个部分肆意进行抽象,应对程序中呈现频繁变化的部分进行抽象,拒绝不成熟的抽象和抽象本身一样重要。
Liskov Substitution Principle(LSP) 里氏替换原则
Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
定义
- 在对象 x 为类型 T 时 q(x) 成立,那么当 S 是 T 的子类时,对象 y 为类型 S 时 q(y) 也应成立。
- 简而言之,即对父类的调用同样适用于子类
理解
1.谁替换谁
子类替换父类
2.替换后的结果
完全符合,没有错误行为
假设有个函数F,其形参是基类B,基类B有个派生类D,如果把D对象作为B类传递给F,会导致F出现错误行为,那么D就违反了LSP。
举个简单的例子,比如,有个类Brid,里面有一个函数Fly,KingFisher(翠鸟)类继承了Bird,OK,没问题,翠鸟是可以飞!
然后又有个Ostrich(鸵鸟)类继承了Bird, 鸵鸟是鸟吗,是啊,但是它能飞吗,不能!
3.微妙的违规
一般意义上,一个正方形就是一个矩形,因此,把Square类视为Rectangle类的派生类是合理的。
class Rectangle {
var width: Double = 0
var height: Double = 0
func area() -> Double {
return width * height
}
}
class Square: Rectangle {
}
对正方形而言,长与宽等长,所以,对宽和长的赋值,其行为是一样的,所以,需要对width和height进行处理
class Square: Rectangle {
override var width: Double {
didSet {
height = width
}
}
override var height: Double {
didSet {
width = height
}
}
}
再考虑下面函数g:
func g(r: Rectangle) {
r.width = 5
r.height = 4
assert(r.area() == 20)
}
对于函数g, 如果传递进来的是Rectangle,那么函数运行正确,但如果是Square,就会发生断言错误,对g函数的编写者来说,改变Rectangle的宽不会导致其长的改变。
对这个函数而言,Square不能替换Rectangle,因此,Square和Rectangle之间的关系是违反LSP的。
4.怎么实现LSP
即怎么使得继承的子类能透明地替换父类?
IS-A是关于行为的
我们经常说继承是IS-A关系,即如果一个新类型的对象被认为和一个已有类的对象之间满足IS-A关系,那么这个新对象的类应该从这个已用对象的类派生。
LSP清楚地指出,OOD中IS-A关系是就行为方式而言的,行为方式是可以进行合理假设的,是客户程序所依赖的。
就如刚刚那个微妙的违规例子,对于函数g,Square和Rectangle的行为方式不一致的,但对于其他函数,比如求面积,他们的行为方式就是一致的。
有效性并非本质属性
一个模型,如果孤立地看,并不具有真正意义上的有效性,模型的有效性只能通过它的程序来表现。
在考虑一个特定设计是否恰当,不能完全孤立地来看这个解决方案,必须根据该设计的使用者所做出的合理假设来审视它。
但是,大多数这样的假设都难以预测,如果试图预测所有假设,那么系统会过于负责且不合理的,因此,像其他原则一样,最好的方法是只预测那些明显的对于LSP的违反情况而推迟其他预测,直到出现相关的预测才去处理它们。
那么怎么明确这些假设呢?我们可以借助下DBC。
基于契约设计(Design By Contract , 简称DBC)
基于契约设计,可以使这些合理的假设明确化。
使用DBC,类的编写者显式地规定针对该类的契约。客户代码的编写者可通过该契约获悉可以依赖的行为方式。契约是通过每个方法声明的前置条件和后置条件来指定的,要使一个方法得以执行,前置条件必须为真,执行完毕后,必须保证后置条件为真。
派生类的前置条件和后置条件规则:
a. 只能使用相等或更弱的前置条件替换原始的前置条件 (子类必须接受父类可接受的一切)
b. 只能使用相等或更强的后置条件替换原始的后置条件 (父类的用户不应被使用的子类的输出所扰乱)
术语“弱“是一个容易混淆的概念,如果X没有遵从Y的所有约束,那么X就比Y弱。X所遵从的新约束的数目是无关紧要的。
对于上述的函数g例子而言,width赋值语句的后置条件可看
assert(self.width == newValue && self.height == old.height)
old是指width被赋值前的Rectangle,明显我们的Square类是比原始的后置条件弱的。
常见违反LSP情况
1.派生类中的退化函数
父类提供一个函数f并默认实现,而派生类重写了函数f,并什么也没有做
class Base {
func f() {
print("**** Base Class")
}
}
class Derived: Base {
func f() { }
}
在派生类中存在退化函数并不总是表示违反了LSP,但当 存在这种情况时,还是值得注意一下
2.从派生类中抛出异常
在派生类的方法中添加了其基类不会抛出的异常,若基类的使用者不期望这些异常,那么把它们添加到派生类的方法中就会导致不可替换性。此时要遵循LSP。要么就必须改变使用者的期望,要么,派生类就不该抛出这些异常。
如上面那个微妙违规的例子。
小结
LSP:在对象 x 为类型 T 时 q(x) 成立,那么当 S 是 T 的子类时,对象 y 为类型 S 时 q(y) 也应成立。
LSP是OCP成为可能的主要原则之一,正是子类型的可替换性才使得使用基类类型的模块在无需修改的情况下就可以扩展。
IS-A过于宽泛以至于不能作为子类型的定义。子类型的正确定义是“可替换性的“,可通过DBC来定义,进行透明替换。
Interface Segregation Principle(ISP) 接口隔离原则
A client should never be forced to implement an interface that it doesn’t use or clients shouldn’t be forced to depend on methods they do not use.
定义
- 不应强迫客户程序实现一个它用不上的接口,或是说客户端不应该*依赖它们不使用的方法。
理解
1. 为什么要分离接口
如果客户程序依赖于一个含有它不使用的方法的类,会有如下问题:
1. 如果其他客户程序需要使用该方法并要求要求这个类改变时,就会影响到这个客户程序;
2.这个客户程序不仅需要了解自己需要使用的方法,也需要了解自己不需使用的方法;
3.这个类可能会变得非常庞大,臃肿,变成胖类。
2.举个例子
先举一个例子:
有个灯的接口 Lamp,有开启,关闭方法
protocol Lamp {
func turnOn()
func turnOff()
}
现在,有一个需求,有那么个TimerLamp,如果灯打开后的时间过长,需要它自己关闭,想象下走廊的感应灯。
而系统有这么一个TimerServer类,对所有遵守TimeClient协议的对象都可以注册,然后调用开始计时函数后,当时间超过timeout时,就执行TimerClient的timeOut函数,来做一些它需要做的操作。
protocol TimeClient {
func timeOut()
}
class TimerServer {
var client: TimeClient?
var timeout: Int = 0
var timer: Timer?
var count: Int = 0
func register(timeout: Int, client: TimeClient) {
self.timeout = timeout
self.client = client
}
func startTimer() {
// 计时器,每秒会执行timerAction函数一次
if timer != nil {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerAction(_:)), userInfo: nil, repeats: true)
}
}
private func stopTimer() {
timer?.invalidate()
timer = nil
}
@objc private func timerAction(_ timer: Timer) {
if count >= timeout {
stopTimer()
client?.timeOut()
}
count += 1
}
}
那么我们要怎么将TimerLamp和TimerServer联系在一起呢?让TimerLamp可以在超时时,通知到TimerLamp可以关灯呢?
让我们试写一下:
protocol Lamp: TimeClient {
func turnOn()
func turnOff()
}
class TimerLamp: Lamp {
func timeOut() {
turnOff()
}
func turnOn() {
// 开灯
}
func turnOff() {
// 关灯
}
}
看起来,貌似没有问题,但是如果有些灯并不需要超时的功能啊!!!
1.如果创建了其他实现Lamp协议但不需要超时功能的的类,但是却必须要提供timeOut方法的退化实现,这明显是不合理的。
2.如果其他子类需要添加其他功能,比如,增加一个彩虹灯,每隔一段时间换一个颜色,又在Lamp里添加一个方法,如果持续这么做的话,那么每次子类需要一个新方法,这个方法都会被加到Lamp中,这个Lamp就会越来越胖。
开什么玩笑,我用不着的功能,凭什么要维护
3. 怎么分离接口
可以使用两种方法,
a.委托分离接口,即对象适配器
b.多重继承分离接口 (即类适配器)
适配器 Adapter
将一个类的接口转换成客户希望的另一个接口
适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作
使用委托分离接口 (对象适配器)
对象适配器:对象适配器不继承被适配者,而是组合了一个对它的引用
- Client:客户程序
- Target: 具体/抽象类或者接口
- Adaptee: 被适配器类
- Adapter: Target类型,包含一个对Adaptee的引用,重载Target
request
方法,在request
方法中调用Adaptee 的specificRequest
方法,以间接访问它的行为。
Client只需了解调用Target接口中的方法,而不需要关心Adaptee接口的方法。
让我们来修改一下,刚刚那个例子:
我们创建了适配器类LampTimerAdaptar来连接TimerServer和TimerLamp
protocol Lamp {
func turnOn()
func turnOff()
}
class TimerLamp: Lamp {
func turnOn() {
// 开灯
}
func turnOff() {
// 关灯
}
}
class LampTimerAdaptar: TimeClient {
var timerLamp: TimerLamp?
func timeOut() {
if timerLamp != nil {
timerLamp?.turnOff()
}
}
}
使用多重继承分离接口 (类适配器)
类适配器 : 通过继承来适配两个接口。Gof介绍是通过多重继承来实现的,而在OC可通过实现协议,同时继承父类,达到多继承的效果。
- Client:客户程序
- Target: 具体/抽象类或者接口
- Adaptee: 被适配器类,
- Adapter: 即是Target类型,也是Adaptee类型,重载Target
request
方法,在request
方法中调用父类specificRequest
方法。
让我们来再此修改一下刚刚那个例子:
这一次让TimerLamp去实现TimerClient
class TimerLamp: Lamp, TimeClient { func turnOn() { // 开灯 } func turnOff() { // 关灯 } func timeOut() { turnOff() } }
(相信大多数人都是用第二种吧)
优先使用第二种,只有必须用到Adapter的转换时(对已有接口进行转换),或不同的时候需要不同的转换时,才选择第一种
ISP v.s. SRP
- 区别:
ISP针对接口(抽象),SRP针对类(主要是约束类,其次是抽象类和接口)
ISP针对客户,SRP针对职责
ISP偏向架构,SRP偏向业务(针对实现和细节) - 联系:
一个类可能需要实现多个接口,这会产生职责耦合;
但分离多个接口就是一种解耦表现,一个接口相关职责的变化一般不会引起其他接口的变化。
ISP里最小接口有时可以是多个单一职责的公共接口。
小结
- ISP:客户端不应该*依赖它们不使用的方法
- 胖类会导致它们的客户程序之间产生不正常复杂的耦合关系,我们可以将胖类的接口拆分成更小,更具体的接口
- 可以通过多重继承或委托来分离接口
Dependency Inversion Principle(DIP) 依赖倒置原则
High level modules should not depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions。
定义
- 高层模块不应该依赖于底层模块。二者都应该依赖于抽象。
- 抽象不应该依赖于细节,细节应该依赖于抽象。
理解
1.什么是依赖
需要
2.为什么需要
为了达到目的,功能。比如A需要B,来达到目的C。
3.举个违反DIP的例子
A 需要 B 达到 目的C
我需要食物 -》 填饱肚子
A:我,B:食物,C:填饱肚子
a. 昨天,我吃了面条来填饱肚子
class Noodle {
func fill() {
print("吃面条")
}
}
class People {
private var noodle: Noodle
init() {
self.noodle = Noodle()
}
func eat() {
noodle.fill()
}
}
let me = People()
me.eat() // 吃面条
b. 今天,我不想吃面条了,想吃汉堡
OK,让我们改一下
class Hamburger {
func bite() {
print("咬汉堡")
}
}
class People {
private var burger: Hamburger
init() {
self.burger = Hamburger()
}
func eat() {
burger.bite()
}
}
let me = People()
me.eat() // 咬汉堡
黑人问号.jpg
如果我后面想吃其他东西怎么办,换一次改一次?换一百种就改一百次???
我的目的是填饱肚子,为什么要绑定某一个特定的食物???
从上例子可知:
我们真正所需要、依赖的不是实际具体的类或物件,而是它们所拥有的功能。
4.什么是高层模块,什么是低层模块
高层与低层是相对而言,也就是调用者与被调用者的关系。如上述例子,我需要食物填饱肚子,则,People是高层模块,Hamburger/Noodle是低层模块。
5.什么是抽象
指应用背后的抽象,那些不随具体细节的改变而改变的真理,在代码中指接口或抽象类,即非具体实现对象。
6.为什么需要DIP
高层模块包含了一个应用程序中的重要的策略选择和业务模型,这才使得该应用程序区别于其他,但如果高层模块依赖于低层模块,那么对低层模块的改动就会直接影响到高层模块,从而迫使它们依次做出改动。
比如,上面的例子,在低层模块加多一种食物,高层模块(我)就是需要修改一次,太惨了。
7.将例子改一下
上述例子,第一次,我吃面条来填饱肚子的例子中,高层模块People 的事件(即函数eat
)中直接调用了 Noodle的相应动作,第二次,如果将食物换成了Hamburger, People模块也会收到影响,好麻烦,必须改啊!
在这个例子中,依赖的功能是啥,是能填饱肚子,管它是面条、汉堡、米饭,重点是能填饱肚子!!!
那我们定义一个接口好了(Swift 用 Protocol,相当于接口),再让Noodle去实现他,修改如下:
protocol Stuffer {
func stuff()
}
class Noodle: Stuffer {
func stuff() {
print("吃面条")
}
}
class People {
var stuffer: Stuffer
init() {
stuffer = Noodle()
}
func eat() {
stuffer.stuff()
}
}
画个图,原来的关系:
改变为:
想想,DIP怎么说的:高层模块不应该依赖于底层模块。二者都应该依赖于抽象。
很好,现在我们People类依赖的对象,从具体实际的对象(Noodle),变成了抽象的对象(Stuffer),Noodle对象也依赖抽象对象(Stuffer)
8.为什么叫依赖倒置(依赖反转)
还没修改的例子,高层模块(People)总是依赖于低层模块(Noodle/Hamburger),那么依赖倒置的话,不是应该让低层模块依赖于高层模块吗,但是现在他们都依赖于抽象啊???没有倒置啊???
请注意:
此时,食物(Noodle/Hamburger)依赖 【填饱肚子】这个需求(Stuffer),但这个需求(Stuffer)是人的(People)啊,所以食物间接依赖了人,即低层模块间接依赖了高层模块。
9.等等???
骗人!!!People明明还依赖了Noodle,它们的关系明明是这样的:
这句代码搞什么??!!!
init() { stuffer = Noodle() }
是的,刚刚是骗你们的,刚刚修改的例子并没有完全符合DIP,此时我们的高层模块(People)还依赖着低层模块的具体实现类(Noodle),那么怎么修改呢,在修改之前,我们先说说 IoC/DI
(小声BB:其实是故意这样写的,我猜大家肯定知道怎么改,只是不知道那样写还有个高大上的名字)
控制反转(Inversion of Control)
定义
- 是一种设计原则,借由分离组建的设置与使用,来降低类别和模块之间的耦合度
- 将对象的创建和获取提取到外部,由外部容器提供需要的组件
- 实例依赖物件的控制流程(Control Flow),由主动变为被动。
什么意思呢?
比如,你出门去餐馆吃了顿晚饭
注意,你此时吃的晚饭是你自己做的吗?不是,是餐馆的厨师给你做的的!
你需要 晚饭 , 不用自己 做, 而是 餐馆 提供给你的
||
需要 物件, 不用自己 取得,而是 服务容器 提供给你的
||
需要 依赖的实例,不用 主动 建立,而是 被动 接收
Inversion of Control v.s Dependency Inversion
两者不想等!!!
回去翻一下DIP的定义,他们倒转的是依赖关系,而控制反转,倒转的是实例依赖物件的控制流程
举个例子
还是填报肚子的那个例子
class Noodle {
func fill() {
print("吃面条")
}
}
class People {
private var noodle: Noodle?
init(stuffer: Stuffer) {
self.stuffer = stuffer
}
func eat() {
noodle.fill()
}
}
let noodle = Noodle()
let me = People()
me.setNoodle(noodle)
me.eat() // 吃面条
此时,主程序需要自己创建Noodle,自己创建People,然后再把实例化的noodle传递给实例化后的people,这个就是自己主动去建立的例子
OK,我们改一下,让它符合IoC
class Noodle {
func fill() {
print("吃面条")
}
}
class People {
private var noodle: Noodle?
init() {
self.noodle = Noodle()
}
func eat() {
if noodle != nil {
noodle.fill()
}
}
}
let me = People()
me.eat() // 吃面条
很明显啊,这个程式是违反了DIP的,但是它能透过IoC,被动获得类别为“Noodle“的实例noodle啊,此时解除了高层模块主动对底层模块的实例方法,却没有解除高层模块对底层模块的依赖关系
注意
IoC/DI,并非实现DIP的唯一解,只是实现DIP的一种方式。
依赖注入(Dependency Injection)
定义
- 顾名思义:将所需的依赖实例,注入到高层模块中
- 是实现IoC的一种方式
三种形式
其实,你们一定都用过,只是名字很高大上(装B)
- 1.构造器注入 (Constructor Injection)
- 2.设值方法注入(Setter Injection)
- 3.接口注入 (Interface Injection)
class People {
private var stuffer: Stuffer
// 构造器注入
init(stuffer: Stuffer) {
self.stuffer = stuffer
}
// 设值方法注入
// 就是Setter,Swift不需要写
// 接口注入
func injectStuffer(stuffer: Stuffer) {
self.stuffer = stuffer
}
func eat() {
stuffer.stuff()
}
}
由上,高层模块完全依赖于抽象,没有与具体的实现类耦合,符合DIP
最后的修改
说完依赖注入,大家知道那个例子该怎么修改了吧
protocol Stuffer {
func stuff()
}
class Hamburger: Stuffer {
func stuff() {
print("咬汉堡")
}
}
class Noodle: Stuffer {
func stuff() {
print("吃面条")
}
}
class People {
var stuffer: Stuffer
init(stuffer: Stuffer) {
self.stuffer = stuffer
}
func eat() {
stuffer.stuff()
}
}
现在People没有依赖于Noodle了吧,使用DI,也符合了DIP
小结
- DIP : 高层模块不应该依赖于底层模块。二者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
- 可以通过IoC/DI 实现 DIP
- DIP有助于保持低耦合,易扩展、增强可重用性
参考:
1.《敏捷软件开发:原则、模式与实践》第8-12章
2. S.O.L.I.D:面向对象设计的头 5 大原则
3. 面向对象设计原则和创建SOLID应用的5个方法
4.依賴倒置原則 (Dependency-Inversion Principle, DIP)
5.控制反轉 (IoC) 與 依賴注入 (DI)