我第一次听说反腐败层(ACL)一词是在Eric Evans的书“领域驱动设计”中。那些日子,DDD是我正在探索的一个新领域,我对所有这些新概念感到非常兴奋,但我没有实现大部分概念。
近年来,在我所做的几乎所有开发中,我不得不处理遗留代码,数据存储库或“第三方”子系统,并且ACL已经“形成”,当我们处理其他子系统时必须特别注意它。
在wiki.c2.com(http://wiki.c2.com/?AnticorruptionLayer)中,我们可以找到ACL的定义:
“如果您的应用程序需要处理数据库或其他应用程序,这些应用程序的模型不合适或不适用于您自己的应用程序中所需的模型,请使用反腐败层转换为该模型或从您的模型转换为您的模型”
在Eric Evans的书中,他写了两句相信非常有趣的句子:
“但是当边界的另一边开始泄漏时,转换层可能会采取更加防御的语气。”
“当基于不同模型的系统结合在一起时,新系统适应其他系统语义的需求可能导致新系统自身的损坏”
基本概念非常清楚。当我们需要与其他系统,数据存储库或遗留代码(甚至是“我们的”遗留代码)“交谈”时,我们应该阻止我们的模型与其他“外面模型”混合。
我们必须将这些外部系统或数据存储库视为不同的有界上下文,当然,它将拥有自己的模型,它将与我们的模型建立关系。
大多数情况下,这种映射将是customer/supplier(发布订阅)类型,我们通常将是消费订阅一方。
即使您没有在您的开发中实施DDD,ACL也是一个非常好的“工具”。
如您所见,ACL不仅是一个简单的翻译层。如果我们开发ACL,我们应该设计:
- 翻译模型
- 防止失败
- 监控
- 改进我们的集成测试
一个例子
想象一下,我们正在开发一个银行相关的应用程序。我们需要处理附加到帐户的信用卡。
要获取此信息,我们需要与旧的且不太可靠的SOAP服务器通信,以检索附加到帐户的信用卡信息。
实现ACl层的方法有很多种,但我们总是使用外观设计模式,如外观,适配器和翻译器。
翻译模型
这是我们的模型。我们有一个基本的信用卡信息,我们可以用一些众所周知的算法验证它。
class CreditCard attr_accessor :card_number, :card_holder, :expiration_year, :expirartion_month, :type
def initialize(args) @card_number = args[:card_number] @card_holder = args[:card_holder] @expiration_year = args[:expiration_year] @expirartion_month = args[:expirartion_month] @type = args[:type]
validate_card_number end
private def validate_card_number CreaditCardValidator::validate(self) end
end
class CreaditCardValidator def self.validate(credid_card) # Validate or raise CreditCardInvalidNumber end end
class CreditCardInvalidNumber < StandardError; end
|
这里我们有一个服务,可以从SOAP服务器中恢复信用卡:
class SoapClientWrapper
def get_card_data(account_number) foreign_cards = get_fixtures(account_number) foreign_cards.map { |c| yield c } end
Customer = Struct.new(:name, :surname, :number, :date, :type)
def get_fixtures(card_number) [ Customer.new("Dave", "Foo", "371449635398431", "02/20", "American Express"), Customer.new("Jane", "Bar", "5555555555554444", "10/21", "MasterCard"), Customer.new("Micha", "Jar", "4111111111111111", "14/23", "visa" ) ] end
end
class CreaditCardService
attr_reader :cards
def initialize(soap_client) @soap_client = soap_client end
def get_card_from_account(account_number) begin @cards = @soap_client.get_card_data(account_number) do |card| CreditCard.new( :card_holder => card.name + card.surname, :card_number => card.number, :expiration_year => card.date.split('/')[1], :expirartion_month => card.date.split('/')[0] ) end rescue Exception => e raise CreaditCardServiceError end end end
class CreaditCardServiceError < StandardError; end
|
正如您在代码中看到的,我们会恢复信用卡信息并转换为我们的模型。
这里的要点是,我们总是会提供一个帐号,我们会恢复附加到此帐户的信用卡列表,如果出现问题我们会捕获异常。
如果第三方系统发生变化,我们只需要更改我们的翻译器,或者即使服务协议发生变化,我们也只需要修改一个类来保持模型的完整性。
准备好失败
如果信用卡服务器掉落,我们的申请会怎样?我们的应用程序会看到错误500吗?也许警告页面对我们的用户来说更加容易,或者隐藏这部分页面信息并显示其余部分,或者我们可以使用“cirtuit破坏者”并提供缓存响应。
也许你需要不止一个例外之王来为故障添加更多粒度(即CardServiceConnectionError,CardServiceTranslationError)
改善我们的服务
这个抽象层的存在允许我们装饰它并添加更多功能,以增加我们系统的防御。可能我们需要记录所有交易以进行一些审计。也许,我们可以测量时间响应或响应代码并将所有遥测发送到ELK堆栈。
我们来装饰我们的服务:
class TimeCreaditCardService
def initialize(credit_card_service) @credit_card_service = credit_card_service end
def get_card_from_account(account_number) measure { @credit_card_service.get_card_from_account(account_number) } end
def measure start = Time.now result = yield finish = Time.now @delta = finish - start # in seconds return result end
def get_time puts "Time elapsed #{(@delta)*1000} milliseconds" end
end
|
根据我的经验,当我们开发一个“前端”时,这可能是一个很好的实践,因为我们是冰山的可见部分。大多数时候,监控子系统将帮助我们检测应用程序中的错误原因。
集成测试
当我们创建集成测试时,我们需要从众所周知的系统状态开始。例如,如果我们想测试存储库和数据库之间的集成,我们需要创建表,填充一些具体数据,执行测试并检查数据库状态是否按照我们预期的方式更改。
如今,使用虚拟化(如Docker)复制数据库非常容易,我们可以在CI管道中的集成测试中运行它。
如果系统不在我们的控制之下,这可能会更复杂甚至不可能,并且我们无法确保在运行测试时我们始终具有相同的状态。
ACL允许我们模拟这个系统,这样,我们可以测试我们的代码在失败前做出反应。
我们可以模拟子系统响应并测试如何使用这个新伙伴来运行我们的有界上下文。
结论
使用ACL,我们将获得:
- 翻译信息
- 防止其他子系统故障
- 记录和监视关系
- 良好的集成测试