Artsy 工程师总结的一些 Cocoa 开发设计误区

简介: 本文讲的是Artsy 工程师总结的一些 Cocoa 开发设计误区,在开发 Artsy 这款 iOS app 的时候,我们尝试了一些设计模式。现在我想要谈谈现在我们有的和已经被移除的设计模式。我不会面面俱到,毕竟已经历了那么长时间
本文讲的是Artsy 工程师总结的一些 Cocoa 开发设计误区,

在开发 Artsy 这款 iOS app 的时候,我们尝试了一些设计模式。现在我想要谈谈现在我们有的和已经被移除的设计模式。我不会面面俱到,毕竟已经历了那么长时间,有那么多人参与过。我想从更高的层面去审视,关注那些总体上更重要的东西。

很重要的一点需要先声明下,我不相信有完美的代码,或者说我喜欢重写代码。我们可以发现一个坏的模式而什么都不做。毕竟我们有 app 需要完成,而不可能纯粹为了技术,追求更完美的代码库。

用 NSNotification 解耦

大量 Energy 的初始代码库依靠 NSNotification 在应用程序内传递信息。这些通知用于用户设置调整,下载状态更新,授权与相应的错误状态,以及一些 app 特性。这些 Energy 代码太过于依赖这些全局通知进行交流,而鲜有尝试去窥探对象之间的关系。

NSNotificationCenter 的通知在 Cocoa 是一种观察者模式的实现。 他们是初学者到中级程序员设计范式的梦想。它提供一种解耦的方式让对象相互发送消息。这对于刚入门的 iOS 开发者来说很容易上手。

使用 NSNotification 最大的弊端在于容易使得开发者变懒。它允许你不去深究对象之间的关系,假装它们是松耦合的。而实际当他们是耦合的时候,却通过字符串形式的通知传递消息。

松耦合(Loose-coupling)有它的作用,但是一不小心容易存在多个对象都去监听通知。学会注销通知也是一个棘手的问题,这与我们熟悉的默认内存管理行为不同(了解更多)。

我们在 Energy 还是存在大量的通知,而在 Eigen 和 Eidolon 几乎没有。我们甚至没有一个具体的文件来储存常量。

#define kARConstant

这里不用多说,当我学习 Objective-C 的时候,确实喜欢使用 #defines 声明常量。就像 C 语言里面的 throw back。使用#defines 声明常量并不会消耗设备内存来储存常量。这是因为 #defines 在预编译阶段直接将源代码替换为值,而使用静态常量会消耗设备的内存空间。我们以前对此很在意。但很可能是现代版本的 LLVM 在需要时才分配设备内存,特别是那些被标记为 const 的。转化为真实变量意味着你可以在调试模式下检查和使用,同时更好地依赖类型系统。

说了这么多,其实就是当我们#define TICK_DIMENSION 32 的时候,应该改成 static const NSInteger ARTickViewDimensionSize = 20;

撒点分析(Sprinkling Analytics)

统计分析中,我们采用面向切面编程AOP的思想。

过去代码是这样:

 
     
@implementation ARAddToAlbumViewController
- ( void)tableView:( UITableView *)tableView didSelectRowAtIndexPath:( NSIndexPath *)indexPath
{
if (indexPath.row < [ self.albums count]) {
Album *selectedAlbum = ((Album *) self.albums[indexPath.row]);
ARTickedTableViewCell *cell = (ARTickedTableViewCell *)[ self.tableView cellForRowAtIndexPath:indexPath];
if ([cell isSelected]) {
[ARAnalytics event:ARRemoveFromAlbumEvent withProperties:@{
@"artworks" : @( self.artworks.count),
@"from" : [ARNavigationController pageID]
}];
[...]

现在是这样

 
     
@implementation ARAppDelegate (Analytics)
- ( void)setupAnalytics
{
ArtsyKeys *keys = [[ArtsyKeys alloc] init];
[...]
[ARAnalytics setupWithAnalytics: @{ [...] } configuration:
@{
ARAnalyticsTrackedEvents:
@[
@{
ARAnalyticsClass: ARAddToAlbumViewController.class,
ARAnalyticsDetails: @[
@{
ARAnalyticsEventName: ARRemoveFromAlbumEvent,
ARAnalyticsSelectorName: NSStringFromSelector( @selector(tableView: didSelectRowAtIndexPath:)),
ARAnalyticsProperties: ^ NSDictionary*(ARAddToAlbumViewController *controller, NSArray *_) {
return @{
@"artworks" : @(controller.artworks.count),
@"from" : [ARNavigationController pageID],
};
},
[...]
]
},
[...]

这样就不会把统计代码分散到各个文件中,让每个对象的职责变得单一,这也是我们在 Eigen 中的实现。但我们并未移植到 Energy 中,因为它的依赖库 ReactiveCocoa 过于庞大。目前我们一直以内联的方式进行统计,因为 Energy 只有很少地方需要单独进行统计。如果你想要了解更多这个模式,请查看面向切面编程与 ARAnalytics

把类方法当做全局 API

很长一段时间,我更喜欢基于类的 API 美学。比如使用类方法而不是实例方法。我一直是这么做。然而,一旦你开始给项目添加测试,这就会产生一些问题。

我热衷于在测试内应用依赖注入的思想。这个有点复杂,简要来说,就是传入一个额外的上下文,而不是一个对象自己找到上下文。常见的例子就是 NSUserDefaults。你的类可能并不需要知道你使用的是哪个 NSUserDefault 对象,而是你调用的方法在决定,比如 [[NSUserDefaults standardUserDefaults] setObject:reminderID forKey:@"ARReminderID"];。使用依赖注入将允许通过方法从外部传入对象。如果你想更深入了解这块,可以看看 Jon Reid 这篇 objc.io 译文:依赖注入

基于类的 API,它的问题在于变得很难注入对象。这不利于写出简洁快速的测试。你可以使用一个模拟(mocking)库来伪造类 API,但这感觉很奇怪。模拟(mocking)应该被用于你不控制的事物。如果你正在写 API,那么你就控制了这个对象。拥有一个实例对象意味着可以给不同的版本提供不同行为和值,如果你可以通过 协议(protocol) 减少实例上的行为,那会更好。

对象隐匿地联网

当你有一个复杂的应用,会有很多地方可以执行网络操作。我们在模型,视图控制器和视图都有过网络操作。基本上把纯粹的 MVC 模式给抛弃了。我们开始意识到 eigen 的设计模式,因为它以前的网络层并未抽象出来。如果你想要看到完整内容,请查看moya/Moya README

我们企图通过构建不同类型的网络客户端来尝试修复这种模式,这客户端就是我所刚提到的 Moya

另一方面,是将网络层抽象成一个独立的对象。如果你听过 Model-View-ViewModel (MVVM),这很相似,只是视图(View)换成网络操作。网络操作模型给我们提供了一个方法来将网络操作抽象成一系列行为。额外的抽象意味着“我想要关于 x 的东西,而不是发送一个 GET 给 x 地址,并且变成 y”。

网络模型也使得在测试中交换行为变得极为容易。在 eigen,我们拥有异步网络,能在测试中同步运行,但我们还是一直使用网络模型,从而可以在测试中提供我们期望服务端返回的数据 。

子类化超过两次

为了提供一个类似但有点不同的行为,通过子类化是非常简单的。可能你需要重写某个方法,或者添加一个特殊的行为。但就像温水煮青蛙的故事,随着层级结构加深,期望的行为被改变,最终你将获得难以理解的代码库。

处理这种情况的一种模式是类组件。其思想是通过多个对象一起工作来取代一个对象处理多个事情。给每个对象提供更多的空间来遵循单一职责原则(SRP)。如果你对这有兴趣,你可能也会对类簇模式感兴趣。

举一个来自 Energy 的好例子,我们的根视图控制器 ARTopViewController 过去常常控制自己的工具栏项目(toolbar items)。经过四年的时间这变得难以管理,在视图控制器有大量的额外代码。通过抽取控制工具栏项目(toolbar items)的实现细节到他们自己的类,从而让 ARTopViewController 展示自己想要做的而不是怎么做的。

通过类间通信配置类

Energy 最重要的一部分就是 email artworks。配置你想要发送的邮件,然后根据设置生成 HTML,因此这会产生大量的代码。起初很简单,因为我们只有很少的应用设置。随着时间的推移,依据设置和它们如何影响邮件内容决定我们需要显示什么,这会变得很复杂。

视图控制器里允许小伙伴选择各自想要传递给对象的细节,然后生成 HTML,这部分最终变得具有很强的代码异味,让你想要重写。我发现想要给类的行为写个简单的测试是很困难的。一开始我要模拟(mock) email 组件,然后检查我所调用的方法。这感觉是错的,因为你不应该模拟(mock)你自己的类。类提供了重要的功能,对于如何改进这部分代码,我想了很久。

问题的解决灵感来自 Justin Searls 演说,“有时控制器只是控制器”,特别是第55张 PPT。他谈到对象,要么持有并描述一个值,要么执行一个有用的行为,绝不要两者都有。

我采纳了这个建议,重新评估了设置控制器和组件对象之间的关系。在改动之前,设置控制器会直接配置组件。现在,设置控制器创建了一个配置对象,供组件使用。这就使得给两个对象写测试变得极其简单,因为他们都有很明显的输入和输出,其格式是AREmailSettingsAREmailComposerTests 也变得非常优雅。

直接使用响应链

在我去 Artsy 工作之前,我是一名 Mac 开发者,在 iOS 存在之前就一直是,所以这也影响了我的代码风格。Cocoa 工具链最主要的部分之一是响应链,一个很好的记录方法传递一个已知的对象链。它解决了复杂视图结构的常见问题。你可以通过运行时,在很深的视图层级生成一个按钮,然后当它被点击时被视图控制器接收处理。你可以使用很长的代理方法链,或者使用私有方法获得视图控制器实例的引用。在 Mac 开发,使用响应链是一种常见的模式,在 iOS 就使用得很少。

我们在 Eigen 的视图控制器也有这种问题。有一些按钮在很深的视图层级,需要将信息传递到视图控制器。当我们第一次碰到这种问题,立即使用了响应链,你写了几行代码类似:[bidButton addTarget:self action:@selector(tappedBidButton:) forControlEvents:UIControlEventTouchUpInside];,其中 self 指向视图。这会向上传递信息 tappedBidButton: ,直到被ARArtworkViewController 响应。

我必须解释,响应链的前提是大多数人接触这块代码区域。在 “lucky 10,000” 这是可行的,但意味着这模式对于之前没听说过的人并不直观。还有一个的问题,缺乏耦合意味着通过重构重命名 selector 会打破响应链条。

减少认知负担的方式是通过协议,所有响应链将会使用的动作都会通过类似 ARArtworkActionsViewButtonDelegate 的协议映射。这有点善意谎言的意味,没有通过直接的关系来使用协议,但是它使得关系更加明显。我们使用类拓展(class extension)来遵守这些类型的协议,从而保持所有动作都在同一个地方。

总结

设计模式有很多,而它们全都来源于权衡。随着时间的推移,我们对于什么是“好的代码”的标准会变,这是好事。重要的是,作为开发者,我们明白,能改变我们思想的,才是我们工具链中最必不可少的技能之一。这意味着你要走出自己原本的认知范围,乐于接受那些外来的信息,或许,你会从中获得一些很不错的点子。对于创造应用持有热情是好的,不过我想,最好的程序员选择实用主义而不是理想主义。





原文发布时间为:2016年01月07日

本文来自云栖社区合作伙伴掘金,了解相关信息可以关注掘金网站。
目录
相关文章
|
26天前
|
移动开发 开发工具 UED
谈谈你对跨平台移动应用开发的看法。
跨平台移动开发降低开发成本和时间,提升效率,使应用快速上市。它确保多平台用户体验的一致性,但可能牺牲特定功能和性能,且兼容性有时成问题。原生开发与跨平台各有优劣,选择取决于项目需求、预算和时间限制。
16 4
|
测试技术 数据库 安全
带你读《C++代码整洁之道:C++17 可持续软件开发模式实践》之二:构建安全体系
如果想用C++语言编写出易维护的、扩展性良好的以及生命力强的软件,那么,对于所有的软件开发人员、软件设计人员、对现代C++代码感兴趣或想降低开发成本的项目领导者来说,本书都是必需品。如果你想自学编写整洁的C++代码,那么本书也是你需要的。本书旨在通过一些示例帮助各个技术层次的开发人员编写出易懂的、灵活的、可维护的和高效的C++代码。即使你是一名资深的开发工程师,在本书中也可以找到有价值的知识点。
|
26天前
|
程序员 开发者
代码之禅:在软件开发中寻求简化之道
当面对错综复杂的系统和冗长的代码时,程序员们往往渴望找到一种简洁而高效的解决方式。本文探讨了如何在软件工程实践中追求简洁性,介绍了几种提升代码质量、优化开发流程的策略,并强调了持续学习与反思的重要性。通过一系列实用的技术感悟,文章旨在引导读者思考如何将“简化”作为编程哲学的一部分,以期达到技术上的精进和心智上的清晰。
|
7月前
|
存储 JavaScript 前端开发
关于缺少编程基础的朋友想转行 ABAP 开发岗提出的一些咨询问题和解答
关于缺少编程基础的朋友想转行 ABAP 开发岗提出的一些咨询问题和解答
53 0
|
Web App开发 前端开发 JavaScript
干了SAP开发这么多年,我都积累了哪些程序调试技巧
干了SAP开发这么多年,我都积累了哪些程序调试技巧
137 0
干了SAP开发这么多年,我都积累了哪些程序调试技巧
|
架构师 城市大脑 开发者
为什么大部分人做不了架构师?这2点是关键 | 开发者必读(106期)
最炫的技术新知、最热门的大咖公开课、最有趣的开发者活动、最实用的工具干货,就在《开发者必读》!
1132 0
|
C++ 测试技术
带你读《C++代码整洁之道:C++17 可持续软件开发模式实践》之三:原则
如果想用C++语言编写出易维护的、扩展性良好的以及生命力强的软件,那么,对于所有的软件开发人员、软件设计人员、对现代C++代码感兴趣或想降低开发成本的项目领导者来说,本书都是必需品。如果你想自学编写整洁的C++代码,那么本书也是你需要的。本书旨在通过一些示例帮助各个技术层次的开发人员编写出易懂的、灵活的、可维护的和高效的C++代码。即使你是一名资深的开发工程师,在本书中也可以找到有价值的知识点。
|
程序员 开发工具 数据格式