数字化转型:服务化设计原则 | IDCF
来源:技术琐话 作者:钟华 本文由机械工业出版社独家授权发布,中台圣经——《企业IT架构转型之道》作者钟华新作《数字化转型的道与术:以平台思维为核心支撑企业战略可持续发展》
Façade(外观)模式
接下来在介绍服务化设计原则时,会多次出现Façade模式。
外观模式的使用原理如图4-11所示。
外观模式的优点如下:
松散耦合:外观模式使得前台应用与中台服务中心可以进行松散耦合,让服务中心内部的模块能更容易地扩展和维护。
简单易用:外观模式让服务中心的服务更加易用,前台应用不再需要了解服务中心内部的实现,也不需要跟服务中心内部众多的功能模块进行交互,只需跟外观类交互就可以了。
更好地划分访问层次:通过合理使用外观模式,可以更好地划分访问的层次,有些方法是对系统外的,有些方法是系统内部使用的。把需要暴露给外部的功能集中到外观中,这样既方便客户端使用,也很好地隐藏了内部的细节。
DTO的使用
DTO可以将服务中心复杂或易变的数据对象对前台应用屏蔽,让前台具备更好的稳定性。DTO是系统分层设计和服务化架构中经常使用的技术,概念本身也容易理解,如图4-12所示。
服务接口的设计原则
提供仅有几个方法的很多服务。 数十或数百个操作均集中在几个服务中。
ClaimEntryService {
createClaim(String userId);
ClaimItemDetails[] getClaimItems(int );
ClaimErrors[] validateClaim(int claimId);
void removeClaimItem(int claimId, int itemId);
int addClaimItem(int claimId, ClaimItemDetails details)
int submitClaim(int claimId);
}
ClaimApprovalService {
int approveClaimItem(int claimId, int itemId, String comment);
void approveClaim(claimId)
void returnClaim(claimId)
ClaimItemDetails[] getClaimItems(int );
ClaimErrors[] validateClaim(int claimId);
}
ClaimPaymentService {
void payClaim(int claimId);
}
List <Article> getArticles(...)
List<ArticleSummary> getArticleSummaries(...)
Class Foo {
private Pattern regex;
}
Class FooDto {
private String regex;
}
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元。
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)
上可依赖下。越上层的服务实现可以依赖下层的服务,也可跨级依赖。 下不可依赖上。下层的服务实现和运行一定不能依赖上层的服务,否则就会出现因为上层服务质量问题和不稳定的表现影响到下层的重要服务,而下层服务的故障将会影响到依赖这一服务的所有平级服务中心和前台应用的情况,会出现严重的“雪崩”效应。 平级可依赖,避免循环依赖。这一原则最典型的体现是业务中台的各服务中心在服务层级中均属于平级,它们均有同级别的服务运营要求,是可以互相依赖的。 高级别不可依赖低级别。业务重要性明显高的服务不能依赖业务重要性低的服务,应做好相应的服务降级,或者通过前台业务隔离这种情况的服务依赖。
总 结