当前位置: 首页 > 科技观察

谈谈如何从容应对复杂性

时间:2023-03-20 20:17:57 科技观察

软件的复杂性是一个非常宽泛的概念。但一直是开发过程中的难题。本文旨在探讨如何从容应对复杂性。一、软件的熵增与构造规律1、熵增规律从最初的集中、有序排列状态,趋向于分散、混乱、无序;当熵达到最大值时,系统将处于安静状态。软件系统也是如此,在软件系统的维护过程中。软件的生命力会从最初的集中有序排列状态,逐渐走向复杂无序的状态,直至软件不可维护而被迫下线或重构。2.构造法则大自然是如何处理这种复杂性的?这在物理学中被称为构造法则(ConstructalLaw),由AdrianBejan于1995年提出:对于一个有限大小的系统来说,要在时间上持续存在(生存),它必须以这样一种方式演化,使其提供更容易的访问到流过它的强加电流。一种更容易获得的流动方式。这个定理在自然界中比比皆是。最典型的例子就是水循环系统。海水蒸发到大气中,下雨时落到地上。一部分渗入地下流入河流,一部分继续蒸发,如此循环往复。设计的自发性反映了这一趋势:它们让实体或事物更容易流动——以最少的能量消耗到达最远的地方。即使是人工建造的物体,如街道和道路,也常常是有序的图案。,以提供最大的灵活性。二、如何应对软件系统的复杂性?软件系统的复杂性常常被低估。复杂度越高,开发人员就越不舒服。理解它的认知负荷成本越高,我们就越不快乐。真正的挑战是构建我们的系统以使其保持井井有条以及工程师高效工作的方式。Ousterhout教授在书中提到《软件设计的哲学》:软件设计的最大目标是降低复杂性。就是设计一种符合业务结构规律的演化方式,一种能够以最小的开发和维护成本,让业务流转和发展得更快更好的方式。3、软件复杂性从何而来,如何解决?一、不确定性的来源(1)业务的不确定性(2)技术的不确定性(3)人员流动的不确定性2、如何面对不确定性面对外部确定性,性转化为内部确定性。面对外部不确定性,找到稳定的核心基础。关注问题域当前互联网发展迅猛,软件形态也在不断变化和演化。面对未来的业务和变化,横向业务和纵向业务的发展具有不确定性。RobertC.Martin提到的BDUF,永远不要一开始就想着设计一切(bigdesignupfront),一定要避免过度设计。除非可以非常确定可预见的变化和业务边界,否则1-2年内重点解决当前的业务变更设计,讲好当前的用户故事,重点解决眼前的问题域。对于不确定的设计,增量敏捷开发。确认稳定的系统内核会随着业务变化和系统设计不断演进升级。没有从一开始就完美的架构,好的架构设计必须是演进的,而不是一开始就设计好的。随着一个健康的公司的成长,横向和纵向的业务发展会越来越复杂,支撑业务的系统也会越来越复杂。系统演进过程中的成本会受到系统初始设计和初始核心的影响。面对外部业务的不确定性、技术的不确定性、对外依存度的不确定性。稳定的内核应该尝试隔离外部不确定性。业务和技术分离。以业务为核心,将业务复杂性和技术复杂性分离。隔离内部系统和外部依赖;隔离系统的恒定和不变部分;复杂性隔离(将复杂部分隔离在一个模块中,尽量不与其他模块交互)。3.混乱系统和代码像线团一样散落一地,混乱无章。4、如何面对无序(1)统一认知(ordering)(2)清晰清晰的系统架构(structuring)(3)业务开发流程(standardization)注:这里说的流程并不代表一定要用像BPM这样的流程编排系统。而是说对于一个需求,业务的发展是有一定的顺序的。可以精简有计划的做一部分工作,先开发哪个模块再做剩下的工作。5、规模业务规模的扩大,开发团队的扩大,都会增加系统的复杂度。6、如何面对规模扩张带来的复杂性(1)业务隔离,分而治之;(2)注重产品核心竞争力的培养;(3)场景分层。重点场景在重点场景投入更多的开发、测试资源和业务资源(比如单元测试覆盖率90%以上)。普通场景更快、成本更低、占用资源更少,可以完成普通场景的迭代。7.认知成本是指开发人员完成一项任务需要多少知识。在引入新变化时,需要考虑收益是否大于系统认知成本的增加。比如上面提到的BPM流程编排引擎如果不能给系统带来足够的收益,也会增加认知成本。种类。不合适的设计模式也是一种认知成本。前端同学吐槽的中端结构学习成本比较高,也是一种认知成本。8.如何降低认知成本(1)系统与真实业务之间更自然真实的映射,对业务进行抽象建模。软件工程师其实只是在做一件事,就是把现实世界的问题搬到计算机上,通过信息化提高生产力。(2)代码含义清晰,没有歧义。(3)代码的整洁性。(四)制度有序,结构清晰。(5)避免过度设计。(6)减少复杂重复的概念,降低学习成本。(7)谨慎引入会带来系统复杂性的变化。4.应对复杂性的利器1.领域驱动设计——DDDDDD是一种将业务模型转化为系统架构设计的方法,领域模型是对业务模型的抽象。并非所有业务服务都适合DDD架构。DDD适用于产品化、可持续迭代、业务逻辑足够复杂的业务系统。小型系统和简单业务不适合使用。毕竟和MVC架构相比,认知成本和开发成本都会高很多。但是我觉得DDD里面的一些战略思想比较笼统。共同语言的提炼,促进清晰的语言认知,比如之前的详细装饰系统中:ItemTemplate:表示当前具体的装饰页面ItemDescTemplate,Template,这两个都可以代表模板的概念。刚接触这块的时候,很难理解这块的逻辑。之后,当我负责设计详图编辑器的集成项目时,我做的第一件事就是重新统一团队内部的认知。装饰页面统一使用-页面概念模板统一使用-模板概念不把模板和页面的概念混在一起,模棱两可,避免概念定义重复和混淆。贫血模型和充血模型1)贫血模型贫血模型的基本特征是乍一看真的是这个样子。项目中有很多对象,它们的名字都是基于领域模型的。然而,当你真正考察这些对象的行为时,你会发现它们基本上没有任何行为,只是一堆getter/setter方法。这些贫血对象在设计之初就被定义为只包含数据,不能添加到领域逻辑中;所有的业务逻辑都放在所谓的业务层(xxxService、xxxManager对象),需要用到这些模型来传递数据。@DatapublicclassPerson{/***name*/privateStringname;/***age*/privateIntegerage;/***birthday*/private日期birthday;/***当前状态*/privateStatsstauts;}publicclassPersonServiceImplimplementsPersonService{publicvoidsleep(Personperson){person.setStauts(SleepStatus.get());}publicvoidsetAgeByBirth(Personperson){Datebirthday=person.getBirthday();if(currentDate.before(birthday)){thrownewIllegalArgumentException("生日早于现在,太不可思议了");}intyearNow=cal.get(Calendar.YEAR);intdayBirth=bir.get(Calendar.DAY_OF_MONTH);忽略月份等,age为当年减去出生年份*/intage=yearNow-yearBirth;person.setAge(年龄);}}}publicclassWorkServiceImplimplementsWorkService{publicvoidcode(Personperson){person.setStauts(CodeStatus.get());}}这段代码贫血对象处理过程,Person类,通过PersonService和WorkingService来控制Person的行为。乍一看似乎没什么问题,但仔细想想WorkingService的整个流程,PersonService到底是一个怎样的存在?与现实世界的逻辑相比,实在是太抽象了。传统的基于贫血模型的开发模式将数据与业务逻辑分离,违背了OOP的封装特性,实际上是一种面向过程的编程风格。但是,现在几乎所有的Web项目都是基于这种贫血的开发模式,甚至JavaSpring框架的官方demo也是按照这种开发模式编写的。面向过程的编程风格有各种缺点。比如数据和操作分离后,数据本身的操作就不受限制了。任何代码都可以随意修改数据。2)拥塞模型拥塞模型是一种行为模型。模型中的状态变化只能由模型上的行为触发,所有的约束和业务逻辑都汇聚在模型上。@DatapublicclassPersonextendsEntity{/***name*/privateStringname;/***age*/privateIntegerage;/***birthday*/private日期birthday;/***当前状态*/privateStautsstauts;publicvoidcode(){this.setStauts(CodeStatus.get());}publicvoidsleep(){this.setStauts(SleepStatus.get());}publicvoidsetAgeByBirth(){生日日期=this.getBirthday();日历currentDate=Calendar.getInstance();if(currentDate.before(birthday)){thrownewIllegalArgumentException("生日早于现在,太不可思议了");}intyearNow=currentDate.get(Calendar.YEAR);intyearBirth=birthday.getYear();/*粗略计算,忽略月份等,年龄为当前年份减去出生年份*/intage=yearNow-yearBirth;this.setAge(年龄);}}3)贫血模型和充血模型的区别/***贫血模型*/publicclassClient{@ResourceprivatePersonServiceper儿子服务;@Resource私有工作服务工作服务;publicvoidtest(){Personperson=newPerson();personService.setAgeByBirth(person);工作服务代码(人);personService.sleep(人);}}/***拥塞模型*/publicclassClient{publicvoidtest(){Personperson=newPerson();person.setAgeByBirth();人.code();人.睡眠();认知成本较低,这在充满服务和管理的系统中更为明显。人的行为是由他自己管理的,而不是由各种服务管理的。贫血模型在事务脚本模式下比较简单。模型上只有数据没有行为,业务逻辑由xxxService、xxxManger等类承载,比较直白。对于简单的业务,贫血模型可以快速交付,但是后期维护成本比较高,很容易成为我们说的意大利面条代码。充血模型是领域模型模式。充血模型的实现比较复杂,但是所有的逻辑都由自己的类负责,职责比较明确,方便后期迭代和维护。面向对象设计提倡将数据和行为绑定在一起,也就是充血模型,而贫血领域模型更像是面向过程的设计。很多人认为这些贫血领域对象是真实的对象,从而完全误解了面向对象设计。意义。当MartinFowler和EricEvans聊起这件事时,这个模型似乎越来越受欢迎。作为领域模型的推动者,他们觉得这不是什么好事,强烈反对这种做法。贫血领域模型的根本问题是它引入了领域模型设计的所有成本,却没有任何好处。最重要的成本是将对象映射到数据库,从而产生O/R(对象关系)映射层。只有充分利用面向对象的设计来组织复杂的业务逻辑,才能抵消这种成本。如果您将所有行为写入服务对象,您最终会得到一组事务脚本,并且会错过域模型的好处。当业务足够复杂时,您最终会遇到交易脚本的爆炸式增长。对业务的理解和抽象定义了业务边界,业务更自然地从现实中被理解和抽象出来。数据模型与业务模型隔离,将业务映射成领域模型存放在系统中。结构和防腐层用户界面负责对外交互,提供对外远程接口。应用程序应用程序执行其任务所需的代码。它协调域层对象以执行实际任务。该层适用于交叉事务、安全检查和高级日志记录。域负责表达业务概念。业务的分解、抽象和建模。业务逻辑,程序的核心。防腐层界面就放在这里。基础设施为其他层提供通用的技术能力。比如repository(ibatis、hibernate、nosql)的实现,中间件服务等反腐层的实现都放在这里。防腐层的作用:封装第三方服务。将内部系统与外部依赖隔离开来。对隐含概念的显式文档和注释可能会失去实时性(文档和注释没有持续维护),但线上生产代码是业务逻辑最真实的展示,减少代码中的歧义,让业务逻辑显式体现,改进代码明晰。如果(itemDO!=null&&MapUtils.isNotEmpty(itemDO.getFeatures())&&itemDO.getFeatures().containsKey(ITEM_PC_DESCRIPTION_PUSH)){itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_PC_TEMPLATEID,""+templateIdBO);getFeatures().put(ItemTemplateConstant.FEATURE_TSP_SELL_PC_PUSH,""+pcContent.hashCode());}else{itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_PC_TEMPLATEID,""+templateId);.FEATURE_TSP_WL_TEMPLATEID,""+templateId);itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_SELL_PC_PUSH,""+pcContent.hashCode());itemUpdateBO.getFeatures().put("ItemTemplateConstant".));}比如这段代码就把业务逻辑隐藏在了判断中。这段代码的实际业务逻辑是判断商品是否有PC装饰内容。做一些操作,不做一些操作,展示hasPCContent的逻辑,一眼就能看出大致的业务逻辑,让业务逻辑可见,让代码更清晰。它可以重写如下:booleanhasPCContent=itemDO!=null&&MapUtils.isNotEmpty(itemDO.getFeatures())&&itemDO.getFeatures().containsKey(ITEM_PC_DESCRIPTION_PUSH);if(hasPCContent){itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_PC_TEMPLATEID,""+templateId);itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_SELL_PC_PUSH,""+pcContent.hashCode());}else{itemUpdateBO.getFeatures().put(ItemTemplateConstant.PCID+SPATE_TEL);itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_WL_TEMPLATEID,""+templateId);itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_SELL_PC_PUSH,""+pcContent.hashCode());.FEATURE_TSP_SELL_WL_PUSH,""+content.hashCode());}2.简单的设计原则——《Clean Code》(1)尽可能保持系统的可测试性只要系统是可测试的,单元测试越丰富,越会导致保持类的简短和目的单一的设计,遵循一个单一职责类,更容易测试。通过遵循关于编写测试和一致地运行它们的简单、明确的规则,系统更接近于低偶然性和高内聚性的OO目标。您编写的测试越多,您就越遵循像DIP这样的规则,编写最可测试的可以改进并导致更好的系统设计。(2)避免重复重复是拥有好的设计系统的大敌。它代表着额外的工作、额外的风险以及额外的和不必要的复杂性。除了相同的代码,还可以将功能相似的方法进行封装,减少重复,“小规模复用”可以大大降低系统复杂度。要实现大规模复用,就必须了解如何实现小规模复用。共性的抽取也会让代码更好的遵守单一职责原则。(3)明确表达开发商的意图。软件项目的主要成本是长期维护。随着系统越来越复杂,开发者需要越来越多的时间来理解它,产生误解的可能性很大。所以作者需要把代码写得更清楚:选择好的名字,保持函数和类的简短,使用标准的命名法,标准的设计模式名称,写出好的单元测试。注意力是最宝贵的资源。清晰:选择好名字,保持函数和类简短,使用标准命名法、标准设计模式名称,编写好的单元测试。注意力是最宝贵的资源。(4)尽可能减少类和方法如果过度使用以上原则,为了保持类的功能短小,我们可能会创建过多的小类和方法。所以这条规则也提倡函数和类的数量要少。如果要为每个类创建一个接口,则必须将字段和行为拆分为数据类和行为类。应该抵制这种教条,支持更实际的手段。目标是保持系统小而精,同时保持函数和类小。但这是优先级最低的一个。更重要的是测试,消除重复,表达清楚。5.最后,总而言之,做业务开发其实一点都不简单。面对不确定的问题域和复杂的业务变化,如何更好地理解和抽象业务,如何更优雅地处理复杂性,一直是软件开发的课题。一个难题。我们一直在与软件熵增作斗争的路上,寻找一种与软件复杂性作斗争、符合业务构建规律的进化方式。参考文献[1]《Domain-Driven Design》:https://book.douban.com/subject/1629512/[2]《Implementing Domain-Driven Design》:https://book.douban.com/subject/25844633/[3]《Clean Code》:https://book.douban.com/subject/4199741/[4]《A Philosophy of Software Design》:https://book.douban.com/subject/30218046/