本文以个人视野聊下软件架构师的工作以及软件架构设计知识。做开发工作接近10年了,期间主要做Windows应用开发。在成熟的“华南区最大WPF团队”希沃白板呆了较长一段时间、后面从0到1构建Windows技术栈以及会议屏软件集,在软件设计这块自己成长了很多。需要梳理了解的软件设计,自己阶段性总结一下
先讲软件架构师,职责是服务团队和项目产品,角色对应的工作范围:
-
跟进项目进展 - 根据优先级随时支撑需求开发延迟、客户严重问题
-
关注项目质量 - 审核开发提交代码、浏览仓库最近改动,代码是否有实现缺陷、设计扩展问题,确认需求开发、问题处理有无更优解决方案
-
解决疑难杂症 - 项目及产品肯定会遇到一些难复现、路径深的技术问题,业务功能会有性能瓶颈及框架类问题,这些都需要架构师参与定位解决、提供技术解决方案。不要让问题阻塞项目节点,或者沉淀为团队的技术债务,要尽快解决
-
持续技术创新 - 当前产品的性能、框架是否需要继续优化,产品可预见的发展方向上技术可行性是否都已验证
-
推进团队成长 - 组织技术栈培训技、把技术知识变成团队内的常识,带头分享技术实现、软件设计概念
架构师如何做好呢,我们下面以项目、团队、个人三个角度详细说下
架构师需要善于发现问题。架构师是全局性角色,需要关注的面很多,可以站在团队、项目、公司的角度多思考技术层面自己还能做啥。发现问题比解决问题更重要,产品功能是否不是最优方案,小伙伴提交代码是否有缺陷,发现问题不一定要你自己去解决,可能其它人解决比你更适合。在会议屏软件全家桶开发过程中,软件从0到1开发的数量多,另外团队人员最多的时候12个,每个人都在疯狂开发需求、提交代码,有些功能没有想清楚实现或者设计就拍脑袋敲代码了,最后方案改了一版又一版。也有一些功能没办法尽善尽美,针对一些小使用场景、逻辑边界测试提了BUG,开发未能权衡取舍,最后代码改了又撤回。如果架构师去关注项目开发流程,以自己扎实的技能、敏锐的直觉去把一些技术风险找出来,减少测试成本、软件后续维护成本,降低低级严重向客户暴露的风险、提交用户体验。还有产品的启动性能、业务耦合问题等,要以全局性眼光多发现问题,先把问题抛出来
架构师需要学会打杂。功能迭代开发进度延迟、客户反馈线上版本紧急问题,人员不够或者时间原因,需要你顶上去,架构师不只是写自己代码、更应该是一个能救场的。问题无论简单或者复杂,代码不论是UI还是框架,你都应该能Hold住,而不是按照自己偏好只搞架构、框架。首先架构师的工作肯定是饱满的,问题过来了你能合理评估优先级,能及时调整去协助。这中间需要你提升平时的工作效率,效率提升才能完成更多的任务。
架构师需要技术创新。当前项目那些遗留的技术债务要抽时间突破;之前急于开发但未确认的技术选型,要严格对比高性能、高可用确认最终的技术方案;产品后续发展方向或者3-5年可能会去考虑的功能,要去提前确认、研究技术可行性形成技术组件,提前设计相应的架构/框架去满足后续的业务复杂度。这些都应该是常态化的工作,定期思考、整理下,还有哪些技术后续需要考虑的。当然这技术创新,不只是功能开发,还可以是非功能比如代码的稳定性、业务操作的CPU性能/内存损耗、软件的后续扩展性等等,比如设计高性能、可扩展、可维护的通信组件也是一个NICE成果。
架构师需要考虑团队效率。如何提高团队开发速度,如何降低维护成本、保障产品稳定性,如何让团队开心的写代码?首先,就需要清真的代码逻辑、清晰合理的模块分层,除了个人开发的能力外,架构师可以多考虑组件化比如抽取日志、配置、通信等通用组件以及部分逻辑不变的业务组件,组件化设计概念可以看看 组件/框架设计原则 - 唐宋元明清2188 - 博客园。组件化做好,代码复用提升了,模块解耦了,不只是软件开发维护效率提高,后续定位问题效率也大幅提升,因为不再需要看一个问题沉入到复杂的代码堆里去一行行确认,你怀疑通信问题那就把通信组件拿出来,建个demo试试也许能很快把问题复现确认。另外,站在团队的角度看看小伙伴们的工作,简单重复、没有技术含量的流程性操作,没准通过框架、小工具就可以减少,比如Nuget源代码替换 .NET 高效Nuget管理工具(开源) - 唐宋元明清2188 - 博客园
架构师需要带团队一起成长。组织技术栈培训技,比如我们团队是Windows技术栈 Windows应用技术栈知识图谱 - 唐宋元明清2188 - 博客园,.NET/WPF有哪些主要知识点、有哪些方法/工具定位问题,争取把这些技术知识变成团队内的常识,提升团队整体素养。带头分享技术实现案例、软件设计原则概念,培养团队内向上的技术氛围,他们在这个团队能学到很多东西、项目产品随着小伙伴们成长也能有更好的技术实现与用户体验。
架构师需要持续提升自己能力。架构师基本上是团队内技术天花板了,所以你不能让自己停下来,持续学习、让自己保持一个“专家”才能更好的服务团队、做好大家的榜样。你持续的学习总结、思维创新让你在技术上持续领先,当然技术上架构师不可能都比小伙伴厉害。极客时间的“让学习养成习惯”相当有道理,工作前面几年学习是让自己更专业,工作很多年的学习是让自己更领域,不管是行业技术还是架构思维你都会有自己的想法、能把知识串起来并落地。
我们再聊下软件架构知识,这里只谈理论哈,像互联网公司的系统中台架构、Redis负载均衡高并发集群,就是具体的落地方案了。我最熟悉的Windows应用,相对大厂中台只是小卡拉米。。。先把自己学习、掌握的软件设计整理清楚就行了。我们都知道架构是解决业务及系统复杂度的解决方案,架构设计是关注软件长期需求变化的理论,如果后续没有需求或者已经实现的功能不再变化,那就需要架构了随便码代码就好。另外如果架构一开始设计复杂了,开发成本会提高、项目周期会拉长,软件发布版本可能就会遥遥远期,项目所以针对变化,如何恰到好处的完成当前软件业务开发、又能满足后续项目需求发展需要,就值得多琢磨琢磨
-
软件解耦
- 封装与抽象
- 分离关注点
- 中间层/模块化
-
编程范式
- 面向对象编程
- 函数式编程
- 结构化编程
-
软件设计原则
- Solid原则
- Kiss原则
- YAGNI原则
- DRY原则
- 设计模式
-
架构设计原则
- 简单
- 合适
- 演进
-
设计衡量指标
- 可维护性 - 可读性、简单设计、稳定性
- 可用性 - 比如高性能、高安全
- 可扩展性 - 为后续修改及扩展的变化,提供最小改动方法
- 可测试性
软件解耦,指的是降低系统内不同类、不同模块之间的依赖,降低依赖关系能减少软件业务的理解成本、维护成本,改代码不再是牵一发动全身或者定位一个BUG不会沉到代码的海洋里。耦合首先需要解决业务混杂的问题,需要将业务拆分为各个独立的小模块,然后再将相关的小模块组合成较大模块、添加有限的对外暴露接口,也就是大家常说的高内聚低耦合。封装组件/模块时,可能会遇到通用代码调用业务代码的问题,这就需要分离关注点,也叫分离“变化”。我们都知道软件后续的修改或者扩展,本质上还是“变化”,所以我们可以“变与不变”拆开,高层使用抽象接口,低层变化的部分通过接口实现业务,后续扩展也可以通过多态实现新的特性。分离关注点,还有“高频与低频”即变化多的与变化少的,也要实现分层,变化多的挪到低层,变化少的放在相对高层。还有平行的逻辑关注点如读写分离,也是一样的道理,读写操作都比较复杂的话建议拆开。根据实际场景考虑下代码后续可能的变化,将变化隔离开来,隔离开之后就有了所谓的中间层和模块,模块化可以是文件夹,也可以是项目、组件,Nuget包就是某一类功能的技术组件。如果需要总结的话,那就是把变化的部分往上浮、不变的部分往下沉,分层合理软件就基本解耦了。
面向对象OOP,封装继承多态大家都有了解,刚工作时就记住了对吧,但我想说的是工作很多年后你再琢磨下可能有更多的理解。封装我理解并不是简单的组合,最重要的是先拆分,只有拆细了才能识别其中的变化,把变化放在封装之外;继承,尽量减少使用父子类的堆叠,而是使用抽象继承,使用抽象类或者接口固定不变的操作,其它的代码放在多个子类中分别去实现,如果是公共代码可以通过组合的方式添加新的类来调用;而多态呢,接口是有意义的,很多开发人员滥用了接口,接口定义一大堆结果没有实际用处,如果后续没有扩展就不要添加接口了。接口我理解只有俩种场景,多个类来实现一个接口这叫多态,一个类实现多个接口这叫接口隔离,其它的场景就不要滥用接口了。
编程范式,指的是结合不同编程思维来编写代码。面向对象OOP、函数式编程、结构化编程,这三种编程方式各有各的优势,我理解的是,先使用面向对象能帮你抽象化业务、梳理类与类之间的关系、搭建系统结构,即类以及类之间的设计让面向对象编程解决。然后再使用函数式编程定义接口API、方法,函数需要减少全局字段的使用,能减少函数与函数之间的干扰和耦合,输入同等参数情况下输出结果是一致的,即满足可测试性。当然函数式编程不只是定义函数,函数可以作为参数传递,也称作函数组合,相比对象组合函数颗粒化更小。最后在函数内使用结构化编程方式输出具体代码,结构化的代码有顺序结构、选择结构、循环结构,可读性相比面向对象好很多,相比跳来跳去的逻辑,结构化代码对开发者是最友好的。我的建议是自上向下、自外向内,依次使用面向对象方式、函数式编程理念、结构化编程工具来完成软件功能。
软件设计原则,是指导我们具体代码如何设计的一类理论,针对复杂的软件业务我理解本质还是如何识别变化、隔离变化、提前设计变化。最常见是Solid原则,单一职责SRP、开放封闭OCP、里氏替换LSP、接口隔离原则ISP、依赖倒置DIP。SRP告诉我们一个类或者模块只能做一类事情、要习惯去拆解业务,拆解的过程就是识别变化,拆解之后我们就能识别哪些是不变的、少变的、后续多变的,所以一个模块里面最好的是不变代码、然后是少变的代码,不变代码的比重是衡量模块内部设计好坏的标准。OCP大家都知道是扩展开放、修改封闭,这里讲的是把刚刚SRP识别出来的不变定义成高层代码,高层代码内部给外部的低层代码即后续变化留下扩展点比如抽象、接口,外部可以通过多态实现新的特性、接口注入后续可能变化的实体等,所以OCP告诉我们要提前考虑好后续变化的部分,后续业务变化尽可能只新增代码、不要去修改原有代码。LSP则是对变化的部分有一定约束,子类能替换父类出现的任何位置而不出现问题,那就需要子类要按照原有父类的规则去添加扩展而不是破坏或者新增其它特性。当然这里的子类父类不单是指继承关系,我们上面说了推荐使用抽象继承,所以里氏替换强调的是变化部分要沿用原来定义好的抽象设计规则。ISP告诉我们接口不是越大越好,如果接口类较复杂、实现类不需要接口类中这么多冗余接口,那就需要拆解接口类,让实现类按需要去实现接口。另外拆解接口也有一个好处,某个实现类可以实现多个接口,即实现类与接口类的关系可以是1对多、也可以多对1,针对变化部分后续扩展会更加灵活。DIP是针对高低层代码循环嵌套场景问题给的解决方案,上面OCP我们讲了高层代码我们想把不变的部分固定下来,但在高层代码内需要触发执行某些低层变化,新手很容易直接调用低层代码导致依赖混淆、流程嵌套,这时可以让高层对变化部分定义抽象接口、低层去实现此接口的具体逻辑、然后将低层实现通过接口注入高层代码来执行,原本的高层依赖低层情况就变成了低层依赖高层定义的接口,低层代码说你不要依赖我、我俩共同依赖抽象接口,即依赖倒置。所以Solid原则依次是针对变化,如何识别变化、扩展变化不要影响不变部分、变化要按规则实现、通过接口拆分隔离变化、通过接口注入扩展变化。
软件设计原则,比较知名的还有KISS原则、YNGNI原则、DRY原则,我们分别简单介绍下。KISS是Keep it simple and stupid缩写,代码越简单越好,设计对当前业务发展够用就行,保持简单能降低开发者学习成本、理解成本,为了体现自己的设计能力把系统设计搞复杂的话,那后续变化的部分实现会很难。YNGNI-You aren't gonna use it 指不会用到的设计就不要添加,应该考虑到当前产品业务以及后续几年内可预见的发展情况,对变化留下相应扩展点就行了,网上说的“如非必要,勿增实体”就是这个意思。DRY-Don't repeat yourself原则针对的是代码重复,很多时候我们经常重复造轮子,或者新增功能时把其它地方的代码复制一份修改一下,不要小看重复带来的问题,共性的代码如果不是统一放在一块维护,你可能改完一个问题其它位置又暴露出来同样问题。通过分离关注点,将共性代码拆分出来、做好封装、针对变化留下扩展点,你后续维护会容易很多,另外共性代码也有机会独立去深挖性能、替换更优的技术实现。
架构设计原则,与上面介绍一堆讲模块分层以及模块内设计略有点不同,针对的是系统/框架的选型以及设计。架构一开始要尽量简单,简单优于复杂,前期简单的设计对团队刚起步、项目刚开展会非常友好,复杂的设计会让团队开发人员开发成本高、BUG多产品不稳、甚至是项目延期。合适也是类似道理,有些资深开发一开始就选用一个超级牛逼的架构,对标竞品的业界领先方案,完全复刻对手产品技术架构、甚至设计了一个比竞品架构更复杂的高性能高扩展架构,这种拍脑袋拍胸脯的设计很大概率会让项目一地鸡毛,因为你的团队没那么多人、没那么牛逼的同事、甚至烧钱金额也有限制,所以适合当前团队、项目才是最好的,你可以规划后续扩展、未来的风险但没必要落地实现。简单、合适说的都是当前设计方案需要满足的,那后续企业壮大了、用户活跃群体暴增了之后,就需要架构的演进了。一开始架构可以只设计一个雏形、满足当前业务,有精力的话可以为3-5年或者可预见的产品发展添加扩展设计,随时间推移你要拥抱变化、复盘当前架构问题以小的幅度调整架构,后面企业壮大也有更多的资金、更牛逼的伙伴来同你设计、甚至大改架构。
软件/架构设计的好坏,上面的理论指导并不能作为衡量标准、只能说是给我们一个设计方向,评估设计标准有可维护性、可用性、可扩展性、可测试性。可维护性包括代码易读、简单设计、代码稳定等,一个架构它的可读性很重要,首先是分层合理、模块耦合少且逻辑清晰,然后架构能支撑复杂的业务而且设计还简单明了的话也是我们需要的,其它的比如接口对外暴露是否最少,存在冗余接口函数或者参数会对使用者造成不必要的困扰,设计对外要简洁、单一。以技术组件为例,组件都是给自己以及其它开发人员使用的,给各个应用调用的,那我们就需要考虑使用成本以及学习成本。另外,软件设计需要稳定,大家都知道重构,有些开发人员经常大改流程和架构,这也是有些不了解软件设计的测试或者主管在听到“重构”俩字的时候经常会表示抗拒,他们只会看你版本的质量和项目开发时间,经常大改肯定不行啊,开发人员功能开发时除了考虑业务需求还要自己多考虑些非功能性需求比如小步的重构、优化之前的设计来满足当前业务以及后续扩展,这些都考虑到了软件都会容易维护。可用性是指要满足业务场景使用,相关的有高性能、高安全,像互联网大厂他们那些服务集群架构满足高性能、高安全,业务才能正常开展,否则时不时来个宕机对公司影响是很大的,所以架构方案要能满足业务场景正常使用。可扩展性讲的是软件有没有为后续业务添加足够的扩展点,当前架构是否能够支持后续业务的扩展,不要需求过来了你再说架构需要重新调整,这是很严重的问题,扩展性是需要提前去考虑的,上面说的很多设计理论都是针对变化的准则,后续有哪些变化、如何更好的实现变化,不需要你现在去实现那些需求,而是留下扩展点。当然架构设计是要掌握一个度,过度设计也很不好,搞复杂、冗余的架构设计你团队小伙伴肯定会吐槽。可测试性是指软件内部小颗粒的模块或者类是否支持测试,如果可测试性好那我们模块集成时就更加容易,定位复杂问题时能快速的验证各独立模块是否正常。提高可测试性,可以考虑减少全局字段的使用、减少代码耦合、保障清晰合理的代码依赖,用Kiss原则让设计保持简单,少使用单例模式、模块能够快速初始化,使用函数式编程让模块有一致性的输入输出结果等等。
以上这些是我个人经验总结,肯定不全面建议大家多阅读架构书籍(这里我列了一些书籍,大家可以找里面架构相关的购买.NET开发一些书箱推荐 - 唐宋元明清2188 - 博客园)、自己多琢磨整理整理,我的知识图谱不一定适合你、尤其是这类需要理解体会的软件设计思想。我自己还有很多需要学习成长的地方,这也是我阶段总结、分享的原因,大家有觉得不合理的及时提出来、互相学习学习。零零散散整理花了1天多时间,写的略微有点乱。。。望大家多包容。