如果说学习数据结构和常用算法可以帮助我们写出更高效的代码,那么学习软件设计知识可以帮助我们写出更高质量的代码。本文是我的《软件设计之美》课程学习总结的第一部分。
1什么是软件设计?
软件设计,这是一门着眼于长期变化的学科,并不是面向开发人员的入门课程。作为一个初级程序员,第一选择是实现一个特定的功能,而看不到一个软件的长期变化。
它旨在使软件更容易适应长期变化。
—肯特·贝克
以排序算法为例,快速排序的平均复杂度为O(nlogn),而插入排序的平均复杂度为O (n 2)。所以一般来说,我们说快速排序优于插入排序。但这种优势只有在数据规模达到一定程度时才能体现出来,否则两者的区别并不明显。所以比较这两种排序算法优劣的关键在于数据的规模。
因此,我们可以发现算法类似于软件设计,都面临规模问题。算法面对的是数据规模,软件设计面对的是需求规模。
换句话说,只有在长期需求积累的前提下,规模的问题才会凸显,(好的)软件设计才是解决需求规模问题的方法。
那么,到底什么是软件设计?
是具体的技术实现吗?是框架和中间件吗?是设计模式吗?……。
如上所述,软件设计要关注长期变化,需要应对的是需求规模的扩大。但是上面说的误区都是不断变化的东西,并没有直击问题的本质。
软件设计其实就是在软件开发的过程中建立一个统一的结构,让参与这个过程的每个人都能有一个共同的认识,类似于建筑图纸。
而这个统一的结构,我们可以理解为一个模型,它是一个软件的骨架,是一个软件的核心。
模型的粒度可以小到一个类,也可以大到一个系统。
一般好的模型都是“,高内聚,低耦合”。
模型可以分层,底层模型提供构建上层模型的接口。所以我们需要做的是了解模型,建立模型,判断其优劣等等。
然而,对于软件设计中要解决的问题,仅仅有一个好的模型是不够的。软件设计还需要另外一部分,就是规范。所谓规范,就是定义什么样的需求应该用什么方式来完成。
规格说明的主要作用是维护软件的长期演进。就像我们在实际的项目开发过程中,团队成员的基础不一样。如果其中一个老成员不注意,新成员可能会创造一种新的写作方式,我们的项目就会失控。所以我们可以看到无数的规范,比如代码规范,开发规范,数据库规范,DevOps规范等等。但是,如果发现规范不符合软件设计的原则,就要修改规范,使之可以匹配,以免拖累演化。就像每个项目因为业务不同,需要不同的架构一样,也因为业务处于不同的阶段,需要的架构也不同。同样,规范也取决于模型的开发。如果模型发展了,规范阻碍了模型的发展,也需要调整。
模型和规范是软件设计的主要内容,两者相辅相成。
换句话说,软件设计=模型+规格。
2软件设计的第一步:分离关注点
对于稍微大一点的软件设计,我们最常用的方法是把大问题一个个分解成小问题,然后组合起来。如何分解和组合是软件设计中需要考虑的重要问题。就像现在流行的微服务架构风格,是分解组合的典型案例。
软件设计的第一步是考虑分解的粒度。粒度不合适会为软件未来的进化埋下很多坑。因此,我们经常听到软件设计需要首先“分离关注点”。
图像“软件设计之美”
更加关注非功能性需求
对于一个系统,其软件设计不仅要考虑其功能需求,还要考虑其非功能需求。叶正老师强调我们需要主动去发现和挖掘这些非功能性需求。这也让我想起了过去的一些架构设计,往往是由很多数据一致性要求、响应速度要求、可用性要求等等非功能性要求组成的。
图像“软件设计之美”
关于分离关注点的常见问题
(1)将业务处理与技术实现混淆
在分离关注点的常见问题中,最典型的就是混淆了业务处理和技术实现这两个关注点。
比如把业务代码和多线程放在一起很常见,但是无限制的修改代码,很容易导致资源竞争、数据同步等多线程相关的问题。再比如,我们可能涉及到一个需要分布式事务和子数据库、子表的场景,但是我们应该认为我们的业务真的需要分布式事务吗?是不是业务划分不清晰,造成了DB的压力?很多老板说分布式事务最好的解决方案是,上不了分布式事务,就上不了分布式事务。
程序员往往喜欢认为所有问题都是技术问题,试图用技术问题解决所有问题。然而,叶正老师强调说任何试图用技术来解决他所关心的问题的努力只能越陷越深。
(2)数据变化方向不同造成的混乱
在分离关注点的常见问题中,另一个典型问题是不同的数据变更方向。
比如一个. NET应用,数据库访问用EF好,Dapper还是SqlSugar,还是直接原生ADO.NET?对于普通的增删改,用ORM,比如EF,会很快很简单。但是对于一些复杂的场景,我们会担心自动生成的SQL的性能问题,仍然觉得手写的SQL优化直接实用,这让我们感到相当纠结。然而,工具选择的困难往往是由于我们混合了两种使用频率不同的场景,例如前端访问(CRUD)和后端访问(统计报告)。如果分开了,就没有纠结了吗?
再比如,我们经常听到的动静分离,其实就是把改变的内容和不改变的内容分开;数据库读写分离是指读写操作从数据库层面分离;CQRS将命令和查询操作从代码层分离出来。……
因此,不同的数据变化方向是可以分开的关注点。
分离关注点的目的
关注点分离,一方面可以避免后续演化过程中可能出现的很多相关问题,另一方面可以帮助我们找到不同模块之间的共性,更好地进行设计。
因此,我们在软件设计中的第一步是找到并分离关注点。我们发现的关注点越多越好,粒度越小越好。
3忽略了一个重要因素:可测试性
关注点分离是软件设计的第一步,这一点经常被我们忽略。此外,还有一个经常被忽略的因素:可测试性。我们可以看到,可测试性其实是非功能需求中的一个需求点,但是这个点往往被我们忽略了,所以经常被忽略。
图像“软件设计之美”
叶正老师说,我们在开发过程中欠下的很多技术债,本质上都是因为忽略了“可测试性”的要求。因为,我们把软件拆分后,都是一个个小模块。如果不仅能保证每个小模块的正确性,那么从最外层整体系统的角度来验证正确性就很困难。众所周知,在软件开发过程中,Bug修复的成本是逐渐增加的。如果能在前面发现尽可能多的bug,成本会比网上排查修复低很多。因此,尽早暴露问题而不是总是等待集成测试早已成为我们的共识,可测试性是将帮助我们尽早暴露问题的解决方案之一。
那么,如何考虑可测性呢?总之我们在设计的时候问自己,这个方法/模块/系统怎么测试?如果每个小模块都经过足够多的测试,那么就会有足够多的稳定模块,然后才会有高效的集成测试。
比如我们开发的时候。NET应用,我们通常使用依赖注入和接口设计来隔离外部依赖,然后使用一些Mock框架(如Moq、NSub等。)来模拟这些外部依赖,然后根据这些模拟对象编写单元测试。或者对于数据访问层的单元测试,我们经常使用Mock框架在内存中模拟DB。我们要做的就是确保模拟的内存模拟实现与接口定义的行为一致。
下面是一段常见的。不可测试的. NET代码:
(1)服务层调用数据库访问层的代码,不考虑可测试性。
public class ProductService{????private?DBProductRepository?repository?=?new?DBProductRepository(); public Product GetProduct(long id) { return repository.GetProduct(id); }}
(2)服务层调用数据库访问层的代码,考虑可测试性。
public class ProductService{ private readonly IProductRepository _repository; public ProductService(IProductRepository repository) { _repository = repository; } public Product GetProduct(long id) { return _repository.GetProduct(id); }}
在实际项目中,我们经常会遇到一些遗留系统的维护。这些系统的设计往往没有考虑可测试性,所以很难做单元测试。这时候我们可以采用一些强大的Mock框架,比如JustMock(收取许可费的),可以帮助我们模拟静态函数之类的对象,。NET基本函数库、日期对象等开源的mock框架,比如Moq,这些都是无法模拟的,从而有助于提高模块的可测试性。当然,使用强大的Mock框架只是一个小技巧,还是要在设计中考虑可测试性。
关于编写单元测试。NET应用程序,我也写过一个小编文章。欢迎阅读。门户:点击这里。
图片来源:JustMock官网
4摘要
在这篇文章中,我们学习了什么是软件设计。一句话,软件设计=好的模型+合适的规范。软件设计的第一步是分离关注点。分离的关注点越多越好,粒度越细。设计软件的时候,不要忘记可测性。只有小模块测试够了,才会有稳定的积木,便于后续的集成测试。
本文来自水洗晴空投稿,不代表舒华文档立场,如若转载,请注明出处:https://www.chinashuhua.cn/24/553441.html