库开发:从API到公开发布

让我们从我们最熟悉的方面(即用户的角度)来看这些库,而不是从移动开发库的开发人员的角度来看。 让我们讨论一下在开发库时应遵循的方法。 当然,我们从设计您自己想使用的API开始,这很方便。 我们将考虑需要考虑的事项,以便不仅制作有效的代码,而且制作一个真正好的库,并发布真正的成人公开发行版。 Asya Sviridenko将为此提供帮助,她将分享她在Yandex中开发SpeechKit移动库的丰富经验。

该材料不仅对参与库或框架开发的人员有用,对于希望将其应用程序的一部分分离到一个单独的模块中,然后再使用它的人员,或者例如,通过将其发布到其他开发者社区共享其代码,也将是有用的。公共访问。

对于其他所有人,这个故事将充满来自移动SpeechKit团队生活中的真实故事,因此应该很有趣。


目录内容


  • SpeechKit分钟
  • 设计要使用便捷,易于理解的API。
  • 发展 。 添加到代码中的内容,以便它不仅可以工作和执行功能,还可以帮助您的用户。
  • 启动 -发行时不应该忘记的内容。


SpeechKit分钟


我不会问您是否听说过SpeechKit,因为即使在Yandex中,也不是每个人都知道它是什么。

SpeechKit是所有Yandex语音技术的大门 。 使用此库,您可以将语音技术集成到您的应用程序中:语音识别和合成,语音激活。

您可能听说过语音助手爱丽丝-她只是在SpeechKit的基础上工作。 SpeechKit本身不包括识别或综合,它发生在服务器上。 但是,通过我们的库,所有内容都可以集成到应用程序中。

通常会出现以下问题:如果服务器上的所有事情都发生了,那么库将做什么? 为什么需要它?

该库做了很多事情:

  1. 所有进程的同步。 例如,使用语音助手,用户单击一个按钮,说些什么,打断助手,发出请求-所有这些都要经过库。 对于我们图书馆的用户而言,这是透明的,他们不必担心所有这些。
  2. 联网 由于一切都在服务器上发生,因此您需要从那里获取数据,对其进行处理并将其提供给用户。 现在,SpeechKit可以访问同一网络连接中的多个不同服务器:一个从事识别,另一个从事意义分配,第三个从事音乐识别,等等。 这全部隐藏在库中,用户无需担心。
  3. 使用音频源。 我们正在处理人类的语音,并且在SpeechKit中也可以使用音频。 此外,我们不仅可以从标准设备进行写入,还可以从任何地方接收数据。 它可以是文件或流-我们可以处理所有这些。

SpeechKit用于内部团队。 现在,它已经由16个Yandex团队集成。 我们甚至知道几个外部团队也这样做。

设计方案


让我们考虑一下便捷应用程序的含义。 通常,这是一个周到且易于理解的用户体验,它是解决我们问题,稳定运行等的解决方案。

当我们说一个库很方便时,首先是指它具有易于使用的API。 如何实现呢?

基本原则


这些是我从SpeechKit的经验中学到的一些方面。

  • 首先,请记住您的用户是开发人员

一方面,这很好,因为您无法向普通用户解释:“您知道,后端在我们身边,因此没有任何用处,我们很好!” 您可以向开发人员解释-您可以向开发人员解释很多!

另一方面,您会得到这样的用户,他们一定会抓住机会发现一个孔,如果您离开它,则将其破坏。 我们所有人都使用库,并尝试最大程度地利用它们。 他们声称自己只是在做这件事,而在做这件事,我们在想:“不,现在我们在这里玩点恶作剧,继续进行下去,一切都会如期进行。”

此外,用户是开发人员这一事实意味着,您将始终拥有大量的技巧和窍门,以了解如何进行开发以及如何使一切变得更好。

第二个重点与第一个重点完全一致。

  • 应该禁止您的图书馆中不允许的所有内容,
    这样就不会出现不必要的漏洞。

如果您的用户开始使用您不期望的库进行操作,那么这将是直接查找错误以及难以调试的错误的途径。 尝试使用语言提供的所有内容以及您使用的技术:公共/私有,最终,不推荐使用,只读。 缩小范围,禁止继承和使用某些方法,标记无法更改的属性-提供所有可能的措施来防止您的库根本不打算这样做。

  • 请勿在解释库API时含糊不清。

如果可以通过一种方式创建此特定类,则拒绝所有其他方式。 如果此属性不能为null,则明确指定它。 在iOS中,有可为空/为非空的指定初始化器,与Java和Android中相同。 使用所有这些命令,以便用户打开文件,打开您的课程,翻阅它,并立即了解可以做什么和不能做什么。

Case SpeechKit API


以SpeechKit为例,我将告诉您我们如何将版本2重构为版本3。我们对API进行了大量更改,并尝试使用所有这些原理。

由于API复杂且“理论上”,因此产生了需求 。 其中必须首先调用全局组件,而不是全局组件,否则一切都无法正常工作。 设置了非常奇怪的设置。 该API是“理论上的”,因为SpeechKit最初是Navigator的一部分,然后将这一段带到了库中。 该API本质上适用于Navigator中使用的案例。

用户的数量逐渐增加,我们开始了解他们真正需要的是什么方法,回调,参数。 他们向我们提出了API不允许实施的要求。 一遍又一遍地重复,很明显API达不到要求。 因此,我们参与了重构。

重构过程很长(一年半)并且很痛苦(每个人都不满意) 。 主要的困难是不花大量的代码并重写它。 简单地进行重构是不可能的,但是有必要支持所有正在使用的活动版本。 我们不能仅仅告诉我们的用户:“伙计们,是的,它不适用于您,是的,您需要此功能-我们将在版本3中进行所有操作,请等待六个月!”

结果,重构花费了很长时间,并且过程很痛苦,对用户而言也是如此。 因为最后,我们更改了API,但没有向后兼容性。 他们来到他们面前说:“这是一个新的漂亮的SpeechKit,请接受!” -他们回答:“不,我们根本没有计划升级到您的3.0版。” 例如,我们有一个团队在一年内切换到了此版本。 因此,整整一年,我们都为他们支持以前的版本。

但是结果值得。 我们得到了简单的集成,错误更少 。 这就是我在API的基本设计原则中提到的内容。 如果您确定您的API使用正确,那么这部分肯定没有问题:正确调用所有类,所有参数正确。 查找错误要容易得多,出现问题的情况更少。

以下是重构前处理识别的主类的示例。

// SpeechKit v2 @interface YSKRecognizer: NSObject @property (nonatomic, strong, readonly, getter=getModel) NSString* model; @property (nonatomic, assign, getter=isVADEnabled) BOOL VADEnabled; - (instancetype)initWithLanguage:(NSString *)language model:(NSString *)m; - (void)start; - (void)cancel; - (void)cancelSync; @end @interface YSKInitializer: NSObject - (instancetype)init; - (void)dealloc; - (void)start; + (BOOL)isInitializationCompleted; @end extern NSString *const YSKInactiveTimeout; extern NSString *const YSKVADEnabled; @interface YSKSpeechKit: NSObject + (instancetype)sharedInstance; – (void)setParameter:(NSString *)name withValue:(NSString *)value; @end 

这是一个继承自NSObject的常规类。 让我们分别考虑其每个细节。 显然,我们可以从中继承,重新定义其中的一些方法-NSObject可以完成的所有工作。

然后,在创建时,将两行(语言和模型)传递给它。 这些是什么线? 如果您输入的语言是“ Hello,world”,那么输出将是翻译,还是什么? 不太清楚。

另外,由于这是NSObject的后继产品,因此我们可以在其上调用init,new等。 会发生什么? 它会工作还是会等待一些参数?

当然,我知道这些问题的答案,我知道这段代码。 但是第一次看这个的人根本不明白这是为什么。 甚至使用setter和getter的方法也无法完全查看iOS中的外观。 start,cancel,cancelSync方法(以及刚刚取消的方法-是aSync吗?)-如果将它们一起调用会发生什么? 此代码有很多问题。

接下来是我谈到的对象(YSKInitializer),必须启动该对象才能使所有功能正常工作-通常这是某种魔术。 可以看出,该代码是由不为iOS编写但使用C ++的开发人员编写的。

此外,此识别器的设置是通过传输到另一个全局对象的全局组件设置的,实际上,不可能用不同的参数集创建两个不同的识别器。 这可能是不支持API的最受欢迎的情况之一。

比v3优于v2


重构并切换到版本3后,我们得到了什么?

  • 完全本地API。

现在我们的iOS API看起来像iOS-API,Android API看起来像Android。

我们没有立即意识到的重要一点是,平台指南比您的库API的统一性重要得多。

例如,用于Android的类是使用构建器创建的,因为对于Android开发人员而言,这是一种非常容易理解的模式。 在iOS中,它不是很流行,因此使用了不同的方法:我们使用特殊的设置类创建对象。

我记得我们在这个问题上争论了很长时间。 对我们而言,开发人员在iOS或Android上采用我们的代码似乎很重要,匹配率将达到99%。 但是事实并非如此。 更好的是,代码将类似于为其开发平台的代码。

  • 简单直观的初始化

该对象是必需的-这是它的设置,创建它们,转移它们-利润! 也就是说,没有隐藏的全局设置需要转移到某个地方。

  • 缺少全局组件。

我们抛出了混淆,害怕并引起很多问题的全局组件,甚至在该库的开发人员中,不仅是用户。

现在,新版本中的同一个类看起来像这样(它仍然是Objective-C-那时您不能切换到Swift)。

 // SpeechKit v3 NS_ASSUME_NONNULL_BEGIN __attribute__((objc_subclassing_restricted)) @interface YSKOnlineRecognizer: NSObject<YSKRecognizing> @property (nonatomic, copy, readonly) YSKOnlineRecognizerSettings *settings; - (instancetype)initWithSettings:(YSKOnlineRecognizerSettings *)s audioSource:(id<YSKAudioSource>)as NS_DESIGNATED_INITIALIZER; + (instancetype)new __attribute__((unavailable("Use designated initializer."))); - (instancetype)init __attribute__((unavailable("Use designated initializer."))); @end NS_ASSUME_NONNULL_END @protocol YSKRecognizing <NSObject> - (void)prepare; - (void)startRecording; - (void)cancel; @end @interface YSKOnlineRecognizerSettings: NSObject<NSCopying> @property (nonatomic, copy, readonly) YSKLanguage *language; @property (nonatomic, copy, readonly) YSKOnlineModel *model; @property (nonatomic, assign) BOOL enableVAD; - (instancetype)initWithLanguage:(YSKLanguage *)l model:(YSKOnlineModel *)m NS_DESIGNATED_INITIALIZER; @end @interface YSKLanguage: YSKSetting + (instancetype)russian; + (instancetype)english; @end 

这是NSObject的后继者,但现在我们正在明确地谈论不能从其继承的事实。 此对象特有的所有方法都将传输到特殊协议。 它是使用设置和audioSource创建的。 现在,所有设置都封装在一个对象中,在这里专门传输该对象以设置特定识别器的设置。

此外,我们从这里删除了音频的工作,也就是说,现在不再使用切蛋器来编写音频。 此组件处理识别问题,任何源都可以在此处转移。

禁止通过new或init的其他创建方法,因为此类需要默认设置。 请使用它,至少创建一些默认设置。

最主要的是,此处传输的那些设置是不可变的,也就是说,您无法在此过程中更改它们。 识别出某些内容后,无需尝试替换模型或语言。 因此,我们不会给用户提供使用已经转移的设置来更改对象的机会。

宏NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END是为了强调这些设置不能为空:audioSource不能为空-所有它们都必须具有一些特定的值才能正常工作。

就像我说的那样,启动和取消方法(cancelSync左)移到了单独的协议。 图书馆中有一些地方您不能使用我们的切管器,但可以使用其他任何地方。 例如,我们使用Apple的本机程序,它实现了此协议,并且可以将我们的组件传输到该协议中。

这里的设置是NSCopying,因此我们可以复制它们,并且在此过程中无法更改它们。 在初始化中,所需的参数是语言,模型和NS_DESIGNATED_INITIALIZER。 这不是与弃用方法相同的代码段,但是思路很明确。 这些是用于创建设置的必需参数。 它们必须是并且必须为非零。

其余的设置大约是在此处设置识别器的20个设置。 甚至语言或模型的设置也都是单独的类,这些类不允许我们传达抽象的内容,而我们无法使用这些抽象的东西。 也就是说,我们明确地说:“请不要给我们一些我们不知道如何使用的东西。 编译器不会让您这样做。”

因此,我们讨论了您可以使用API​​做什么。 发展也有自己的细微差别。

发展历程


首先,该库应该执行您为它编写的内容-才能很好地执行其功能。 但是您可以使您的代码成为一个非常好的库。 我提出了一些在我开发SpeechKit期间收集的评论。

该代码不仅适合您自己


绝对有必要收集Debug信息 ,因为您不希望用户说您的服务由于您的库而无法运行。

IOS具有调试信息级别,该级别显示需要收集哪些信息。 默认情况下,它将绝对收集可以找到的所有内容:所有调用,所有值。 很好,但是数据量非常大。 设置-gline-tables-only允许您收集有关函数调用的信息。 这足以发现并解决问题。

这包含在Xcode设置(构建设置)中,称为调试信息级别。 例如,通过打开此设置,我们将SpeechKit二进制文件的大小从600 MB减小到90 MB。 这不是非常必要的信息,我们只是将其丢弃。

第二个重要的事情是隐藏私人角色 。 大家都知道,每次将库上载到iTunes时,都有冒新警告,警告您使用了错误的东西而不添加东西。 因此,如果您使用Apple认为私有的库,请确保将其隐藏。 这对您没有任何意义,您也可以与他们合作,但是一旦您的用户尝试将包含您的音乐库的应用程序上载到iTunes,他们将收到错误消息。 并非所有人都会要求您修复它;大多数人只会拒绝使用您的解决方案。

避免字符冲突 :在所有内容,类和类别中添加前缀。 如果库的类别为UIColor + HEX,请确保您的用户具有完全相同的类别,并且在集成库时,他们将收到字符冲突。 再说一次,并不是每个人都想告诉你并说出来。

另一个问题是您自己何时在库中使用第三方库。 有一些细微差别值得记住。 首先,如果您使用的内容比库的版本早,请不要忘记使用弱链接 (Xcode->构建阶段->与库链接二进制文件->状态已打开)。 如果突然没有这个库,这将使您不会掉队。

Apple的文档详细说明了它的工作方式。 但是弱链接并不意味着如果不使用该库就不会加载。 也就是说,如果应用程序的启动时间对您的用户而言很重要,并且您可能不需要使用第三方库的库中的那一部分并且需要花费一些时间来启动,那么弱链接将无济于事。 有了它,无论是否使用它,库仍将加载。

如果要加载到运行时,这将有助于消除启动时的链接问题,那么您需要使用dlopen和动态加载。 这需要大惊小怪,您必须首先了解这是否有意义。 Facebook提供了一个非常有趣的代码来举例说明它们如何动态链接。

最后尝试不要在其中使用全局实体 。 每个平台都有一些全局组件。 建议不要将它们拖到您的库中。 这似乎很明显,因为它是一个全局对象,您的库用户可以随意使用它并进行配置。 您在库中使用它时,需要以某种方式保存其状态,重新配置,然后恢复状态。 有许多细微差别,而且哪里有可能犯错误。 记住这一点,并尽量避免。

例如,在第三个版本之前的SpeechKit中,我们在库中使用了音频,然后显式设置并激活了音频会话。 iOS中的音频会话是如此,每个应用程序都有-不要说您没有一个。 它是在开始时创建的,负责应用程序和系统媒体守护程序的交互,并说明您的应用程序要如何处理音频。 从字面上最真实的意义来看,这是一个单例对象。 我们从容地接受它,并根据需要进行设置,但这导致用户遇到一些小问题,例如更改音量。 负责设置的音频会话的另一种方法很长。 这大约需要200毫秒,这在激活或停用方面会明显变慢。

在第三个版本中,我很高兴地从库中退出了一个音频会话。 之后,几乎所有集成了SpeechKit的服务用户都表示非常不高兴。 现在他们应该知道,需要为SpeechKit专门配置某种音频会话。

由此得出的结论是:都一样,请不要使用全局实体,而要为用户不会总是对您的决定感到满意这一事实做好准备。

我们为用户提供方便


您还能如何帮助您的用户?

  • 添加日志:不同级别,动态包含

最简单的方法是附加文件,对于该文件,将启动大型调试模式。 在用户有错误的用户并且您需要了解实际情况的情况下,进行调试确实有帮助。

  • 支持所有用户操作系统版本。

请记住,当您谈论库中的版本支持时,它与常规应用程序中的版本支持不同。 在常规应用程序中,我们查看的统计数据表明,例如,只有2%的用户使用iOS 8,这意味着您可以停止支持iOS8。在库中并非如此,此处拒绝OS版本意味着完全放弃您的用户及其所有用户。 原则上,这可能是您一半的用户。

因此,您需要监视使用您的库的那些应用程序使用的版本,并且基于此,您应该已经得出结论是否支持某些功能。 我们已经很长时间没有拒绝iOS 7了,在我看来,已经有人放弃了iOS 8并准备放弃iOS9。我们仍然支持iOS 7,因为我们拥有的浏览器直到最后一个都保留了所有用户,我们与他紧密合作,不能让他处于这种情况。

同样,您的用户不会说:“让我们在不支持该功能的版本上关闭此功能。”不,他们只是删除您的库并找到一个支持整个系列版本的库。

  • 在新版本中添加最小增量。

对于库开发人员来说,这是“非常”。 我想发布准备发布的所有内容。 您已经制作了功能,修复了一些错误-现在,我们将整个数据包放到发行版中。 发布也是一个过程。 对于您的用户而言并非如此。 当他们正在测试产品并准备发布产品时,他们不想从您那里收到带有需要额外测试的新功能的部件。

我们确实有一些案例,当我们回滚一些发行版,将它们分成几部分,然后已经分段发行时。 然后,那些我们实施了变更的团队可以完全采用其中只有少量变更而不是一次全部变更的版本。

这对于开发来说确实不是很方便,但是版本的最小增加将使您的用户更快乐。

从来没有太多的测试


常规应用程序和库均是如此。 但是对于库来说,又有一些功能。

当然,需要进行自动测试 ,但是除了它们之外,为您的库提供测试应用程序也很棒。 它将帮助您整合自己编写的内容,并了解可能出现的问题或陷阱。 您可以亲自感觉到您的用户是什么。

如果您的磁带库以某种方式与网络交互(包括加密),则至少与数据和安全性有关,将其交给安全防护人员进行验证 。 您绝对不想成为他们在其中发现漏洞的图书馆-这是生活的耻辱。 几乎所有大公司都有一个负责检查产品安全性的整个部门-将产品交给他们。 如果您没有,请进行外部审核 。 如果您负担不起外​​部设备的费用,请在网络上查找测试,然后运行它们,并确保您的库不允许泄漏用户数据。

在测试中非常重要的最后一件事是,从一开始就尝试添加对可能的所有内容的度量 :时间,功耗,特定库中典型的所有内容。 最后,您仍然必须这样做,所以为什么不从一开始就考虑测量。

这不能防止更改,也不需要加速库,但是可以帮助找出问题所在。 如果您有图表,这将有助于实时监视哪些功能增加了时间延迟或增加了功耗。

几乎没有时间这样做,因为这不是库的功能,也不是您要为其开发的库。 但是,这可以帮助您保持良好的状态和高质量。

在这里,您可以了解Yandex中我们如何测量移动设备的能耗。 关于时间测量是一个有趣的故事。 作为图书馆开发人员,我们很难衡量特定情况下的行为,因为并非所有团队都使用了所有SpeechKit脚本。 有时,我们使用了测试应用程序。 编写了特殊的使用案例,例如,用于语音合成的识别器或组件,记录了每个步骤并保存了日志,因此,构建了精美的图形。

一切都很好,但是我们可以处理音频,并且为了检查所有内容,在某些情况下确实可以播放音轨。 此外,有必要进行大量测量,因此他们晚上进行了测试:放置扬声器,在其旁边放置某种设备,然后启动音频文件。 早晨一切都关闭了,第二天晚上又重复了一次,然后又一次。 根本不是一些神奇的生物在办公室里走来走去-只是清洁工很害怕。 确实确实有一段很奇怪的文本,每隔一段时间就会被阅读一次。

结果,决定制作一个本地测试台,我们称为内阁。 这是一个自然的柜子,只有隔音的。 它包含许多设备,一个带有设备的整个服务器场,每个设备在工作日内可以启动多次,因为它不会伤害任何人。

发射


最后,我们进入最后一个重要部分-这就是启动。 编写了代码,并设计了一个好的API来方便用户使用。 现在如何将所有这些发布。

我将从Yandex内部用户的本地发行版开始。 这里的计划与常规应用程序的开发相同:定期,每月或每周发布。

该过程包括通常的阶段,但是在开发库时,这些要点都有各自的特点。

规划中


对我来说,这是最痛苦的部分,因为该库有几个产品团队。 在常规应用程序中,只有一名产品经理来设置团队确定优先级的任务,并开始一次执行一项任务。

如果有多个产品团队,那么每个产品团队都会收到必须实时处理的请求。 我会提供建议:如果有人知道如何一次处理大量任务,请尝试在团队中接他。 因为在所有外部经理和开发人员之间必须有一个人-负责优先任务的功能。

SpeechKit , , , . , , - . , — . , n . , . , , - .



, , : , , . Agile- .

, , , — . , . !

Scrum . , , , . . « », .

Scrum , , , — — , . . — , - . , ? , : «, , , ». , , - , , Scrum . ! , .

. , , . , . , , , , . , . .



, — , - . , , , , . , . : « 4, 3 — ». , . - , , , , .

. Continuous Integration , , , .



, . .

1. .

. - - , , , . .

. , — , , , , . , , - .

2. .

, , , — , , , , , !

. , . , , -, .

. SpeechKit . , , — , - . — , - .

, . , 4 2, , . , -, , .

. , , , .

. .


, -, . . , , , help , .

. : -, GitHub, , . — , — . , , .

, , - , , ..



, , :

  • . , . - , , .
  • . , . , OpenSource , , , , .
  • . , . , , . . SpeechKit .

总结


  • , API.
  • , , .
  • .
  • , - :) - , .

Yandex.SpeachKit GitHub iOS , Android , Mobile SDK.

AppsConf — — 22 23 2019 , , .

. , , .

Source: https://habr.com/ru/post/zh-CN429912/


All Articles