标签:golang

Go的软件架构方法论迷思

Go的软件架构方法论迷思

奇怪的思潮

最近东瀛Go业界有种奇怪的思潮:把Clean Architecture带进Go。我一直以为这只是某些小公司的自娱自乐,后来跟一些同行谈了一下,发现也不是个别现象。一个项目能不能用、活不活得下来还不知道,domain、repository、entity、use-case倒是整了一大堆。

其实Go的设计的最佳实践,官方文档和博客已经写得非常明白,从包名设计,到错误以处理,都有一套官方标准。这也是Go这门语言的优秀之处:简单,节制,让所有人都在一个约束下跳舞。至于应该用何种软件架构,却甚少见人提及。

如果大家读Go标准库或者一些优秀的Go的OSS代码,像HashiCorpCoreOSPingCAP,甚至是面向初学者的CURD项目RealWorld,会发现并没人把Clean Architecture等方法论用在这些项目上。这些代码点到即止,都在努力用最少的代码用最简练的方式实现它们所定义的目标。不止Go,著名的C、Rust、Python项目里我也没见过所谓的Clean Architecture。这股工业界的暗涌,跟OSS最佳实践显然是割裂的。

Clean Architecture试图解决的问题

不管是Clean Architecture,还是DDD、TDD、MVC,其诞生都有其道理。所有的软件设计框架和设计模式,它们都是对现实中某类复杂的问题的一般解。这些解不是也不可能是银弹,有其制约性。

我们看看Clean Architecture试图达成的主要目标:

  • 从粗到细的开发流程,延迟细节决定。
  • 责任分离。
  • 可测试。

首先,从粗到细的开发其实并不需要什么架构,只要写好伪代码就好。很多程序员并不写伪代码,一上来就写细节,这是一种不好的习惯。不仅程序,文章、计划书,也是这么写的。写程序不是DFS,而是BFS。很庆幸我初中的第一本JAVA书教会了我这个道理。

其次,责任分离在Go下只要用好interface就好,也跟架构关系不大。

最后,可测试当然很重要,前提是你TMD要写测试啊!各位Clean Architecture信者问一下自己,你们真的有写测试吗?有吗?!

Go的解

Go的设计之初,就考虑过各种软件架构和设计模式的问题。作者给出的答案,就是Go本身。比如刚才提到的interface,其目的之一就是为了解决责任分离。一个语言特性,胜千万架构与模式。

大家可以翻一下Java的23种设计模式,再对照Go的实现,就会发现很多原来在Java下得用设计模式解决的问题,在Go下都在语言层面下解决了。

让上帝的归上帝,凯撒的归凯撒

难道Go就不用设计模式了吗?也不全是。Go给的解也非银弹,不能解决所有问题,其设计目标是系统语言,而并非全栈语言,它并没有义务对它设计目标以外的问题给出一般解。

来看看号称“世界上第一个全栈语言”的Red所给出的图,看看语言的栈都有哪些:

Scope of Red compared to other programming languages

虽然这图没给出Go的栈范围,但我们知道它是在OS到Applications之间的非常狭窄的一段。

正因Go并非全栈语言,抛开设计模式用Go做其不擅长的业务逻辑密集的应用就会显得捉襟见肘。Go的Channel,Goroutinue,为系统应用设计的高级特性全用不上;而没有泛型、没有模式匹配、没有高级的错误处理,则让人如鲠在喉。

Google发明并送给所有人一台从清洗到叠衣服全自动的洗衣机,但你只关心它能不能把土豆洗干净。

也许有人会说,语言特性不重要,关键是写程序的人。然而,历史上所有的杀手级应用,都是有其相匹配的语言基础作支撑的。比如DHH写Rails时,找不到一门语言去实现它的想法,直到遇上的Matz的Ruby,其强大的元编程特性才成就了Rails。而Rails的大部分模仿者,都因为语言的原因无法达到Rails一样的高度。

全自动洗衣机让最厉害的工程师改装一下当然也能洗土豆,但我相信没一个工程师会想在自己简历上写自己干过这种浪费人力物力蠢事:毕竟出门左转就有农用器械专卖。

原罪与未来

问题的原罪,就是Google的工程师太优秀,没有考虑到这个星球上还有这么多写程序不动脑子的拿来主义码农。这问题显然是无解的,未来也不乐观。但既然献身了开源,自然就得有被全世界批斗的觉悟。Go从开源到现在已有十载,早已不是那个仅为了满足Google需要而开发的语言,它开始承载越来越多不在当初设计范围的特性。我们看到了Go2的改变,还有像v语言这种挑战。

Go开发团队是节制的,然而同人逼死官方的事也不是没发生过,Java就是一个活生生的例子,它在发明的时候也不是企业级开发的代名词。在破乎上对于Go是否要用DI这个问题有过激烈的讨论,其中某大V的一句话震耳发聩:

等什么时候出现了50万行的go程序,用户压低价格导致工资还开不了太高的时候,你们鄙视的东西统统都回来了。想想Java,也不是天生需要设计模式的。

大家还是洗洗写Rust去吧。

为什么说gorm的设计很糟糕

如果大家写Golang有点时日,很有可能听说过https://github.com/jinzhu/gorm,一个非常有名的Golang的ORM库。我自己是不用的,一来是不会用选择用Golang做CURD开发,吃力不讨好;二来是我觉得gorm的API设计很糟糕。

写一个ORM库是很困难的,不但要对不同的关系数据库特性有高度归纳能力,而且要对所使用的语言有深入的理解。ORM是这两种不同专业领域之间的桥梁,其复杂性可想而知。

可能是因为实现难度高,Golang下一直没有比较好用的ORM库。要么是直接使用官方的 https://golang.org/pkg/database/sql/ ,要么是用简单的上层建筑如 https://github.com/jmoiron/sqlx 或某种QueryBuilder如https://github.com/gocraft/dbr。这些库都各有优点,但抽象能力都远不如Rails里的ActiveRecord或者Python里的SQLAlchemy,在这种背景下gorm就显得鹤立鸡群:又能关联表,又能eager load,官人要啥就实现啥。

可惜的是,ORM是个硬骨头,gorm想做的很多,但能力却没跟上。

ORM是复杂的。gorm最大的问题,就是试图对复杂的问题抽象却没法给出完美的方案。这让我想起我学习Rust时印象最深刻的关于字符串的一节:

Rust has chosen to make the correct handling of String data the default behavior for all Rust programs, which means programmers have to put more thought into handling UTF-8 data upfront. This trade-off exposes more of the complexity of strings than is apparent in other programming languages, but it prevents you from having to handle errors involving non-ASCII characters later in your development life cycle.

https://doc.rust-lang.org/book/ch08-02-strings.html#strings-are-not-so-simple

字符串处理的本质是复杂的,Rust在设计这部分的API的时候,刻意选择了暴露其复杂性,而不是隐藏细节。这虽然增加了开发者的学习成本,但收益是更高的软件质量。

而官方库及其他的上面提及的库其实也反应了类似的思想:只要开发者对数据库有一定的理解,就能想像出每个API背后生成的SQL。这种API的结果是准确的可预测的,可能不方便,但不会让开发者产生困扰。

而gorm选择的是最大限度的抽象,提供的是一个大而全的DB对象,用一种类似Chaining的设计,几乎所有的API都调用它,而它所有的结果都返回它。它是不可知的,你不知道你的数据从哪里来,到哪里去,发生了什么事。 gorm的作者试图用黑魔法把关系数据库的细节都隐藏起来让傻瓜都能用,然而傻瓜根本就不应该用数据库。

gorm的这种设计显然违背了单一功能原则(Single responsibility principle)。我们看回去官方的API:DBConn方法返回ConnConn调用Begin返回TxTx调用Query可以返回RowsRows可以查看结果,可以查看错误。每个结构体都只有最低限度的功能,但是只要一看文档就能知道要用什么方法生成什么,一目了然。

黑魔法是很危险的,就算优秀设计如ActiveRecord,用不好一样会出N+1问题。gorm的这种设计更危险,你不知道哪个方法会返回错误,哪个方法不会。这种缺乏一致性的设计让开发者困扰,也违反了Golang对错误处理的最佳实践,一不小心就会出bug。

gorm的这种设计导致的另一个问题是:你要弥补设计上的问题,不能改API,那只能牺牲性能。比如说你打算用goroutine同时Update两个表,因为返回的都是*DB类型,你会很困惑到底是不是同一个实例,结果里的Error会不会有race condition问题。于是你打开源码一看,发现gorm好样的,它会给你自动clone一份。你又看了一下其他代码,好样的,不管有没用,居然全给你clone了。

gorm还有一些不大不小的问题,就按下不表了。自己也不想对开源代码太苛刻,毕竟谁没写过烂代码?最后介绍一本书,总结了很多Golang下的设计模式,共勉。