在很多大团队开发的时候,将需要用到很多项目的组合开发一个软件,一个软件需要用到的项目有很多个,很少会用到一个项目就能做到。但多个项目一起开发,在配置管理和团队管理有不同的策略,一个就是让项目拆分为多个代码仓库,另一个就是将这些项目合在一个代码仓库。两个策略不能说哪个更好,本文和大家分享我所在的团队和我参与的其他团队的策略
单代码仓库的优势在于管理方便,将所有的项目都放在一个代码仓库里面,此时团队所有成员都可以方便了解所有的代码,可以减少很多重复的代码的编写。我参与的最大的一个单代码仓库的项目就是 dotnet runtime 这个 dotnet 的最基础库。在2018的时候,微软官方采用了多代码仓库的方法,此时官方遇到的问题是多个代码仓库之间的同步,同步包括引用同步以及发布同步都存在比较大的管理问题
因为 dotnet runtime 是支持全球任何开发者进行贡献,这些开发者之间属于不同的公司,沟通的难度比较高。此时带来的问题是引用同步的问题,如我的一个坑我需要在 CoreClr 先开启某个 API 然后在 Library 库使用这个 CoreCLR 的接口进行修复。此时我需要等待 CoreCLR 的合并然后等待发布,然后等待官方合入 Library 库,接着在 Library 库修复这个问题。如果坑的是我一次发现这样的开启的接口实际修复不了,需要再来一次,这就会哭了
我就尝试过这样玩了一次,经过了从超级长的时间,最后我放弃了
于是为了解决以上的问题,官方就将多个仓库合为一个 dotnet runtime 代码仓库。在合并之后又遇到了一些问题,此时的代码需要有强的代码审查解决耦合的问题。但是当前也逐渐的,整个 dotnet runtime 仓库的耦合度也相比之前大了一点,这个耦合度不好衡量,因此这句话也只是我认为的
另一个问题是在于 commit 的内容上,现在整个 PR 和 commit 的历史有包含了各个模块的逻辑,相对来说比较乱。虽然不是很多小伙伴都会关注这部分的内容
而相对于官方仓库有强大的程序员进行代码审查和解决大量的耦合问题,我所在的团队,有一个代号是 EN 的软件,这个软件里面开始有用到很多基础库,有一个代号是 EUI 的基础库是一个界面库,这个界面库被合到了 EN 的主代码仓库里面。合到主代码仓库是为了解决开发的时候进行频繁的改动的开发难度问题,小伙伴都知道 UI 是不断改改改的,如果改动的 UI 会涉及部分的业务逻辑的使用,例如我的某个业务逻辑用到了某个新的按钮的时候,此时如果按钮的样式定义在 UI 库里面和按钮的动画和一些有趣的逻辑都定义在UI库里面,在进行业务开发的时候就需要有部分是在进行 UI 库的更改。在进行新控件开发时是符合开发规范的,因此不知道这个新控件的业务边界,也为了更好的敏捷,因此不断根据业务更改是符合预期的。但是如果是多个代码仓库,就会存在这样的问题,在更改UI库时,需要同步到主代码仓库,而调试的时候更是坑。于是就决定将EUI这个库合入到主代码仓库里面
在合入之后经过了一段时间的开发,就发现了整个EUI库不能成为基础库了,这个库耦合了大量的业务逻辑等,也再也拆不出去了
此时其他的软件,如代号是 MH 的软件,和 CM 的软件,此时需要使用 EUI 库就做不到了
而 EN 这个软件还用到了其他组件库,如 Core 基础库,这个基础库就在独立的代码仓库里面,因为 Core 基础库是被很多个团队的很多个软件所使用的,被定义为所有项目都引用的基础库。此时如果想要将这个基础库合入到某个项目的代码仓库,是无法撕的决策,因为合到哪个代码仓库好?如合到你的团队的某个软件的代码仓库,那如果耦合了你的软件的业务我可不同意。我想要更改基础库的功能,我需要去你的团队项目去提代码合并,去占用你的团队开发者的代码审查时间,需要构建你的团队的代码创建 NuGet 库等,想想就了解这是一个坑
多个代码仓库的优势在于复用,如果有多个团队和多个软件,此时拆分代码仓库可以提升整体的效率。多个软件可以复用更多的逻辑,多个软件不会污染基础库。但是多个代码仓库一定会降低单个软件项目的开发效率,但长远说也许效率是提升的。为什么说长远说是提升的,因为分多个代码仓库就一定从逻辑上解决了耦合的问题,都不在相同的代码仓库,此时的耦合很难做到。不过也存在的问题是会有一些重复的代码,因为代码是放在不同的仓库里面,有些逻辑也许小伙伴们不知道,于是就将功能重复的实现多次
多个代码仓库的优势在于让代码独立,方便作为组件库。可以让多个软件的业务逻辑不会污染组件库,或者说组件库同时被多个软件的业务逻辑污染而做到通用。这句话需要用一个例子说明,我有组件库 Core 库,这个基础库的代码将会被超级多的项目使用,而这些项目除了共同点是 dotnet 的之外,有超级多的不同。如框架上有 .NET Framework 和 .NET Core 而 .NET Framework 有从 3.5 到 4.8 的不同版本,当然现在最低是 .NET 4.5 的支持了。而开发框架上有 WPF 和 WinForms 和 Xamarin 和 ASP.NET Core 和 Unity3D 等。业务范围是有教育、企业和游戏等,此时可以看到各个业务都会期望有底层库的支持。小伙伴可以了解到移动端的需求和 ASP.NET Core 的需求是不相同,而 Unity3D 又有不同的需求,需求不同在于移动端要求极高的启动性能,也就是任何需要占用启动过程的依赖注入就需要被拖出去扔掉。而移动端不看重运行时的依赖注入反射耗时,此时就和服务器端冲突,因为服务器端无视启动性能,但是要求每次对象创建的速度足够快。而游戏端要求对象创建数量的控制和内存的控制和垃圾回收次数,这两个刚好在移动端和服务器端都是不看重的。这几个需求合在一起就会让这个 Core 基础库不断磨,做到极高的启动性能,极高的对象创建速度,极低的对象创建和内存占用
用一个小的例子说明,我的基础库包含了日志模块,所有的项目都可以使用相同的日志接口模型,特别是其他基础库,此时可以在最顶层按照自己的需求定制日志。如果我的设计不够通用,如果我的这个 Core 基础库放在了某个软件的代码仓库里面,此时的设计理论上就会很难照顾到其他的项目。例如我就会尝试写了一段字符串序列化方法,只是用于我这个客户端业务的日志,而忽略了服务器端的格式规范。又例如我是将 Core 基础库放到某个服务端的软件代码仓库里面,这个服务端因为无视软件启动性能,可以在日志初始化的时候创建文件和文件夹,初始化日志服务器等。此时的 Core 基础库被用在客户端就发现日志模块占用了太多的启动性能
将基础库作为单独的代码仓库的优势就在于这里,尽管有自己的软件的业务需求,期望加入到基础库里面,但不同的团队都会去按照自己的需求去磨这个基础库,很少有完全冲突的需求,这些冲突都能找到解决方法,于是整个基础库就会不断强大
在多个团队都会使用相同的基础库,就会在代码审查的时候找到一些业务耦合,此时分开的多个代码仓库就可以让代码的质量更高。这句话说起来其实有点比较难理解,但是我也不敢在这里将某个团队项目作为例子在这里聊
但基础库分为多个代码仓库也如开始聊的,会存在开发效率的降低,包括代码引用的同步和调试的难度。我所在的团队了解决这几个问题,于是设计了一些规范,如通过推 Tag 打包的方法,详细请看 dotnet CBB 为什么决定推送 Tag 才能打包
多个代码仓库的代码引用问题是指我的某个功能需要多个代码仓库的多个库配合做到,例如我有功能需要 A 和 B 和 C 三个库同时更改才能做到,此时是 A 引用 B 而 B 引用 C 库,那么如何管理好这些同步依赖?我更改了 A 库,然后我在本地让 B 引用了 A 的我的本地开发路径,我更改了代码,调试完成,然后上传代码?这样做的坑在于这段代码将只有我才能构建通过,因为小伙伴没有我的本地开发路径
那换个方法,我让 B 引用了 A 的我的本地开发路径,但是我不上传 csproj 文件的更改?这个方法的坑在于如果有 API 变更,这个 commit 是构建不通过的,因为此时的 B 依然使用 A 的旧版本。而 commit 构建不通过对于回溯是神坑
于是一个解决方法是在更改 A 之后打包 NuGet 包让 B 引用,这个解决方法有两个坑是谁的 NuGet 包和如何回溯 NuGet 包对应的代码。解决这两个问题可以使用 Tag 号和 NuGet 版本号关联的方法解决。通过在本地创建 Tag 号推送服务器,让服务器触发打包,打出来的 NuGet 包对应 Tag 号,此时就能满足让服务器打包以及使用 Tag 关联代码和 NuGet 包版本。当然另一个方法是修改版本号文件,服务器打包 NuGet 之后自动创建 Tag 号
而多个代码仓库的调试问题可以通过一个强大的 VisualStudio 插件解决,请看 dotnet-campus/DllReferencePathChanger: VS DLL引用替换插件
这个插件能做到将 NuGet 引用替换为本地的开发路径,也就是我原先使用了 NuGet 引用了 A 库,我可以利用这个插件将 NuGet 引用替换为 csproj 的本地代码引用,此时就适合进行复杂的调试和大量的 API 更改
而这个插件的不足在于如果我只是想进行快速的调试底层库,用这个插件的效率不够,于是此时可以使用另一个工具 dotnet-campus/UsingMSBuildCopyOutputFileToFastDebug: 通过复制输出文件让 VisualStudio 外部启动快速调试底层库 Using MSBuild Copy Output File To Fast Debug
但无论这几个开发工具做的多好,对单个项目的开发效率都无法和单代码仓库相比
因此我的推荐是按照团队的规模和软件数量决定,将一部分足够独立的逻辑作为单独的代码仓库,而其他的都合在一起
本文会经常更新,请阅读原文: https://dotnet-campus.github.io//post/dotnet-%E5%BC%80%E5%8F%91%E7%9A%84%E5%8D%95%E4%BB%A3%E7%A0%81%E4%BB%93%E5%BA%93%E5%92%8C%E5%A4%9A%E4%BB%A3%E7%A0%81%E4%BB%93%E5%BA%93%E7%9A%84%E4%BC%98%E5%8A%A3.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 lindexi (包含链接: https://dotnet-campus.github.io/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 。