Objective-C 的 API 设计(转)
英文原文:API Design
转自oschina 参与翻译(14人):
我最常做的开发任务是设计一个可重用的API组件。组件通常为iOS(尽管有时它们是OS X) 设计的,且总是GUI控件或某种视图。 多年来,我为客户开发了很多API组件,其中包括像Apple这样的客户,而且我已经很了解这个过程。我也定期发布开源组件,并且我把曾经对我有帮助的资料和API设计指南放在一起与大家分享。 这是一个重要的主题,无论你是一个开源贡献者,或作为团队的一员参与开发大型的应用,或者只是设计自己的软件。正如开发一个应用的过程,API接口是使用你代码的开发者对你代码的第一印象,将严重影响着开发者决定是使用或扔掉它。 APIs是开发者的用户体验。我一直惊讶,具体到这个流行平台上没有很多的资料是写我们这方面工作的。 当我们阅读一些设计指南时,必要的时候,我将要用我最近发布的开源GUI组件MGTileMenu作为一个例子。你可以在这里先阅读所有关于MGTileMenu的信息,如果你喜欢。 |
![]() hyaicc
|
其它翻译版本(1) |
如何令人满意应用程序接口(API)设计和用户界面、用户体验设计很相像。你的目标用户有不同的需求和特点,但归根结底他们的目标还是把需求完成而已。就像一个设计友好、易用的应用程序的用户界面一样,你需要让你的API有以下的特点:
如同人们设计的其它的软件一样,我们首先需要考虑的是使用案列。我们的设计需要使最经常被用到的的功能简单易用,不需要过度的配置。在默认配置下软件就应该是可用的,并且具有一定的可配置性。软件的设计应该具有可探索性,而且应该允许用户从已知的的范例中推广到其他应用场景。这和我们创建一个用户界面的规则非常的相像。 |
![]() WangWenjing
|
其它翻译版本(1) |
开发者的界面用于和开发者交互的元素使用四个主要的显示意味着:
我们需要把每一个都设计成:明智和慎重的,用于人类使用。这里有2个问题当你设计API的时候需要考虑:
我们的核心原则是让已有的类和模型保持一致性,以用来保证我们可以把一个开发者不熟悉的控制让他很轻松的在他可以理解的平台上使用。使用标准APIs,模型,和模式无论是不是可能(并且这个应该是总用的)。对于终端用户,熟悉和直觉性是和代码层级一样重要的。 让我们看看我们之前提到的这四个元素: |
![]() 周荣冰
|
其它翻译版本(1) |
类接口Here’s the interface file for MGTileMenu. 在我们讨论具体的接口之前,这有一些涵盖范围比较广泛的规则: Rule 1: 使用方言我所看到最常见的错误是API的设计利用了外来的约定。APIs 属于固定平台和固定的开发者生态系统。你根本无法使用任何习语和你用过的其他平台的架构,这样做会污染您当前的代码库,并对其他开发人员的效率造成损害。 在coding之前要了解你目标平台的约定,比如,在iOS 或者 OS X,不使用异常对待control的流程 。以适当的方式命名你的方法(通常指有足够详细,但也应该有足够的简洁)。 了解协议,和委托,类别分别是什么。在你的代码中使用他们。学习相关的构造函数和析构函数的命名方案。请遵守内存管理规则。词汇和语法是不可分割的,你要么发展为一个固定的的平台,或者你跨平台。 |
![]() 魏涛
|
Rule 2: 设计解耦任何component的设计应该没有连接到你当前创建的项目,如果他是一个GUI control或者一个视图,它应该默认显示一些东西。使用现有的框架作为一个指南,与委托协议,精心设计的/命名的API方法和通知在适当的地方保持松耦合。 一个很明显的,但非常有效的方式,是每次为你的component创建一个项目,并逐渐的隔离开发component。强迫自己使用自己的API。远离无关的类。 接下来,让我们来适当谈谈类的接口。初始化方法的接口中最重要的部分之一,因为他们是人们如何开始使用您的组件。你的类将有一定的初始配置所需的设置。所以,一个明显的规律: |
![]() 魏涛
|
其它翻译版本(1) |
Rule 3: 必须设置初始化参数如果有什么需要设置的,不要等待 -需要它了就去做,如果你没有得到的东西的立即返回nil。
Rule 4: 允许访问初始化参数Allow access to initializer parameters这个前一个结果的必然结果: 记住不要仅仅传入参数,应该可以通过属性或者赋值来访问他们,如果他们可以通过任何方式来一场“按摩”(修改,重写等)
|
![]() 魏涛
|
||||
其它翻译版本(1) |
Rule 5: 注释你的header文件 (包含默认值)实际上,你不总为component提供单独的文档。如果你不提供文档,你的.h文件(包括demo app)就是你的文档。他们应该适当的描述,我的意思是:
特别是,你应该简要注释在属性或访问器旁边;头文件扫描比在初始化实例的时候更容易。
|
![]() 魏涛
|
||||||
其它翻译版本(1) |
Rule 6: 习惯于运行3行代码你的类应该设计成只需要最少的代码来集成(包括后续将用到的委托/数据源协议)。但不包括委托方法,你应该着手于用3行代码就可以达到测试的目的。 这3行代码如下:
|
![]() haoio
|
||||||||||||||||
其它翻译版本(1) |
Rule 7: 臃肿的demo通常意味着组件是糟糕的另一个推论:您的demo的大小是衡量你component质量的标准,其值越小越好。Demo/Code 应该尽可能的小巧而又精简(用于演示,旨在描述所有组件的定制或功能)。 核心思想是当你的代码从你的空的Xcode项目模板到你的demo中应该保持最小化的修改。这并不是一个好的借口当你需要复制粘贴demo来让你的component运行。 |
![]() 魏涛
|
Rule 8:分析特定的场景我对于apps的准则就是:不要让用户去做选择。选择满足多数人的人性化的默认设置,略去参数设置窗口。毕竟,好的软件都是有倾向性的。 由于运用场景不是那么的清晰明确,所以不同的组件面对的情况也有些不同。你当然可以做一个只满足某种特定情况的组件,但是,通常我们都希望有些灵活性。你绝不会准确的知道另一个开发者将会怎样使用你的组件,所以你必须做到有一定的通用性。 认真的选择你的定制点是很重要的。考虑依赖关系更加的重要——不是对编译/链接的理解,而是定制类型之间的逻辑关系。我的方法就是尽量从“方面”的层次上考虑而不是实例变量的层次上。你希望你的组件的那些方面允许被定制化?那么你就知道哪些特定的属性需要暴露。 通过不暴露足够的的配置点,就可以很容易的弱化某个特定的定制类型。例如:1.如果没有考虑圆角半径,就不要暴露宽度和高度。 2.如果没有高亮的背景颜色,就不要暴露背景颜色。 3.如果没有空间,就不要暴露大小。 具体的情况取决于具体的组件,但是需要从外观或者功能角度来考虑属性之间的关系。学会理解开发者。不要禁止组件的个性化,让它灵活些。
|
![]() crAzyli0n
|
Rule 9: 多点属性,少点方法有一个特定的模式持续出现在我所喜欢的一些来自标准库、开源的第三方以及我自己的一些代码组件。它是一个组件中属性(或者访问器,定制点)个数与方法(也就是所有其它的,从初始化到状态更新)个数的比率。 多属性少方法(再申明一次,方法不是指在Interface Builder中的那些)。MGTileMenu有一个初始化函数和四个实际上供公共使用的愿意非常(每一个都很方便调用另一个方法)。对定制点而言,它的比率有4倍之多。我认为这是一个非常好的比率,使组件不但在功能上变得简洁,而且在定制时更加灵活。
|
![]() showme
|
Rule 10: 在你的控件中使用控件 一个同时简化组件API和实现的好方法就是在你的实现中使用己有的控件。具有统一的外在并不意味着你不可以使用已经存在的组件(确实,这是软件工程当中的一个基本原则)。 考虑是什么让UITableViewCell和UIButton拥有简单的API接口,发现这是因为它们使用诸如UIImageView和UILabel这样的子控件。你也可以,并且该这样去做,并且如果可行的话使用相应的子控件来使你的类接口保持简单不变。 举个例子,在MGTileMenu中,它的外表是常规的UIButtons(不只是子类)。跟在一个的view中自定义去画它的样式、处理输入事件和支持访问而言,极大地简化了它的实现。 |
![]() showme
|
Rule 11: 于人方便就是于己方便你会很自然地在实现的过程中加入合适的方法并下意识地将其设置为私有的。相反,应该考虑是否可以公开这些方法,使这些组件能被集成到别人的应用程序。 对你而言那些如何简单方便地加入一个方法或函数的方式,对开发者而言同样如此。 举例来说,在MGTileMenu中,我创建了这些合适的函数:
|
![]() showme
|
Rule 12:魅力可以,魔数却不行。你迟早都会在你的component中加入一些魅力。人人都希望有大量的Steve Jobs式的直观、宜人、富于掌控力的魅力,但是我所说的却是代码中的一些东西,诸如拥有特殊含义的数字或者值。例如,-1在某项设置或者某个特殊场景中,就有着某个特定的意义。 很好,那样做真的挺好的。不爽的是你的代码中充满一些莫名其妙的原始值,更不爽的是还将它们暴露在API中。如果你正在暴露一些魔数,那么为了便于使用,最好还是用#define或者常量或者其他的东西包装一下它们,这样会让魔数更加的直观和易于理解。
|
![]() crAzyli0n
|
代理和数据源协议代理协议是奇妙的。它们是实现MVC模式的简单的、熟悉的和灵活的方式,它们更会使你养成松耦合的好习惯并且教你明智的API设计。 这个是 MGTileMenu’s delegate protocol. 有太多经典的代理和数据源协议供我们用到几乎所有的组件中。如果你正在显示数据,这个准确的数据源协议可能更接近于:
同样的,在几乎任何的情况中,这个准确代理协议可能采用下面的形式:
这也被称为Should, Will, Did协议模式,这也巧妙的连结了之后的Will-Did通知模式。 让我们来讨论一下你可能会觉得有争议的话题:我发现将代理协议合并到数据源协议中被完美的接受了(也就是说将他们整合到一个协议中)。我在MGTileMenu和几个其他的组件中这样做了。 我完全接受分离他们的原则,并且我能想到很多你想保持他们分离的例子。通常,苹果也是保持他们分离的。那好吧。 可是,就我的经验看,在大多数例子中合并他们是很好的。大多数人将数据源方法和代理方法放到同一个地方。我从来没有因为合并这些协议而抱怨过,我几乎不能记得一个存在分开的协议在不同的地方使用的情况。 如果你重视清晰,或者有将代理从数据源中分离出来的需求,那么很明显你应该那样做。我只是不认为如果你合并他们会觉得不好。 |
![]() 李远超
|
规则 13:限制‘required’代理方法的数量 当你选择设置哪些代理方法为required(必须实现)时一定要小心。太多的required方法将会表明:
一个精心设计的组件应该有很少很少required代理方法-仅仅是那些不得不的方法。小心选择。同样的,记住再后来添加optional(可选择实现)方法是很简单的,但是添加required方法就非常的困难(很多时候人们会抱怨)。 在MGTileMenu中有5个required方法,其中有4个是数据源方法:
前两个属于一个真正的数据源协议。第三第四个也是,但是这也表明了我的观点:我认为软件应该是容易理解的,并且我正在强制你为每一个tile提供一个标签和表述来共读者们阅读。我很享受这些。 当然也有一个代理方法
这个方法是required,因为他告诉你如何来发现一个tile是激活状态的。如果你不打算实现这个方法,MGTileMenu将不会做任何有用的事情,甚至你可能就根本不能用它。所以他是必须实现的。 |
![]() 李远超
|
规则 14:设计成容易理解的 立刻跟上这最后的规则:使设计是容易理解的。不要在最后才注意到它,恰恰相反,应该是在起初就设计成容易理解的。如果你遵循了“在你的控件中运用控件”原则,那么你可能在不知不觉中就遵循了这个原则。 上面展示的代理(倒不如说是数据源)方法是另一个开发者为了使他们给语音辅助提供一些东西的地方。 如果你能够在视觉上(就像展示一个文本标签)自动地改变一些事情的意图就像一个语音辅助标签那样,那该多好啊(此外,在大多数例子当中语音辅助已经为你实现了这个)。 在交流方面保持清醒。做到不容易理解的设计倒是困难的。我也写了另外一篇关于在IOS应用中支持语音辅助功能的文章,这也是苹果向在容易理解的程序有联系的合伙人们推荐的。我也推荐它,但是我写它就是为了你可以接受它。 |
![]() 李远超
|
规则 15:利用语义对象来做参数 这不仅仅适用于协议,但协议是尤其重要的部分。最好是在实际应用中,用合适的语义对象作为数据,虽然在你的实现中这样做可能会更加的麻烦。 如果你需要一个日期,不要用一些数字-而应该是一个真实的日期对象。对象或者结构体可以表示每一样事物,并且你应该有意地去用它们。如果需要的话可以创建一个类(你可能不会需要)。 当然一个标准的目录除外-除了基元没有任何理由可以把他们变成任何事物,自从NSNumber的加入,对于抵消打包/非打包带来的麻烦没有任何事物是语义地足够重要的。 |
![]() 李远超
|
规则 16:如果语义不合适的话就提高API 我时刻都在注意这一点。我曾在早些时候提到过,你怎么能就像一些事物已经存在(通常,就像是已经存在了的在你的实现中无意间用到的东西)了那样考虑几乎任何新的定制的控件呢? 那非常好,并且你是很聪明的,但是不能让语义胜过相似点。为了使语义合适,在一个已经存在了的API上叠加一个新的API绝对是好的(或者漂亮的)。例如:
诸如此类的等等。不要时常地强制你自己(或者其他的开发者)在抽象的实现API和真实的组件语义之间做精神上的转变 - 反而应该让这个API反映出组件真实的目的。 MGTileMenu 的代理协议通过不把菜单当做UIBUttons(实现了的)的集合而宁可说是菜单的统一做到了这一点,利用每个菜单中相关的有限的tile来展示内容。 |
![]() 李远超
|
Rule 17: 高亮是有趣的当我不得不回过头来添加一个新的代理方法和通知到我认为已经完成了的API中,我才意识到这一点。对交互控件来说,高亮是有趣的。通过“高亮”,意味着其在应用程序中潜在的重要性。 任何控件都会通知App(在某种意义上来说,可能只是通过调用一个动作或方法),当它被完全触发时;但是当它们被高亮(选中,按住)或取消高亮被触发时,只有比较少的情况才会通知。这说明它实际上非常重要。应用程序可能需要:
|
![]() showme
|
Rule 18: 可选的方法不是一个保证我们大部分人都把可选的委托方法当做二选一的情形:如果你不实现他们,就使用默认的行为;如果你要实现他们,那你就要为将发生的事情全部负责。那不是理想的事情。 在任何的提供一个可选的委托方法的实现中,你应该仍然返回去默认的行为,即使这个方法已经被实现了,但不要返回一些明显的东西。这听起来很明显,但是令人惊讶到底有多少组件无忧无虑地然后委托对象返回任何类型没有经过精细检查的愚蠢东西,仅仅是因为这委托莫名奇妙地答应通过实现这个方法来管理自己的行为。 我要具体地讨论下可视化的定制,如背景颜色和图片。非常非常仔细地考虑下,你是否不应该去干涉那种情况,同时依靠于你的默认界面。他们真的不想展示些东西吗?甚至让感觉好一点?它会让控制看起来很糟糕吗?如果如此,插手介入吧,同时仅在如果委托方法从来不在开始的地方就实现的时候准备默认的。 相关地,用一个有文档的、标准的及不是很突兀的方式去慎重地从每个可选的委托方法中通过返回一些如空的东西来执行默认的行为。 例如,MGTileMenu有一个比较复杂的层次方式让你可以自定义标题的背景。你可以实现三个委托方法中的任何(或者全部,或者不)方法去为每个标题提供一个背景图片、梯度或者颜色。你也能够在任何时候为任何标题选择默认的行为,通过返回空或者NULL等适当的类型。 你将不得不尝试相当困难地去使标题的背景透明(通过返回清澈的颜色或者使用空的UIImage对象)。 |
![]() throwable
|
Rule 20: 在查询方法中首先放置可区分的参数一个真正的数据源协议总有将最感兴趣东西放在前边的方法。比如,你请求的指定质量或属性等。像这样:
不要像这样:
返回类型自然而然应作为方法名的第一部分,这并不会另人奇怪(请注意上边两个函数的不同处)。数据源协议中经常有许多命名相似的方法,因此我们应该最先考虑保持函数的唯一性和感兴趣部分。这样的话,(这些方法)更容易读,更容易做到自动补全。 有人指出:Apple公司的UITableViewDataSource协议并没有按照那些做,他们是把sender放在第一位的,例如:
|
![]() weizhe72
|
Rule 21:在通知方法中把sender放到第一位一个真正的委托(Deletage)协议不是用来查询,而是用来通知的。在这种情况下,你应该将sender放到第一位(参考 “说出谁正在说话” 规则)。
取而代之,你应该说出谁正在说话。这是一个习惯,可以很方便地将查询(数据源)与通知(委托)方法区分开来。 |
![]() weizhe72
|
规则 22:如果约定被打破了,那就抛开它吧! 上面已经说了这么多,记住约定和一致性一定是在某些时刻要屈服于优秀的观点的 - 在这个例子当中,或者你的例子中。如果约定被打破,那就好不担心的去跳过它。如果你的观点真的更好,那就去做吧! 举个例子,在菜单控件中已经存在一个约定了,靠这个约定你能够通过代理来使菜单选项可用或者不可用,利用calledvalidateMenuItem:方法。为了一致性的缘故,我曾考虑过给我代理协议中的一部分方法用相同的名字。但是我最终决定没有那样做,因为:
相反的,我继续为了更简单并且更加的容易理解而打破了约定:
我们能够为了特殊的语法而战,但是当你遇到那个方法时,你应该立刻就知道它是做什么的?如何来使用它。我认为,那样会更好。 |
![]() 李远超
|
通知通知是委托协议的另一部分。我认为,如果你使用委托协议(如果合适,你应当用它),你最好加上通知,不然它不算完整。 在 MGTileMenu 中,你可以在 MGTileMenuController 的接口文件 中找到的通知。 |
![]() Khiyuan
|
规则 23:通知和代理方法并行 在代理方法(注意;不是数据源方法)和通知之间有一个剪不断理还乱的联系。在你的代码中同样的地方你会同时用到它们,并且起到同样的作用。 如果一个代理方法告诉代理发生了一些事情,你通常应该为了起到相同的作用发送一个通知。就像代理方法一样加上通知,去除掉模棱两可的方法,并且落实你的通知列表。 代理方法的参数应该和通知的userInfo的内容相匹配,这是很明显的除非你作为对象来传递sender,而不是捆绑在字典信息中。 代理方法:
|
![]() 李远超
|
||||||||
其它翻译版本(1) |
Rule 24: 通知关联的 userInfo 要尽量详细尽量为通知提供有用的信息。记住:通知接收方(很)可能与你组件里面的代理方法或者数据源链完全无关。 你要想想到底哪些信息会有用,并提供相应的信息。至少,你必需确保代理方法的所有参数都包含在 userInfo 对象里。 代理方法:
|
![]() daxiaoming
|
Rule 25: 最终测试 最终有些事我们已经知道了. 软件工程和敬业精神的第101条: 确保它真地有效. 是否用正式TDD测试取决于你,但是测试是不可缺少的。每个可选的委托方法。每一个发布的通知。定制的每一个点,在每一个可能的组合。组件提供了一千个微妙的问题的机会。 可能有一些缺陷。找到他们,先解决这些问题。如果你的时间推后,切功能,而不是调试。你要受的苦没有出货的错误。 |
![]() 寂寞沙洲
|
最后的思考我制定上述的规则是通过我自己这几年在创建components和APIs时所犯的错误中总结的。我也努力的尝试的遵守我制定的规则,但是难免的在某些情况下我没有。 虽然不可能在每个场景or每个事件中应用到这些规则,但是如果尽可能的遵循这些规则,你将可能创造出一个设计良好的,灵活,可重用的components。让其他人一起享用的components。 你也许想要一个简单的提纲关于这些规则,下面的图片就是。我有全尺寸的图片版本托管在Flickr上。 如果你喜欢发布自己的components给别人去使用,就像我的MGTileMenu一样。那么也许你也想去读读我发的open source code(开源代码),其中几个点也许会触及到。这篇文章也讨论了一些README文件,协议选择的相关事宜。 |