杂谈 - 最近的一些心得体会和感想 (一)

夏天不知不觉就到了,在忙着补完今年 WWDC 的大部分视频以后,又要进入忙碌的适配期和调动期了。趁这个空档,简单汇报一下最近的一些感想。基本上从小到大可以归纳成四个方面:

  • 关于使用 Swift 协议确保类型的思考
  • 关于 PromiseKit 6 的变化所带来的思考
  • 关于以前对架构方面不够重视的思考
  • 关于最近五年开发历程和自我成长的思考

在同一篇文章里说完这些全部事情不太现实,所以我可能会在最近分几次,来对这几个思考做些总结。

关于使用 Swift 协议确保类型的思考

问题背景

相比于 Objective-C 这类“动态”语言,Swift 在类型安全上强制性要高出许多。配合上协议和 associatedtype,更是能做到另一个极致,很多时候可以让我们写出“无脑”的,能通过编译就不会有太大问题的代码。

在我现在手上的 LINE LIVE 项目中,有一个这样的使用协议,协议继承和关联类型来强化类型安全来进行开发的例子。在这个直播 app 中我们需要将用户的评论显示在聊天框里,大概看起来是这样的:

可以看到,需要显示的聊天的种类是蛮多的,不仅有用户的普通的输入聊天,还有像是礼物啊,系统信息啊之类的东西。截图中显示的是一小部分内容,实际上现在我们会从 server 端接收到 23 种不同类型的 message,而且这一数量预计还会随着 app 特性的增加持续增长。另一方面,我们在观众侧和主播侧共用了这个聊天显示的模块,但是在 UI 表现上,略有不同,比如为了让主播侧更容易看到某些消息,需要对字体大小颜色进行调整,进行突出显示等。另外,我们之前整个 app 是暗色模式,而现在调整为了浅色调,在 design 上也有大幅的变更。

如何清晰简洁 (或者说无脑) 地维护这些东西,来快速响应新特性的增加和设计的调整,变成了一个不小的挑战。

不太好的做法

四年前刚开始这一块的时候,想法非常单纯,那时候没有这么多需求,也没有像是 protocol extension 这样的工具。所以初始的实现可以说非常 naive,大概看起来就是这样的:

let attributedString: NSAttributedString
if message is ChatMessage {
    attributedString = //...
} else if message is GiftMessage {
    attributedString = //...
} else if message is ShareMessage {
    attributedString = //...
} else if message is LoveMessage {
    attributedString = //...
} else if /*...*/ {
    // 其他各种情况
}

cell.label.attributedText = attributedString

为了减少 view 的使用,并保证 table view 布局性能,我们没有使用 auto layout 做 cell 布局,而是保证基本每个 cell 都只包含一个 attributed string。这让我们可以尽快计算和缓存 cell 高度。

可以想象一下,首先你需要按照不同的消息类型去拼接和生成合适的 string,然后在生成 attributedString 时你还需要处理不同类型消息和不同 design 的情况,这又进一步涉及到。另外,在处理像是 GiftMessage 的情况的时候,我们有内嵌图片的问题,如果图片没有缓存,就还需要异步做下载;对于 LoveMessage 的情况,设计希望消息前面的心能够随机颜色,这也会带来额外的逻辑。

另外,为了效率的提升,我们希望对于 cell 的 attributedString 的内容,每条消息只进行一次处理,然后把结果缓存下来。这样,在用户上下滑动时就可以避免再进行 attributedString 的创建了。除了表示以外,在 app 的其他地方我们还会用到原来的 message (比如 block 用户,查看某个用户的详细信息等),所以我们还需要一种方式来将输入的 message 也存下来。

改进方法

定义一个协议来表示处理过的 message,比如:

protocol ParsedMessageType {
    associatedtype Original
    associatedtype ParsedResult

    var original: Original { get }
    var output: ParsedResult { get }
}

这是一个最泛的协议,只是说明了我们有一个表示原本的类型,和一个转换后的结果类型。

Original 转换为 ParsedResult 的步骤,通过一个 parser 来完成,我们叫它 MessageParser

protocol MessageParser {
    associatedtype Message
    associatedtype Target: ParsedMessageType where Message == Target.Original
    func parse(message: Message) -> Target.ParsedResult
}

MessageParser 中,我们限定了 Target 比如遵守 ParsedMessageType,并且它的 Original 需要和 Message 一致。这让我们可以确保编译器严格检查类型匹配,而不会意外地破坏 parsing 时候的类型约束。

然后,我们就可以在协议规定的类型约束下实现各种 message 的解析了。比如对于一般的用户评论:

```swift
final class ParsedChatMessage: ParsedMessageType {
let output: NSAttributedString
let original: ChatMessage
let highlightName: Bool

init(message: ChatMessage, highlightName: Bool) {
    self.original = message
    self.highlightName = highlightName

    let parser = Parser(textStyle: ChatMessageLabelStyle(), senderStyle: ChatSenderLabelStyle(), giftStyle: ChatGiftLabelStyle(), shouldHighlight: highlightName)
    self.output = parser.parse(message: message)
}

struct Parser<TextStyle: MessageLabelStyle,
    SenderStyle: MessageLabelStyle,
    GiftStyle: MessageLabelStyle>: MessageParser
{
    typealias Target = ParsedChatMessage

    let textStyle: TextStyle
top Created with Sketch.