设计原则
0.引言
文章是重复造轮子,记笔记给自己看,勿喷。
重新认识面向对象
-
理解隔离变化
从宏观层面来看,面向对象的构建方式更能适应软件的变化,能将变化所带来的影响减为最小 -
各司其职
• 从微观层面来看,面向对象的方式更强调各个类的“责任”
• 由于需求变化导致的新增类型不应该影响原来类型的实现—是所谓各负其责 -
对象是什么?
• 从语言实现层面来看,对象封装了代码和数据。
• 从规格层面讲,对象是一系列可被使用的公共接口。
• 从概念层面讲,对象是某种拥有责任的抽象。
1.设计原则
1.1.依赖倒置原则( DIP)
- 高层模块(稳定)不应该依赖于低层模块(变化),二者都应该依赖于抽象(稳定) 。
- 抽象(稳定)不应该依赖于实现细节(变化) ,实现细节应该依赖于抽象(稳定)。
(电脑主板案例)模块间的依赖通过抽象发生,实现类之间不直接发生依赖关系,其依赖关系是通过接口或抽象类产生的。针对接口编程(虚基类),不针对实现编程
- 针对接口编程,而不是针对实现编程。
- 使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不是使用具体的类
- 变与不变的分离。共性的部分为父类,变化的部分为子类。
Apollo 代码基本都是这样:
通过基类隔离变化。
1.2.开放封闭原则( OCP)
- 对扩展开放,对更改封闭。
- 类模块应该是可扩展的,但是不可修改。
如果新增加一个逻辑功能点,需要增加新的else 或者 else if ,势必修改了已有代码,则破坏了OCP原则
1.3.单一职责原则( SRP)
- 一个类应该仅有一个引起它变化的原因。
- 变化的方向隐含着类的责任。
1.4.Liskov 替换原则( LSP)
- 子类必须能够替换它们的基类(IS-A)。
- 继承表达类型抽象。
如果一个程序P(T1) ,如果将输入T1 替换为T2 ,而且 P(T1) = P(T2),那么T2 是T1的子类型。
- 简单的概述就是 : 所有引用基类的地方必须能透明地使用其子类的对象。
- 里氏代换原则的根本,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常。
- 里氏替换是继承复用的基石,只有当子类可以替换父类,且软件单位的功能不受到影响时,父类才能真正被复用,而子类也能够在基类的基础上增加新的行为
1.5.接口隔离原则( ISP)
- 不应该强迫客户程序依赖它们不用的方法。
- 接口应该小而完备。
1.6.优先使用对象组合,而不是类继承
- 类继承通常为“白箱复用”,对象组合通常为“黑箱复用” 。
- 继承在某种程度上破坏了封装性,子类父类耦合度高。
- 而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低。
1.7.封装变化点
- 使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合。
1.8.针对接口编程,而不是针对实现编程
- 不将变量类型声明为某个特定的具体类,而是声明为某个接口。
- 客户程序无需获知对象的具体类型,只需要知道对象所具有的接口。
- 减少系统中各部分的依赖关系,从而实现“高内聚、松耦合”的类型设计方案。
2. 设计原则要求
设计原则是指导思想,将规则落实到具体的类/接口的设计、功能逻辑的划分上,可以转化成以下要求。
- 面向抽象(抽象类、接口)编程,而不是面向实现编程
- 接口和类的功能要尽可能的单一,避免大而全的类和接口
- 优先使用组合,而不是继承
- 子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法
- 子类应该尽可能的与父类保持一致,不要重写父类原有逻辑
- 如果类之间没必要直接交互,可以通过“中介”,而不是直接交互,降低耦合性
- 实现和细节可以通过DI的方式,最大程度减少“硬编码”
- 如果没有什么明显弊端,类应该被设计成不变的
- 降低其他类对自身的访问权限,不要暴露内部属性成员,如果需要提供相应的访问器(属性)
3.将设计原则提升为设计经验
-
1.设计习语 Design Idioms: Design Idioms 描述与特定编程语言相关的低层模式,技巧,惯用法。
-
2.设计模式 Design Patterns: Design Patterns主要描述的是“类与相互通信的对象之间的组织关系,包括它们的角色、职责、协作方式等方面。
-
3.架构模式 Architectural Patterns: Architectural Patterns描述系统中与基本结构组织关系密切的高层模式,包括子系统划分,职责,以及如何组织它们之间关系的规则。
4.重构获得模式 Refactoring to Patterns
-
面向对象设计模式是“好的面向对象设计”,所谓“好的面向对象设计”指是那些可以满足 “应对变化,提高复用”的设计 。
-
现代软件设计的特征是“需求的频繁变化”。设计模式的要点是“寻找变化点,然后在变化点处应用设计模式,从而来更好地应对需求的变化” .“什么时候、什么地点应用设计模式”比“理解设计模式结构本身”更为重要。
-
设计模式的应用不宜先入为主,一上来就使用设计模式是对设计模式的最大误用。没有一步到位的设计模式。敏捷软件开发实践提倡的“Refactoring to Patterns” 是目前普遍公认的最好的使用设
计模式的方法。
5.重构关键技法
- 静态 --> 动态
- 早绑定 --> 晚绑定
- 继承 --> 组合
- 编译时依赖 --> 运行时依赖
- 紧耦合 --> 松耦合
设计模式概览
0.引言
1.创建型
将对象的部分创建工作延迟到子类或者其他对象,从而应对需求变化为对象创建时具体类型实现引来的冲击。
1.0.创建型概述
- ReflectFactory反射工厂模式:通过反射,以完成⼯⼚对对象的创建。可以最⼤限度的解耦。
- Factory Method工厂方法模式:定义⼀个⽤于创建对象的接⼝,让⼦类决定实例化哪⼀个类。⼯⼚⽅法将类的实例化延⻓到⼦类。
- Abstract Factory抽象工厂模式(用的很少):提供一个创建一系列相关或互相 依赖对象的接口,只需要知道对象的系列,无需知道具体的对象。
- Singleton单例模式:独一无二的存在
- Prototype原型模式:显式实现的拷贝构造函数, 重复构造,打出“原型”
- Builder建造者模式:组装自行车 (关注产出,不关⼼细节). 将⼀个复杂对象的构建与它的表⽰分离,使得同样的构建过程可以创建不同的表⽰。
1.1.简单工厂模式(Simple Factory)
- 1.Factory简单工厂模式:⽤来创建⽬标对象的类,将相似对象的创建⼯作统⼀到⼀个类来完成。
- 2.工厂类包含了必要的逻辑判断,根据客户端的选择条件动态实例化相关的类。对于客户端来说,去除了对具体产品的依赖。不符合开闭原则。
1.2.工厂方法模式(Factory Method)
- 1.工厂方法模式:定义一个用于创建对象的接口,让子类决定实例化哪个类。
- 2.工厂方法把简单工厂的内部判断逻辑移到了客户端代码,本来需要修改工厂类,现在是修 改客户端。
- 3.简单工厂模式违背了开放-封闭原则,工厂方法模式借助多态,克服了该缺点,却保持了封装对象创建过程的优点。
1.3.抽象工厂模式(Abstract Factory)
- 1.抽象工厂模式:提供一个创建一系列相关或互相依赖对象的接口,只需要知道对象的系列,无需知道具体的对象。
- 2.在客户端中,具体工厂类只在初始化时出现一次,更改产品系列即可使用不同产品配置。
- 3.利用简单工厂类替换抽象工厂类及其子类,可以使客户端不再受不同系列的影响。
- 4.结合反射机制,Assembly.Load(“程序集名称”).CreateInstance(“命名空间”.“类名”),可以直接通过字符串创建对应类的实例。所有在简单工厂中,都可以通过反射去除switch或if,解除分支判断带来的耦合。
- 5.反射中使用的字符串可以通过配置文件传入,避免更改代码。
1.4.单例模式(Singleton)
- 1.单例模式:让类自身保证它只有一个实例,并提供一个全局访问点。
- 2.多线程下单例模式可能失效,需要采取双重锁定的的方式,确保被锁定的代码同一时刻只被一个进程访问。
- 3.饿汉式单例:即静态初始化方式,在类初始化时产生私有单例对象,会提前占用资源;渴汉式单例:在第一次被引用时将自己初始化,会产生多线程访问安全问题,需要添加双重锁定。
1.5.建造者模式(Builder)
- 1.建造者模式:将复杂对象的创建与表示分开,使得相同的创建过程可以有不同的表示。用户只需制定需要建造的类型,不需要知道建造的过程和细节。
- 2.指挥者是建造者模式中重要的类,用于控制建造过程,也可以隔离用户与建造过程的关联。
- 3.建造者隐藏了产品的组装细节,若需要改变一个产品的内部表示,可以再定义一个具体的建造者。
- 4.建造者模式是在当前创造复杂对象的算法,独立于该对象的组成部分和装配方式时适用的模式。
1.6.原型模式(Prototype)
- 1.原型模式:用原型实例指定创建对象的种类,并通过拷贝这些原型创建对象。本质是从一个对象再创建另一个可定制的对象,并且不需要知道创建细节。
- 2.原型抽象类的关键是有一个Clone()方法,原型具体类中复写Clone()创建当前对象的浅表副本。
- 3.原型拷贝无需重新初始化对象,动态获取对象的运行状态。既隐藏了对象创建的细节,又提升性能。
- 4.在具体原型类中,MemberwiseClone()方法是浅拷贝,对值类型字段诸位拷贝,对引用类型只复制引用但不会把具体的对象值拷贝过来。
- 5.比起浅拷贝,深拷贝把引用对象的变量指向新对象,而不是原被引用的对象。对于需要深拷贝的每一层,都需要实现ICloneable原型模式。
1.7.对比总结
- 工厂方法模式:为不同子类创建不同工厂;
- 抽象工厂模式:为不同系列建造不同工厂;
- 单例模式:保证实例唯一;
- 建造者模式:为不同类组装出一套相同的方法;
- 原型模式:实现深拷贝。
2.结构型
通过类继承或者对象组合获得更灵活的结构,从而应对需求变化为对象的结构带来的冲击。
2.0.结构型概述
- Bridge桥接模式:牵线搭桥。将抽象部分与它的实现部分分离,使它们都可以独⽴地变化。
- Adapter适配器模式:顾名思义,新旧对接。将⼀个接口转换成客⼾希望的另⼀个接口,使得原本由于接口不兼容而不能⼀起⼯作的那些类可以⼀起⼯作。适配器模式的别名是包装器模式(Wrapper),是⼀种结构型设计模式。
- Decorator装饰模式:你是超人,内裤外穿。动态的给一个对象添加一些额外的职能,把所需功能按顺序串联起来并进行控制。 Decorator 提供了一种给类增加职责的方法,不是通过继承实现的,而是通过组合。
- Composite组合模式:部分整体,树形结构,组合模式来帮忙。将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
- Adapter适配器模式:中间的翻译官。当系统数据和行为都一致,只有接口不符合时,将一个类的接口转化为客户端期望的另一个接口。
- Proxy代理模式:为别人做嫁衣,帮忙表白闹哪样。为其他对象提供一种代理以控制对这个对象的访问。实际上是在访问对象时引入一定程度的间接性。
- Facade外观模式:为子系统中一组接口提供一个一致的界面,即定义一个高层接口,增加子系统的易用性。
- Flyweight享元模式:对象复用,『享元模式』。运用共享技术有效支持大量细粒度对象。
2.1.代理模式(Proxy)
- 1.代理模式:为其他对象提供一种代理以控制对这个对象的访问。实际上是在访问对象时引入一定程度的间接性。
- 2.远程代理:为一个对象在不同地址空间提供局部代表,隐藏一个对象存在于不同空间的事实。如.Net加入Web引用,引入WebService,此时项目会生成WebReference的文件夹,就是代理。
- 3.虚拟代理:根据需要创建开销很大的对象,通过它存放实例化需很长时间的真实对象。HTML中的多图,就是通过虚拟代理代替了真实图片,存储路径和尺寸。
- 4.安全代理:控制真实对象的访问权限,用于对象应该拥有不同的访问权限时。
- 5.智能指引:当调用真实对象时,代理处理一些另外的事情。通过代理在访问对象时增加一些内务处理。
2.2.适配器模式(Adapter)
- 1.适配器模式:当系统数据和行为都一致,只有接口不符合时,将一个类的接口转化为客户端期望的另一个接口。
- 2.适配器模式用于服用一些现存的类,常用在第三方接口或软件开发后期双方都不易修改的时候。
2.3.外观模式(Facade)
- 1.外观模式:为子系统中一组接口提供一个一致的界面,即定义一个高层接口,增加子系统的易用性。
- 2.外观模式完美体现了依赖倒转原则和迪米特法则。
- 3.设计初期阶段,在MVC三层架构中,任意两层间建立外观Facade。
- 4.子系统会因不断演化变得复杂,增加外观Facade提供简单简单接口减少依赖。
- 5.在维护一个大的遗留系统时,新的开发又必须依赖其部分功能。此时,开发一个外观Facade类,从老系统中抽象出比较清晰的简单接口。让新系统只与Facade交互,而Facade与遗留代码交互所有的工作。
2.4.装饰模式(Decorator)
- 1.装饰模式:动态的给一个对象添加一些额外的职能,把所需功能按顺序串联起来并进行控制。
- 2.每个要装饰的功能放在单独的类中,并让这个类包装它所要修饰的对象。当需要执行特殊行为时,客户端就可以根据需要有选择的、有顺序的使用装饰功能包装对象了。
- 3.装饰模式有效的把类的核心职能和装饰功能区分开了,并且可以去除相关类中重复的装饰逻辑。
2.5.桥接模式(Bridge)
- 1.对象的继承关系编译时已确定,所以无法在运行时修改从父类继承的实现。由于紧耦合,父类中任何的改变必然会导致子类发生变化。当需要复用子类,但继承下来的方法不合适时,必须重写父类或用其他类替代。这种依赖性限制了灵活性和复用性。
- 2.合成/聚合复用原则:尽量使用合成和聚合而不是继承。可以保证每个类封装集中在单个任务上,不会出现规模太大的类及继承结构。
- 3.桥接模式:抽象类和其派生类分离,各自实现自己的对象。若系统可以从多角度分类,且每种分类都可能变化,则把多角度分离独立出来,降低耦合。
2.6.享元模式(Flyweight)
- 1.享元模式:运用共享技术有效支持大量细粒度对象。
- 2.在享元模式对象内部不随环境改变的共享部分是内部状态,不可共享需要通过调用传递进来的参数是外部状态。
- 3.使用享元模式的场景包括,一个应用程序产生了大量的实例对象,占用了大量内存开销;或对象的大多数状态为外部状态,删除内部状态后可以用较少的共享对象来取代组对象。
- 4.应用场景有正则表达式、浏览器、机器人指令集等。
2.7.组合模式(Composite)
- 1.组合模式:将对象的组合以树形的层次结构表示,对单个对象和组合结构的操作具有一致性。
- 2.透明方法:叶子和分枝对外接口无差别;安全方法:分枝具有添加删除叶子的接口,低层抽象接口和叶子没有。
- 3.基本对象组合成组合,组合又可以被组合,不断递归下去,在任何用到基本对象的地方都可以使用组合对象。
2.8.对比总结
- 代理模式:控制访问;
- 适配器模式:将接口转换为客户端期望的形式;
- 外观模式:整理出一套可用接口;
- 装饰模式:动态修改类的职能;
- 桥接模式:将多角度分类分离独立;
- 享元模式:共享实例;
- 组合模式:递归生成树形结构的组合对象。
3.行为型
通过类继承或者对象组合来划分类与对象间的职责,从而应对需求变化为多个交互的对象带来的冲击。
3.0.行为型概述
- Template Method模板方法模式:金庸考试试题,你的答案在后面(子类)。定义一个操作中的算法框架,将一些步骤延迟到子类中。子类在不改变框架的前提下就可以重新定义某些特定步骤。
- Strategy策略模式:商场满减还是打折。定义算法家族并分别封装,他们完成的工作相同,只是实现不同,可以互相替换。继承有助于析取这些算法的公共功能。此模式让算法的变化不会影响到使用算法的用户。
- Observer观察者模式:上班摸鱼看NBA看股票。定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。
- State状态模式:今天工作状态如何(工作类–状态类)。一个对象可能拥有多种状态,当内在状态改变时允许改变行为。优化if-else状态语句。
- Memento:备忘录模式后悔药.
- Chain of Responsibility职责链模式:流程业务,各司其职,『责任链模式』。使多个对象都有机会处理请求,解除请求发送者和接收者的耦合。将对象连成一条链,并沿这条链传递请求直到请求被解决。
- Iterator迭代器模式:玩转集合容器,『迭代器模式』。提供一种方法顺序遍历一个聚集对象,为不同的聚集结构提供遍历所需接口,而不暴露对象内部的表示。
- Command命令模式:任务分发,只管下达『命令模式』即可。将请求分装为对象,将请求和执行分开,可以用不同的请求对客户参数化。可以对请求排队、通过或否决、记录日志、撤销或重做。
- Mediator中介者模式:系列对象,依赖交互,『中介者模式』来帮忙。用一个中介对象来封装一系列对象间的交互。
- Interpreter解释器模式:给定一种语言,定义它文法的一种表示,再定义一个解释器,使用该表示来解释语言中的句子。
- Visitor访问者模式:这一次数据说了算,『访问者模式』。在不改变各元素的前提下定义作用于这些类的新的操作。
3.1.职责链模式(Chain of Responsibility)
- 1.职责链模式:使多个对象都有机会处理请求,解除请求发送者和接收者的耦合。将对象连成一条链,并沿这条链传递请求直到请求被解决。
- 2.请求交付给最小接受者,职责链中每一环保存后继的引用,使得请求有序沿链传递。
- 3.通过合理设置后继以及分支关系,避免一个请求到了链末端依旧无法被处理,或因配置错误得不到处理的情况。
3.2.策略模式(Strategy)
- 1.面向对象中并非类越多越好,类的划分是为了封装,但分类的基础是抽象,具有相同属性和功能的对象的抽象集合才是类。
- 2.策略模式:定义算法家族并分别封装,他们完成的工作相同,只是实现不同,可以互相替换。继承有助于析取这些算法的公共功能。此模式让算法的变化不会影响到使用算法的用户。
- 3.策略与工厂模式结合,使客户端需要认识的类减少,耦合度更加降低。
- 4.策略模式可以简化单元测试,因为每个算法可以通过自己的接口单独测试。
- 5.只要在不同时间内应用不同的业务规则,就可以考虑用策略模式来处理这种变化的可能性。
3.3.状态模式(State)
- 1.拥有过多分支的过长方法违背了单一职责原则,而且当需求变化时修改代码往往会违背开放-封闭原则,应该将分支变成一不同小类,将状态的判断逻辑转移到小类中。
- 2.状态模式:一个对象可能拥有多种状态,当内在状态改变时允许改变行为。
- 3.状态模式的好处是将与特定状态有关的行为局部化,并将不同状态的行为分隔开。
3.4.观察者模式(Observer)
- 1.观察者模式:多个观察者对象同时监听某一主题(通知者)对象,当该主题对象状态变化时会通知所有观察者对象,使它们能更新自己。
- 2.具体观察者保存一个指向具体主题对象的引用,抽象主题保存一个抽象观察者的引用集合,提供一个可以添加或删除观察者的接口。
- 3.抽象模式中有两方面,一方面依赖另一方面,使用观察者模式可将两者独立封装,解除耦合。
- 4.观察者模式让主题和观察者双方都依赖于抽象接口,而不依赖于具体。
- 5.委托就是一种引用方法类型。委托可看作函数的类,委托的实例代表具体函数。在主题对象内声明委托,不再依赖抽象观察者。
- 6.一个委托可以搭载多个相同原形和形式(参数和返回值)的方法,这些方法不需要属于一个类,且被依次唤醒。
3.5.迭代器模式(Iterator)
- 1.迭代器模式:提供一种方法顺序遍历一个聚集对象,为不同的聚集结构提供遍历所需接口,而不暴露对象内部的表示。
- 2.在高级编程语言如c#、c++、java等,都已经把迭代器模式设计进语言的一部分。
- 3.迭代器模式分离了对象的遍历行为,既不暴露内部结构又可以让外部代码透明的访问集合内部的数据。
3.6.备忘录模式(Memento)
- 1.备忘录模式:不破坏封装,获取对象内部状态并在其之外保存该对象,以便其未来恢复到当前状态。
- 2.Orginator负责创建Memento,Memento封装Originator状态细节,Caretaker负责保管和交付Memento。
- 3.备忘录模式适用于需要维护历史状态的对象,或只需要保存原类属性中的小部分。
3.7.命令模式(Command)
- 1.命令模式:将请求分装为对象,将请求和执行分开,可以用不同的请求对客户参数化。可以对请求排队、通过或否决、记录日志、撤销或重做。
- 2.基于敏捷开发原则,不要给代码添加基于猜测而实际不需要的功能,在需要的时候通过重构实现。
3.8.模板方法模式(Template Method)
- 1.模板方法模式:定义一个操作中的算法框架,将一些步骤延迟到子类中。子类在不改变框架的前提下就可以重新定义某些特定步骤。
- 2.当不变和可变的行为在子类中混到一起时,可以通过把重复的行为移到同一地方,帮助子类摆脱重复不变行为的纠缠。
3.9.中介者模式(Mediator)
- 1.中介者模式:用一个中介对象来封装一系列对象间的交互。
- 2.中介者模式在系统中易用也容易被误用,当系统中出现了多对多的交互复杂的对象群时,更应考虑设计的问题。
- 3.由于控制集中化,中介者模式将交互复杂性变成了中介者的复杂性,中介者类会比任何一个同事类都复杂。
中介者模式应用的场合有,一组对象以定义良好但复杂的方式进行通信,以及想定制一个分布在多个类中的行为却不想产生太多子类。
3.10.解释器模式(Interpreter)
- 1.解释器模式:给定一种语言,定义它文法的一种表示,再定义一个解释器,使用该表示来解释语言中的句子。
如果一种特定类型发生的频率足够高,就可以将其实例表达为一个句子,构建解释器来解析。
2.解释器模式就是用“迷你语言”来表现程序要解决的问题,将句子抽象为语法树。由于各个节电的类大体相同,便于修改、扩展和实现。
3.解释器为文法中的每条规则定义了一个类,当文法过多时将难以维护,建议使用其他技术如语法分析程序或编译器生成器处理。
3.11.访问者模式(Visitor)
- 1.访问者模式:在不改变各元素的前提下定义作用于这些类的新的操作。
- 2.访问者模式使用双分派,将数据结构和作用于结构上的操作解耦,意味着执行的操作决定于请求的种类和接收者的状态。
- 3.如果系统具有较为稳定的数据结构,又有易于变化的算法操作,则适合使用访问者模式。
3.12.对比总结
- 职责链模式:按顺序让负责的对象们依次处理;
- 策略模式:将算法族抽象封装;
- 状态模式:将复杂的状态转移方式下发到每个状态内部;
- 观察者模式:发布和订阅;
- 迭代器模式:遍历容器;
- 备忘录模式:在对象之外备份及恢复;
- 命令模式:封装请求与执行分开;
- 模板方法模式:提炼共有方法。
- 中介者模式:用中介对象封装交互。
- 解释器模式:迷你语言;
- 访问者模式:解耦数据结构及操作。