假如你们都是服务呢
我曾经,写出过类似这样形式的业务代码。
API 层
from business import biz
class XXXAPI(APIView):
...
def get(self, request):
...
data = biz.cu.list(request.company.id, keyword, user_type, status, permission_ids, sort_by, from_feishu)
return Response(data)
业务层
from services import svc
class XXXBusiness(object):
def do_sth(self):
...
operator = svc.user.get_user_by_id(user_id)
company = svc.company.get_company_by_id(company_id=company_id)
...
svc.app_push.notify_account_removed(company=company, user=user, operator=operator)
svc.event.user_quit_company(cu.user_id, company.id)
svc.activity.log_company_user_removed(ip=ip, operator=operator, company_user=cu, company=company)
...
这里面,由于它是业务层逻辑,所以它由前端提交的一个请求过来后,后端需要执行非常多的操作,这里的操作,有些是必要的,有些是可有可无的,但是如果有则更好。
当然,也有人看到这里会非常快速地想到,是不是用订阅者模式或者是事件驱动模式会更好?
这个就不去细究了,承认有时候高级的设计模式可能更好,但是,得知道这是业务系统,它从 0-1 再从 1-10 逐渐迭代,时间跨度好几年,期间不断增加新的需求和逻辑,不断有需求的变化,经过好几任开发者的手,它的实际演化过程,远比某个设计良好的第三方库要复杂和困难得多。
更何况,你要知道这是 Python ,还有什么状况是不可能出现的呢?
这里就不去贴上面这份样例代码它在我接手之前的样子了,太占屏幕空间同时也太费眼睛了。
最近,稍稍地看到了 Python openai-python 和 Rust async-openai 这样的库设计,但是首先你也需要明白,openai api reference 里面所包含的服务,不算太多,但是也算已经非常多了。
但是,对外设计的库的使用,无论里面的服务有多少,业务调用时始终需要关心的,核心就是实例化的 client 而已,它就是所有服务的统一调用入口。
Python
from openai import OpenAI
client = OpenAI()
stream = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "Say this is a test"}],
stream=True,
)
for chunk in stream:
if chunk.choices[0].delta.content is not None:
print(chunk.choices[0].delta.content, end="")
Rust
use async_openai::{types::CreateCompletionRequestArgs, Client};
use futures::StreamExt;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let request = CreateCompletionRequestArgs::default()
.model("text-davinci-003")
.n(1)
.prompt("Tell me a bedtime story about Optimus Prime and Bumblebee")
.stream(true)
.max_tokens(1024_u16)
.build()?;
let mut stream = client.completions().create_stream(request).await?;
while let Some(response) = stream.next().await {
match response {
Ok(ccr) => ccr.choices.iter().for_each(|c| {
print!("{}", c.text);
}),
Err(e) => eprintln!("{}", e),
}
}
Ok(())
}
这其实就又让我想起了曾经写过的 biz.xxx.yyy 和 svc.aaa.bbb 调用形式。
以及,它们在后续也影响了 Go 出现类似这种调用形式的写法,只不过,命名有所区别。
API 层
func (c *Controller) GetArchiveList(ctx *context.Context) {
...
resp, err := c.services.ArchService.GetArchiveList(ctx.Request().Context(), companyId, userId, param)
if err.NotOK() {
apis.WriteResp(ctx, nil, errno.SrvErr.WithErr(err))
return
}
apis.WriteResp(ctx, resp, errno.OK)
}
业务层
func (s *Service) FindEmployee(ctx context.Context, matchEmpReq calc_entity.DtoMatchEmpReq) ([]string, error) {
empIds, err := s.adaptors.BizAdaptor.FindEmployee(ctx, &matchEmpReq)
if err != nil {
return nil, err
}
return empIds, nil
}
只不过由于语言差异,前面又多了一层挂载对象,变成了 4 层写法,但思路基本还是非常一致的。
这种 biz-svc 的写法,我曾经叫它:分层架构。
为了解决 import 内容极为混杂,业务代码冗长又难读的问题,通过不断地对原来的业务代码进行“抽离”,把细节“下放”服务层 services 里面的各个模块中去,对外仅有暴露出来的函数和参数声明供调用,形成了我所谓的这种“分层架构”,使我们当时的业务代码编写,有了一个非常清晰的设计方向,从最终的代码所呈现的形式结构来看,质量还算是非常不错的。
当然,仔细看看,这不就是面向过程么?确实是,但是它是业务代码。业务过程,它描述了它对所有领域对象的各自的业务能力的组合过程。
为什么它走向了这个方向,跟当时的主要考虑有关:代码要能随意挪动模块,方便理解和重构,不能有内部隐藏逻辑依赖和数据依赖,它会影响逻辑拆解,增加复杂性,而重构最怕的就是无谓的复杂性。
总之就是:要足够简单。
只要足够简单,易学易理解,模块分的清晰,又同时遵循统一的模式,意外出错的可能性就最低。
出错的概率低,理解又清晰,跑的也就更快,这个跑得快指的是业务编码,即产品迭代过程。
现在回过头来看,即便是后来我又尝试用 Rust 跟着去实现了下什么 Clean 架构、六边形架构,其实最终理解下来,发现它们与这个 biz-svc 结构是有着非常类似的形式。
然后又看到第三方库的这种 client 的调用方式,我在想:其实,假如你们都是服务呢?
服务 A 服务 B 服务 C 服务 D 服务 E …
按业务能力分模块,内部实现能力暴露函数供调用,至于内部是用了什么,不必关心细节。里面什么 sql 什么 redis 什么 mq 什么第三方 API 都无所谓。
然后都有一个顶级入口 svc,就成了类似这种 svc.a.xx, svc.b.yy 调用形式,非常能描述业务逻辑过程。
殊途同归。
叫啥其实不重要,重要的,是结构内在。
这种分层架构,很多时候,体现的是内部的思想,体现你如何看待它们,最终也决定了服务本身的复杂度和心智负担有多少。
好的代码结构,能让人少很多烦恼。
跟优秀的设计学习,共勉。