企业网站建设中在方案设计上,门户网站舆情怎么做,做类似美团的网站,wordpress标题字体大小简介#xff1a;软件的复杂性#xff0c;是一个很泛的概念。但是一直都是开发过程中的一个难题#xff0c;本文旨在探讨如何去从容应对复杂性。 作者 | 無涯 来源 | 阿里技术公众号
软件的复杂性#xff0c;是一个很泛的概念。
但是一直都是开发过程中的一个难题#xf…简介软件的复杂性是一个很泛的概念。但是一直都是开发过程中的一个难题本文旨在探讨如何去从容应对复杂性。 作者 | 無涯 来源 | 阿里技术公众号
软件的复杂性是一个很泛的概念。
但是一直都是开发过程中的一个难题本文旨在探讨如何去从容应对复杂性。
一 软件的熵增、构造定律
1 熵增定律
熵的概念最早起源于物理学热力学第二定律又称“熵增定律”表明了在自然过程中一个孤立的系统总是从最初的集中、有序的排列状态趋向于分散、混乱和无序当熵达到最大时系统就会处于一种静寂状态。
软件系统亦是如此 在软件系统的维护过程中。软件的生命力会从最初的集中、有序的排列状态逐步趋向复杂、无序状态直到软件不可维护而被迫下线或重构。 2 构造定律
自然界是如何应对这复杂性
这在物理中被称为构造定律 Constructal Law), 是由Adrian Bejan于1995提出的
For a finite-size system to persist in time (to live), it must evolve in such a way that it provides easier access to the imposed currents that flow through it.
对于一个有限大小的持续活动的系统它必须以这种方式发展演进它提供了一种在自身元素之间更容易访问的流动方式。
这个定理在自然界中比比皆是最典型的比如水循环系统海水蒸发到大气下雨时降落在地面一部分渗入地面流入江河一部分继续蒸发不断循环。这种自发性质的设计反映了这一趋势他们允许实体或事物更容易地流动 - 以最少的能量消耗到达最远的地方就连街道和道路这些人为地构建物体往往也是有排序的模式以提供最大的灵活性。
二 如何应对软件系统的复杂性
软件系统的复杂性往往是被低估的。复杂越高开发人员会感到不安。对其的理解认知负荷代价就越高我们就更不快乐。真正的挑战是在构建我们的系统时要保持其有序以及工程师的生产方式。
Ousterhout教授在《软件设计的哲学》书中提到软件设计的最大目标就是降低复杂度complexity。
就是设计符合业务的构造定律的演进方式一种可以以最小的开发维护成本 使业务更快更好的流动发展的方式。 三 软件复杂性来自哪里 如何解决 1 不确定性的来源
1、业务的不确定性
2、技术的不确定性
3、人员流动的不确定性
2 如何面对不确定性
面对外部的确定性转化为内核的确定性。
面对外部的不确定性找到稳定的内核基础。
专注问题域
当下互联网发展速度是迅猛的 软件的形态也在不断的变化演进。面对未来的业务及变化横向业务与纵向业务的发展都是不确定性的。
Robert C. Martin提到的BDUF永远不要想着在开始就设计好了全部的事情(big design up front)一定要避免过度设计。除非能够十分确认的可预见变化 业务边界否则专注解决当前1-2年内业务变化设计, 讲好当下的用户故事专注解决眼前的问题域。 面向不确定设计增量敏捷开发。
确认稳定的系统内核
随着业务的变化、系统设计也要持续演进升级。没有一开始就完美的架构 好的架构设计一定演化来的不是一开始就设计出来的。
一个健康公司的成长业务横向、纵向会发展的会越来越复杂支持业务的系统也一定会越来越复杂。
系统演进过程中的成本会受到最开始的设计、系统最初的内核影响的。面对外部业务的不确定性 技术的不确定性外部依赖的不确定性。一个稳定的内核应该尽量把外部的不确定性隔离。
业务与技术的隔离
以业务为核心分离业务复杂度和技术复杂度。
内部系统与外部依赖的隔离系统中常变部分与不常变部分的隔离隔离复杂性把复杂性的部分隔离在一个模块尽量不与其他模块互动
3 无序性
系统和代码像多个线团一样散落一地一样混乱不堪毫无头绪。
4 如何面对无序性
1、统一认知秩序化
2、系统清晰明了的结构结构化
3、业务开发流程化标准化
注这里说的流程化并非指必须使用类似BPM的流程编排系统
而是指对于一个需求业务开发有一定的顺序 有规划的先做一部分事情开发哪一个模块再去做剩下的工作是可以流程化的。
5 规模
业务规模的膨胀以及开发团队规模的膨胀都会带来系统的复杂性提升。
6 如何面对规模膨胀带来的复杂性
1、业务隔离 分而治之
2、专注产品核心竞争力的发展
3、场景分层
关键场景
投入更多的开发、测试资源、业务资源(比如单元测试覆盖率在90%以上)在关键场景。
普通场景
更快更低成本、更少资源投入地完成普通场景的迭代
7 认知成本
是指开发人员需要多少知识才能完成一项任务。
在引入新的变化时要考虑到带来的好处是否大于系统认知成本的提升比如之前提到的BPM流程编排引擎如果对系统带来的好处不够多也是增加认知成本的一种。
不合适的设计模式也是增加认知成本的一种前台同学吐槽的中台架构比较高的学习成本 也是认知成本的一种。
8 如何降低认知成本
1、系统与现实业务更自然真实的映射对业务抽象建模 软件工程师实际上只在做一件事情即把现实中的问题搬到计算机上通过信息化提升生产力。
2、代码的含义清晰不模糊
3、代码的整洁度
4、系统的有序性 架构清晰
5、避免过度设计
6、减少复杂、重复概念 降低学习成本
7、谨慎引入会带来系统复杂性的变化
四 应对复杂性的利器
1 领域驱动设计——DDD
DDD是把业务模型翻译成系统架构设计的一种方式, 领域模型是对业务模型的抽象。
不是所有的业务服务都合适做DDD架构DDD合适产品化可持续迭代业务逻辑足够复杂的业务系统小规模的系统与简单业务不适合使用毕竟相比较于MVC架构认知成本和开发成本会大不少。但是DDD里面的一些战略思想我认为还是较为通用的。
对通用语言的提炼和推广
清晰语言认知 比如之前在详情装修系统中
ItemTemplate : 表示当前具体的装修页面
ItemDescTemplate、Template两个都能表示模板概概念
刚开始接触这块的时候比较难理解这一块逻辑之后在负责设计详情编辑器大融合这个项目时第一件事就是团队内先重新统一认知。
装修页面统一使用 —— Page概念模板统一使用 —— Template概念
不将模板和页面的概念糅杂在一起含糊不清避免重复和混乱的概念定义。
贫血模型和充血模型
1贫血模型
贫血模型的基本特征是它第一眼看起来还真像这么回事儿。项目中有许多对象它们的命名都是根据领域模型来的。然而当你真正检视这些对象的行为时会发现它们基本上没有任何行为仅仅是一堆getter/setter方法。
这些贫血对象在设计之初就被定义为只能包含数据不能加入领域逻辑所有的业务逻辑是放在所谓的业务层xxxService, xxxManager对象中)需要使用这些模型来传递数据。
Data
public class Person {/*** 姓名*/private String name;/*** 年龄*/private Integer age;/*** 生日*/private Date birthday;/*** 当前状态*/private Stauts stauts;
}public class PersonServiceImpl implements PersonService {public void sleep(Person person) {person.setStauts(SleepStatus.get());}public void setAgeByBirth(Person person) {Date birthday person.getBirthday();if (currentDate.before(birthday)) {throw new IllegalArgumentException(The birthday is before Now,Its unbelievable);}int yearNow cal.get(Calendar.YEAR);int dayBirth bir.get(Calendar.DAY_OF_MONTH);/*大概计算 忽略月份等年龄是当前年减去出生年*/int age yearNow - yearBirth;person.setAge(age);}
}
}
public class WorkServiceImpl implements WorkService{public void code(Person person) {person.setStauts(CodeStatus.get());}}
这一段代码就是贫血对象的处理过程Person类 通过PersonService、WorkingService去控制Person的行为第一眼看起来像是没什么问题但是真正去思考整个流程。WorkingService PersonService到底是什么样的存在与真实世界逻辑相比 过于抽象。基于贫血模型的传统开发模式将数据与业务逻辑分离违反了 OOP 的封装特性实际上是一种面向过程的编程风格。但是现在几乎所有的 Web 项目都是基于这种贫血模型的开发模式甚至连 Java Spring 框架的官方 demo都是按照这种开发模式来编写的。
面向过程编程风格有种种弊端比如数据和操作分离之后数据本身的操作就不受限制了。任何代码都可以随意修改数据。
2充血模型
充血模型是一种有行为的模型模型中状态的改变只能通过模型上的行为来触发同时所有的约束及业务逻辑都收敛在模型上。
Data
public class Person extends Entity {/*** 姓名*/private String name;/*** 年龄*/private Integer age;/*** 生日*/private Date birthday;/*** 当前状态*/private Stauts stauts;public void code() {this.setStauts(CodeStatus.get());}public void sleep() {this.setStauts(SleepStatus.get());}public void setAgeByBirth() {Date birthday this.getBirthday();Calendar currentDate Calendar.getInstance();if (currentDate.before(birthday)) {throw new IllegalArgumentException(The birthday is before Now,Its unbelievable);}int yearNow currentDate.get(Calendar.YEAR);int yearBirth birthday.getYear();/*粗略计算 忽略月份等年龄是当前年减去出生年*/int age yearNow - yearBirth;this.setAge(age);}}
3贫血模型和充血模型的区别
/*** 贫血模型*/
public class Client {Resourceprivate PersonService personService;Resourceprivate WorkService workService;public void test() {Person person new Person();personService.setAgeByBirth(person);workService.code(person);personService.sleep(person);}
}/*** 充血模型*/
public class Client {public void test() {Person person new Person();person.setAgeByBirth();person.code();person.sleep();}
}
上面两段代码很明显第二段的认知成本更低 这在满是ServiceManage 的系统下更为明显Person的行为交由自己去管理 而不是交给各种Service去管理。
贫血模型是事务脚本模式
贫血模型相对简单模型上只有数据没有行为业务逻辑由xxxService、xxxManger等类来承载相对来说比较直接针对简单的业务贫血模型可以快速的完成交付但后期的维护成本比较高很容易变成我们所说的面条代码。
充血模型是领域模型模式
充血模型的实现相对比较复杂但所有逻辑都由各自的类来负责职责比较清晰方便后期的迭代与维护。
面向对象设计主张将数据和行为绑定在一起也就是充血模型而贫血领域模型则更像是一种面向过程设计很多人认为这些贫血领域对象是真正的对象从而彻底误解了面向对象设计的涵义。
Martin Fowler 曾经和 Eric Evans 聊天谈到它时都觉得这个模型似乎越来越流行了。作为领域模型的推广者他们觉得这不是一件好事极力反对这种做法。
贫血领域模型的根本问题是它引入了领域模型设计的所有成本却没有带来任何好处。最主要的成本是将对象映射到数据库中从而产生了一个O/R对象关系映射层。
只有当你充分使用了面向对象设计来组织复杂的业务逻辑后这一成本才能够被抵消。如果将所有行为都写入到Service对象那最终你会得到一组事务处理脚本从而错过了领域模型带来的好处。而且当业务足够复杂时 你将会得到一堆爆炸的事务处理脚本。
对业务的理解和抽象
限定业务边界对业务进行与现实更自然的理解和抽象数据模型与业务模型隔离把业务映射成为领域模型沉淀在系统中。
结构与防腐层 User Interfaces
负责对外交互, 提供对外远程接口
application
应用程序执行其任务所需的代码。
它协调域层对象以执行实际任务。
该层适用于跨事务、安全检查和高级日志记录。
domain
负责表达业务概念。
对业务的分解抽象建模 。
业务逻辑、程序的核心。
防腐层接口放在这里。
infrastucture
为其他层提供通用的技术能力。如repository的implementationibatishibernate, nosql中间件服务等anti-corruption layer的implementation 防腐层实现放在这里。
防腐层的作用
封装三方服务。
隔离内部系统对外部的依赖。
让隐性概念显性化
文档与注释可能会失去实时性文档、注释没有人持续维护但是线上生产代码是业务逻辑最真实的展现减少代码中模糊的地方让业务逻辑显性化体现出来提升代码清晰度。
if (itemDO ! null MapUtils.isNotEmpty(itemDO.getFeatures()) itemDO.getFeatures().containsKey(ITEM_PC_DESCRIPTION_PUSH)) {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.FEATURE_TSP_PC_TEMPLATEID, templateId);itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_WL_TEMPLATEID, templateId);itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_SELL_PC_PUSH, pcContent.hashCode());itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_SELL_WL_PUSH, content.hashCode());
}
比如这一段代码就把判断里的业务逻辑隐藏了起来这段代码其实的业务逻辑是这样 判断商品是否有PC装修内容。如果有做一些操作 如果没有做一些操作将hasPCContent 这个逻辑表现出来 一眼就能看出来大概的业务逻辑让业务逻辑显现化能让代码更清晰。可以改写成这样
boolean hasPCContent 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.FEATURE_TSP_PC_TEMPLATEID, templateId);itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_WL_TEMPLATEID, templateId);itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_SELL_PC_PUSH, pcContent.hashCode());itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_SELL_WL_PUSH, content.hashCode());
}
2 简单设计原则——《Clean Code》
1、保持系统最大可测试
只要系统可测试并且越丰富的单元测试越会导向保持类短小且目的单一的设计方案遵循单一职责的类测试起来比较简单。
遵循有关编写测试并持续运行测试的简单、明确规则系统就会更贴近OO低偶尔度高内聚度的目标。编写测试越多就越会遵循DIP之类的规则编写最大可测试可改进并走向更好的系统设计。
2、避免重复
重复是拥有良好设计系统的大敌。它代表着额外的工作、额外的风险和额外且不必要的复杂度。除了雷同的代码功能类似的方法也可以进行包装减少重复“小规模复用”可大量降低系统复杂性。要想实现大规模复用必须理解如何实现小规模复用。
共性的抽取也会使代码更好的符合单一职责原则。
3、更清晰的表达开发者的意图
软件项目的主要成本在于长期维护当系统变得越来越复杂开发者就需要越来越多的时间来理解他而且也极有可能误解。
所以作者需要将代码写的更清晰选用好名称、保持函数和类的短小、采用标准命名法、标准的设计模式名编写良好的单元测试。用心是最珍贵的资源。
4、尽可能减少类和方法
如果过度使用以上原则为了保持类的函数短小我们可能会造出太多细小的类和方法。所以这条规则也主张函数和类的数量要少。
如应当为每个类创建接口、字段和行为必须切分到数据类和行为类中。应该抵制这类教条采用更实用的手段。目标是在保持函数和类短小的同时保持系统的短小精悍。不过这是优先级最低的一条。更重要的是测试消除重复和清晰表达。
五 最后
总而言之做业务开发其实一点也不简单面对不确定性的问题域复杂的业务变化
如何更好的理解和抽象业务如何更优雅的应对复杂性一直都是软件开发的一个难题。
在对抗软件熵增寻找对抗软件复杂性符合业务的构造定律的演进方式我们一直都在路上。
原文链接
本文为阿里云原创内容未经允许不得转载。