返回

设计模式高频考点<第一季>

2022-03-02 by yexiayun

设计模式高频考点<第一季>

本文带有很浓重的主观色彩,有不同观点是非常正常的,欢迎指正。

目录

设计模式是干什么的?

产品设计,就是尽可能让用户用得更舒服。不严谨的说,我们的OOP设计模式,就是尽可能让你写的代码(大部分情况是类Class)尽可能让你的「同事」包括自己用的舒服。

产品、外观、OOP对于好的设计有很多共同点,比如:简单,干净,不会绕来绕去。

设计模式中「模式」二字,代表类的设计是有「道」和「术」的规律可循的。

设计原则「道」(2/7)

单一职责原则 (Single Responsibility Principle)

一句话:越简单越幸福。你起了一个类名LoginViewController,那你最好别做和Login无关的事情。

反例1:Register和Login的VC界面很像,只有部分不一样,这个时候比较糟糕的做法就:

class LoginViewController {

  enum Type {
  case login
  case register
  }

  var type: Type = .login

  func doSomething1() {
    if type == .login {
      /*.login 时的逻辑..*/
    } else {
      /*.register 时的逻辑..*/
    }
  }

  func doSomething2() {
    if type == .login {
      /*.login 时的逻辑..*/
    } else {
      /*.register 时的逻辑..*/
    }
  }
}

LoginViewController 承担了注册和登录的职责,显然是不满足单一原则的。

显而易见的缺陷:

解决方案:

开放-关闭原则 (Open-Closed Principle)

修改/新增一个功能,「尽可能」不用动到原来的代码,视情况而定。


图1小霸王游戏只要换卡就行了,图2俄罗斯方块换游戏的话要修改主板里的代码。。。显然小霸王更复合「开闭原则」

这个描述比较简略,如果我们详细表述一下,那就是,添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。

举一个商城这边的实际例子:

1、上传订单信息到棒糖服务器,服务器下发一个pid。

2、调用支付宝sdk,并把pid传给支付宝sdk。

很容易得到这样的伪代码

class func Payment {

  private var alipaySdk = AlipaySDK()

  init() {
    alipaySdk.delegate = self
  }

  func pay(with order: OrderInfo) {
    let request = BMRequest(order: order)

    request.start { pid
      alipaySdk.pay(amount: order.amount,
                    pid: pid)
    }
  }

  // 支付宝的回调用
  func alipay(_ sdk, status: ...)
  func alipay(_ sdk, userInfo: ...)
}

如果现在要加一个微信呢?你不得不在原来代码上做修改,pay方法很可能会变成这样。

class func Payment {

  private var wechatSdk = WeChatSDK() // 😓新增
  private var alipaySdk = AlipaySDK()

  init() {
    alipaySdk.delegate = self
    wechatSdk.delegate = self
  }

  // ❌修改代码
  func pay(with order: OrderInfo, type: PayType) {
    let request = BMRequest(order: order)

    request.start { pid
      // ❌修改代码
      if (type == .alipay) {
        alipaySdk.pay(amount: order.amount, 
                         pid: pid)
      } else if (type == .wechat) {
        wechatSdk.pay(amount: order.amount, 
                         pid: pid)
      }
    }
  }

  // 支付宝的回调用
  func alipay(_ sdk, status: ...)
  func alipay(_ sdk, userInfo: ...)

  // 😓新增代码
  func wechat(_ sdk, status: ...)
  func wechat(_ sdk, userInfo:...)
}

如果要加一个京东支付,我们还得继续在源代码上修改

我们的做法:

protocol PaySdk {
  pay(amount: Int, pid: Int)
}

class Alipay: PaySdk {
  func pay(amount: Int, pid: Int) {
    ...支付报相关的逻辑
  }

    // 支付宝的回调用
  func alipay(_ sdk, status: ...)
  func alipay(_ sdk, userInfo: ...)
}

class WeCheat: PaySdk {
  func pay(amount: Int, pid: Int) {
    ...微信支付逻辑
  }

  // 微信回调用
  func wechat(_ sdk, status: ...)
  func wechat(_ sdk, userInfo:...)
}

class Payment {
  private sdk: PaySdk!

  init(sdk: PaySdk) {
    self.sdk = sdk
  }

  func pay(with order: OrderInfo) {
    let request = BMRequest(order: order)
    request.start { pid
      sdk.pay(amount: order.amount, 
                       pid: pid)
    }
}

//  使用
let alipay = Payment(sdk: Alipay())
alipay.pay(amount: ...)

let wechatPay = Payment(sdk: WeCheat())
wechatPay.pay(amount: ...)

之后如果要再来一个京东支付

你只要实现PaySdk 这个协议即可:

class JdPay: PaySdk {
  func pay(amount: Int, pid: Int) {
    ...京东支付逻辑
  }

  // 京东的回调
  func jd(_ sdk, status: ...)
  func jd(_ sdk, userInfo:...)
}

// 使用
let jdpay = Payment(sdk: JdPay())
jdpay.pay(amount: ...)

新增功能写「新代码」,不需要改老代码,做到了开闭原则。

设计模式「术」<3/23>

代理模式

官方说法:为其他对象提供一种代理以控制对这个对象的访问。

想想我们的科学上网,就是代理模式.

假如我们访问YouTube,只发了一个POST请求,过程如下:

上行:客户端发起请求 —-> 代理服务器——> Youtube服务器

下行:Youtube服务器 —–> 代理服务器——> 客户端

下面还是来一个实际例子:

protocol User {
  var isPrime: Bool { set get }
  func syncToServer()
}

class FemometerUser: User {

  var isPrime: Bool

  init(_ isPrime: Bool) {
    self.isPrime = isPrime
  }

  func syncToServer() {
    // .. 服务器..
  }
}

假设iOS没有 KVO功能,我们想监听isPrime属性的改变,但是又不想改动原来FemometerUser、User的代码,该怎么做呢?

protocol User {
  var isPrime: Bool { set get }
  func syncToServer()
}

class FemometerUser: User {

  var isPrime: Bool

  init(_ isPrime: Bool) {
    self.isPrime = isPrime
  }

  func syncToServer() {
    // .. 服务器..
  }
}

// 代理
class UserIsPrimeObserverProxy: User {

  private var target: User // 真实对象
  public var isPrimeDidChanged: ((Bool) -> Void)?

  var isPrime: Bool {
    set {
      // 转发给真实的对象, 可以加一些自己的逻辑
      if newValue == target.isPrime {
        // 去重
        return
      }
      target.isPrime = newValue
      isPrimeDidChanged?(newValue)
    }
    get {
      // 从真实的对象中获取价值
      return target.isPrime
    }
  }

  func syncToServer() {
    // 转发给真实的对象
    target.syncToServer()
  }

  init(_ target: User) {
    self.target = target
  }
}

使用:

let user = FemometerUser(false)

let observerProxy = UserIsPrimeObserverProxy(user)

observerProxy.isPrimeDidChanged = { newValue in
  print("change:\(newValue)")
}

// 使用代理访问
observerProxy.isPrime = false
observerProxy.isPrime = true
observerProxy.isPrime = true
observerProxy.isPrime = true
observerProxy.isPrime = false

// 使用代理访问同步服务器的方法
observerProxy.syncToServer()

// 输出
change:true
change:false

下面是KVO的官方实现原理图,运用了isa_hook实现对象级别的hook实现的,其本质还是代理模式。

享元模式

运用共享技术有效地支持大量细粒度的对象。

享元(Flyweight)的核心思想很简单:如果一个对象实例一经创建就不可变,那么反复创建相同的实例就没有必要,直接向调用方返回一个共享的实例就行,这样即节省内存,又可以减少创建对象的过程,提高运行速度。

享元模式在Java标准库中有很多应用。我们知道,包装类型如Byte、Integer都是不变类,因此,反复创建同一个值相同的包装类型是没有必要的。以Integer为例,如果我们通过Integer.valueOf()这个静态工厂方法创建Integer实例,当传入的int范围在-128~+127之间时,会直接返回缓存的Integer实例:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Integer n1 = Integer.valueOf(100);
        Integer n2 = Integer.valueOf(100);
        System.out.println(n1 == n2); // true 说明地址相同
    }
}

想想我们的UITableView的复用机制,是不是用的享元模式?

迭代器模式

官方说法:迭代器模式是一种行为设计模式,让你能在不暴露集合底层表现形式 (列表、 栈和树等) 的情况下遍历集合中所有的元素。

还是来一个fm实际例子,我们想实现一个这样的开发体验

let body1 = BodyStatus(type: .bbt, id: 1)
let body2 = BodyStatus(type: .period, id: 2)
let body3 = BodyStatus(type: .bbt, id: 3)
let body4 = BodyStatus(type: .bbt, id: 4)
let body5 = BodyStatus(type: .period, id: 5)
let body6 = BodyStatus(type: .bbt, id: 6)

let seq = PeroidSequence(origin: [body1,
                                  body2,
                                  body3,
                                  body4,
                                  body5,
                                  body6])
                                  
for i in seq {
  print(i.id)
}
// 输出结果 为 2 和 5

下面是实现方法

struct PeroidSequence: Sequence {

  let origin: [BodyStatus]

  func makeIterator() -> PeriodIterator {
    return PeriodIterator(self)
  }
}

struct PeriodIterator: IteratorProtocol {

  typealias Element = BodyStatus
  private var index = 0
  private let seq: PeroidSequence

  init(_ seq: PeroidSequence) {
    self.seq = seq
  }

  mutating func next() -> BodyStatus? {
    let count = seq.origin.count
    while (index < count && seq.origin[index].type != .period) {
      index += 1
    }
    if index >= count {
      return nil
    }
    let result = seq.origin[index]
    index += 1
    return result
  }
}

可以看到,我们我们把「访问type为period的bodystatus的逻辑」封装起来了,下次使用只要用「更加优雅」for ..in 就可以了,不需要每次在for循环里做判断。

此外,我们还可以为类似二叉树这样的非线性结构创建不同的迭代器,例如:创建一个「先序遍历迭代器」和 「层次遍历迭代器」,原理同上就不写具体代码了。

最后

同事就是你的用户,互相给一些feedback吧。