接口设计注意点
对于SDK而言,接口是连接SDK与客户产品的纽带,接口设计的优劣是衡量SDK产品易用性的重要指标。糟糕的SDK接口不仅仅给开发者带来的难用的主观印象,更有可能增加客户的开发成本,甚至影响产品质量。
笔者在从事SDK开发的几年起初并不重视,在一次次填坑的过程中也逐渐意识到优秀的SDK接口设计必须要思考以下几点:
-
风格统一
统一的接口设计风格不仅仅是为了给开发者留下专业的印象。更进一步的,它可以传递给开发者SDK的设计理念。开发者通过这种风格上的暗示,可以更直观,不易犯错的调用SDK功能。
举例来说,我们可以设计SDK所有接口都通过init方法初始化,uninit方法反初始化。开发者一旦接受了这种设定,会潜意识的注意有没有调用过uninit释放资源。 -
平台惯例
一款SDK产品底层通常是跨平台实现,上层则是针对不同平台做一层封装,封装层特别要考虑的是平台的开发惯例。由于对接SDK的开发者通常是专注于某一特定平台的开发,不符合本平台接口命名风格,设计惯例的接口会给他们带来额外的学习成本,增加对接难度。 -
升级扩展
接口设计考虑升级扩展这一点对每一个负责持续迭代的产品的程序员来讲都是基本的要求。但是对于SDK产品,这一点显得尤为重要。原因在于,一般接口设计如果不合理可以通过后期迭代重构来补救,而SDK一旦发布之后,想要客户修改代码来升级SDK的成本是很高的。接口兼容性如果做的不好,客户就不会愿意升级,随之带来的问题是维护多个版本给我们自身带来的巨大的支撑压力。 -
注释说明
注释和接口是SDK的一体两面,它在SDK中有着和接口同等重要的作用。SDK没有一个接口是多余的,同样,没有一个接口是可以被允许没有注释的。我们在开发SDK时,始终要预设这样一个前提:开发者不会看我们的集成文档。因此,我们只有尽可能的丰富注释,才有可能最大程度将问题消灭在开发者翻阅文档或是提问之前。
SDK接口设计规范
针对上述几个注意点,我们基于通用编码规范,平台命名规范,以及行业通用惯例制定了自己的SDK接口设计规范。目前还没有特别完善和细化,但有了一个统一的标准之后,相信对未来的接口设计会有一个原则性的指导。我们的SDK主要是针对移动端iOS/android平台,采用的语言主要是oc/java,其他平台的设计规范可以做一个参考。
类/接口
类/接口的命名必须使用名词。
- android
接口需要加I字符来区分。
// good case
public interface AliyunIRecorder; // android; 接口加I区分
// bad case
public interface AliyunIImport; // android; import不是名词
方法/函数
方法/函数名足够清晰易懂,除了行业内通用的专有名词,其他情况下不能使用缩写。
// good case
void setExposureCompensationRatio(float value); // android; 方法名即解释
// bad case
- (int)removeTransitionAtIndex:(int)clipIdx; // iOS; 参数名应该使用全称clipIndex
参数尽可能少,超多4个参数需要考虑采用结构体/类封装。
采用结构体/类封装的好处除了减少方法长度,更重要的意义在于未来版本接口功能升级只需要新增配置属性,而不用提供新的接口。
// bad case
void setMusic(String path,long startTime,long duration); // android; 使用music类封装
能采用同步接口的尽量不要用异步,异步接口需要在注释里说明。
设计同步接口的行为是明确的,设计异步接口可能让开发者误以为接口行为已经完成,从而做出一些错误调用。异步接口必须强调说明,并注明对应的回调方法是什么。
// bad case
int finishRecording(); // android; 异步接口需要说明对应的回调方法
一个接口只做一件事,使用doSomething
命名。
// bad case
int editCompleted(); // android; 改为stopEdit/finishEdit
枚举
- iOS
采用平台惯例的驼峰命名法,使用typedef NS_ENUM()
语法定义。
// good case
typedef NS_ENUM(NSInteger, AVCaptureFlashMode) {
AVCaptureFlashModeOff = 0,
AVCaptureFlashModeOn = 1,
AVCaptureFlashModeAuto = 2,
} //iOS; 系统api命名
// bad case
typedef enum : NSUInteger {
DIRECTION_LEFT = 0,
DIRECTION_RIGHT = 1,
DIRECTION_TOP = 2,
DIRECTION_BOTTOM
} DIRECTION_TYPE; //iOS; 没有使用驼峰命名,枚举值命名不规范,未使用oc声明规范
- android
采用平台惯例的命名方法,枚举成员名称需要全大写,单词间用下划线隔开。
// good case
public enum FormatStyle {
FULL,
LONG,
MEDIUM,
SHORT;
} // android; 系统api命名方法
// bad case
public enum AlivcLogLevel {
AlivcLogLevelDebug(0),
AlivcLogLevelInfo(1),
AlivcLogLevelWarn(2),
AlivcLogLevelError(3),
AlivcLogLevelFatal(4);
} // android; 枚举值命名不规范
成员变量/属性
-
iOS
只有在同时需要getter/setter方法的时候暴露属性,否则使用方法代替。
使用oc基本数据类型,不要使用c/c++的基本数据类型。 -
android
不要直接暴露属性,提供getter/setter方法。
Param类必须有构造器。
// bad case
public int videoWidth; // android; 提供getter/setter方法
@property (nonatomic, assign) int bitrate; // iOS; 使用NSInteger代替int
命名惯例
获取已经存在的对象使用get
命名。
// bad case
AliyunICanvasController obtainCanvasController(Context var1, int var2, int var3); // android; 使用get代替obtain
获取一个new的新对象使用create
命名。
create可以给开发者暗示资源的是被创建出来的。
// good case
public static AliyunIEditor createAliyunEditor(); // android; 工厂方法使用create创建新实例
// bad case
AnimPlayerView newPasterPlayer(); // android; 使用create代替new
参数设置类/结构体命名需要声明用途,以Param
结尾。
后缀可以是Config,Setting等等,一旦确定好需要所有接口保持统一。
// good case
public class AliyunVideoParam; // android; 命名符合规范
// bad case
public class CropParam; // android; 没有前缀
public class MediaInfo; // android; 没有声明是录制参数,没有以Param结尾
设置效果:setXxx
,移除效果:clearXxx
// bad case
int addAnimationFilter(EffectFilter var1); // android; 不符合规范
- (void)deletePaster:(AliyunEffectPaster *)paster; // iOS; 不符合规范
新增:addXxx
,删除:removeXxx
,清空:clearXxx
// good case
int addMediaClip(AliyunClip clip); // android; 替换为clearPartList
// bad case
void deletePart(); // android; 替换为removePart
void deleteAllPart(); // android; 替换为clearPartList
数组/字典命名使用变量名+List/Map/Array/Dictionary
之所以不用复数形式定义变量名,主要考虑到我们开发团队对名词的复数形式并没有深入掌握。与其命名不专业的变量名,不如用这种方式更为直观。
// good case
public List<Frame> getFrameList(); // android; 符合命名规范
// bad case
@property (nonatomic, copy) NSArray<AliyunFrameItem *> *frameItems; // iOS; 替换为frameItemArray
回调
回调方法除非特殊需求一般在主线程回调,回调接口注释必须说明在哪个线程。
保证开发者的所有接口调用都在主线程执行可以避免很多线程问题。
有一些回调需要在特定线程如渲染回调等需要注释说明。
-
iOS
回调类统一以Delegate
结尾,会调方法以回调类名+Did开始,一个回调对应一个delegate属性。 -
android
回调类统一以Callback
结尾,会调方法以on+回调类名开始。
// bad case
@protocol AliyunIPlayerCallback
@end //iOS; 不能使用callback结尾
初始化/反初始化
主要模块必须通过init方法并使用Param
配置参数初始化。
统一的初始化方式能让开发者快速熟悉SDK调用方式。
- iOS
oc类不应提供反初始化方法。有可能的条件下,尽量按以下优先级,在特定位置构造与析构native资源:
-
调用关键方法时构造native资源,完成回调后内部析构native资源。
例如:调用transcode方法构造资源,transcode完成回调后析构资源。 -
对外提供成对方法处构造与析构native资源。
例如:startPreview与stopPreview,startEdit与stopEdit,open与close。 -
以上都不合适,考虑在
dealloc
处内部析构native资源。
// bad case
- (void)destroyRecorder; // iOS; 考虑是否可以通过1,2,3条避免
- android
主要模块类一定要有release
方法,release
删除底层资源与java同生命周期。
// bad case
void dispose(); // android; 没有使用release()方法
前缀
类,接口,结构体,枚举值必须加前缀。
java虽然有包名区分不同类,但还是建议加前缀,这样可以让开发者更容易区分SDK接口。
包名
安卓所有SDK接口必须在统一的包名内。
注释
对外接口使用/** */
注释,不要使用//
注释。
很多接口需要多行注释才能解释清楚,所以在这里统一使用多行注释。
网友评论