API 设计哲学——封装

引言

      面向对象的编程语言有四大特性:抽象、封装、继承、多态,这些都是我们学习 Java 语言的基石,你我早已将这些理论知识熟记于心,熟悉得甚至不需要经过大脑思考和反射:新建一个类,private 修饰属性,用 IDE 或者 lombok 自动生成属性的 public getter/setter,这些都是在几秒内一气呵成,完美!

         如果你认为,private + getter/setter 就是封装,我仔细的想了想,emmmm,貌似也没错,但就是感觉有地方不太对,到底是哪里不对呢?我想我们还是从定义开始吧。

何为封装?

      In object-oriented programming (OOP), encapsulation refers to the bundling of data with the methods that operate on that data, or the restricting of direct access to some of an object’s components. Encapsulation is used to hide the values or state of a structured data object inside a class, preventing unauthorized parties’ direct access to them. Publicly accessible methods are generally provided in the class (so-called “getters” and “setters”) to access the values, and other client classes call these methods to retrieve and modify the values within the object.

      从定义中,我们确实看到了 getter/setter 是实现封装的方式方法,但我们忽略了原因直接跳到了结果,为什么要 private 修饰一下,然后再提供一对 public getter/setter?为什么不直接用 public 修饰属性?答案显然是:

Encapsulation is used to hide the values or state of a structured data object inside a class, preventing unauthorized parties’ direct access to them.

用中文理解一下:

  • 封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。

要实现这个,必须要 private 修饰属性

  • 要访问该类的代码和数据,必须通过严格的接口控制。

要实现严格的控制,千篇一律的 public 恐怕是办不到了,emmm,我知道哪里不对了,用 IDE 自动生成的全部是 public 修饰,少了控制,少了思考:这个属性该不该暴露出去?暴露的 scope 是 package 包范围,还是 public?如果 public 暴露,未来如果有变更,修改的代价有多高?…..

  • 封装最主要的功能在于,隐藏细节,我们能修改自己的实现代码,而不用修改那些调用我们代码的程序片段

这里要敲黑板了,这个是我认为封装带给我们的最最最重要的功能了,这也是在第一篇中提到过的,设计要考虑调用方。

没有对比没有伤害

案例一:club boot Result

       我们都对 club boot 中的 Result 对象非常熟悉,我们也对其中的isSuccess方法习以为常,可能会说,这不是 Result 对象的标配吗,有啥奇怪的呢,但请别事后诸葛,如果让你去实现,你真的会想到要提供并实现这个方法吗?

      巧合的是,我们的 club boot 中,就有两个版本的 Result 对象,一个实现了,另一个则没有实现,让我们一起来看看有什么区别吧。

正例

实现isSuccess方法的版本,com.clubfactory.boot.toolkit.result.Result

调用方在使用时,示例代码如下:

Result<User> rpcResult = userService.findUser(userId);
if (rpcResult.isSuccess()) {
    User user = rpcResult.getModel();
}

反例

未实现isSuccess方法的版本,com.clubfactory.boot.client.result.Result

调用方在使用时,示例代码如下:

Result<User> rpcResult = userService.findUser(userId);
if (rpcResult.getCode() == 0) {
    User user = rpcResult.getModel();
}

      观察调用方的代码,同样都有 4 行语句,并没有很大的不同,只有一个很小很小点,前者判断响应是否成功用的是isSuccess(),而后者则用的是getCode() == 0,不要小看这一行代码,这其中的理念却大不相同:
      有人可能会说,不要小题大做,调用方在运行到这行代码时,isSuccess()方法内的逻辑同样在调用方的 JVM 去执行,而且比后者还多了一次入栈、出栈的操作,是的,从执行角度,没错。但从 OOP 的角度,却有待商榷:
* 前者,通过封装,隐藏了判断是否成功的细节;
* 后者,则完全暴露了细节,需要调用方自行判断是否成功,且成功的让调用方引入了魔法值这个 devil。

      我相信大家都能理解为什么后者暴露细节了,就不过多阐述了。最后,针对这个 case,请大家思考一下,为什么 Result 对象会有很多静态构造方法,直接用默认构造器,然后自行 set 其他值不就可以了么?设计者为什么要多此一举呢?特别请以为 API 和调用方无关的同学认真思考。

案例二:枚举(status/state/type/…..)

枚举字段对我们来说,出现频率真是非常之高,几乎可以赶上id出现的频率,可我们有认真的去对待它吗?

假设我们要设计一个资金账户,你会如何做?

id、balance、status,没错,核心的三个属性。

版本一

@Data
public class AccountDTO {
    private Long id;
	private Long balance;
	private int status;

}

public enum AccountStatus {
    NORMAL(10),//正常
	CLOSED(20),//已注销
	FROZEN_IN(30), //冻结收入
	FROZEN_OUT(31), //冻结支出
	FROZEN_IN_AND_OUT(32); //冻结收入和支出
	
	private int code;
	private AccountStatus(int code){
	    this.code = code;
	}
	
	public int getCode(){
	    return this.code;
	}
}

public interface AccountService {
	Result<AccountDTO> findById(long id);	
	//转入资金
	Result<AccountDTO> transferIn(long id, long amount);
}

这应该是,我们最常见的实现方式了,也是我们代码里随处可见的方式。

假设,我们要给一个可转入资金的账户 转入资金,调用方示例代码如下:

Result<AccountDTO> rpcResult = accountService.findById(id);
if (rpcResult.isSuccess()) {
    AccountDTO account = rpcResult.getModel();
	// 如果账户状态可转入资金
	if (account.getStatus() == AccountStatus.NORMAL.getCode() || 
	    account.getStatus() == AccountStatus.FROZEN_OUT.getCode()) {
	  //转入some money
	  accountService.transferIn(account.getId(), 1000);
	}
}

调用方的代码看上去也没什么问题,是的,调用方这样的书写方式是非常正确的。
但并不是所有的开发者都能这样规范的书写,在时间紧张的时候,或者开发者在不知道枚举的包路径时,可能的书写方式是这样的:

Result<AccountDTO> rpcResult = accountService.findById(id);
if (rpcResult.isSuccess()) {
    AccountDTO account = rpcResult.getModel();
	// see, the magic number devil is showing up....
	if (account.getStatus() == 0 || account.getStatus() == 31) { 
	    //转入some money
	    accountService.transferIn(account.getId(), 1000);
	}
}
我 ***,真的会有人这样写吗?嗯,你还别不信,就是会有人这么写!瞧:

那这时 API 提供者委屈的说了,我提供了枚举类,他不用,也怪我喽?
不怪你,但你可以做的更好。让我们优化一下吧:

版本二

//第一步,为status 重写lombok生成的getter
@Data
public class AccountDTO {
    private Long id;
	private Long balance;
	private int status;

   //lombok注解生成的是:public int getStatus()
   //将出参类型 int 修改为 AccountStatus枚举
   public AccountStatus getStatus(){
       return AccountStatus.fromCode(status);
   }
}

//第二步,给枚举添加一个静态方法
public enum AccountStatus {
    NORMAL(10),//正常
	CLOSED(20),//已注销
	FROZEN_IN(30), //冻结收入
	FROZEN_OUT(31), //冻结支出
	FROZEN_IN_AND_OUT(32); //冻结收入和支出
	
	private int code;
	private AccountStatus(int code){
	    this.code = code;
	}
	
	public int getCode(){
	    return this.code;
	}
	
	public static AccountStatus fromCode(int code) {
            for (AccountStatus status : AccountStatus.values()) {
                if (status.getCode() == code) {
                    return status;
                }
            }
            return null;
        }
}

调用方的示例代码如下:

Result<AccountDTO> rpcResult = accountService.findById(id);
if (rpcResult.isSuccess()) {
    AccountDTO account = rpcResult.getModel();
	if (account.getStatus() == AccountStatus.NORMAL || account.getStatus()  == AccountStatus.FROZEN_OUT ) {
	 //转入some money
	  accountService.transferIn(account.getId(), 1000);
	}
}

     如此这般,调用方的开发者只能拿到枚举,强迫开发者在使用时要使用枚举,不给开发者写出 bad code 的机会,这样是不是对双方都好了呢?那这样就完美了吗?

     不,你还是暴露了太多细节给调用方,你让调用方关心哪些状态下才能转入资金,哪些状态才能转出资金,这些信息,只有实现者最清楚,假设未来又要新增其他几种禁止转入资金的状态呢?是不是需要让所有的调用方都要修改逻辑?

所以,作为实现者来说,还能更好!

版本三

@Data
public class AccountDTO {
    private Long id;
	private Long balance;
	private int status;

   
   public AccountStatus getStatus(){
       return AccountStatus.fromCode(status);
   }
   
   //再进一步,对于调用方常用的方法进行封装
   public boolean canTransferIn() {
       AccountStatus status = getStatus();
       return status == AccountStatus.NORMAL || status == FROZEN_OUT;
   }
   
   public boolean canTransferOut() {
       AccountStatus status = getStatus();
       return status == AccountStatus.NORMAL || status == FROZEN_IN;
   }
}

调用方的示例代码如下:

Result<AccountDTO> rpcResult = accountService.findById(id);
if (rpcResult.isSuccess()) {
    AccountDTO account = rpcResult.getModel();
	if (account.canTransferIn()) {
	  //转入some money
	  accountService.transferIn(account.getId(), 1000);
	}
}

如此这般,
对于调用方来说,只需要知道账户可否转入资金即可,至于账户的状态是什么,他不需要关心
对于实现者来说,当未来新增状态时,你只需要修改你的 client 包,让调用方升级,你来保证可否转入资金逻辑的正确性,这是你的控制权!

上面这个资金账户的例子是假想的 case,但在我们的代码中却真实存在,比如:

* 商品模型中的bizModel(直营 / 平台 / 供应商独占),三种类型,虽然提供了枚举类,但代码中还是随处可见:if (product.getBizModel() == 2); 像这种枚举值比较少的,建议采用对枚举的再次封装的方式。
* 订单模型中的isDelivery(0- 普通商品; 1- 运费 or cod_fee; 3- 税费; 4-cod 会员),这个连枚举都没提供,只能口口相传了 😬

出现这种情况,调用方要负主要责任,但提供方如果多考虑一步,也是可以避免的。

总结:隐藏细节

封装 = 隐藏细节 != getter/setter。
    至于哪些需要隐藏,哪些需要封装,我也穷举不出,只希望大家在 coding 的时候,不要一味的自动生成,要加一些思考在里面,这个属性 / 逻辑我该不该暴露出去?暴露出去后,调用方的实现代码是否是丑陋的?至于什么是美什么是丑,就让它归于哲学吧。
    最后,我认为,抽象、封装是最能体现一个编码人员功底的地方,千万别简单的认为把对象的属性放在一个类中就是封装,还是要花一些功夫去思考一下的,所以,让我们在编写优秀代码的道路上继续前行吧。

最后的最后,也用一句话结尾吧,与大家共勉!

细微之处见真章
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
后台采用apache服务器下的cgi处理c语言做微信小程序后台逻辑的脚本映射。PC端的服务器和客户端都是基于c语言写的。采用mysql数据库进行用户数据和聊天记录的存储。.zip C语言是一种广泛使用的编程语言,它具有高效、灵活、可移植性强等特点,被广泛应用于操作系统、嵌入式系统、数据库、编译器等领域的开发。C语言的基本语法包括变量、数据类型、运算符、控制结构(如if语句、循环语句等)、函数、指针等。下面详细介绍C语言的基本概念和语法。 1. 变量和数据类型 在C语言中,变量用于存储数据,数据类型用于定义变量的类型和范围。C语言支持多种数据类型,包括基本数据类型(如int、float、char等)和复合数据类型(如结构体、联合等)。 2. 运算符 C语言中常用的运算符包括算术运算符(如+、、、/等)、关系运算符(如==、!=、、=、<、<=等)、逻辑运算符(如&&、||、!等)。此外,还有位运算符(如&、|、^等)和指针运算符(如、等)。 3. 控制结构 C语言中常用的控制结构包括if语句、循环语句(如for、while等)和switch语句。通过这些控制结构,可以实现程序的分支、循环和多路选择等功能。 4. 函数 函数是C语言中用于封装代码的单元,可以实现代码的复用和模块化。C语言中定义函数使用关键字“void”或返回值类型(如int、float等),并通过“{”和“}”括起来的代码块来实现函数的功能。 5. 指针 指针是C语言中用于存储变量地址的变量。通过指针,可以实现对内存的间接访问和修改。C语言中定义指针使用星号()符号,指向数组、字符串和结构体等数据结构时,还需要注意数组名和字符串常量的特殊性质。 6. 数组和字符串 数组是C语言中用于存储同类型数据的结构,可以通过索引访问和修改数组中的元素。字符串是C语言中用于存储文本数据的特殊类型,通常以字符串常量的形式出现,用双引号("...")括起来,末尾自动添加'\0'字符。 7. 结构体和联合 结构体和联合是C语言中用于存储不同类型数据的复合数据类型。结构体由多个成员组成,每个成员可以是不同的数据类型;联合由多个变量组成,它们共用同一块内存空间。通过结构体和联合,可以实现数据的封装和抽象。 8. 文件操作 C语言中通过文件操作函数(如fopen、fclose、fread、fwrite等)实现对文件的读写操作。文件操作函数通常返回文件指针,用于表示打开的文件。通过文件指针,可以进行文件的定位、读写等操作。 总之,C语言是一种功能强大、灵活高效的编程语言,广泛应用于各种领域。掌握C语言的基本语法和数据结构,可以为编程学习和实践打下坚实的基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值