本文代码基于Swift 5
题目
当前有个聊天页面,需要支持发送文字、图片功能,文本直接发送即可,但图片发送功能有不同,需要:
- 先从服务端获取一个资源ID
- 然后通过该ID将图片上传到服务器
- 最后将图片发送给对方
目前以上功能代码(如准备文字、图片,发送网络请求等逻辑)都写在聊天页面的ViewController中,后序可能还要支持发送音频、视频等功能,为防止后序页面代码膨胀不好维护,请给出优化方案
解题思路
该题目想考察的点在于,
- 有多个功能,且每个功能的实现细节不太一样,如何将他们从ViewController里拆分出去
- 如何做到好的扩展性,使得新增的音频视频功能可以无缝加入,尽量少地降低对已有代码的修改以保持较高的质量
将发消息逻辑抽离ViewController
这一步最容易想到,我们可以简单地将发消息逻辑抽离到一个单独类--MessageSender中,大致代码如下
class MessageSender {
func sendText(_ message: TextMessage, completion: (Bool) -> Void) {
}
func sendImage(_ message: ImageMessage, completion: (Bool) -> Void) {
}
// MARK: - Image helper functions
/// 申请资源ID
/// - Parameter completion:
private func applyResourceID(_ completion: (Result<String, Error>) -> Void) {
}
/// 上传图片,获得资源地址
/// - Parameters:
/// - image: 图片
/// - completion:
private func uploadImage(_ image: UIImage, completion: (Result<String, Error>) -> Void) {
}
}
- 增加了对应的send方法来抽离发送逻辑
- 但,不满足扩展性的要求
- 假设后序加入发送音视频逻辑,势必要在MessageSender中加逻辑
- 这一方面导致MessageSender膨胀,同时也违背了开闭原则,增加了该类引发bug的风险
封装发送逻辑
那接下来的设计思路是,想办法将不同发送逻辑封装起来
class MessageSender {
func sendText(_ message: TextMessage, completion: (Bool) -> Void) {
let operation = TextOperation(message)
operation.completion = { error in
}
operation.execute()
}
func sendImage(_ message: ImageMessage, completion: (Bool) -> Void) {
let operation = ImageOperation(message)
operation.completion = { error in
}
operation.execute()
}
}
/// 不同类型消息发送逻辑的抽象基类
class MessageSendOperation {
func execute() {}
}
class TextOperation: MessageSendOperation {
private let message: TextMessage
var completion: ((Error?) -> Void)?
init(_ message: TextMessage) {
self.message = message
}
override func execute() {
print("send text message!")
completion?(nil)
}
}
class ImageOperation: MessageSendOperation {
private let message: ImageMessage
var completion: ((Error?) -> Void)?
init(_ message: ImageMessage) {
self.message = message
}
override func execute() {
// apply resource ID
// upload image
print("send image!")
completion?(nil)
}
// MARK: - Image helper functions
/// 申请资源ID
/// - Parameter completion:
private func applyResourceID(_ completion: (Result<String, Error>) -> Void) {
}
/// 上传图片,获得资源地址
/// - Parameters:
/// - image: 图片
/// - completion:
private func uploadImage(_ image: UIImage, completion: (Result<String, Error>) -> Void) {
}
}
在此方案基础上,如果需要新增一个发送视频的逻辑,需要做以下步骤:
- 新增一个发送视频的Operation
- MessageSender中加入sendVideo方法
- ViewController中调用sendVideo
虽然仍然没办法做到完全符合开闭原则(对MessageSender做了修改),但毕竟都是新增代码,且在MessageSender中增加的代码比之前要少,所以出错概率也小
其实该方案中的MessageSendOperation,和iOS中的Operation是类似的,所以此处使用Operation也是完全ok甚至更好的选择
该方案的目的就是将发送逻辑通过某种形式封装起来,方便独立维护和复用
这个设计就有点像命令模式了
还有什么问题
有人可能发现上面代码存在问题:
- operation.execute()结束后operation会释放,导致任务执行后不能成功回调
确实有该问题,但为何还这样写?
因为真正的MessageSender其实远比上面代码要复杂,比如要考虑
- 支持多条消息并发
- 支持取消发送
- 支持发送失败后重试功能
- 消息过多时要考虑使用队列等类似逻辑避免内存爆炸
上面代码仅是在设计层面给出一个方向和示例,具体还有许多细节需要继续设计
为何不建议使用策略模式
iOS应用架构谈 view层的组织和调用方案中建议使用策略模式,经过一番查阅和思考,我对该思路有不同看法
先贴一下策略模式的定义
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
策略模式核心思路是将同一个功能的不同策略封装起来,不同的策略在外部看来是同样的抽象(即可以无缝的替换)
这里面很重要的一点是--针对同一个功能的不同策略,暗含的意思是,不同的策略的输入和输出是一样的,比如卖同一个商品,但不同的活动对应着不同的折扣
而该问题中,不同消息的发送的逻辑,不能看做是发送功能的不同策略,不同消息的发送逻辑可能天差地别。
在代码层面上,不同消息的发送逻辑很难做到无缝的替换。就比如文章中给出的MessageSener的代码示例
@property (nonatomic, strong) NSArray *strategyList;
self.strategyList = @[TextSenderInvocation, ImageSenderInvocation, VoiceSenderInvocation, VideoSenderInvocation];
// 然后对外提供一个这样的接口,同时有一个delegate用来回调
- (void)sendMessage:(BaseMessage *)message withStrategy:(MessageSendStrategy)strategy;
@property (nonatomic, weak) id<MessageSenderDelegate> delegate;
@protocol MessageSenderDelegate<NSObject>
@required
- (void)messageSender:(MessageSender *)messageSender
didSuccessSendMessage:(BaseMessage *)message
strategy:(MessageSendStrategy)strategy;
- (void)messageSender:(MessageSender *)messageSender
didFailSendMessage:(BaseMessage *)message
strategy:(MessageSendStrategy)strategy
error:(NSError *)error;
@end
- 由于使用了策略模式,所以就假定了不同发送逻辑(策略)的输入和输出一样了,所以sendMessage:withStrategy:方法使用了一个基类BaseMessage来将不同的消息进行抽象
- 那带来的问题是,执行具体的发送逻辑时,就不得不通过强制类型转换,转为具体的Message类型
- 另外,潜在的风险是,如果几个消息的发送逻辑的输入输出在后序基本保持一致那还好,一旦变动较大时,那可能MessageSender的send方法就得进行大的调整以适配这种变动