(6)隔离变化原则
当服务中心核心领域模型的对象进入前台应用中,要避免服务中心内部的重构或者模型变更导致前台应用也跟着变化。
比如前面描述的“文档服务”,其中Article对象在服务中心内部可能作为核心建模的领域模型,甚至作为对象和数据库映射(O/R mapping)等。如果文档服务给服务消费者直接返回Article,即使没有前面所说的冗余字段、复杂类型等问题,也可能让服务外部用户与服务内部系统的核心领域模型产生一定的关联,甚至可能与O/R mapping机制、数据表结构等产生关联,这样一来,内部的重构很可能影响到服务外部的用户。
同样,可采用外观模式和DTO作为中介者和缓冲带,隔离内外系统,把内部系统变化对外部的冲击降到最低。
(7)契约包装
虽然使用了DTO和外观模式将服务生产端的变化与服务消费端进行了隔离,但DTO和外观模式可能被服务消费端的程序到处引用,这样消费端程序就较强地耦合在服务契约上了。一旦契约更改,或者消费端要选择完全不同的服务提供方(有不同的契约),修改时工作量可能就非常大了。在较理想的面向服务设计中,可以考虑包装远程服务访问逻辑,也称为服务代理(Delegate Service)模式,由消费端自己主导定义接口和参数类型,并将服务调用转发给真正的服务客户端,从而让服务使用者完全屏蔽服务契约。
服务代理示例如下:
//ArticlesService是消费端自定义的接口
class ArticlesServiceDelegate implements ArticlesService {
//假设是某种自动生成的service客户端stub类
private ArticleFacadeStub stub;
public void deleteArticles(List<Long> ids) {
stub.deleteArticles(ids);
}
}
在此示例的前台应用中,所有有关文档服务调用的地方引用的都是ArticlesService,而不是“文档服务”提供的ArticleFacadeStub,这样就算服务提供端的ArticleFacadeStub发生了变更或者重构,也只需要在ArticlesService类中进行相应的调整,而无须更改更多的代码。
(8)服务无状态原则
为了保证服务中心的服务稳定性以及可扩展性,必须将服务设计为可伸缩的且可部署到高可用的基础结构中。此重要原则的一个推论就是,服务不应为“有状态型”的。即服务不应依赖于服务使用者和服务生产者之间长期存在的关系,服务调用也不应显式或隐式地依赖于前一次调用。为了说明这一点,我们举一个简单的例子,下面是一个电话对话:
问:小明的账号余额是多少?
答:320元。
问:他的信用额度是多少?
答:2000元。
此示例演示了典型的有状态模式。第二个问题通过使用“他的”引用第一个问题。这个示例中的操作依赖于转换上下文。现在让我们考虑一下所提供的应答,请注意,回答中没有上下文信息。只有在被询问者知道所询问的问题时,这个回答才有意义。在此示例中,要求使用者维护对话状态,以便解释所得到的应答。
首先,我们考虑一下依赖于前一操作建立的上下文的操作。假如这是一个与呼叫中心的交互,只要与同一个操作人员对话,对话就可以有效地结束。但我们假设呼叫被中断了,如下所示:
问:小明的账号余额是多少?
话务员1:320元。
此时通话中断,被转接到另一个话务员:
问:他的信用额度是多少?
话务员2:谁?
中断导致上下文丢失,因此第二个问题是没有意义的。就这个电话对话而言,我们可以通过重新建立上下文而抵消中断带来的后果:“我在问小明的银行账户的信息,您能告诉我他的信用额度吗?”不过,在可扩展服务调用领域,有状态对话通常更为麻烦,重新建立上下文也许在技术上可行,但很可能带来很大的性能开销。
是否要求使用关联性。即相同的服务使用者发出的连续请求是否必须交付到相同的服务提供者实例,要求使用关联性是一种有状态性与可伸缩性及可靠性冲突的情况。为了保持服务中心各服务能力的服务质量,我们必须优先考虑最终服务架构的可伸缩性和可靠性。所以笔者强烈建议,将服务设计为可避免维护会话上下文的需求。
回到上面电话对话的示例,我们可以通过将服务设计为在响应中包含合适的关联信息,从而避免对会话状态的需求,如下所示:
问:小明的信用额度是多少?
答:小明的信用额度是2000元。
在响应中包含关联信息是很好的做法,原因很多。首先,它简化了可伸缩解决方案的构造,还能提供更多的诊断帮助,且在不可能向原始请求程序交付错误响应时非常重要。总之,仔细地进行服务设计可以避免对状态的需求,从而简化可靠的、可伸缩服务结构的实现。
(9)服务命名原则
我们在选择服务、操作、数据类型和参数的名称时有一个指导原则:希望最大化服务的易用性。我们希望帮助业务应用开发人员标识实现业务流程所需的服务和操作,因此,强烈建议对服务使用者定义专业领域内有意义的名称,优先选用业务概念而不是技术概念。
建议就是:应使用名词对服务进行命名,使用动词对操作进行命名。例如,以下是使用动词短语和IT构造的服务定义:
ManageCustomerData {
insertCustomerRecord();
updateCustomerRecord();
//etc ... }
接下来是使用名词和动词短语及业务概念的服务定义:
CustomerService {
createNewCustomer();
changeCustomerAddress();
correctCustomerAddress();
// etc ... }
比较明显,第二个示例的易用性更好一些。在第二个示例中,服务的业务用途非常清楚,而不仅仅指示其输出。因此,建议不要使用“update-CustomerRecord”(可以为出于任何原因进行的任何更新),而使用“enable-OverdraftFacility(启用透支能力)”。与此类似,在客户搬迁时,我们使用“changeCustomerAddress”方法更改客户地址;而在希望更正无效数据时使用“correctCustomerAddress”更正客户地址,因为这样很容易看出这两个操作采用了不同的服务逻辑。
(10)服务操作设计原则
这是对于服务操作命名设计原则的进一步深化:应当使用具体的业务含义而不是泛型操作对操作进行定义。例如,不要使用泛泛的update-CustomerDetails操作,而要创建changeCustomerAddress、recordCustomer-Marriage和addAlternativeCustomerContactNumber之类的操作。此方法具有以下好处:
操作与具体业务场景对应。此类场景可能不仅是简单地更新数据库中的记录。例如,更改地址或婚姻状况可能需要更改其他业务模块中的相关信息,比如婚姻状况的修改可能会引起会员权益的改变。如果使用不太具体的操作(如UpdateCustomerDetails),则不适合实现此类业务场景。
各个操作接口将非常简单,且易于理解,从而提高易用性。
每个操作的更新单元有清楚的定义(在我们的示例中为地址、婚姻状况和电话号码)。在实现具有高并发性要求的系统时,我们可以基于操作的要求采用更细粒度的锁定策略,从而减少资源争用。
针对操作中参数的设计,应采用粗粒度和灵活性强的参数,目的是尽量减少因为需求变更带来的参数结构变化。以CreateNewCustomer操作的两个接口为例:
采用细粒度参数的CreateNewCustomer操作接口如下:
int CreateNewCustomer(String familyName,String givenName,
String initials, int age,String address1,
String address2, String postcode // ... )
采用单个粗粒度参数的CreateNewCustomer操作接口如下:
int CreateNewCustomer( CustomerDetails newDetails)
以上两段示例代码显示了一个具有很多细粒度参数的操作和采用结构化类型作为单个粗粒度参数的操作。之所以建议使用粗粒度参数,是因为这样能够在很大程度上避免因为细粒度参数变化带来服务整体版本升级。
从参数灵活性的角度看,要考虑服务需求的多样性和灵活性。比如,在查询商品信息时,商品定义的字段很多,不同的业务关注的字段不一样,所以在定义接口时,可通过传入业务方需要返回的商品的字段,将这些字段保存在List对象中,服务获取对应字段的值后封装成对应的Map对象返回。这样通过一个商品查询的操作方法就能满足不同应用系统对商品字段的信息获取需求。
(11)重要的服务不能依赖非重要的服务
中台建设是以服务为中心,即整个体系间的交互均以服务的形式进行。不仅前台应用和中台的各服务中心会以服务的方式进行交互,而且各服务中心之间也会这样交互。在有些情况下,前台应用在业务复杂度发展到一定程度后,也会建立起在该应用系统内部的服务体系。比如,天猫和淘宝这样的业务前端应用已经非常复杂,其内部就构建起了一个多层的服务体系。业务中台的各服务中心为这个服务体系的最下层,之上的各前端业务系统中又会按照自身业务的特点建立起自己的服务层级。
在整个服务体系中,有交易、商品、订单相关等这一类非常核心和重要的服务,也有相对不重要的服务,如运费计算或者前端应用中所创建的服务。从服务对业务的影响程度、服务范围就会体现出服务重要性不同,而且服务重要性的不同也直接决定了能得到的支持和保障资源会有差异,从而最终会体现在服务的稳定和可靠性方面。所以越在下层的服务会越稳定,越往上层的服务则不管是稳定性还是业务兼容性方面都不如下层服务。
“重要的服务不能依赖非重要的服务”这一原则可以更加细化,如下所示:
- 上可依赖下。越上层的服务实现可以依赖下层的服务,也可跨级依赖。
- 下不可依赖上。下层的服务实现和运行一定不能依赖上层的服务,否则就会出现因为上层服务质量问题和不稳定的表现影响到下层的重要服务,而下层服务的故障将会影响到依赖这一服务的所有平级服务中心和前台应用的情况,会出现严重的“雪崩”效应。
- 平级可依赖,避免循环依赖。这一原则最典型的体现是业务中台的各服务中心在服务层级中均属于平级,它们均有同级别的服务运营要求,是可以互相依赖的。
- 高级别不可依赖低级别。业务重要性明显高的服务不能依赖业务重要性低的服务,应做好相应的服务降级,或者通过前台业务隔离这种情况的服务依赖。
总结:简单就是美,过多的原则可能会让整体的设计变得臃肿,在什么情况下采用什么样的原则,需要建立在对业务理解的基础上,而且需要在实践过程中不断练习,从而能更从容地应对服务设计相关的问题。
本文由机械工业出版社独家授权发布,中台圣经——《企业IT架构转型之道》作者钟华新作!《数字化转型的道与术:以平台思维为核心支撑企业战略可持续发展》。
十余年数字化实战经验再升华!开创性提出数字化转型中平台思维的十大要素。来自实践,并能指导实践。系统化介绍数字化转型的思路与方法,以及产业互联网平台的建设思路,为各种业务模式的数字化转型提供高价值参考。