一次正交设计和组合设计之旅
引言
Design is there to enable you to keep changing the software easily in the long term. – Kent Beck
Everything should be made as simple as possible, but not simpler. – Albert Einstein
一、回顾设计的定义及价值
- 定义:如引言中Kent Beck言,软件设计是为了长期更加容易地适应未来的变化。
- 价值:正确的软件设计方法是为了长期地、更好更快、更容易地实现软件价值的交付。
二、软件设计的目标
软件设计就是为了完成如下目标,其可验证性、重要程度依次降低。由Kent Beck大师提出简单设计四原则一一对应软件设计的目标,我们可以根据这些原则来指导开发,并及时验证开发的结果。
对软件设计目标的注解
1. 实现功能
软件作为解决客户某类问题的载体,其首要目标是满足客户需要,即实现功能的目标是核心,也是软件设计的首要标准。如何判定系统功能的完备性呢?所谓完备性可以理解满足客户需要,那么怎样衡量软件是否满足客户需要呢?就是能通过客户的所有测试用例。
测试用例的作用:
- 从
TDD
的角度看,测试用例就是对需求的阐述,是一个闭环的反馈系统,保证其系统的正确性; - 保证设计的合理性,恰如其分,不多不少;
- 理解系统行为最重要的依据。
【号外 TDD原则和基本流程】
三原则:
- 除非能让失败的单元测试通过,否则不允许去编写任何的产品代码。对于任何功能需求,都是先从写测试用例入手,为满足测试用例才能去写产品代码。
- 只允许编写刚好能够导致失败的单元测试。(编译失败也属于一种失败)
- 只允许编写刚好能够使一个失败的单元测试通过的产品代码。
基本流程:
编写单元测试 --> 运行单元测试-失败
编写生产代码 --> 运行单元测试
重构代码 -->运行单元测试保证通过
从流程还可以看出,测试驱动开发将持续的重构纳入流程之重,并通过测试保证了重构的正确性。因为单元测试运行失败为红色,通过为绿色,因此TDD流程也可以简单描述为:红、绿、重构。
2. 易于理解
好的设计不只是指导计算机执行指令,更重要的是能让其他人也能容易地理解,包括系统的行为,业务的规则。那么,什么样的设计才算得上易于理解的呢?
- Clean Code # 这个包含很多,如格式、封装等等,可以参阅鲍勃大叔的《代码整洁之道》
- Implement Patterns # 采用通用的实现模式,特别是一个组织或团队开发。如DDD、架构模式、DCI、设计模式等等,是开发人员形成的共识
- Idioms # 遵循一些设计原则,如简单设计原则、正交设计原则、面向对象设计原则、高内聚低耦合、小类大对象等等原则
3. 易于重用
易于重用的软件结构,使得其应对变化更具弹性;可被容易地修改,具有更加适应变化的能力。
最理想的情况下,所有的软件修改都具有局部性。但现实并非如此,软件设计往往需要花费很大的精力用于依赖的管理,让组件之间的关系变得清晰、一致、漂亮。
4. 没有冗余
没有冗余的系统是最简单的系统,恰如其分的系统,不做任何过度设计的系统。
- Dead Code # 从未被执行的代码
- YAGNI: You Ain’t Gonna Need It # **不要做过度设计:**你没必要那么着急,不要给你定义的类实现太过于早的功能,只需要先实现好现在需要的功能点。
- KISS: Keep it Simple, Stupid # 尽量保持简单
【号外 KISS & YAGNI】
如何写出满足 KISS 原则的代码?
- 不要使用同事可能不懂的技术来实现代码。
- 不要重复造轮子,要善于使用已经有的工具类库。经验证明,自己去实现这些类库,出 bug 的概率会更高,维护的成本也比较高。不要过度优化。
- 不要过度使用一些奇技淫巧(比如,位运算代替算术运算、复杂的条件语句代替 if-else、使用一些过于底层的函数等)来优化代码,牺牲代码的可读性。
YAGNI和KISS 是一回事吗?
- YAGNI的核心是不要过度设计
- KISS 原则讲的是“如何做”的问题(尽量保持简单),而 YAGNI 原则说的是“要不要做”的问题(当前不需要的就不要做)。
软件设计的最高准则
从软件实践的历史发展中,人们总结出提高可重用性的最高原则是**「高内聚、低耦合」**,是软件设计这个发展中的树的根。
为了实现高内聚,低耦合的软件设计,袁英杰提出了**「正交设计」**的方法论。
三、正交设计
何为软件设计上的正交?
「正交」是一个数学概念:所谓正交,就是指两个向量的内积为零。简单的说,就是这两个向量是垂直的。在一个正交系统里,沿着一个方向的变化,其另外一个方向不会发生变化。为此,Bob
大叔将「职责」定义为「变化的原因」。
「正交性」,意味着更高的内聚,更低的耦合。为此,正交性可以用于衡量系统的可重用性。那么,如何保证设计的正交性呢?袁英杰提出了「正交设计的四个基本原则」,简明扼要,道破了软件设计的精髓所在。
正交设计四原则
五、C++正交设计实战
需求1: 存在一个学生的列表,查找一个年龄等于
18
岁的学生
快速实现
struct Student
{// ...string getName() const { return name; }int getAge() const { return age; }bool isMale() const { return sex == Sex::MALE; }bool isFemale() const { return sex = Sex::FEAMLE; }
private:string name;int age;Sex sex;// ...
};
Student findByAge(const std::vector<Student>& students)
{static Student null;for (int i = 0; i < students.size(); ++i){if (students[i].getAge() == 18)return studens[i];}return null;
}
上述设计中存在的bad smell(坏味道):
- 缺乏弹性参数类型:只支持vector数组类型,list, set等列表类型都被拒之门外;
- 容易出错:操作数组下标,往往引入不经意的错误;
- 魔数:硬编码,将算法与配置高度耦合;
- 返回null对象:再次给用户打开了犯错的大门;
优化一 使用模板和range-for
按最小依赖原则去除对vector和下标等具体实现细节的依赖,使用模板抽象出列表数据类型,这样find算法可以支持多种技术数据类型;使用range-for降低使用下标时发生错误的可能性。
template<typename ListType>
Student findByAge(const ListType& students)
{static Student null;for (auto& student : students){if (students.getAge() == 18)return student;}return null;
}
需求2: 查找一个名字为
xunye
的学生
重复设计
拷贝和粘贴(copy-paste)是最快的实现方法,但会产生「重复设计」。
template<typename ListType>
Student findByName(const ListType& students)
{static Student null;for (auto& student : students){if (students.getName() == "xunye")return student;}return null;
}
分析需求一和需求二,及当前的设计实现,发现存在两个变化方向,一是查找的算法(range-for),二是比较准则(age和name)。因此,为消除重复需要想办法分离出这两个变化方向,使二者可以独立的变化,互不影响。
那么,如何做到分离呢?
抽象
可以采用高层抽象的思想,在现实和计算机中抽象都是忽略细节达成共识的思维手段,在C++实现抽象手段的技术主要有接口类和模板。
需求一和需求二的实现中,不变的是查找算法,变化的是比较准则(是比较age和name在变)。为消除重复(查找算法),需要抽象变化的比较准则,使它们达成抽象的统一。
这里使用抽象接口类实现,当然也可以采用模板实现。
struct StudentMatcher
{virtual ~StudentPred() {}virtual bool operator()(const Student& student) const = 0;
};
将各变化的原因(age和name)对象化,实现两个匹配器。
struct AgeMatcher : private StudentMatcher
{explicit AgeMatcher(int age) : age_(age) {}bool operator()(const Student& student) const override{return student.getAge() == age_;}private:int age_;
};
struct NameMatcher : private StudentMatcher
{explicit NameMatcher(const string& name) : name_(name) {}bool operator()(const Student& student) const override{return student.getName() == name_;}private:string name_;
};
重构查找算法
- 重命名查找算法,使其保持在同一个抽象层次上
- 通过增加Matcher入参引入查找算法和抽象匹配规则的联系
template<typename ListType>
Student find(const ListType& students, StudentMatcher&& matcher)
{static Student null;for (auto& student : students){if (matcher(student))return student;}return null;
}
通过这次重构,同时也消除了幻数bad smell。
客户使用代码实现sample:
客户端的调用根据场景,提供算法的配置(组合)
assert(find(students, AgeMatcher(18)));
assert(find(students, NameMatcher("xunye")));
需求3: 存在一个老师列表,查找第一个女老师
类型重复
按照既有的代码结构,可以通过Copy-Paste
快速地实现这个功能。
struct TeacherMatcher
{virtual ~StudentPred() {}virtual bool operator()(const Teacher& student) const = 0;
};
struct FirstFemaleMatcher : private TeacherMatcher
{bool operator()(const Teacher& teacher) const{return teacher.isFeamle();}
};
template<typename ListType>
Teacher find(const ListType& teachers, TeacherMatcher&& matcher)
{static Teacher null;for (auto& teacher : teachers){if (matcher(teacher))return teacher;}return null;
}
客户使用代码实现sample:
assert(find(students, FirstFemaleMatcher()));
当然也可以通过lambd表达式实现
assert(find(students, [](const Teacher& teacher) -> bool { return teacher.isFemale(); }));
类型参数化
分析StudentMatcher/TeacherMatcher
, find(students)/find(teachers)
的重复,为此引入类型参数化的设计。
首先消除StudentMatcher
和TeacherMatcher
的重复设计。
template<typename T>
struct Matcher
{virtual ~StudentPred() {}virtual bool operator()(const T& t) const = 0;
};
再对find
进行类型参数化设计。
template<typename ListType, typename People>
People find(const ListType& peoples, Matcher<People>&& matcher)
{static People null;for (auto& people : peoples){if (matcher(people))return people;}return null;
}
类型概念化
通过分析find可以发现Matcher<People>
可以被进一步的抽象化,将Matcher<People>
变成一种Matcher的概念,可以匹配任意ListType
中的元素,前提只要Matcher支持bool operator()(ListType::ElementType)
这种操作规律。
再次对find进行重构:
- 删除
Matcher<T>
类,将Matcher<People>
抽象提升为Matcher模板参数,对Matcher的约束由find内部机制决定,即Matcher概念。 - 因
ListType
的元素类型未知,我们不能再用null对象作为异常返回值。同时也消除了这种bad smell。 - 消灭了继承关系
template<typename ListType, typename Matcher>
decltype(auto) find(const ListType& elements, Matcher&& matcher)
{for (auto it = elements.begin(); it != elements.end(); ++it){if (matcher(*it))return it;}return elements.end();
}
需求4: 查找年龄不等于18岁的女生
客户使用代码实现sample:
assert(find(students, [](const Student& stu){ return stu.getAge != 18 and stu.isFemale(); } != students.end());
客户无需改变find内部实现逻辑,只需添加新的Matcher,就可以快速响应新需求的变化。
六、回顾
通过4
个需求的迭代和演进,通过运用正交设计和组合式设计的基本思想,加深对正交设计四原则的理解。
其实,上述设计之旅所得结果,由C++标准库的find函数具有一致性,完全可以用C++标准库的find函数代替。上述活动是一次设计活动及重构的思维记录,我们不仅追求的是结果,同时追求结果的过程也许更有启发意义。
【后言】本文的整体思路和框架来源于实战正交设计 - 简书 (jianshu.com),是模仿和学习探索的结果,感谢大牛的启发。
一次正交设计和组合设计之旅相关推荐
- 正交设计 python算法_Python实现正交实验法-高级篇
前情介绍: 测试过程中,对于多参数参数多值的情况进行测试用例组织,之前一直使用[正交分析法]进行用例组织,就是把每个参数的所有值分别和其他参数的值做一个全量组合,用Python脚本实现,就是itert ...
- hadoop学习之旅1
大数据介绍 大数据本质也是数据,但是又有了新的特征,包括数据来源广.数据格式多样化(结构化数据.非结构化数据.Excel文件.文本文件等).数据量大(最少也是TB级别的.甚至可能是PB级别).数据增长 ...
- 基于设计模式的学习之旅-----访问者模式(附源码)
基于设计模式的学习之旅-----访问者模式 1.初始访问者模式 2.什么是访问者模式 表示一个作用于某对象结构中的各元素的操作.它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作. 3.模 ...
- 使用CEfSharp之旅(7)CEFSharp 拦截 http 请求 websocket 内容
使用CEfSharp之旅(7)CEFSharp 拦截 http 请求 websocket 内容 原文:使用CEfSharp之旅(7)CEFSharp 拦截 http 请求 websocket 内容 版 ...
- VC开发Windows客户端软件之旅——前言
从第一次拖着行李入京找活,至今已工作若干年了.这些年一直追逐自己的梦想,跑过三个城市,换了三份工作,认识了很多业内的朋友.和朋友们闲聊时,发现很多人都已经不再做客户端软件了.有的转去做管理,有的转去做 ...
- unity 200.8m yoy_专场分享会|大会最新Unity、中创文旅专场预告来啦!
北京国际游戏创新大会将于9月25日-27日分别在中华世纪坛发布厅.中华世纪坛剧场.京都信苑国际厅.京都信苑欧式厅.京都信苑圣马可厅.京都信苑第三会议室,6大场馆举办129场游戏行业主题分享,之前小编已 ...
- 自定义html托管,10分钟搞定“傻瓜式”的静态网站搭建托管之旅
原文发布于微信公众号:腾讯云存储(关注有惊喜) 静态网站:有别于动态网站,它就是只包含静态内容(如图片.音频.视频.HTML.CSS.JS)的网站,不依赖服务器端动态渲染页面. 那么静态网站托管应该如 ...
- C#发现之旅第一讲 C#-XML开发
C#发现之旅第一讲 C#-XML开发 袁永福 2008-5-15 系列课程说明 为了让大家更深入的了解和使用C#,我们将开始这一系列的主题为"C#发现之旅"的技术讲座.考虑 ...
- puppeteer爬虫的奇妙之旅
@(爬虫)[puppeteer|] 爬虫又称网络机器人.每天或许你都会使用搜索引擎,爬虫便是搜索引擎重要的组成部分,爬取内容做索引.现如今大数据,数据分析很火,那数据哪里来呢,可以通过网络爬虫爬取啊. ...
最新文章
- opencv图像旋转
- Java 8按属性区分
- batch 批处理获取系统时间
- 云炬创业政策学习笔记20210116
- 关于客户端用ASP参生报表
- java wav 切割_WAV音频定长分段切取
- dropbox_来自提示框:望远镜激光瞄准器,Dropbox桌面和Kindle剪辑转换
- P4630-[APIO2018]Duathlon铁人两项【圆方树】
- C++primer第九章 顺序容器 9.5 额外的string操作
- 湖南省普通招生2021高考成绩查询,湖南省2021八省联考成绩可查,附查询入口及往年分数线...
- 【知识】OpenStack计算设施----Nova
- WLAN定位技术——(无线信号定位2)
- 9. GD32F103C8T6 定时器2的更新中断触发定时器0开始计时
- Python 学习笔记:class
- 李宏毅机器学习——课后作业1
- 可道云、NextCloud和Seafile使用体验比较哪个好用
- 镇海计算机信息编程,镇海区加工中心编程培训
- 质心公式_No.217 质心位置的求法(基础篇)
- 让 Linux 更安全
- Same Parity Summands