找回密码
 会员注册
查看: 15|回复: 0

原来编写SDK是一件有趣的事

[复制链接]

2万

主题

0

回帖

7万

积分

超级版主

积分
72245
发表于 2024-10-6 22:25:44 | 显示全部楼层 |阅读模式
点击上方蓝字关注我们导读:在平时工作中,我们会把通用的代码,合并到一个通用的SDK中,增加大家工作效率,本文主要分享我们在编写SDK时候的准入标准以及相关编码思想。全文7721字16图,预计阅读时间21分钟前言首先需要回答,为什么要编写SDK?避免重复造轮子减少线上bug概率避免重复造轮子:好的sdk可以帮助团队省时省力,将相同的功能抽象到一个通用sdk中,前人栽树后人乘凉。减少线上bug概率:经过大家共同的优化出bug的可能性较低,即使出bug,也只需要修改sdk即可;若每个代码库都实现一遍,那每个代码库就都需要修复这个bug;每个同学能力不同,写出代码的质量参差不齐;一、遵循的设计理念SOLID在程序设计的领域内,SOLID是由Robert C.Martin提出的面向对象编程以及面向对象设计的五个基本原则的缩写,这五个原则分别是:单一职责原则(Single Responsibility Principle)开闭原则 (Open Close Principle)里氏替换原则 (Liskov Substitution Principle)接口隔离原则 (InterfaceSegregation Principle)依赖反转原则 (Dependence InversionPrinciple)在编写SDK中,我们坚信好的代码是设计出来的,而不是编写出来的这一理念,所以在设计之初我们就会按照这五大原则,逐一考量我们是否设计得足够优秀,我们是否违背了某项原则。接下来我们来逐一介绍我们对于这五大原则的理解:1.1、单一职责原则定义类的设计,尽量做到只有一个可以引起它变化的原因理解单一职责原则的存在是为了保证对象的高内聚、保证同一对象的细粒度。在程序的设计上,如果我们采用了单一职责原则,那么就要注意,开发代码过程中不要设计那些功能五花八门、包含很多不相关功能的类,这样的类我们可以认为是违反单一职责原则的。按照单一职责原则设计的类中,应该有且只有一个改变因素;举例子package phone// 只负责调整电话音量type MobileSound interface { // 音量+ AddSound() error // 音量- ReduceSound() error}// 只负责调整照片格式type MobilePhoto interface{ // 亮度 Brightness() error // 纵横比 AspectRatio() error}这段代码是对于手机的两个类的构造,在设计的时候,我们将手机的音量控制和手机图片的调整类进行了区分,在同一个类中不同功能的共同载体分别是音量控制和图片调整,这两个类中的功能是不产生依赖的,相互平行的关系,当手机的配置发生改变,那么这两个类中的功能都会产生一定的变化;如果我们要对其中一个类进行调整,那么只需要在相应的interface中进行功能的优化即可,不会影响到其他功能的结构。1.2、开闭原则定义:一个软件实体,应该对扩展开放,对修改关闭;理解针对于这个问题的产生是来自于对于代码进行维护的时候,如果对旧代码进行修改,那么可能会引入错误,这可能会导致我们对整个功能进行重新架构,并且重新测试;为了防止这种情况的发生,我们可以采用开闭原则,我们只对代码进行添加功能,扩展类中的功能,而不去修改原先的功能,这样可以避开因为修改老代码而产生的坑(深有体会);举例子// 只负责调整电话音量,同时具备一键最大和一键最小的功能type MobileSound interface { // 音量+ AddSound() error // 音量- ReduceSound() error // 静音 Mute() error // 一键最大 MaxSound() error}还是这个手机的类,如果后期业务需求增加,要求我们加入静音和一键音量最大的功能,那么我们可以直接在这个音量控制接口中进行功能扩展,而不是去在音量加减的功能里进行修改,这样可以避免因为对内部逻辑的调整而产生的功能架构问题。注意点开闭原则是一个非常虚的原则,我们需要在提前预期好变化并做出规划,然后需求的变化总是远远超过我们的预期,遵循这个原则我们应该把频繁变化的部分做出抽象,但却不能将每个部分都刻意的进行抽象;如果我们无法预期变化,就不要去做刻意的抽象,我们应该拒绝并不成熟的抽象1.3、里氏替换原则定义所有引用基类的地方必须能透明的使用其子类的对象;理解子类对象替换父类对象的时候,程序本身的逻辑不能发生变化,同时不能对程序的功能造成影响;举例仍然是用手机来举例子,我们定义的手机类中,加入了一个充电功能接口;其中包含了无线充电和有线充电两个功能;type Charge interface { //有线充电 ChargeWithLine() error //无线充电 ChargeWithoutLine() error}但是我们并没有想到8848钛金手机这么一款高端商务上流手机并不具备无线充电的功能;因为我们定义的父类并不能由子类完全替换,8848也是手机,但是因为它的功能并不完全具备手机类的功能,所以导致这个问题的产生,那么如何来解决这个问题呢?我们可以将手机类的充电功能进行拆分,在父类中,我们只定义充电功能,那么我们就可以在8848子类里面设计具体的充电方式,来完善这个功能的接口。我们在手机类中仅定义了最基础的方法集,通过子接口SpecialPhone添加ChargeWithLine方法;type MobilPhone interface { Name() string Size() int}type NormalCharge interface { MobilPhone ChargeWithLine() error ChargeWithoutLine() error}type SpecialCharge interface { MobilPhone ChargeWithLine() error}我们再通过SpecialPhone来提供对MobilPhone的基本实现:type SpecialPhone struct { name string size int}func NewSpecialPhone(name string, size int) *SpecialPhone { return &SpecialPhone{ name, size, }}func (mobile *SpecialPhone) Name() string { return mobile.name}func (mobile *SpecialPhone) Size() int { return mobile.size}最后,Mobil8848通过聚合SpecialPhone实现MobilPhone接口, 通过提供ChargeWithLine方法实现Mobil8848子接口:type Mobil8848 struct { SpecialPhone}func NewMobil8848(name string, size int) MobilPhone { return &Mobil8848{ *NewSpecialPhone(name, size), }}func (mobile *Mobil8848) ChargeWithLine() error { return nil}注意点在项目中,采用里氏替换原则时,尽量避免子类的特殊性,采用该原则的目的就是增加健壮性,在版本升级时也能有很好的兼容性,而一旦有了特殊性,那么代码间的耦合关系就会变得极其复杂;1.4、接口隔离原则定义客户端不应该依赖它不需要的接口;类间的依赖关系应该建立在最小的接口上;理解客户端不能依赖于它所不需要使用的接口,这里的客户端我们可以理解为接口的调用方或者使用者;尽量保证一个接口对应一个功能模块;接口隔离原则和单一职责原则的区别是什么?单一职责原则主要是在业务逻辑层面上,保证一个类对应一个职责,注重的是对于模块的设计;而接口隔离原则则是主要针对接口的设计,对不同的模块提供不同的接口。举例比如下面这个例子,这段代码就是清晰的将接口按照不同的返回值进行了区分,这样在调用的时候就可以避免因为不清楚返回的认证信息状态而造成的逻辑上的错误。// CarOwnerCertificationSuccessScheme 返回认证成功的schemefunc CarOwnerCertificationSuccessScheme() string { return CertificationSuccessScheme}// CarOwnerCertificationFailedScheme 返回认证失败的schemefunc CarOwnerCertificationFailedScheme() string { return CertificationFailedScheme}// CarOwnerMessageRecallScheme 返回车主认证邀请的schemefunc CarOwnerMessageRecallScheme() string { return MessageRecallScheme}注意点同单一职责原则类似:在接口设计时,如果粒度太小会导致接口数据太多,开发人员被众多的接口淹没;如果粒度太大,又可能导致灵活性降低。无法支持业务变化;我们需要深入了解业务逻辑,不可盲目照抄大师的接口;1.5、依赖倒置原则定义高层模块不能依赖底层模块,两者都应该依赖于其抽象;抽象不应该依赖细节;细节应该依赖抽象;理解好的抽象更稳定,可以覆盖到更多的细节;具体的业务则要面临着各种方法和细节,非常多样,是不确定不稳定的;举例比如下面这个例子,如果我们想要充实手机的功能,那么我们在定义的时候,对手机这个类只定义抽象类,类里面并不涉及到具体的功能设计,具体的功能设计我们要放在功能里面,这样就可以保证类结构的稳定,不会因为功能的调整而导致整个手机类的调整。package phonetype Phone interface{ // 音量调整 MobileSound // 照片调整 MobilePhoto}// 只负责调整电话音量type MobileSound interface { // 音量+ AddSound() error // 音量- ReduceSound() error}// 只负责调整照片格式type MobilePhoto interface{ // 亮度 Brightness() error // 纵横比 AspectRatio() error}注意点依赖倒置最难支持就是找到抽象,好的抽象可以减少大量的代码,糟糕的抽象,就突增工作量。关于设计理念,我们坚信好的系统是设计出来的,而不是开发出来的。所以在任何项目的启动开发之前,我们都会进行极其严格的设计评审。二、遵循的编码原则2.1、稳定、高效公共库是提供给众多业务方使用的第三方组件,如果公共库运行时程序崩溃,会危及业务方的项目,可能会造成线上事故,所以稳定是一个公共库的基本保证。暴露异常?异常可以通过日志和接口返回暴露给用户。对于异常情况一定要打日志,方便用户排查具体问题,并且约定错误码,通过错误码和error message将错误信息暴露给用户。在公共库内部,所有可能返回错误的地方都不能忽略。参数校验?用户很有可能无意中给你封装的方法传入了不合法的参数,你的方法要能处理任何不合法的参数,让你的公共库不至于因为传入不合法的参数造成崩溃。Panic和Recover?在Go语言中,一些非法的操作会导致程序panic,例如访问超出 slice 边界的元素时,通过值为 nil 的指针访问结构体的字段,关闭已经关闭的 channel 等。当一个 panic 在调用栈中的没有被recover时,程序终将因堆栈溢出而终止。在编写公共库时,我们不希望某个地方出现异常就导致整个程序崩溃。正确的做法是recover这个panic,转换成error返回给调用方,同时输出到日志,使得公共库及整个工程项目还能保持正常运行。如示例代码:func server(workChan
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-11 06:25 , Processed in 0.461475 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表