5.1 软件设计
- 设计有好有坏
糟糕的设计
- 僵化设计 —— 无法响应新的需求
- 写死文件路径
- UI控件写死提供数据源的字段
- 数据库表增加一个新的字段,N处地方需要修改
- 错弱设计 —— 不敢对软件进行改动
- 无法重用
- 迁移到其他系统
- 过于复杂的设计
- python能干,用c++写
- 想得过多,过度设计
- 用这个模式吧,刚刚学过
指导原则(整体设计4个原则)
- 重用前人的设计经验(DRY)
- case:写了一个日志函数,复制粘贴了N份,一处发现忘记释放对象,N处修改,还有可能因为疏忽造成内存泄漏。
- 当你发现在程序中有多个地方都需要实现同一个功能时,请将此功能代码抽取为独立的可重用的组件,不要到处copy & paste。
- 自己能注意到,也能遵守
- 绝不要画蛇添足(YAGNI)
- case:用户可以利用鼠标滚轮选中列表项
- 用最简单的方法,仅仅实现你所需要的功能;确实需要,再抽象、封装;不要自作主张添加新功能
- 自己能注意到,但是什么是最少?少到能支持演化和重构
- 简单即美(KISS)
- case:业务逻辑简单小规模程序,利用Transcation Script Pattern,一个功能一个函数,全部放到一个类中。
- case:针对数据库,Active Record Pattern,类+字段与表一一对应,只提供曾删改查4中方法。
- short and simple
- 自己能注意到,但是什么是最简?需要了解一些模式!
- 不要一切自己动手(DDIY)
- 存储数据库、开发web网站、克隆对象、自动创建对象、单元测试、日志、交互性网页都有成熟的框架
- 多数场景尽可能的复用,特别需要,再造轮子
- 自己能注意到,单元测试在用,最好log也用;时不时地搜索一下各个语言的框架也是不错的。
5.2 类设计原则
合理封装(最重要)—— 找出变化点,封装它(黄金准则)
- 指导思路
- 当你需要设计一个类的时候,从一开始就用封装的思想来设计它
- 设计类时,不仅隐藏数据,同时也隐藏底层数据存储方式、算法等具体实现细节
- => 一个封装良好的类,不用看文档就会用,也不易被误用
- case:java中integer100/1000的不同
- 具体操作:从类中抽象出接口或抽象基类,推动系统架构进行演化
- case 1
- 一个类里面有保存到本地/网盘这两个方法
- 问题:保存到网盘的方法的参数太多
- 解决:抽象出一个保存接口,文件保存一个类,网盘保存一个类,把多余的参数抽取成一个辅助类
- case 2,把用户界面与底层数据库分开,没看懂
基于职责
- 指导思路
- 每个类的职责必须明确;每个类只承担一项职责
- 避免多大的类
- 具体操作(GRASP)
- Expert/Informatioin Expert: 信息保存在哪,职责就在哪儿
- Creator:谁负责创建类?都行,对应不用的设计模式 -> Factory Method, Builder, Prototype, Singleton…
- Controller:谁负责显示数据,谁负责相应事件,谁负责处理数据?经典设计模式:MVC
- High Cohesion/Low Coupling:高内聚、低耦合 -> 组合优先于继承
- Indirection:间接性,也是实现高内聚,低耦合的方法;多对多的关联 -> 多对一
- Pure Fabrication:硬把一些跟业务无关的方法放在一起,常命名为XXXService,它是聚合的、提供功能访问点的类,比如说可以把一些系统操作,保存系统参数、退出、备份放在一个叫做SystemService的类中。
- Protected Variations:最终目的之一,尽量让类少受外界变化的影响。有很多方法,以后慢慢涉及。
- Polymorphism:多态,把变化封装到子类中。对象注入!
5.2 类方法设计原则
Tell,Don’t Ask -> 低耦合
- 指导思路
- 告诉对象你需要他们干哪些事情,而不是要“查询他们的状态,再依据这些状态来决定你该采取什么行动”。这些“查询+决策”工作应该封装到类的内部,由类自己完成
命令 or 查询 -> 可读性
- 指导思路
- 一个方法要么是命令,要么是查询,不能同时是命令又是查询
- 命令方法会改变对象的状态,而查询不会
- 命令方法内部可以调用查询方法,反过来不行
- 命令方法通常没有返回值,而查询方法肯定有返回值
- 查询方法应该能够被安全的调用多次
- 一个方法要么是命令,要么是查询,不能同时是命令又是查询
5.3 SOLID
SRP(single responsibility principle),单一职责 -> 分层架构
- 指导原则:have one, and only one, reason to change
- 每个类都必须要有一个唯一的明确的职责,只做一件事,并且把这件事做好
- 仅仅当某种情况发生时(通常与这个类的职责直接相关)才需要修改这个类的代码
- 类名和方法名应该与其所承担的职责相符
- 具体操作
- SOC,seperation of coucerns:具体来说就是选择一个分类角度
- 正例,发票类:价格、税率、计算税、计算总额、打印,把打印分出去。
- 反例,计算机:UI代码和功能代码混在一起;一个窗体包容一切
- 反例,Web网页:在表示层直接读写数据库
OCP(open closed priciple),开闭原则
- 指导原则:对扩展开放、对修改封闭 -> 不引入新的BUG
- 一个软件开发完毕以后,当需要扩展其功能时候,不要修改原来的代码(封闭),而应该派生出新类(开放)
- 仅仅源程序有BUG时候,才对源代码进行修改
- 具体操作
- case:有3个类依赖MyClass,不能轻易改MyClass。新写一个MyClass2,继承MyClass。把其他对MyClass的依赖转到MyClass2上,最后在MyClass中添加弃用标记。
LSP(liskov substituion priciple),李氏替换原则
- 指导原则
- 子类必须能够替换掉它们的基类型
- 具体操作
- case,浏览器插件
- 针对接口或抽象基类进行编程,让实现统一接口或抽象基类的子对象可以相互取代而不影响到程序的其他部分。这对调用方提出了要求,不去调用子类拥有的特殊方法!!!
- 当子类型不能支持基类型所定义的方法的时候,可以抛出不支持异常,但是不能不实现(不得已而为之)
ISP(interface segragation principle),接口隔离
- 指导原则
- 不要让一个组件依赖于他们不用的方法
- 避免弄出一个什么都有的大接口,要切成小的东西
- 具体操作
- 有一个定义了曾删改查功能的接口,但是我们有一个业务用不到那么多的功能,需要重构。具体来说,把原来的曾删改查的接口分为两大类,命令接口和查询接口,这样能比较好的与类中方法分命令接口相对应。
DIP(Dependency Inversion Principle),依赖倒置,用在大量出现对象依赖,
- 指导原则
- 高层模块不应该依赖于底层模块,两者都应该依赖于抽象
- 抽象不应该依赖于细节,细节应该依赖于抽象
- 具体操作
- 类应该依赖于一个抽象的接口,而不是一个具体的实现类
- 当实例在高层模块new的时候,还是有一个间接的依赖存在。要求外界new好一个对象进行注入。