设计模式六大原则(Swift)

什么是设计模式

每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动

在历史性著作《设计模式:可复用面向对象软件的基础》一书中描述了23种经典面向对象的设计模式,创立了模式在软件设计中的地位。由于《设计模式》一书确定了设计模式的地位,所以通常所说的设计模式隐含地表示"面向对象设计模式"。但这并不意味"设计模式"就等于"面向对象设计模式",在Swift中,我们编程会更倾向于面向协议的设计模式。

设计模式六大原则

虽然各种编程语言因为各自语言的特性,会偏好不同的设计模式,但是设计模式的原则是不会改变的,目前有六大原则:

  1. 单一职责原则(Single Responsibility Principle)
  2. 里式替换原则(Liskov Substitution Principle)
  3. 依赖倒置原则(Dependence Inversion Principle)
  4. 接口隔离原则(Interface Segregation Principle)
  5. 迪米特法则(Law Of Demeter)
  6. 开闭原则(Open Closed Principle)

直接解释原则有点抽象,我们先看一个案例,看完案例后再解释原则会容易理解很多

设计案例

我们做一个简单一点的需求,做一个画板,上面画不同的形状,然后做两种方案对比下:

方案一:

swift 复制代码
struct Point {
    var x = 0
    var y = 0
}

struct Line {
    var start: Point
    var end: Point
}

struct Rect {
    var leftUp: Point
    var width = 0
    var height = 0
}

class MainForm {
    var sharps: [Any] = []
    func draw(sharp: Any) {
        sharps.append(sharp)
    }
    
    func OnPaint() {
        sharps.forEach { sharp in
            if let point = sharp as? Point {
                // draw point
            } else if let line = sharp as? Line {
                // draw line
            } else if let rect = sharp as? Rect {
                // draw rect
            }
        }
    }
}

let form = MainForm()
form.draw(sharp: Point(x: 12, y: 13))
form.draw(sharp: Rect(leftUp: Point(x: 0, y: 0), width: 10, height: 15))
form.draw(sharp: Line(start: Point(x: 0, y: 1), end: Point(x: 10, y: 11))) 
form.OnPaint()

方案二:

swift 复制代码
protocol Sharp {
    func draw()
}

struct Point: Sharp {
    var x = 0
    var y = 0
    func draw() {
        // draw point
    }
}

struct Line: Sharp {
    var start: Point
    var end: Point
    func draw() {
        // draw Line
    }
}

struct Rect: Sharp {
    var leftUp: Point
    var width = 0
    var height = 0
    func draw() {
        // draw Rect
    }
}

class MainForm {
    var sharps: [Sharp] = []
    func draw(sharp: Sharp) {
        sharps.append(sharp)
    }

    func OnPaint() {
        sharps.forEach { sharp in
            sharp.draw()
        }
    }
}

let form = MainForm()
form.draw(sharp: Point(x: 12, y: 13))
form.draw(sharp: Rect(leftUp: Point(x: 0, y: 0), width: 10, height: 15))
form.draw(sharp: Line(start: Point(x: 0, y: 1), end: Point(x: 10, y: 11)))
form.OnPaint()

我们画了3个形状,最后让图案显示出来。上面两种方案明显第二种会好一点,第二种更符合设计模式原则,我们通过第二种案例讲解下设计原则

六大原则详解

单一职责原则(Single Responsibility Principle)

单一职责原则,简称SRP。其定义是应该有且仅有一个类引起类的变更,这话的意思就是一个类只担负一个职责。就如同上面的案例里,原本各个形状的draw实现都是在MainForm中实现的,现在各自形状负责各自的draw实现。现在逻辑比较简单,所以从MainForm拆分出来没感觉出什么优势,但是随着逻辑的增多,职责越单一,被修改的原因就越少。

单一职责原则的优点:

  • 类的复杂性降低,实现什么职责都有明确的定义;
  • 逻辑变得简单,类的可读性提高了,而且,因为逻辑简单,代码的可维护性也提高了;
  • 变更的风险降低,因为只会在单一的类中的修改。

当然,如果类一味追求单一职责,有时会造成类的大爆炸,单一职责原则提出了一个编写程序的标准,用"职责"或"变化原因"来衡量接口或类设计得是否优良,但是"职责"和"变化原因"都是不可以度量的,因项目和环境而异。

里式替换原则(Liskov Substitution Principle)

先看下它的定义:

如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有对象o1都替换成o2的时候,程序P的行为都没有发生变化,那么类型T2是类型T1的子类型。

看起来有点绕口,它还有一个简单的定义:

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

通俗来讲,只要有父类出现的地方,都可以使用子类来替代,而且不会出现任何错误或者异常,所以使用继承的时候需要谨慎。如果可以,优先使用对象组合,而不是类继承。

继承的优点就不多说了,代码共享,减少创建类的工作量,提高代码的重用性等。但继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法,增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改。

所以在设计父类的时候往往定义成抽象类(Java)或者虚类(C++),然后定义抽象方法或者虚函数,显式的让子类实现抽象方法或者虚函数。当然,在Swift中没有这样的概念,与之对应的就是协议(protocol)了。使用了该种设计,父类本身就没有具体的实现,或者实现的内容比较少,很少会违背里式替换原则。

依赖倒置原则(Dependence Inversion Principle)

先看下定义:

高层模块不应该依赖底层模块,二者都该依赖其抽象;

抽象不应该依赖细节;

细节应该依赖抽象;

高层模块就是调用端,低层模块就是具体实现类。抽象就是指接口或抽象类。细节就是实现类。

在我们上面的案例里,MainForm就是高层模块,各形状的实现就是底层模块。在案例一中,我们MainForm在实现OnPaint的方法的时候,既依赖了底层模块也依赖了实现细节,完全违背了依赖倒置原则。

而在案例二中,MainForm没有依赖各形状的实现,而是MainForm和各形状都依赖于各形状的抽象protocol Sharp,抽象protocol Sharp不依赖于各形状draw的实现细节,反而实现细节因为protocol Sharp的声明而存在,这样完美遵循了依赖倒置原则

接口隔离原则(Interface Segregation Principle)

不应该强迫客户程序依赖它们不需要的方法

接口应该小而完备

首先说下什么是接口,一个模块对外暴露给外界使用的函数调用就是接口,它大多数呈现形式是一个类,当然也包含抽象类,在Swift中对应的就是协议。对于上面案例二中的MainForm来说,Sharp就是接口。那什么是"不应该强迫客户程序依赖它们不需要的方法"呢?

举个例子,假如我现在有个功能,需要得到图形的面积,那么我们是否应该在Sharp里面添加一个获取面积acquireArea的接口呢?答案是否定的,因为对于MainForm来说,acquireArea这个接口是不需要的,而我们每一个形状都要为了这个接口而实现这个功能,但对于那些不封闭的图形(比如点、线、曲线等)acquireArea是没有意义的,当然你可以返回为0,但这个是不合理的。正确的做法应该是在新增一个接口(协议),比如叫AreaSharp,然后在那些有封闭区域的形状中遵守协议并实现acquireArea方法:

swift 复制代码
protocol AreaSharp {
    func acquireArea() -> CGFloat;
}

struct Rect: Sharp, AreaSharp {
    var leftUp: Point
    var width = 0
    var height = 0
    
    func draw() {
        // draw Rect
    }
   
    func acquireArea() -> CGFloat {
        return CGFloat(width * height)
    }
}

这样,MainForm使用Sharp接口,而刚才想新增得到图形面积的功能用AreaSharp接口

那什么是"接口应该小而完备"呢,这个和单一职责原则那边有点像,不能过度设计,举个极端点例子,你不能为每一个需求的方法来设计一个接口类吧,所以接口虽然尽可能小,但是方法能整合在一起的,也尽可能要放一个接口类里。

迪米特法则(Law Of Demeter)

简称LoD,也被称为最少知识原则,它描述的规则是:

一个对象应该对其他对象有最少的了解

法则强调了以下两点:

  • 从被依赖者的角度来说:只暴露应该暴露的方法或者属性
  • 从依赖者的角度来说:只依赖应该依赖的对象

这个应该比较好理解,一个类不要暴露多余的功能函数给外界,防止外界乱调用出现异常。

开闭原则(Open Closed Principle)

软件实体应该对扩展开放,对修改封闭。

"扩展"就是新增的意思,我们还是看我们的那个案例,比如说,我们现在想增加一个圆的功能,对于方案一来说,除了必须要定义一个Cycle的结构体外,那么还需要在MainFormOnPaint方法里实现圆的绘制,这个就属于修改了,所以方案一不是一个好的设计。

反观方案二,我们只要新增如下定义就能完美解决问题:

swift 复制代码
struct Cycle: Sharp {
    var dot: Point
    var radius: CGFloat = 0
    func draw() {
        // draw Cycle
    }
}

方案二只有扩展而没有修改

总结

一款程序的实现是复杂,而在敏捷开发的现在,需求变化又是迅速的,所以我们设计模式的初衷是抵御变化

我们需要在代码中分清哪些是会"变化"的,哪些是"不变"的,我们尽可能把变化的部分关在一个笼子里,不要四散在各处代码中,也就是所谓的高内聚低耦合。

当然,我所说的"变化"和"不变"是相对的,没有完全不变的代码,如果有个逻辑三个月会变,另外一个逻辑三年才会变,那么相对来说,前者是"变化"的,后者是"不变"的。

后续,我可能会讲一些经典的设计模式。

相关推荐
我是陈泽2 小时前
一行 Python 代码能实现什么丧心病狂的功能?圣诞树源代码
开发语言·python·程序员·编程·python教程·python学习·python教学
肖哥弹架构1 天前
Spring 全家桶使用教程
java·后端·程序员
IT杨秀才4 天前
自己动手写了一个协程池
后端·程序员·go
程序员麻辣烫6 天前
像AI一样思考
程序员
一颗苹果OMG7 天前
关于进游戏公司实习的第一周
前端·程序员
万少8 天前
你会了吗 HarmonyOS Next 项目级别的注释规范
前端·程序员·harmonyos
楽码8 天前
彻底理解时间?在编程中使用原子钟
后端·算法·程序员
江南一点雨9 天前
又一家培训机构即将倒闭!打工人讨薪无果,想报名的小伙伴擦亮眼睛~
java·程序员
用户86178277365189 天前
ELK 搭建 & 日志集成
java·后端·程序员
河北小田9 天前
局部变量成员变量、引用类型、this、static
java·后端·程序员