软件建模与设计过程可以拆分**成需求分析、概要设计和详细设计(类图)**三个阶段。UML 规范包含了十多种模型图,常用的有 7 种:类图、序列图、组件图、部署图、用例图、状态图和活动图(UML中没有流程图,用这个代替)
类以及类之间的关系:。类之间有 6 种静态关系:关联、依赖、组合、聚合、继承、泛化。
主要是通过用例图来描述系统的功能与使用场景
对于关键的业务流程,可以通过活动图描述
程序员的差距一方面体现在编程能力、另一方面体现在程序设计方面,好的设计和坏的设计最大的差别就体现在应对需求变更的能力上
一个设计腐坏的例子,例如输入、输出。;各种输入设备、输出设备。----面向接口变成
我们在**开始设计的时候就需要考虑程序如何应对需求变更,并因此指导自己进行软件设计,在开发过程中,需要敏锐地察觉到哪些地方正在变得腐坏,然后用设计原则去判断问题是什么,再用设计模式去重构代码解决问题**。
软件设计的开(拓展)闭(关闭修改)原则:如何不修改代码却能实现需求变更?
开闭原则说:软件实体(模块、类、函数等等)应该对扩展是开放的,对修改是关闭的。当需求变更时拓展。
以通过按钮拨号的电话,0~9,增加按钮类型时,,,似乎对 Button 类做任何的功能扩展,都要修改 Button 类
粗暴一点说,当我们在代码中看到 else 或者 switch/case 关键字的时候,基本可以判断违反开闭原则了。
设计模式中很多模式其实都是用来解决软件的扩展性问题的,也是符合开闭原则的。我们用策略模式对上面的例子重新进行设计。
策略模式是一种行为模式,多个策略(拨号器、密码锁)实现同一个策略接口,ButtonService,编程的时候 client 程序依赖策略接口,运行期根据不同上下文向 client 程序传入不同的策略实现。
利用策略模式来避免冗长的 if-else 或 switch 分支判断,别的模式也能移除分支判断。在工厂中查表法。策略模式实例,文件排序:内排序、外部排序、多线程外部排序、利用MapReduce多机排序。
策略模式的步骤:1. 将策略的定义分离出来。—复用。每种排序类都是无状态的–没有必要每次使用时创建一个新的对象,使用工厂模式封装。修改工厂,违背了开闭原则,此时通过反射读取被 annotation 标注的策略类
策略工厂类读取配置文件或者搜索被 annotation 标注的策略类,然后通过反射了动态地加载这些策略类、创建策略对象。
适配器模式是一种结构模式,用于将两个不匹配的接口适配起来,使其能够正常工作一个按钮控制多个设备----观察者模式,监听者接口,一对多的对象依赖关系
所谓模板方法模式,就是在父类中用抽象方法定义计算的骨架和过程,而抽象方法的实现则留在子类中。
实现开闭原则的关键是抽象,就是接口。依赖接口–就可以随意对这个抽象接口进行拓展,这个时候不需要对现有代码进行任何修改,利用接口的多态性,通过增加一个新的实现该接口的类,就能完成需求变更。
当需求变更的时候,现在的设计能否不修改代码就可以实现功能的扩展?如果不是,那么就应该进一步使用其他的设计原则和设计模式去重新设计。
关键还是看场景
软件设计的依赖倒置原则:如何不依赖代码却可以复用它的功能?接口的所有权是被倒置的
高层模块不应该依赖低层模块,二者都应该依赖抽象。抽象不应该依赖具体实现,具体实现应该依赖抽象。
依赖倒置原则的应用:1.JDBC,2.J2EE规范,不需要依赖Tomcat这种web容器。在框架设计时被用的很多,
利用依赖倒置的设计原则,每个高层模块都为它所需要的服务声明一个抽象接口,而低层模块则实现这些抽象接口,高层模块通过抽象接口使用低层模块。而实现这一特性的前提就是应用程序必须实现框架的接口规范,比如实现 Servlet 接口。
对具体类的继承是一种强依赖关系,维护的时候难以改变
可以把接口看作一组抽象的约定
软件设计的里氏替换原则(子类型必须能够替换掉它们的基类型):正方形可以继承长方形吗?
绝大多数设计模式其实都是利用多态的特性玩的把戏
通俗地说就是:子类型必须能够替换掉它们的基类型。所有使用基类的地方,都应该可以用子类代替。
实例,作为子类的白马可以替换掉基类马,但是小马不能替换马,因此小马继承马就不太合适了,违反了里氏替换原则。
我们判断一个继承是否合理?会使用“IS A”进行判断,类 B 可以继承类 A,我们就说类 B IS A 类 A,比如白马 IS A 马,轿车 IS A 车。
子类不能比父类更严格,否则替换时会因为更严格的契约而失败,反例,在 JDK 中,类 Properties 继承自类 Hashtable,类 Stack 继承自 Vector。
实践中,当你继承一个父类仅仅是为了复用父类中的方法的时候,那么很有可能你离错误的继承已经不远了。一个类如果不是为了被继承而设计,那么最好就不要继承它。粗暴一点地说,如果不是抽象类或者接口,最好不要继承它。如果你确实需要使用一个类的方法,最好的办法是组合这个类而不是继承这个类,这就是人们通常说的组合优于继承
内聚性主要研究组成一个模块或者类的内部元素的功能相关性。
一个类,应该只有一个引起它变化的原因。—单一职责,WEB框架的演进。
如何判断一个类的职责是否单一,就是看**这个类是否只有一个引起它变化的原因**。
Rectangle 类的设计就违反了单一职责原则。Rectangle 承担了两个职责,一个是几何形状的计算,一个是在屏幕上绘制图形。也就是说,Rectangle 类有两个引起它变化的原因,这种不必要的耦合不仅会导致科学计算应用程序庞大,而且当图形界面应用程序不得不修改 Rectangle 类的时候,还得重新编译几何计算应用程序。
软件设计的接口隔离原则:如何对类的调用者隐藏类的公有方法–拆分接口,实现多个接口?
接口隔离原则说:不应该强迫用户依赖他们不需要的方法。
通过使用接口隔离原则,我们可以将一个实现类的不同方法包装在不同的接口中对外暴露。应用程序只需要依赖它们需要的方法,而不会看到不需要的方法。
我们开发的绝大多数软件都是用来解决现实问题的
软件建模比较知名的是 4+1 视图模型,即建模方法论。
针对具体的用例场景,领域,将上述 4 个视图关联起来,一方面从业务角度描述,功能流程如何完成,一方面从软件角度描述,相关组成部分如何互相依赖、调用。
领域驱动设计:业务逻辑围绕领域模型设计,主要是对象的设计,包含领域相关知识,而不是只有getter,setter等
如果你对自己要开发的**业务领域没有清晰的定义和边界,没有设计系统的领域模型,而仅仅跟着所谓的需求不断开发功能,一旦需求来自多个方面,就可能发生需求冲突**,这个需求可能是伪需求,
领域模型是合并了行为和数据的领域的对象模型。
如何用领域模型模式设计一个完整而复杂的系统,有没有完整的方法和过程指导整个系统的设计?领域驱动设计,即 DDD 就是用来解决这一问题的。
实体设计是 DDD 的核心所在,首先通过业务分析,识别出实体对象,然后通过相关的业务逻辑设计实体的属性和方法。这里最重要的,是要把握住实体的特征是什么,实体应该承担什么职责,不应该承担什么职责,分析的时候要放在业务场景和界限上下文中,而不是想当然地认为这样的实体就应该承担这样的角色。
设计模式的精髓在于对面向对象编程特性之一——多态的灵活应用,而多态正是面向对象编程的本质所在。
多态的好处:软件编程时的实现无关性,程序针对接口和抽象类编程,而不需要关心具体实现是什么。
设计模式:模式是**可重复的解决方案**
装饰模式最大的特点是,通过类的构造函数传入一个同类对象,OR接口,也就是每个类实现的接口和构造函数传入的对象是同一个接口。设计模式是一个非常注重实践的编程技能。模板和策略
设计模式应用:编程框架中的设计模式
框架通常规定了一个软件的主体结构
当你设计一个框架的时候,你实际上是在设计一类软件的通用架构,并通过代码的方式实现出来。如果仅仅是提供功能接口供程序调用,是无法支撑起软件的架构的,也无法规范软件的结构。
Servlet实际上是一个接口。
JUnit 是一个 Java 单元测试框架,开发者只需要继承 JUnit 的 TestCase,开发自己的测试用例类,通过 JUnit 框架执行测试,就得到测试结果。
当我们从树的根节点遍历树,就可以执行所有这些测试用例。传统上进行树的遍历需要递归编程的,而使用组合模式,无需递归也可以遍历树。
反应式编程框架设计:如何使程序调用不阻塞等待,立即响应?–异步–回调
软件设计的核心目标就是高内聚、低耦合
组件是软件复用和发布的最小粒度软件单元
The default value of the class path is “.”, meaning that only the current directory is searched.
为何说要多用组合少用继承?如何决定该用组合还是继承?
类的继承层次会越来越深、继承关系会越来越复杂,会导致:1.可读性差,搞清楚类的用途可能需要追溯父类,2.将父类的实现细节暴露给了子类,高度耦合,父类代码的修改,就会影响所有子类的逻辑。
public class Ostrich implements Tweetable, EggLayable {// 鸵鸟
private TweetAbility tweetAbility = new TweetAbility(); // 组合
private EggLayAbility eggLayAbility = new EggLayAbility(); // 组合
//... 省略其他属性和方法...
@Override
public void tweet() {
tweetAbility.tweet(); // 委托
}
@Override
public void layEgg() {
eggLayAbility.layEgg(); // 委托
}
}
继承主要有三个作用:表示 is-a 关系,支持多态特性,代码复用。
比如 is-a 关系,我们可以通过组合和接口的 has-a 关系来替代;多态特性我们可以利用接口来实现;代码复用我们可以通过组合和委托来实现。所以,从理论上讲,通过组合、接口、委托三个技术手段,我们完全可以替换掉继承,在项目中不用或者少用继承关系,特别是一些复杂的继承关系。
仅仅为了代码复用,生硬地抽象出一个父类出来,会影响到代码的可读性,做法:组合,即成员变量是另一个类。
Repository 层负责数据访问,Service 层负责业务逻辑,Controller 层负责暴露接口
public class UserBo {// 省略其他属性、get/set/construct 方法
private Long id;
private String name;
private String cellphone;
}//是一个存储的数据结构,只包含数据,不包含任何业务逻辑,业务逻辑在UserService类中
像 UserBo 这样,只包含数据,不包含业务逻辑的类,就叫作贫血模型(Anemic Domain Model)。
在贫血模型中,数据和业务逻辑被分割到不同的类中。充血模型(Rich Domain Model)正好相反,数据和对应的业务逻辑被封装到同一个类中。因此,这种充血模型满足面向对象的封装特性,是典型的面向对象编程风格。
DDD是伴随着微服务而盛行的。微服务还有另外一个更加重要的工作,那就是针对公司的业务,合理地做微服务拆分。而领域驱动设计恰好就是用来指导划分服务的。所以,微服务加速了领域驱动设计的盛行。
做好领域驱动设计的关键是,看你对自己所做业务的熟悉程度,而并不是对领域驱动设计这个概念本身的掌握程度。即便你对领域驱动搞得再清楚,但是对业务不熟悉,也并不一定能做出合理的领域设计。所以,不要把领
域驱动设计当银弹,不要花太多的时间去过度地研究它。
实际上,基于充血模型的 DDD 开发模式实现的代码,也是按照 MVC 三层架构分层的。Controller 层还是负责暴露接口,Repository 层还是负责数据存取,Service 层负责核心业务逻辑。它跟基于贫血模型的传统开发模式的区别主要在 Service 层。
我们先来回忆一下,**我们平时基于贫血模型的传统的开发模式,都是怎么实现一个功能需求的。**不夸张地讲,我们平时的开发,大部分都是 SQL 驱动(SQL-Driven)的开发模式,—长得差不多、区别很小的SQL语句满天飞。
在这种开发模式(DDD)下,我们需要事先理清楚所有的业务,定义领域模型所包含的属性和方法。领域模型相当于可复用的业务中间层。新功能需求的开发,都基于之前定义好的这些领域模型来完成,不会出现需求保证的情况。
简单系统(不太关注可复用性)用贫血模型,复杂系统用,基于充血模型的 DDD 开发模式
因为交易流水有两个功能:一个是业务功能,比如,提供用户查询交易流水信息;另一个是非业务功能,保证数据的一致性。这里主要是指支付操作数据的一致性。
不涉及复杂业务概念,职责单一、功能通用。
并且将原来在 Service 类中的部分业务逻辑移动到 VirtualWallet 类(领域对象)中,让 Service 类的实现依赖 VirtualWallet 类。
理解OOP,我们就不难理解DDD:
DDD第一原则:将数据和操作结合。(贫血模型将数据和操作分离,违反OOP的原则。)
DDD第二原则:界限上下文。这是将“单一指责”应用于我们的领域模型。
DDD is nothing more than OOP applied to business models. DDD其实就是把OOP…