作者 | 吕毅
希沃软件和软件解决方案的规模正在不断扩大,重构是一个需要持续不断进行的任务。且看如何在奔跑的汽车上换引擎!
一个人的团队
不知你们是否听说过“抛弃式设计”一词?在一些说法中,它竟然也是架构设计的一种方式!在应用开发中以特定目标为导向,只要完成了任务目标即可,压根儿不考虑后续的任何维护问题。为了适应教育和会议场景,交互智能平板内置了 Windows 系统,于是希沃软件的桌面端是以交互智能平板的附带增值软件开始的。
创业团队,差不多也就是这个样子的,一个人挑起一整个公司的软件需求。所以一些大家可能关注的问题并没有满意的答案,比如为什么选择 WPF 作为主要桌面应用开发的 UI 框架?
如果硬要扯一些理由,可能是那个年代(Windows 7 流行的年代),WPF 可是微软推广的 Windows 应用开发技术的首选呢!
一个人的团队,快速迭代的团队,从零起步的团队;你几乎不用指望里面有任何看得出来设计或者框架的影子……嗯,就是这样……
就这样,希沃软件诞生了 EasiShow(据说是首款软件),EasiMeeting,EasiNote 等增值软件。
步入正轨
做 WPF 开发的小伙伴们几乎很快都会知道 MVVM 模式,非常适合做数据驱动的客户端 UI 开发。当然即便不完全是数据驱动的软件,使用 MVVM 模式依然可以大量减少 UI 和业务逻辑的耦合代码。于是,以 EasiNote(希沃白板)为首的几款软件逐渐迁移成使用 MVVM 架构模式。
EasiNote 是什么?
EasiNote 在后来被称为希沃白板,这在当时是一款白板软件,在后来成为了一款备授课工具兼顾白板软件功能。
不过,虽然 MVVM 模式足以支撑起一个软件架构,却扛不住大软件的架构基础。因为一旦单纯的 MVVM 模式成为整个软件架构,那么几乎没有模块间解耦一说了,只剩下 UI 和业务逻辑之间的解耦了。
架构设计
重构,是一个需要持续进行的软件开发行为。但如果需求的增速远高于重构速度,那么软件在技术上崩溃是迟早的事儿——每增加一个需求的边际成本将高的可怕。好吧,996 是常态?不,可能 007 才是……
于是希沃软件桌面应用开发史上开始第一次真正进行软件架构设计。那是在 2014 年。
新的 EasiNote 基于 Prism 框架来设计。MVVM 模式依然还在软件中,因为它依然是优秀的软件架构模式。而且 MVVM 也是 Prism 的组成部分。虽然还是是一个 MVVM 框架,但此时的架构设计是可以撑得起一个大型应用程序的。
Prism 提供了区域将软件中的 UI 视图进行了隔离,提供了事件聚合将软件中不同具体级别的通知与订阅进行隔离,提供基于 Unity 或 MEF 的依赖注入机制用于将软件提供的统一服务接口与具体的实现进行隔离。EasiNote 3 的架构设计可以说基本上是建立在 Prism 之上的。
当然,过多的架构设计和解耦直接带来了近乎 4 倍的软件启动时间,这是不能接受。但在架构设计之初,这些问题并不在考虑之中,所以带来了很差的用户体验。
架构的演进
EasiNote 3 软件升级之后,名称也进行了升级,叫做“希沃白板 5”。我们以希沃白板为例说技术的演进并不是说希沃软件只有这么一款软件在做技术演进这件事,只是希沃白板通常是第一个尝试各种技术的技术孵化池,在这上面试验可行的技术会抽象成公共组件提供给其他的希沃软件使用,当然也与 CVTE 企业内其他使用 Windows 技术栈的团队共享。
希沃白板 5 并没有采用 Prism 框架,但并不是说抛弃了它,只是将其弱化了。为了性能,不得不放弃了相当多的 Prism 特性,转而改为自己实现。但依然有业务进度的要求,多数的为了性能而修改的实现都不如 Prism 本身优雅,甚至破坏了一些原本完全解耦的代码。不过很幸运的是,这些破坏通常都局限在少数的几个模块之中,希沃白板中大多数功能都还处于可控的耦合范围中。
希沃白板 5 开始加入了更多用于解耦的技术和模式。比如用于实现面向切面编程(AOP)的透明代理,比如用于实现多进程架构的 IPC。当然,这是首个开始积累公共组件的希沃软件产品;使用 NuGet 管理希沃白板 5 所有的公共组件依赖。目前已经收录的公共组件簇(指独立功能的组件)数量已经有 66 个,适用于 .NET 的独立 NuGet 包数量差不多有 100 个了。当然,那些特别小的组件我们只收录到一个公共的项目里面,没有单独成为新的包,毕竟程序集越多性能越差,详见:C# 程序集数量对软件启动性能的影响。
作为这次架构演进的结果,希沃白板 5 首个版本于 2016 年 5 月 31 日发布。
新想法/新技术如雨后春笋般涌现
希沃白板 5 一开始的整体架构设计纵使有很多的问题,但不得不说撑得起这个规模的需求,而且还撑得起发布后这三年期间的需求增加!从这一点上说,这至少是最合适的软件架构。当然,这也跟架构设计中各模块比较独立有很大的关系;无论是改模块,还是改架构中的一个部件,都可以做到“在奔跑的汽车上换引擎”。
正因为我们具备了“在奔跑的汽车上换引擎”的能力,所以我们几乎可以在希沃白板架构上尝试任何可能的新想法。
源码包
由于程序集数量过多对启动性能有负面影响,所以我们通过源码合并的方式将原本在多个 NuGet 包中的源代码合并成一个程序集,提升了程序的启动性能。可以阅读了解:
SourceYard 制作源代码包
dotnet-campus/SourceYard: Add a NuGet package only for dll reference? By using dotnetCampus.SourceYard, you can pack a NuGet package with source code. By installing the new source code package, all source codes behaviors just like it is in your project.
预编译框架
前面我们说到程序启动时大量的依赖发现和依赖注入带来了很长的启动时长,于是我们将这些依赖发现做到了编译期间。到现在为止,一旦我们有一个模块接入了预编译,其模块启动时间直接从数百毫秒清零。
可以阅读了解:
课程预编译框架,开发高性能应用 - 微软技术暨生态大会 2018
dotnet-campus/SourceFusion: SourceFusion is a pre-compile framework based on Roslyn. It helps you to build high-performance .NET code.
DAG 启动流程
我们将所有的启动期间执行的代码抽象为一个个的“启动任务”,这些任务可以是同步的也可以是异步的,任务之间指定了依赖关系。
于是我们可以将所有的启动任务构造成一个有向无环图(DAG),确保所有的启动任务可以最大程度地并行执行,且无需担心相互之间的依赖问题。这,可以大幅度提升启动速度,而且非常容易发现和干掉启动过程中的性能瓶颈。
MSTestEnhancer
单元测试很难写很难维护,于是我们自己设计了一套新的单元测试写法,非常直观且利于维护。
可以阅读了解:
不再为命名而苦恼!使用 MSTestEnhancer 单元测试扩展,写契约就够了
dotnet-campus/MSTestEnhancer
依赖树
我们通过定义依赖树的方式,在依赖发现和依赖注入的时候,限制可以注入的依赖类型,同时保持最简单的依赖注入 API。另外我们通过预编译的方式避免依赖发现和依赖注入带来的性能压力。
这部分正在开发,预计在 2019 年年末开源。
在奔跑的汽车上换引擎
希沃软件和软件解决方案的规模正在不断扩大,重构是一个需要持续不断进行的任务。
在奔跑的汽车上换引擎!
参考资料
[1] 《C# 程序集数量对软件启动性能的影响 》https://blog.lindexi.com/post/c-%E7%A8%8B%E5%BA%8F%E9%9B%86%E6%95%B0%E9%87%8F%E5%AF%B9%E8%BD%AF%E4%BB%B6%E5%90%AF%E5%8A%A8%E6%80%A7%E8%83%BD%E7%9A%84%E5%BD%B1%E5%93%8D
[2] 《SourceYard 制作源代码包》https://blog.lindexi.com/post/sourceyard-%E5%88%B6%E4%BD%9C%E6%BA%90%E4%BB%A3%E7%A0%81%E5%8C%85
[3] dotnet-campus/SourceYard https://github.com/dotnet-campus/SourceYard
[4] 《课程 预编译框架,开发高性能应用 - 微软技术暨生态大会 2018》https://blog.walterlv.com/post/dotnet-build-and-roslyn-course-in-tech-summit-2018.html
[5] dotnet-campus/SourceFusion https://github.com/dotnet-campus/SourceFusion
[6] 《不再为命名而苦恼!使用 MSTestEnhancer 单元测试扩展,写契约就够了》 https://blog.walterlv.com/post/get-rid-or-naming-in-unit-test.html
[7] dotnet-campus/MSTestEnhancer https://github.com/dotnet-campus/MSTestEnhancer