假如你们都是服务呢


我曾经,写出过类似这样形式的业务代码。

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 调用形式,非常能描述业务逻辑过程。

殊途同归。

叫啥其实不重要,重要的,是结构内在。

这种分层架构,很多时候,体现的是内部的思想,体现你如何看待它们,最终也决定了服务本身的复杂度和心智负担有多少。

好的代码结构,能让人少很多烦恼。

跟优秀的设计学习,共勉。