设计模式六大原则(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
    }
}

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

总结

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

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

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

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

相关推荐
百万蹄蹄向前冲39 分钟前
2024不一样的VUE3期末考查
前端·javascript·程序员
陈哥聊测试1 天前
软件格局在变,谁能扛起国产替代的大旗?
安全·程序员·产品
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭2 天前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
少年姜太公2 天前
从零开始详解js中的this(下)
前端·javascript·程序员
凌虚2 天前
Kubernetes APF(API 优先级和公平调度)简介
后端·程序员·kubernetes
小华同学ai2 天前
ShowDoc:Star12.3k,福利项目,个人小团队的在线文档“简单、易用、轻量化”还专门针对API文档、技术文档做了优化
前端·程序员·github
小青鱼4 天前
AI编程-Cursor从入门到精通系列之常用概念及解释(二)
人工智能·程序员
捡田螺的小男孩5 天前
参数校验的十个建议!收藏好,别再给测试机会提bug~
java·后端·程序员
哔哩哔哩技术5 天前
B站装机系统实践:从初创到规模化的演进
前端·程序员
程序员鱼皮5 天前
没事别想不开去创业!
计算机·面试·程序员·项目