在日常开发中,我们经常的做法是将代码分为 控制器层 Controller、服务层 Service 和 数据存储层 Repository。
其中控制器层负责暴露接口,服务层负责业务逻辑的实现,存储层负责数据的读写。
控制器层中的一个类代表一个独立模块的所有api接口,其下的每一个方法代表一个api接口;控制器层并不负责业务逻辑本身,而是负责调用业务逻辑方法,因此会简短明了。
服务层中的类又可以包含 Service类 和 Domain类(领域对象)两部分,Service类负责实现一些较为复杂 或者 多个业务对象耦合的业务逻辑,一个Domain类则对应一个具体的实体对象。
例如电商业务中订单、产品、报价单等对象就可以对应 Order、Product、Quotation 类这样的Domain对象。OrderService类会放订单相关的处理逻辑,如果某个需求涉及到订单和产品的相关联的逻辑,也可以放到OrderService类中。
数据存储层负责数据读写,一个Repository对象可以对应一个数据表,并提供增删改查等操作。
了解上面的内容后,就可以说说本节的主角 充血模型 和 贫血模型 了。
贫血模型是指,业务对象(即Domain对象)中只包含数据(即只包含属性和属性的getter、setter方法),但不包含业务逻辑(不包含业务逻辑相关的方法)。
例如:
Student实体类中只包含了对象的属性以及属性的getter、setter方法,不包括该对象的具体行为和业务逻辑,业务逻辑以及调用Repository访问数据库的行为全都放在service层。
贫血模型的缺点
1. 不够面向对象,原因很简单,只有数据没有行为的对象不是真正的对象。
2. 业务逻辑没有经过任何拆分全部怼到Service类中,使得Service类臃肿厚重可读性差扩展性差,未经收归的业务逻辑缺少复用性。
贫血模型的优点
设计简单,开发方便,对于仅需SQL的CURL操作就能满足的简单需求而言再适合不过。
充血模型是指,Domain对象既包含对象的属性,也包含业务逻辑行为的方法,能够用Repository存取数据。而Service类则负责调用Domain对象的业务逻辑,并进行如事务控制、权限控制、幂等、记录日志、发送消息、调用其他系统的RPC接口等与业务无关的工作,以及跨领域模型的业务聚合功能(比如有个需求设计订单和产品两个业务对象的交互,这样的逻辑不适合放到Order或Product的Domain类中,只能放到Service类)。
如此一来,Domain和Service类都包含业务逻辑,但区别是Domain类中放的是和该业务对象相关性强且粒度较小不可分割的逻辑,Service类则负责调用Domain对象的逻辑以及一些非业务的与三方系统的交互工作。因此Service类的内容就会薄很多,逻辑更加清晰。
但这么一来,如何拆分业务逻辑到Domain和Service类就是关键,这需要视具体业务场景和需求而定。
下面我们举个充血模型的例子,假如有个需求,允许用户将阿里巴巴平台上的产品同步到自己公司的erp系统(你可以理解为是公司内部的后台系统)后,生成erp内的本地产品。
此时涉及到四个对象:平台产品SPU(对应的Domain类是PlatformSpu类)、平台产品SKU(对应PlatformSku类)、本地产品SPU(对应LocalSpu类)和本地产品SKU(对应LocalSku类)。
Service类是LocalProductService类。
那么我们对业务逻辑的拆分应该是这样:
PlatformSku类定义获取平台SKU信息的方法;
PlatformSpu类定义一个获取平台产品(既包含SPU,又包含SKU信息)的方法;
LocalSku类定义保存本地SKU信息的方法;
LocalSpu定义保存本地产品的方法(既保存本地SPU,也保存本地SKU,这是个原子操作);
LocalProductService类的生成本地产品方法如下:
保存SPU和SPU下的SKU这两个行为是一个关联性极强的操作,因此需要将这个这两个行为共同放到 LocalSpu 这个Domain类下。
而 获取平台产品信息 和 保存本地产品信息是两个Domain对象的交互逻辑,无论放到哪个Domain类下都不合适,因此放到Service类中。
业界内更推荐使用充血模型,因为其更符合面向对象,并且可扩展性高,能应对复杂度较高的系统模块和需求。