【软件开发】设计模式
面向对象基础
类与对象 - 对象是实体,类是对象的抽象集合。
类的构造函数 - 代替默认
重载- 相同函数名但是不同参数类型
属性 - 一种限制了外部修改情况的 public 方法,与 public 成员区别是可以限制读写或数值区间
封装 - 暴露可操作的接口,减少耦合
继承 - b is a,b 继承 a 的非 private 属性和功能,可以派生和重写。
多态 - 不同的对象,相同的方法不同的结果,父类虚函数子类重写
重构 - 代码冗余后,将子类方法重复的代码变为父类的方法
抽象类 - abstract 抽象类无法定义实例对象,抽象方法必须被重写
接口 - 实现某个功能的方法与属性组合,类可以基于接口更简洁地实现该功能,接口是对行为的抽象
集合 - 实现某一系列功能的类的组合,例如.net framwork 中实现数据存储和检索的集合 ArrayList
泛型 - 可以接收多种数据类型,但预定义会减少拆装箱开销, List
委托与事件 - 类似于触发-回调机制,当 A 类的对应方法发生时,会执行被添加到 event 的 delegate 实例。委托-引用方法。委托的方法可以带参数
类与对象的好处
低耦合 - 》 易扩展,易维护,灵活性高,易于复用
重复代码多会导致维护困难 –
业务逻辑和界面逻辑分离
UML 图
方框-类 类名 字段属性 方法行为 ;
三角-继承 虚线箭头-依赖 实线箭头 -》关联;
虚线三角/棒棒糖 实现接口;菱形-箭头 聚合【真子集】;实心菱形-箭头 组合关系【整体-部分 相同生命周期】;
设计原则
单一职责原则
字面意思是一个类的功能要单一;准确解释,一个类只有一个能引起其变化的原因。
封闭-开放原则
对于扩展是开放的,对于修改是封闭的。
对程序的新增需求通过增加代码来实现。
依赖倒置原则
面向过程开发时,先从底层着手,再逐步到应用层(高层)。高层模块依赖于低层模块,不易复用。
为了高层、底层都具备可扩展、复用,高层模块不应该依赖低层模块,两者都应该依赖于业务对象的抽象。而在初期应该针对交互接口编程,而不是针对业务编程。
针对接口而不是业务细节编程,依赖关系终止于抽象类或接口,是面向对象编程的精华所在。
里氏替换
子类可以替代父类。如果一个程序中的父类都被子类替代,其业务行为不应该发生变化。
高层或低层模块,依赖了抽象接口或抽象类,具体的实现都在类中,可以被子类替换,具备封闭-开发的一些性质,扩展功能的话用扩展后的子类替代即可。
合成-聚合复用原则
之前的很多模式都是基于继承-多态实现的,主要优化是解耦和复用。但是继承是一个紧密依赖的方式,父类的任何变化都会导致子类变化,而需要复用子类时,可能继承的一些非虚后抽象的方法不在适用,需要修改父类。父类过多的继承又会导致修改牵一发而动全身;继承的强依赖关系限制了其灵活性。
合成-聚合复用模式 尽量使用合成 or 聚合,尽量不使用继承。
聚合 - 空心菱形 - 表示一种弱的拥有关系,A 对象可以包含 B 对象,但是 B 对象不一是 A 的一部分。
合成 - 实心菱形 - 强拥有关系,生命周期相同。
使用合成-聚合能保证类被集中在单个任务上,类的继承会保持较小的规模,避免增长到过长的继承链,难以修改。
创建模式
创建模式隐藏了这些需要被创建对象的实例类是如何被创建和放在一起的,创建时只需要知道由抽象类所定义的接口是怎样的。客户端只需要提出我想要什么,具体谁创建、如何创建、何时创建由创建类实现。
使用创建模型时,创建系统对于被创建对象是独立的,创建模式将系统需要使用的哪些类信息会封装,允许用结构和功能差别很大的产品对象配置一个系统,配置可以是动态的 运行时指定的。
简单工厂模式
根据需求,在工厂中生成对应的对象。
需求可以是一类“商品”的具体名称,工厂则负责“生产“出对应的商品。
工厂方法模式
和简单工厂模式不同,工厂方法模式中使用不同的工厂还生成不同的对象,其工厂基类也是抽象类,需要具体的工厂实现。而且工厂方法模式的客户端操作更加复杂,需要在代码中指定工厂,不像简单工厂模式中 工厂类根据 case 选择生成方法。
好处是,符合了封闭-开放原则。当有新的产品添加时,简单工厂类需要添加产品类并修改工厂类的 case 函数–而工厂方法模式不需要修改基类。
抽象工厂模式
实例:一个与数据库交互的程序,当更换底层数据库后,程序修改会非常麻烦。 没有做好解耦,希望的效是如果添加对 access 的支持,只需要在数据库交互类组中添加访问 access 的类即可。
对于一个客户端对应着多个数据库类型交互类的情况,显然可以使用工厂模式,针对不同的数据库类型生成不同的数据库交互对象,它们拥有相同名称的方法, 但是实现不同。
在客户端,与数据库交互需要声明工厂类型,生成对应的数据库交互对象。
此时,如果要针对不同的表操作,新的 department 表,则需要创建新的接口,然后再创建新的产品,并在工厂接口添加对应的生产过程。
抽象工厂模式,和工厂方法模式的不同,在后者的基础上添加了更多的抽象产品,抽象工厂会有多种的实例工厂(取决于有多少类的抽象产品),它们有更多的实现方式。
相较于工厂模式,把多种抽象产品对应的多个抽象工厂在一个接口内实现的好处是,当需要修改产品的系列,可以直接更换需要实现的抽象工厂。而工厂模式中,每一个产品(department user project 等表)都会在抽象工厂中制造一次,导致了一些重复。相当于在工厂的基础上又抽象了一层。
反射 – 很像 matlab 中的某个函数,把字符串当成程序代码来运行。将程序由编译时转换为运行时,从而避免 switch-case;可以使用 config 文件记录字符串,来控制程序的不同运行模式。
单例模式
有印象,类似于全局变量,只生成一个。
某些类,例如窗体实例类,不希望出现多次。这个在我之前设计的.NET 多窗体程序中,我是使用了一个全局变量 ISxxFormOpen 来判断的,如果打开了,就不会重复打开。这显然不是一个好的设计。
稍微好一些的设计是判断是否实例化过 if(formObject != null) 这样会减少一些代码量。 注意实例化后可能会 disposed,这时候窗口对象并没有析构。
需要注意的是,这时候判断是否实例化过的代码是在客户端中的,而一个不能生成多对象的类,是否已开应该由其自身决定,尽量不麻烦客户端。单例模式中的做法是,把构造函数自由化,然后编写一个静态方法,对该对象初始化并返回对象,!=null 确保单例。
多线程 – 单例 – 锁 ; 保证线程安全 - 减少每次调用 lock 的开销 双重锁定
C#中静态初始化可以提供类似的效果。
原型模式
通过原型实例指定所创建对象的种类,并通过拷贝这些原型创建新的对象。
对于某个对象有多个实例,多次创建可能有过多重复的代码段,可以使用克隆,然后对克隆的对象实例进行更改。
实现克隆接口。
在初始化信息不变的情况下,使用克隆可以隐藏创建的过程,并且节省构造函数的开销。
浅复制和深复制 – 值类型和引用类型 ; 浅表复制,会复制值类型的字段,但是引用类型的字段不会。深复制把引种对象的变量指向复制过来的新对象,而不是原来被引用的对象。copy - clone
建造者模式
复杂对象的构建和表示分离,使得同样的构建过程可以创建不同的表示。内部表象与生产过程分离,
创建一个复杂对象时,创造顺序是稳定的,但是每个过程的实现方法可以是各异的。
结构型模式
信息的隐藏促进了软件的复用,减小耦合。
适配器模式
接口转换,convertor,将一个类的接口转换成另一个接口,克服接口不兼容导致的无法共同工作。
当系统的行为和数据都正确,但是接口不符;如果添加适配器的代价比修改交互的两个系统之一的代价小,可以使用适配器模式。在设计以及开放初期尽可能统一接口,避免使用适配器。
桥接模式
桥接模式-将抽象部分与实现部分分离,实现指的是实现其方法的对象,他们可以独立变化。不是强 is a 关系,使用聚合代替继承。实现一个系统,会有多个角度的分类,每一种每类都会有派生变化;应该把多个角度分离让他们独立变化,而不是在大系统的基类上去继承。
组合模式
对象关系以 tree 的形式分布-枝 叶。枝尽可能复用根节点;
透明和安全方法 – 区分枝和叶
开发的对象是体现部分与整体层次结构,用户在使用时可以忽略组合对象与单个对象的不同;一致的使用组合对象与单个对象;
组合控件-自定义控件
装饰模式
一个可以更换小人衣服的系统。小人是核心类,有装饰其他衣服的装饰类。 为已有的功能动态添加更多功能。装饰类为核心的类添加新的方法、字段、逻辑,满足特定情况下的特定需求;在执行时,可以选择性地添加装饰。
把某个类的核心职责和装饰功能区分开,去除相关类中重复的装饰逻辑。动态地给一个对象添加一些额外的职责,比生成对应子类实现添加功能更加灵活。
和之前工厂模式不同的是,装饰模式是在核心类的基础上不断地派生子类,最终承载了所有装饰的子类执行对应方法。而工厂模式则是对每一个任务生成一个子类,然后每个子类都运行一次方法。
外观模式
为子系统的一组接口提供一个组合后的,高一级的接口,减少客户端调用量。
在设计时,有意识地将不同功能层分离,例如数据访问、业务逻辑、展示。不同层之间提供 facade 接口即可,尽可能减少耦合
代理提供某个对象的代表访问,外观提供一个子系统的代表访问。
代理模式
A 对象操作 B 对象,但可能 A 不方便直接与 B 交互,而 C 与 A 实现相同的接口。可以通过 C 来与 B 交互。
在范围 B 对象时加入一定的间接性,而这种间接性可以附加多种用途。
远程代理 - 为 B 对象提供不同地址空间的局部代理。加入 webService 引用,会预先生成 reference 的文件和文件夹。
虚拟代理 - 开销很大的对象,使用虚拟代理实现图片,先打开网页后再加载图片
安全代理 - 控制对实际对象的访问权限。
行为模式
策略模式
策略类也是定义了一系列的算法,和工厂很类似,根据传递的参数选择具体的算法。但与之不同的是,策略模式的策略类是基类的组合,算法是抽象基类的具体实现子类,根据参数生成基类的具体策略算法,而工厂模式中,工厂类是实现子类的子类,根据传参生产具体的子类对象。
模板方法模式
在完成某一系列步骤时,详细实现只有很小的不同。可以把这个不同之处变为父类的虚方法,其余部分在父类实现,而子类只要重写该虚方法,最大程度复用代码。
模板-一个操作中的算法骨架,并且将一些需要动态变化的部分在子类实现,这样可以使用一个算法框架实现特定的步骤。
观察者模式
发布-订阅模式。一种一对多的依赖关系,让多个观察者对象监听某一个主题对象,这个发布对象能通知订阅对象。
解耦合 – subject 不需要知道都有谁订阅了。让发布和订阅都依赖于抽象接口,而不依赖于具体操作流程。
当一个对象改变时,其他对象也需要对应的变化,但是不能确定具体的变化目标和结果。而观察者模式可以把两者分开封装。
观察者模式中,对于已封装的类,让其实现“订阅”修改会有一些困难。一是直接修改违背了封闭-开放原则,另一方面基于订阅发布的接口实现需要依赖于发布的类。这时候可以使用事件委托的方式实现。
委托对象搭载的方法不需要属于同一个类,但方法需要有相同的返回值和参数列表。
状态模式
当一个对象的行为依赖其状态,并且状态转变表达式过于复杂,可以将其拆分,把状态的判断逻辑转移到不同状态的一系列类中。将特定状态相关的行为局部化,并且将不同的状态分割。
当有很长的 if else,可以把状态行为定义为一个抽象类,每一个 if 分支去实现行为。这样当需要修改 if 语句中的某一个判断条件或执行,或者出现特殊情况(例如进度条加入了 pause 判断)添加额外的判断,不用去 那个很长的 if-else 语句中修改。
备忘录模式
状态恢复-从内存而不是硬盘中。不要把备份的细节暴露在客户端
保存的实现可以通过一个 memento 类实现,定义一些需要保存的细则;恢复则在发起备份的类中添加参数为 memento 的 set 方法。
这适用于功能较为复杂,但需要维护或记录所选定的属性历史的类中。
迭代器模式
遍历。顺序方法,访问聚合对象中的各个元素,不暴露内部表示。
当需要访问一个聚合对象,并且无论是何种对象都需要遍历。迭代器支持多种遍历方式。
Foreach in
命令模式
行为请求者和行为实现者耦合过紧密,行为的申请会很复杂。行为实现有延时 、排队 、 请求日志 、 撤销等工作,在客户端实现会很困难。
命令模式可以较轻松地创建一个命令对了,并记录日志,实现请求的撤销和重做;允许接收方拒绝;添加新的具体命令很容易;请求一个操作对象并不用知道操作对象如何执行。
代码不用添加猜测的功能架构,例如命令模块,实在需要时再重构即可。