我要成为一个软件架构师。
对一个年轻的工程师来说,这是一个很好的目标。
我要领导一个团队,还要做所有关于数据库、框架和 Web 服务器的重要决定。
好吧,如果是这样,你就没必要成为一个软件架构师了。
当然有必要了!我要成为一个能够做所有重要决定的人。
这样很好,只是你没有列出哪些才是重要的决定。你刚才说的那些跟重要的决定没有什么关系。
你说什么?难道数据库不重要?你知道我们在数据库上面花了多少钱吗?
可能很多。不过数据库仍然不是最重要的。
你怎么能这么说呢?数据库可是整个系统的心脏啊!所有的数据都保存在这里,它们在这里被排序,被索引,被访问。如果没有数据库,整个系统就无法运作!
数据库只不过是一个 IO 设备,它提供了一些有用的工具对数据进行排序、查询,并生成报表,但这些工具都只是整个系统的附属品。
附属品?真是不可思议。
是的,附属品。你的系统业务逻辑或许会用到这些工具,但这些工具并非业务逻辑固有的组成部分。如果有必要,你可以随时替换掉这些工具,但业务逻辑还是那些业务逻辑。
好吧,不过如果把这些工具替换掉,我们就要重新实现业务逻辑了。
那是你的问题。
为什么这么说?
你认为业务逻辑依赖数据库,但实际上不是这样的。如果你的架构足够好,最起码业务逻辑不应该依赖数据库。
这太疯狂了。我怎么可能创建出不使用这些工具的业务逻辑?
我并没有说业务逻辑不要使用数据库工具,我的意思是它们不应该依赖这些工具。业务逻辑不应该知道使用的是哪一种数据库。
如果业务逻辑对数据库一无所知,它怎么使用这些工具呢?
依赖反转。你要让数据库依赖业务逻辑,而不是让业务逻辑依赖数据库。
你的话让人费解。
费解吗?我讲的可是软件架构。这个就是依赖反转原则,让下层策略来依赖上层策略。
那就更加费解了!既然上层策略(假设你指的是业务逻辑)要调用下层策略(假设你指的是数据库),那么就应该是上层策略依赖依赖下层策略,就像调用者依赖被调用者一样。这是众所周知的!
在运行时确实是这样的,但在编译时我们要把依赖反转过来。上层策略的代码里不要引用任何下层策略的代码。
拜托!不引用代码就无法调用它们。
当然可以调用了。面向对象就可以做到。
面向对象对真实世界进行建模,把数据和函数组合到对象里,把代码组织成直观的结构。
这是他们告诉你的吗?
所有人都知道的,这不是很明显的事情吗?
确实如此。不过,面向对象是可以做到不引用也能调用的。
好吧,那它是怎么做到的?
你应该知道,在面向对象系统里对象会给其它对象发送消息的,对吧?
是的,当然。
那么你就该知道,消息发送者是不知道消息接收者是什么类型的。
这要看使用的是哪一种语言了。在 Java 里,发送者最起码要知道接收者的基本类型。在 Ruby 里,发送者知道接收者一定会处理它所发送的消息。
是的。不过不管是哪一种情况,发送者都不知道接收者具体的类型。
嗯,是的。
所以发送者可以给接收者传递一个函数,让接收者执行这个函数,这样发送者就不需要知道接收者是什么类型了。
没错。我了解你的意思。不过发送者仍然依赖接收者。
在运行时确实是的,但在编译时不是这样的。发送者的代码里并没有引用接收者的代码。实际上,是接收者的代码依赖了发送者的代码。
啊!但发送者仍然会依赖接收者的类。
看来需要用代码来说明了,我用 Java 来写些代码。首先是发送者代码:
复制代码
package sender; public class Sender { private Receiver receiver; public Sender(Receiver r) { receiver = r; } public void doSomething() { receiver.receiveThis(); } public interface Receiver { void receiveThis(); } }
下面是接收者代码:
复制代码
package receiver; import sender.Sender; public class SpecificReceiver implements Sender.Receiver { public void receiveThis() { // 这里会做一些有趣的事情 } }
可以看到,接收者代码依赖了发送者代码,也就是说 SpecificReceiver 依赖了 Sender。同时可以看到,发送者代码对接收者代码一无所知。
哈,你作弊了。你把接收者的接口放到了发送者的类里了。
你开始明白了。
明白什么?
当然是架构原则啊。发送者持有接收者必须实现的接口。
如果这意味着我要使用内部类,那么……
使用内部类只是方法之一,还有其它的方法。
请等一下。最开始我们讨论的是数据库,那这些跟数据库又有什么关系呢?
让我们来看一下其它代码吧。首先是一个简单的业务逻辑
复制代码
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) { // 从 MySQL 里读取一些数据 } public void startTransaction() { // 开始一个事务 } public void saveSomething(Something thing) { // 把数据保存到 MySQL } public void endTransaction() { // 结束事务 } }可以看到,业务逻辑是在运行时对数据库进行调用的。而在编译时,是 database 包引用了 businessRules 包。
好吧,我想我明白了。你用多态性隐藏了数据库实现。不过在业务逻辑里,仍然引用了数据库的工具接口。
不,不是这样的。我们并没有打算为业务逻辑提供所有的数据库工具接口,而是业务逻辑创建了它们所需要的接口。在实现这些接口的时候,可以调用相应的工具。
嗯,这样的话,如果业务逻辑需要所有的工具,那么你必须把所有工具都放到 Gateway 接口里。
哈,我觉得你还是没有明白。
不明白什么?我觉得已经很清楚了。
每个业务逻辑只定义它所需要的接口。
等等,什么意思?
这个叫作接口分离原则。每个业务逻辑只使用一部分数据库工具,所以每个业务逻辑只定义能够满足需要的接口。
这样的话,你就会有很多接口,而且有很多实现类。
哈,是的。你开始明白了。
这样子很浪费时间!我为什么要这样做呢?
这样做是为了让代码更干净,并且节省时间。
算了吧,这样只会增加更多的代码。
相反,这其实是很重要的架构决定,这跟你之前所说的那些所谓的重要决定是不一样的。
什么意思?
还记得你刚开始说你要成为一个软件架构师吗?你还想要做所有重要的决定?
是啊,我是这么想过。
你想做所有关于数据库、Web 服务和框架的决定。
是啊,而你却说它们都不重要,还说它们其实跟重要的决定不相干。
没错,它们确实跟重要的决定不相干。一个软件架构师真正要做的重要决定都在数据库、Web 服务器和框架之外。
但首先要先决定用什么数据库、Web 服务器或框架啊!
不,实际上应该在开发后期才开始做这些事情——在你掌握了更多信息之后。
哀,当架构师草率地决定要使用一个数据库,后来却发现使用文件系统效率更高。
哀,当架构师草率的决定使用一个 Web 服务器,后来却发现团队需要的不过是一个 socket 借口。
哀,当架构师草率地决定使用一个框架,后来却发现框架提供的功能是团队不需要的,反而给团队带来了诸多约束。
幸,当架构师在掌握了足够多的信息后才决定该用什么数据库、Web 服务器或框架。
幸,当架构师为团队鉴别出运行缓慢、耗费资源的 IO 设备和框架,这样他们就可以构建飞速运行的轻量级测试环境。
幸,当架构师把注意力放在那些真正重要的事情上,并把那些不重要的事情放在一边。
我完全不知道你在说什么了。
好吧,如果在若干年后你还没有转做管理,或许会明白的……
感谢徐川对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论