一个小的架构

原文《A Little Architecture》 by Robert C. Martin (Uncle Bob)

我想要成为一个软件架构师。

对于年轻的软件开发工程师来说,那是一个好的目标。

我想要领导一个团队,并决定使用什么数据库,框架和 web 服务器还有类似这些很重要的东西。

好吧,那你就不是想要成为一个软件架构师。

是真的!我想要成为做所有重要决定的那个人。

好吧,但是你没有列出那些重要的决定。你提到的都是一些不相关的事情。

什么意思?数据库不是重要的决定?你知道我们每年要在上面要花掉多少钱吗?

可能很多吧。但数据库也不是最重要的决定。

为什么?数据库是整个系统的核心!在那里数据被组织,排序,索引并被访问。没有数据库就没有系统!

数据库仅仅是个 IO 设备。它只是碰巧提供了一些有用的工具用于排序,查询还有报告。但是相对于系统架构这些只是辅助功能(ancillary)。

辅助功能?这太扯了。

是的,只是辅助功能。你系统的业务逻辑(business rules)可能会使用到一些这类工具;但这些工具不是业务逻辑。如果你愿意,你可以使用别的工具替代这些工具;但是你的业务逻辑还是之前那样,没有改变。

好吧,是那样的,但我不得不把那些之前使用数据库的代码重写一遍。

嗯,但那是你的问题。

怎么讲?

你的问题是,你相信业务逻辑依赖数据库这些工具的使用。但不是这样的。至少是如果你提供了一个好的架构是不应该这样的。

我不明白。怎么才能创建一个业务逻辑,不依赖使用到的工具?

我不是说他们不使用数据库的这些工具;我说的是他们不应该依赖这些工具。业务逻辑不应该知道你使用的是哪种数据库。

怎么才能让业务逻辑使用到这些工具但是不知道他们呢?

你可以反转这些依赖关系。让数据库依赖业务逻辑。并确保业务逻辑不依赖数据库。

你在说什么鬼话?

相反的,我说的是软件架构的语言。这就是依赖反转的原则(Dependency Inversion Principle)。低层的策略应该倚赖上层的策略。

在胡说什么!高层的策略(假设是你说的业务逻辑)调用低层策略(假设是你说的数据库)。那么高层的策略依赖低层的策略,就像调用者依赖被调用者。大家都知道这点!

在运行时是这样的。但是在编译时我们想要把依赖反转。高层策略的代码不应该声明低层策略中代码。

哦,得了!你不可能不声明他就调用它。

当然可以。这就是面相对象所要处理的。

面相对象是关于创建真实世界的模型,把数据和方法整合进相关的对象中。以更直观的结构组织代码。

这就是他们告诉你的?

大家都知道。很明显就是这样的。

没问题。当然没问题。而且,使用面相对象的原则你就可以在不声明它的时候调用某些方法。

好吧。怎么做?

你知道使用面相对象设计的对象们相互之间发送消息吗?

是的,当然知道。

并且也知道消息的发送者并不知道接受者的确切类型。

这和语言有关。对于 Java 语言来说发送者至少要知道接收者的基本类型。对于 Ruby 语言来说发送者至少要知道接收者是否可以处理被发送的消息。

是这样的。但是对这两种情况,发送者都不需要知道接收者的确切类型。

对,是这样的。

因为这样,发送者就可以让接收者在内部执行一个方法,而不必要声明接收者的确切类型。

没错。我知道这个。但是发送者仍旧依赖接收者。

在运行时,是这样的。但是在编译时不是这样。发送者的源代码既没有声明也没有依赖接收者的源代码。实际上是接收者的源代码依赖发送者的源代码。

不!发送者仍旧依赖他要发送消息的那个类。

可能看些代码会更清楚些。我会用 Java 实现。首先是 sender 包:

package sender;

public class Sender {
	private Receiver receiver;

	public Sender(Receiver r) {
		receiver = r;
	}

	public void doSomething() {
		receiver.receiveThis();
	}

	public interface Receiver {
		void receiveThis();
	}
}

接着是 receiver 包。

package receiver;

import sender.Sender;

public class SpecificReceiver implements Sender.Receiver {
	public void receiveThis() {
		//do something interesting.
	}
}

注意下 receiver 包依赖 sender 包。同样的 SpecificReceiver 依赖 Sender。注意下,sender 包一点也不知道 receiver 包。

好吧,但你耍赖了。你把 receiver 的 interface 放到了 sender 的类中。

你开始明白了,年轻人。

明白什么?

架构的原则。发送者包含有接收者必须要实现的接口(interface)。

好吧,要是这意味着我不得不实现一个嵌套类,那么……

嵌套类只是实现的一种方式。还有好多种其他的。

好吧,等等。这和数据库有什么关系。这才是我们最开始讨论的。

让我们再多看些代码。首先是一个简单的业务逻辑:

package businessRules;

import entities.Something;

public class BusinessRule {
	private BusinessRuleGateway gateway;

	public BusinessRule(BusinessRuleGateway gateway) {
		this.gateway = gateway;
	}

	public void execute(String id) {
		gateway.startTransaction();
		Something thing = gateway.getSomething(id);
		thing.makeChanges();
		gateway.saveSomething(thing);
		gateway.endTransaction();
	}
}

这个业务逻辑也没做什么啊。

这只是个例子。你可能有很多像这样的类,实现了很多不同的业务逻辑。

好吧,那么那个 Gateway 是干什么的?

它为业务逻辑提供所有的数据访问方法。这是它的实现:

package businessRules;

import entities.Something;

public interface BusinessRuleGateway {
	Something getSomething(String id);
	void startTransaction();
	void saveSomething(Something thing);
	void endTransaction();
}

注意,这个在 businessRules 包中。

好吧。Something 类是什么?

那个代表一个简单的业务对象。我把它放进了一个叫 entities 的包中。

package entities;

public class Something {
	public void makeChanges() {
		//...
	}
}

最后这个是 BusinessRuleGateway 的实现。这是知道真实的数据库的那个类:

package database;

import businessRules.BusinessRuleGateway;
import entities.Something;

public class MySqlBusinessRuleGateway implements BusinessRuleGateway {
	public Something getSomething(String id) {
		// use MySql to get a thing.
	}

	public void startTransaction() {
		// start MySql transaction
	}

	public void saveSomething(Something thing) {
		// save thing in MySql
	}

	public void endTransaction() {
		// end MySql transaction
	}
}

然后,注意这个,在运行时业务逻辑调用数据库;但是在编译时 database 包声明它依赖 businessRules 包。

好吧,好吧,我想我知道了。你只是使用多态从业务逻辑中隐藏了数据库的实现。但是你还是得有一个接口(interface)提供全部的数据库工具给业务逻辑。

对,但也不完全正确。我们不用尝试把全部的数据库工具都提供给业务逻辑。而是,我们让业务逻辑创建他们需要的接口。这些接口的实现可以调用适合的工具。

好吧,但要是全部的业务逻辑需要所有的工具,那么你就不得不把所有的工具都放到 gateway 接口中。

额,我猜你还没明白。

明白什么?我很清楚。

每一条业务逻辑只规定了自己访问数据需要的接口。

等等。什么?

这叫做接口隔离原则(Interface Segregation Principle)。每个业务逻辑的类,只使用了数据库的一部分功能。那么每个业务逻辑只需要定义接口提供自己访问数据所需要的功能。

但那意味着你将要多写好多的接口,还有好多小的实现类调用其他的数据库类。

嗯,很好。我看你开始明白了。

但是那真是一团糟,还浪费时间!我为什么要这么做?

你这么做是为了保持整洁,还有节省时间。

哦,得了。这只是为了写代码而写代码。

相反的,这些是重要的架构决定,可以让你推迟做一些无关紧要的决定(irrelevant decisions)。

什么意思?

还记得你开始说的想要成为一个软件架构师吗?你想要做所有重要的决定?

是的,我是这么想的。

在这些决定中,你想决定的是数据库,web 服务器和框架相关的。

没错,但你说这些不是重要的决定。你说它们是无关紧要的。

没错。就是这样。软件架构师的重要决定就是让你不用决定使用哪种数据库,web 服务器,还有框架。

但是你得先做这些决定啊!

不需要。事实上,你需要在之后的开发循环中再决定 —— 当你有更多的信息。

有一个架构师叫 Woe, 过早的决定使用数据库,但是最终发现使用 flat file 就够用了。

有一个架构师叫 Woe,过早的决定使用 web 服务器,但最终发现团队需要的只是一个简单的 socket 接口。

有一个团队叫 Woe,它们的架构师过早的引入了一个框架,最后发现这个框架提供的功能他们根本就用不到,还引入了一些新的限制。

有一个团队叫 Blessed,他们的架构师提供了一种方法可以在信息明朗之后再做这些决定。

有一个团队叫 Blessed,他们的架构师把他们从低速的,资源不足的 IO 设备还有框架中隔离,这样他们就能创建快速的,轻量级的测试环境。

有一个叫 Blessed 的团队,他们的架构师关注的是真正重要的事情,把那些不重要的事情放到了一边。

尽胡说。我跟本不明白你在说什么。

好吧,可能十年之后你就会明白…… 要是那时你还没转行去做管理

Written on 2016-01-21