设计模式遵守哪些规则?
- 依赖倒换原则 要针对接口编程,不要对实现编程:高层模块不应该依赖于低层模块、抽象不应该依赖细节,细节应该依赖抽象。
- 单一职责原则 就一个类而言,应该仅有一个引起它变化的原因。如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到意想不到的破坏。比如设计游戏显示区域,将绝对坐标改成相对坐标,实现程序逻辑和界面的分离。
- 开放-封闭原则 软件实体(类、模块、函数等等)应该可以扩展,但是不可修改。 面对需求,对程序的改动是通过增加新代码进行的,而不是更改现有的代码。 最初编写代码时,假设变化不会发生,当变化发生时,就创建抽象来隔离以后发生的同类变化。 开发人员应该仅对程序中呈现出频繁变化的那部分作出抽象,然而对于程序中的每个部分都刻意地进行抽象同样不是一个好主意。
- 迪米特法则 如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。在类的结构设计上,每一个类都应当尽量降低成员的访问权限。其根本思想是强调了类之间的松耦合,类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成波及。
- 里氏代换原则 一个软件实体,如果使用的是一个父类的话,那么一定适用于其子类,而且它觉察不出父类对象和子类对象的区别。正是由于子类型的可替换性才使得使用父类类型的模块在无需修改的情况下就可以扩展。
依赖注入、控制反转
IoC(Inversion of Control)控制反转 DI(Dependency Injection)依赖注入 DI是IoC的一种具体实现,另一种主要的实现方式是服务定位器(Service Locator)。 没有IoC的时候,常规的A类使用C类的示意图: 有IoC的时候,A类不再主动去创建C,而是被动等待,等待IoC的容器获取一个C的实例,然后反向地注入到A类中。
依赖、关联、聚合、组合的区别。
- 依赖:Uses a。这种使用关系是具有偶然性的、临时性的、非常弱的,但是B类的变化会影响到A;比如类B作为参数被类A在某个method方法中使用;
- 关联:Has a。这种关系比依赖更强、不存在依赖关系的偶然性、关系也不是临时性的,一般是长期性的,而且双方的关系一般是平等的、关联可以是单向、双向的;表现在代码层面,为被关联类B以类属性的形式出现在关联类A中,也可能是关联类A引用了一个类型为被关联类B的全局变量;
- 聚合:Own a。聚合是关联关系的一种特例,他体现的是整体与部分、拥有的关系,即has-a的关系,此时整体与部分之间是可分离的,他们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享;比如计算机与CPU、公司与员工的关系等;表现在代码层面,和关联关系是一致的,只能从语义级别来区分;
- 组合:is a part of。这种关系比聚合更强,也称为强聚合;他同样体现整体与部分间的关系,但此时整体与部分是不可分的,整体的生命周期结束也就意味着部分的生命周期结束。
设计模式分类总结
创建型模式
-
抽象工厂;提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。使得改变一个应用的具体工厂变得非常容易,它只需要改变具体工厂即可使用不同的产品配置。此外,它让具体的创建实例过程与客户端分离,客户端是通过它们的抽象接口操纵实例,产品的具体类名也被具体工厂的实现分离,不会出现在客户代码中。
-
生成器;将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。用户只需指定需要建造的类型就可以得到它们,而具体的过程和细节就不需知道了。
-
工厂方法;定义了一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。“MVC三层架构”使用的就是该模式。
-
原型;用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。其实就是从一个对象再创建另外一个可定制的对象。
-
单例;保证一个类仅有一个实例,并提供一个访问它的全局访问点。即,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建,并且它可以提供一个访问该实例的方法。简单来说就是对唯一实例的受控访问。
结构型模式
- 适配器;将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。适配器模式包括类适配器模式和对象适配器模式,类适配器模式需要支持多重继承。
- 桥接;将抽象部分与它的实现部分分离,使它们都可以独立的变化。其实就是实现系统可能有多角度分类,每一种分类都有可能变化,那么就把这种多角度分离出来让它们独立变化,减少它们之间的耦合。
- 组合;将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。 当需求中是体现部分与整体层次的结构时,或者希望用户可以忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象时,就应该考虑用组合模式了。
- 装饰;
- 外观;为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
- 享元;运用共享技术有效地支持大量细粒度的对象。 如果发现某个对象的生成了大量细粒度的实例,并且这些实例除了几个参数外基本是相同的,如果把那些共享参数移到类外面,在方法调用时将他们传递进来,就可以通过共享大幅度减少单个实例的数目。
- 代理;为其他对象提供一种代理以控制对这个对象的访问。代理类和被代理类都继承于同样的基类(因此具有同样的对外接口),代理类中维护一个被代理类的引用,并将实际工作委托给被代理类完成(代理类可以添加额外的操作)。
行为模式
-
职责链;使多个对象都有机会处理请求,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。这就使得接收者和发送者都没有对方的明确信息,且链中的对象自己也并不知道链的结构。
-
命令;将一个请求封装为一个对象,从而使得可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。
-
解释器;给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。解释器模式使得可以很容易地改变和扩展文法,因为该模式使用类来表示文法规则,可使用继承来改变或扩展该文法。也比较容易实现文法,因为定义抽象语法树中各个节点的类的实现大体类似,这些类都易于直接编写。 不足:解释器模式为文法中的每一条规则至少定义了一个类,因此包含许多规则的文法可能难以管理和维护。当文法非常复杂时,使用其他的技术如语法分析程序或编译器生成器来处理更好。
-
迭代器;提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。
-
中介者;用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
-
备忘录;在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可以将该对象恢复到原先保存的状态。
-
观察者;定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。 当一个对象的改变需要同时改变其他的对象,而且它不知道具体有多少对象有待改变时,应该考虑使用观察者模式。观察者模式所做的工作其实就是在解除耦合。让耦合的双方都依赖于抽象,而不是依赖于具体,从而使得各自的变化都不会影响另一边的变化。 观察者模式结构图: 不足:尽管已经用了依赖倒换原则,但是“抽象通知者”还是依赖“抽象观察者”,也就是说,万一没有了抽象观察者这样的接口,通知的功能就完成不了。此外,每个具体的观察者不一定就是调用相同的“更新”方法。 改进(事件委托实现):委托就是一种引用方法的类型。一旦为委托分配了方法,委托将与该方法具有完全相同的行为。委托方法的使用可以像其他任何方法一样,具有参数和返回值。委托可以看作是对函数的抽象,是函数的“类”,委托的实例将代表一个具体的函数。一个委托可以搭载多个方法,所有方法被依次唤起,并且可以使得委托对象所搭载的方法并不需要属于同一个类。不过委托对象所搭载的方法必须具有相同的原型和形式。
-
状态;当一个对象的内在状态改变时允许改变其行为,这个对象看起来是改变了其类。 状态模式主要解决的是当控制一个对象状态转换的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类当中,可以把复杂的判断逻辑简化。当然,如果这个状态判断很简单,就没有必要这么做了。 当一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为时,就可以考虑使用状态模式了。
-
策略;策略模式(Strategy)定义了算法家族,让它们之间可以互相替换,此模式让算法的变化不会影响到使用算法的客户。
-
模板方法;定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。模板方法模式是通过把不变行为搬到超类中,去除子类中的重复代码来体现它的优势。当不变的和可变的行为在方法的子类实现中混合在一起的时候,不变的行为就会在子类中重复出现。通过模板方法模式把这些行为搬移到单一的地方,这样就帮助子类摆脱重复的不变行为的纠缠。
-
访问者;表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。 访问者模式适用于数据结构相对稳定的系统,它把数据结构和作用于结构上的操作之间的耦合解脱开,使得操作集合可以相对自由地演化。访问者的优势在于增加新的操作很容易,因为这就意味着增加一个新的访问者。不足在于,使增加新的数据结构变得困难了。
代码重构方法总结
- 当某个被修改后的函数需要从调用端得到更多信息,为此函数添加一个对象参数,让该对象带进函数所需的信息。
- 两个类之间原有双向关联,但是现在其中一个类不再需要另一个类的特性,所以去除不必要的关联。
- 当有一个引用对象,很小且不可变,不易管理,将它改为一个值对象。
- 当两个类都需要使用对方的特性,但其间只有一条单向连接时,添加一个反向指针,并使修改函数(改变双方关系的函数)能够同时更新两条连接。
- 当从一个类衍生出许多彼此相等的实例时,考虑将它们替换为同一个对象。
- 当超类和子类之间无太大区别时,将它们合为一体。
- 当有一系列条件测试都得到相同的结果(最终行为一致),可以将这些测试合并为一个条件表达式(使用逻辑或、逻辑与),并将这个条件表达式提炼为一个独立函数。
- 在条件表达式的每个分支上有着相同的一段代码时,将这段重复代码搬移到条件表达式之外。
- 对于已有的传统过程化风格的代码,将数据记录变成对象,将大块的行为分为小块,并将行为移入相关对象之中。
- 将条件表达式段落中的内容提炼为独立的函数。
- 有一些领域数据置身于GUI控件中,而领域函数需要访问这些数据时,将该数据复制到一个领域对象中,建立一个Observer模式,用以同步领域对象和GUI对象内的重复数据。
- 当一个函数返回一个集合时,让这个函数返回该集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数。 取值函数不应该返回集合自身,因为这会让用户得以修改集合内容,而集合拥有者却一无所知。
- 当某个函数返回的对象需要由函数调用者执行向下转型时,将向下转型动作移到该函数中,并修改函数的返回类型。
- 将public字段声明为private,并提供相应的访问函数。
- 某个类做了应该由两个类做的事,则建立一个新类,将相关的字段和函数从旧类搬移到新类。 如果某些数据和某些函数总是一起出现,某些数据经常同时变化甚至彼此相依,这就表示应该将它们分离出去。
- 当一个类做了太多工作,其中一部分工作是以大量条件表达式完成的。则建立继承体系,以一个子类表示一种特殊情况。
- 当若干客户使用类接口中的同一子集,或者两个类的接口有部分相同时,将相同的子集提炼到一个独立接口中。
- 对于一段可以被组织在一起并独立出来的代码,将其放进一个独立函数中,并让函数名称解释该函数的用途。
- 当类中的某些特性只被某些(而非全部)实例用到时,新建一个子类,将这些特性移到子类中。
- 当两个类具有相似特性时,为这两个类建立一个超类,将相同特性移至超类。
- 有一些子类,其中相应的某些函数以相同顺序执行类似的操作,但各个操作的细节上有所不同时,将这些操作分别放进独立函数中,并保持它们都有相同的签名,然后将函数上移至超类。
- 当客户类通过一个委托类来调用另一个对象时,在服务类上建立客户所需的所有函数,用以隐藏委托关系。
- 如果某个客户先通过服务对象的字段得到另一个对象,然后调用后者的函数,那么客户就必须知晓这一层委托关系,万一委托关系发生变化,客户也得相应变化,这不是好的封装,应该将这一层委托隐藏起来。
- 对于未被其他任何类使用到的函数,将其修改为private。
- 当某个类没有做太多事情时,将这个类的所有特性搬移到另一个类中(该“萎缩类”的最频繁用户),然后移除原类。
- 当一个函数的本体与名称同样清楚易懂时,在函数的调用点插入函数本体,然后移除该函数。
- 对于只被一个简单表达式赋值一次的临时变量,将对该变量的引用动作替换为对它赋值的那个表达式自身。
- 当某一段代码需要对程序状态做出某种假设时,以断言明确表现这种假设。
- 对于复杂的表达式(通常用于条件判断),将该复杂表达式(或其中一部分)的结果放进一个临时变量,并以此变量名称来解释表达式的用途。
- 有时需要为提供服务的类增加一个函数,但无法直接修改这个类。可以在客户类中建立一个函数,并以第一参数的形式传入一个服务类实例。
- 有时需要为提供服务的类增加一些函数,但无法直接修改这个类。可以建立一个新类,使它包含这些额外函数,然后将该类修改为服务类的子类或者包装类。
- 当需要再三检查某个对象是否为null时,将null值替换为null对象。为类新增isNull()方法,null对象的该方法返回true。
- 当某些参数总是同时出现时,以一个对象取代这些参数。
- 当类的某个字段被另一个类更多地用到时,在目标类中新建一个字段,修改源字段的所有用户,令他们改用新字段。
- 当类的某个函数和另一个类进行更多的交流:调用后者或者被后者调用时,在该函数最常引用的类中建立一个有着类似行为的新函数,将旧函数变成一个单纯的委托函数,或是将旧函数完全移除。
- 当若干函数做了类似的工作,但是在函数体中使用了不同的值时,建立一个单一函数,并以参数表达那些不同的值。
- 当从某个对象中取出若干值,并将它们作为某一次函数调用的参数时,改为传递整个对象。
- 当在各个子类中拥有一些本体几乎完全一致的构造函数时,在超类中新建一个构造函数,并在子类构造函数中调用它。
- 当两个子类拥有相同的字段时,将该字段移至超类。
- 对于在各个子类中产生完全相同结果的函数,将其移至超类。
- 当超类中的某个字段只被部分(而非全部)子类用到时,将这个字段移到需要它的那些子类去。
- 当超类中的某个函数只与部分(而非全部)子类有关时,将这个函数移到相关的那些子类去。
- 不要在函数体中对函数参数进行赋值,这会降低代码的清晰度,混用按值传递和按引用传递这两种参数传递方式。当函数中对形参进行赋值时,以一个临时变量取代该参数的位置(即新增一个临时变量,并将形参赋值给它,然后基于该临时变量进行操作)。
- 当在一系列布尔表达式中某个变量带有“控制标记”(比如用于控制是否继续循环)的作用时,以break语句或return语句取代控制标记。
- 当某个类做了过多的简单委托动作时,让客户直接调用受托类。
- 函数体中不再需要某个参数时,将该参数去除。
- 当类中的某个字段应该在对象创建时被设值,然后就不再改变时,去掉该字段的所有设置函数。
- 当函数的名称未能揭示函数的用途时,修改函数的名称。
- 当类中有一个数组,其中的元素各自代表不同的东西时,以对象替换数组,对于数组中的每个元素以一个字段来表示。
- 当有某个条件表达式,它根据对象类型的不同而选择不同的行为时,将这个条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数。
- 当希望在创建对象时不仅仅是做简单的构建动作时,将构造函数替换为工厂函数。 如以工厂函数取代构造函数中的类型码参数。
- 当一个数据项需要与其他数据和行为一起使用才有意义时,将数据项变成对象。
- 当在两个类之间使用委托关系,并经常要为整个接口编写许多极其简单的委托函数时,让委托类继承受托类。
- 当某个函数返回一个特定的代码,用以表示某种错误时,改用抛出特定的异常。
- 对于一个调用者可以预先检查的条件,应该在使用调用函数之前先做检查,而不是抛出一个异常。
- 当某个类只使用超类接口中的一部分,或者根本不需要继承而来的数据时,在子类中新建一个字段用以保存超类,调整子类函数另它改而委托超类,然后去掉两者之间的继承关系。
- 当有一个字面数值带有特别的意义时,创造一个常量,根据其意义为它命名,并将上述的字面数值替换为这个常量。
- 当有一个大型函数,其中的局部变量的使用阻碍了Extract Method重构方法时,将这个函数放进一个单独对象中,这样局部变量就成了对象内的字段,然后可以在同一个对象中将这个大型函数分解为多个小型函数。
- 在一系列条件判断中,当某个条件极其罕见,就应该单独检查该条件,并在条件为真时立刻从函数中返回,这样的单独检查被称为卫语句。
- 函数中的条件逻辑使人难以看清正常的执行路径时,使用卫语句表现所有特殊情况。
- 当一个函数完全取决于参数值来采取不同的行为时,针对该参数的每一个可能值建立一个独立函数。
- 当对象调用某个函数,并将所得的结果作为参数传递给另一个函数,而接受该参数的函数本身也能够调用前一个函数时,让参数接受者去除该项函数,并直接调用前一个函数。
- 当需要面对传统编程环境中的记录结构时,为该记录创建一个“哑”数据对象。
- 当各个子类的唯一差别只在“返回常量数据”的函数身上时,修改这些函数,使它们返回超类中的某个(新增)字段,然后销毁子类。
- 当程序以一个临时变量保存某一表达式的运算结果时,将这个表达式提炼到一个独立函数中,将这个临时变量的所有引用点替换为对新函数的调用,此后,新函数就可以被其他函数使用。
- 当类中有一个数值类型码,但它不影响类的行为时,以一个新的类替换该数值类型码。
- 当类中有一个类型码,它会影响类的行为,但不能通过继承手法消除它时(比如类型码的值在对象生命周期中会发生变化),以状态对象取代类型码。
- 当类中有一个类型码,它会影响类的行为,以子类取代这个类型码。
- 不直接访问字段,而是为这个字段建立取值、设值函数,并且只以这些函数来访问字段。
- 当某些GUI类中包含了领域逻辑时,将领域逻辑分离出来,为它们建立独立的领域类。
- 当某个函数既返回对象状态值,又修改对象状态时,建立两个不同的函数,其中一个负责查询,另一个负责修改。
- 当有一个临时变量被赋值超过一次,它既不是循环变量,也不被用于收集计算结果时,针对每次赋值创造一个独立、对应的临时变量。
- 将函数本体替换为另一个算法(保持形参列表、返回值不变)。
- 当某个继承体系同时承担两项责任时,建立两个继承体系,并通过委托关系让其中一个可以调用另一个。
纯函数
假如满足下面这两个句子的约束,一个函数可能被描述为一个纯函数:
- 给出同样的参数值,该函数总是求出同样的结果。该函数结果值不依赖任何隐藏信息或程序执行处理可能改变的状态或在程序的两个不同的执行,也不能依赖来自I/O装置的任何外部的输入。
- 结果的求值不会促使任何可语义上可观察的副作用或输出,例如易变对象的变化或输出到I/O装置。 该结果值不需要依赖所有(或任何)参数值。然而,必须不依赖参数值以外的东西。函数可能返回多重结果值,并且对于被认为是纯函数的函数,这些条件必须应用到所有返回值。假如一个参数通过引用调用,任何内部参数变化将改变函数外部的输入参数值,它将使函数变为非纯函数。
一些非纯函数:
- 返回当前天星期几的函数是一个非纯函数,因为在不同的时间它将产生不同的结果,它引用了一些全局状态。同样地,任何使用全局状态或静态变量潜在地是非纯函数。
- random()是非纯函数,因为每次调用潜在地产生不同的值。这是因为伪随机数产生器使用和更新了一个全局的“种子”状态。加入我们修改它去拿种子作为参数,例如random(seed),那么random变为纯函数,因为使用同一种子值的多次调用返回同一随机数。
- printf() 是非纯函数,因为它促使输出到一个I/O装置,产生了副作用。