Skip to content

Latest commit

 

History

History
298 lines (194 loc) · 30 KB

designDiscipline.md

File metadata and controls

298 lines (194 loc) · 30 KB

设计原则/模式

设计原则

单一职责原则

单一职责原则是最简单的面向对象设计原则,它用于控制类的粒度大小。 单一职责原则(Single Responsibility Principle, SRP):一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。

单一职责原则告诉我们:一个类不能太“累”! 单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又最难运用的原则 data access object 数据访问对象

要真正用好单一职责原则并不简单,因为遵循这一原则最关键的地方在于职责的划分,博主的理解是职责的划分是根据需求定的,同一个类(接口)的设计,在不同的需求里面,可能职责的划分并不一样,为什么这么说呢?我们来看下面的例子。

开闭原则

开闭原则是面向对象的可复用设计的第一块基石,它是最重要的面向对象设计原则。开闭原则由Bertrand  Meyer于1988年提出,其定义如下:

开闭原则(Open-Closed Principle, OCP):一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。

在开闭原则的定义中,软件实体可以指一个软件模块、一个由多个类组成的局部结构或一个独立的类。 系统的设计框架是稳定的。 为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。在Java、C#等编程语言中,可以为系统定义一个相对稳定的抽象层,而将不同的实现行为移至具体的实现层中完成。在很多面向对象编程语言中都提供了接口、抽象类等机制,可以通过它们定义系统的抽象层,再通过具体类来进行扩展。如果需要修改系统的行为,无须对抽象层进行任何改动,只需要增加新的具体类来实现新的业务功能即可,实现在不修改已有代码的基础上扩展系统的功能,达到开闭原则的要求。

胖接口

对扩展开放。模块对扩展开放,就意味着需求变化时,可以对模块扩展,使其具有满足那些改变的新行为。换句话说,模块通过扩展的方式去应对需求的变化。 对修改关闭。模块对修改关闭,表示当需求变化时,关闭对模块源代码的修改,当然这里的“关闭”应该是尽可能不修改的意思,也就是说,应该尽量在不修改源代码的基础上面扩展组件。

开闭原则提高系统的可维护性和代码的重用性。

细心的朋友可能觉察到了,以面向抽象编程的方式去达到对扩展开放的目的,这和我们的依赖倒置原则(高层模块不应该依赖低层模块,两者都应该依赖其抽象)怎么这么像。可能又朋友就要不满了,不就是一个面向抽象编程,玩这么多花样干嘛,将简单的问题复杂化。博主的理解是这些设计原则没有绝对的界限,它们只是从不同的侧重点去约束我们的软件架构,使其能使用各种不同的需求。

里氏替换原则

里氏代换原则由2008年图灵奖得主、美国第一位计算机科学女博士Barbara Liskov教授和卡内基·梅隆大学Jeannette Wing教授于1994年提出。其严格表述如下:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1代换o2时,程序P的行为没有变化,那么类型S是类型T的子类型。这个定义比较拗口且难以理解,因此我们一般使用它的另一个通俗版定义: 里氏代换原则(Liskov Substitution Principle, LSP):所有引用基类(父类)的地方必须能透明地使用其子类的对象。

里氏代换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。例如:我喜欢动物,那我一定喜欢狗,因为狗是动物的子类;但是我喜欢狗,不能据此断定我喜欢动物,因为我并不喜欢老鼠,虽然它也是动物。 里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。 在使用里氏代换原则时需要注意如下几个问题:

  1. 子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。根据里氏代换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。
  2. 我们在运用里氏代换原则时,尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。里氏代换原则是开闭原则的具体实现手段之一。
  3. Java语言中,在编译阶段,Java编译器会检查一个程序是否符合里氏代换原则,这是一个与实现无关的、纯语法意义上的检查,但Java编译器的检查是有局限的。 里氏代换原则是实现开闭原则的重要方式之一。

里氏转换原则要避免重写父类的非抽象方法,而多态的实现是通过重写抽象方法实现的,所以并不冲突

为什么要符合里氏替换原则

里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。

  • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
  • 子类中可以增加自己特有的方法。
  • 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

避免继承

避免LSP妨碍的另外一个测试是:如果可能的话,尽量不用继承,在Gamma的大作《Design Patterns – Elements of Reusable Object-Orineted Software》中,我们可以看到如下建议: Favor object composition over class inheritance 尽量使用对象组合而不是类继承

当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。

讨论了组合比继承好的唯一作用是静态类型,基于类的语言 组合倾向于对象更小化,更容易想静态和动态语言语言维护。 里氏替换原则(LSP)的本质不是真的和继承有关,而是行为兼容性。 JavaScript是一个动态语言,一个对象的契约行为不是对象的类型决定的,而是对象期望的功能决定的。里氏替换原则的初始构想是作为继承的一个原则指南,等价于对象设计中的隐式接口。

里氏替换原则(LSP)表达的意思不是继承的关系,而是任何方法(只要该方法的行为能体会另外的行为就行)。

减少LSP妨碍

契约(Contracts)

  1. 检查使用测试驱动开发(Test-Driven Development)来指导你代码的设计
  2. 设计可重用类库的时候可随意使用契约设计技术

依赖倒置原则

依赖倒转原则(Dependency Inversion  Principle, DIP):抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。 依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。为了确保该原则的应用,一个具体类应当只实现接口或抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用到在子类中增加的新方法。 在实现依赖倒转原则时,我们需要针对抽象层编程,而将具体类的对象通过依赖注入(DependencyInjection, DI)的方式注入到其他对象中,依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。常用的注入方式有三种,分别是:构造注入,设值注入(Setter注入)和接口注入。构造注入是指通过构造函数来传入具体类的对象,设值注入是指通过Setter方法来传入具体类的对象,而接口注入是指通过在接口中声明的业务方法来传入具体类的对象。这些方法在定义时使用的是抽象类型,在运行时再传入具体类型的对象,由子类对象来覆盖父类对象。 在上述重构过程中,我们使用了开闭原则、里氏代换原则和依赖倒转原则,在大多数情况下,这三个设计原则会同时出现,开闭原则是目标,里氏代换原则是基础,依赖倒转原则是手段,它们相辅相成,相互补充,目标一致,只是分析问题时所站角度不同而已。

  • 高层模块不应该直接依赖于底层模块的具体实现,而应该依赖于底层的抽象。换言之,模块间的依赖是通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。
  • 接口和抽象类不应该依赖于实现类,而实现类依赖接口或抽象类。这一点其实不用多说,很好理解,“面向接口编程”思想正是这点的最好体现。

被“倒置”的依赖

软件设计架构

BLL即业务逻辑层,主要表示WEB方式,也可以表示成WINFORM方式。如果逻辑层相当强大和完善,无论表现层如何定义和更改,逻辑层都能完善地提供服务。 三层架构(3-tier architecture) 通常意义上的三层架构就是将整个业务应用划分为:界面层(User Interface layer)、业务逻辑层(Business Logic Layer)、数据访问层(Data access layer)。

  1. 数据访问层:主要是对非原始数据(数据库或者文本文件等存放数据的形式)的操作层,而不是指原始数据,也就是说,是对数据库的操作,而不是数据,具体为业务逻辑层或表示层提供数据服务.
  2. 业务逻辑层:主要是针对具体的问题的操作,也可以理解成对数据层的操作,对数据业务逻辑处理,如果说数据层是积木,那逻辑层就是对这些积木的搭建。
  3. 界面层:主要表示WEB方式,也可以表示成WINFORM方式,WEB方式也可以表现成:aspx,如果逻辑层相当强大和完善,无论表现层如何定义和更改,逻辑层都能完善地提供服务。 在经典的三层里面,高层模块直接依赖低层模块的实现,当我们将高层模块依赖于底层模块的抽象时,就好像依赖“倒置”了。这就是依赖倒置的由来。通过依赖倒置,可以使得架构更加稳定、更加灵活、更好应对需求变化。在三层架构里面增加一个接口层能实现依赖倒置,它的目的就是降低层与层之间的耦合,使得设计更加灵活。从这点上来说,依赖倒置原则也是“松耦合”设计的很好体现。 哪些设计模式遵循了依赖倒置的原则呢?这个就多了,比如我们常见的工厂方法模式。下面博主就结合一个使用场景来说说依赖倒置原则如何能够使得设计更加灵活。

上面说了那么多,都是在讲依赖倒置的好处,那么在我们的项目中究竟如何具体实现和使用呢? 在介绍依赖倒置具体如何使用之前,我们需要引入IOC容器相关的概念,我们先来看看它们之间的关系。

依赖倒置原则(DIP):一种软件架构设计的原则(抽象概念)。

控制反转(IoC):一种反转流、依赖和接口的方式(DIP的具体实现方式)。这是一个有点不太好理解和解释的概念,通俗地说,就是应用程序本身不负责依赖对象的创建和维护,而是将它交给一个外部容器(比如Unity)来负责,这样控制权就由应用程序转移到了外部IoC 容器,即控制权实现了所谓的反转。例如在类型A中需要使用类型B的实例,而B 实例的创建并不由A 来负责,而是通过外部容器来创建。 依赖注入(DI):IoC的一种实现方式,用来反转依赖(IoC的具体实现方式)。也有很多博文里面说IOC也叫DI,其实根据博主的理解,DI应该是IOC的具体实现方式,比如我们如何实现控制反转,答案就是通过依赖注入去实现。 IoC容器:依赖注入的框架,用来映射依赖,管理对象创建和生存周期(DI框架),自动创建、维护依赖对象。 关于Ioc容器,各个语言都有自己的成熟的解决方案,比如Java里面最伟大的框架之一Spring,.net里面轻量级的Autofac等。

依赖倒置原则的讲解基本结束了。根据博主的理解,设计模式的这些原则是设计模式的理论指导,而设计模式则是这些理论的具体运用。说一千道一万,要想搞懂设计模式,必须先了解设计模式遵循的原则,无论是哪种设计模式都会遵循一种或者多种原则。

A. High-level modules should not depend on low-level modules. Both should depend on abstractions. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象

Abstractions should not depend upon details. Details should depend upon abstractions. 抽象不应该依赖于细节,细节应该依赖于抽象

依赖倒置原则的最重要问题就是确保应用程序或框架的主要组件从非重要的底层组件实现细节解耦出来,这将确保程序的最重要的部分不会因为低层次组件的变化修改而受影响。

依赖注入的意思就是:依赖提供给组件,而不是组件去获取依赖,意思是创建一个依赖的实例,通过工厂去请求这个依赖,通过Service Locator或组件自身的初始化去请求这个依赖。

依赖倒置原则和依赖注入都是关注依赖,并且都是用于反转。

依赖倒置原则没有关注组件如何获取依赖,而是只关注高层模块如何从低层模块里解耦出来。某种意义上说,依赖倒置原则是控制反转的另外一种形式,这里反转的是哪个模块定义接口(从低层里定义,反转到高层里定义)。

接口隔离原则

接口隔离原则(Interface  Segregation Principle, ISP):使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。 根据接口隔离原则,当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。这里的“接口”往往有两种不同的含义:一种是指一个类型所具有的方法特征的集合,仅仅是一种逻辑上的抽象;另外一种是指某种语言具体的“接口”定义,有严格的定义和结构,比如Java语言中的interface。对于这两种不同的含义,ISP的表达方式以及含义都有所不同:

 (1) 当把“接口”理解成一个类型所提供的所有方法特征的集合的时候,这就是一种逻辑上的概念,接口的划分将直接带来类型的划分。可以把接口理解成角色,一个接口只能代表一个角色,每个角色都有它特定的一个接口,此时,这个原则可以叫做“角色隔离原则”。

 (2) 如果把“接口”理解成狭义的特定语言的接口,那么ISP表达的意思是指接口仅仅提供客户端需要的行为,客户端不需要的行为则隐藏起来,应当为客户端提供尽可能小的单独的接口,而不要提供大的总接口。在面向对象编程语言中,实现一个接口就需要实现该接口中定义的所有方法,因此大的总接口使用起来不一定很方便,为了使接口的职责单一,需要将大接口中的方法根据其职责不同分别放在不同的小接口中,以确保每个接口使用起来都较为方便,并都承担某一单一角色。接口应该尽量细化,同时接口中的方法应该尽量少,每个接口中只包含一个客户端(如子模块或业务逻辑类)所需的方法即可,这种机制也称为“定制服务”,即为不同的客户端提供宽窄不同的接口。 在使用接口隔离原则时,我们需要注意控制接口的粒度,接口不能太小,如果太小会导致系统中接口泛滥,不利于维护;接口也不能太大,太大的接口将违背接口隔离原则,灵活性较差,使用起来很不方便。一般而言,接口中仅包含为某一类用户定制的方法即可,不应该强迫客户依赖于那些它们不用的方法。

迪米特法则

迪米特法则又称为最少知识原则(LeastKnowledge Principle, LKP),其定义如下: 迪米特法则(Law of  Demeter, LoD):一个软件实体应当尽可能少地与其他实体发生相互作用。 如果一个系统符合迪米特法则,那么当其中某一个模块发生修改时,就会尽量少地影响其他模块,扩展会相对容易,这是对软件实体之间通信的限制,迪米特法则要求限制软件实体之间通信的宽度和深度。迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系。 迪米特法则还有几种定义形式,包括:不要和“陌生人”说话、只与你的直接朋友通信等,在迪米特法则中,对于一个对象,其朋友包括以下几类:

  1. 当前对象本身(this);
  2. 以参数形式传入到当前对象方法中的对象;
  3. 当前对象的成员对象;
  4. 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;
  5. 当前对象所创建的对象。

迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。 在将迪米特法则运用到系统设计中时,要注意下面的几点:在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及;在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;在类的设计上,只要有可能,一个类型应当设计成不变类;在对其他类的引用上,一个对象对其他对象的引用应当降到最低。 可以通过引入一个专门用于控制界面控件交互的中间类(Mediator)来降低界面控件之间的耦合度。引入中间类之后,界面控件之间不再发生直接引用,而是将请求先转发给中间类,再由中间类来完成对其他控件的调用。当需要增加或删除新的控件时,只需修改中间类即可,无须修改新增控件或已有控件的源代码,重构后结构如图2所示:

S.O.L.I.D 代表什么:

S – 单一职责原则

英文缩写SRP,全称Single Responsibility Principle 原始定义:There should never be more than one reason for a class to change。 一个类应该有且只有一个去改变它的理由,这意味着一个类应该只有一项工作。 职责即变化的原因 结论 单一职责听起来很简单,做起来却很难,因为我们总是自然而然地倾向与把职责结合在一起(习惯于面向过程的思维)。

O – 开放封闭原则

开闭原则,英文缩写OCP,全称Open Closed Principle。 原始定义:Software entities (classes, modules, functions) should be open for extension but closed for modification。 对象或实体应该对扩展开放,对修改封闭。

L – 里氏替换原则

在对象 x 为类型 T 时 q(x) 成立,那么当 S 是 T 的子类时,对象 y 为类型 S 时 q(y) 也应成立。(即对父类的调用同样适用于子类) 引用基类的地方必须可以使用其子类的对象代替

I – 接口隔离原则

原始定义:Clients should not be forced to depend upon interfaces that they don't use, 还有一种定义是The dependency of one class to another one should depend on the smallest possible interface。 不应强迫客户端实现一个它用不上的接口,或是说客户端不应该被迫依赖它们不使用的方法。 模块依赖接口,细节依赖抽象

D – 依赖倒置原则

依赖倒置原则,英文缩写DIP,全称Dependence Inversion Principle 原始定义:High level modules should not depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions。 实体必须依靠抽象而不是具体实现。它表示高层次的模块不应该依赖于低层次的模块,它们都应该依赖于抽象。 多个专门接口,而不是一个总接口

设计模式

装饰器模式

用于给对象在运行期间动态的增加某个功能,职责等。相较通过继承的方式来扩充对象的功能,装饰器显得更加灵活,首先,我们可以动态给对象选定某个装饰器,而不用hardcore继承对象来实现某个功能点。其次:继承的方式可能会导致子类繁多,仅仅为了增加某一个单一的功能点,显得有些多余了。

**天猫使用的Nodejs框架Koa就基于装饰器函数及ES2015的Generator。**希望这篇博客能起到抛砖引玉的作用,使你编写更优雅的JS代码。

5种不同的实现方式分别是:

  1. 闭包
  2. 猴子补丁
  3. 原型继承
  4. 代理(ES6)
  5. 中间件

官方:什么是闭包?

“即使函数在变量的作用域之外被调用,闭包允许函数访问闭包引用的变量。”(我稍微重新措辞从维基百科的定义)

闭包是 JavaScript 最重要和实用的特性之一,所以确保你现在已经领悟它了。 虽然它和上面的包装方法有一点关系,但事实上,本文所展示的技术都使用了闭包来隐藏私有变量。

什么是猴子补丁?

“动态修改一个类或模型。”   “我将要采用JavaScript的动态性并结合对象可变性的特点来用我的方法取代你的方法!”   “我准备用我的方法替换你的,然后我会从我的方法内部包装并调用你的方法。”

设置对象的原型声明一个对象基于另一个对象。

这就意味着:如果需要访问一个对象成员,对象首先会在自身之中查找,但如果没有找到,它会去它的原型上继续查找,并一直按照这个方式查找到原型链的终点。

函数总是能处理你在 JavaScript 中遇到的任何问题。ES6中增加了代理模块,它看上去有希望去完成一些关于面向切片的编程技术。让我们来看看,它能不能帮我们创建一个装饰器。

面向切面编程AOP

这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。

面向对象的特点是继承、多态和封装。

而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用。面向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来。

也许有人会说,那好办啊,我们可以将这段代码写在一个独立的类独立的方法里,然后再在这两个类中调用。但是,这样一来,这两个类跟我们上面提到的独立的类就有耦合了,它的改变会影响这两个类。那么,有没有什么办法,能让我们在需要的时候,随意地加入代码呢?这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。

一般而言,我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。

这样看来,AOP其实只是OOP的补充而已。OOP从纵向上区分出一个个的类来,而AOP则从横向上向对象中加入特定的代码。有了AOP,OOP变得立体了。如果加上时间维度,AOP使OOP由原来的二维变为三维了,由平面变成立体了。从技术上来说,AOP基本上是通过代理机制实现的。 AOP在编程历史上可以说是里程碑式的,对OOP编程是一种十分有益的补充。

“代理对象通常用来为基本操作定义自定义行为(例如:属性查找,赋值,枚举或函数调用等)。 -MDN

JavaScript ( JS ) 是一种轻量级解释型的,或是JIT编译型的程序设计语言,有着 头等函数 (First-class Function) 的编程语言。虽然它是作为开发web页面的脚本语言而出名的,但是在很多非浏览器环境中也使用JavaScript,例如 node.js 和 Apache CouchDB。JS是一种基于原型、多范式的动态脚本语言,并且支持面向对象、命令式和声明式(如:函数式编程)编程风格。

装饰器将内部对象作为参数输入,并返回它的代理。在代理中,我们只处理一件事:属性访问。我们通过为 “get” 处理程序添加自定义行为来做到这点。

这里的关键点是,虽然为了创建我们的代理对象不得不做一些额外的工作,但无论装饰对象中有多少个成员,装饰器都不会变的更复杂。所以,代理模式有2个优点:

  • 1.它不是猴子补丁
  • 2.不必手动重新定义内部对象的每个成员

这可能有点杀鸡用牛刀了,原型继承有着相同的优势。代理的实现是被用来处理面向切片风格的东西,而不是装饰器。

那么 JavaScript 的动态原型,python 的装饰器,ruby 的重新打开类。看来都是为了实现同样的目的了。

之前的那些例子都有一个非常棒的特性,那就是初始的对象不必知道它被装饰了。通过闭包,猴子补丁,继承或者代理来扩展初始对象的行为而不必修改它,这就是面向对象设计的开闭原则

通过基础对象自身来创建装饰链,能够获得装饰器更多的控制权。

通过将对象设置为可以被装饰和完成建立装饰链的工作,我们达成了这些目标:

  1. 简单的装饰方法
  2. 建立装饰链时,更多的控制权
  3. 简单地建立装饰列表,只需传递一个有顺序的装饰器数组的方法,而不必关心特殊装饰者模式实现的构造机制
  4. 依旧符合开闭原则,基本实现允许在不修改原始对象的情况下完成装饰
  5. 它不是猴子补丁,也不依赖于代理

这是最复杂的实现方式,如果你设置了一些重量级的装饰器,需要更多的管理而不是简单的包装,那么这个实现可能适合你。

我称呼它为“中间件”实现,是因为:

  1. 我想不出比这个更好的
  2. Dan Abramov 使用相同的方法在他的 redux 中间件实现中

结论

我们着眼于用5个不同的技术来实现装饰者模式,在这过程中我们学到了不少。

每个实现方式都使用了闭包。这应该能让你明白它在 JavaScript 中有多重要。如果你仍不理解它,退回去再读一次,或者去阅读一些别人更好的描述。

哪个是明显的赢家?当我开始写这篇的时候,我期望每个技术都有它的优势和劣势,取决于使用场景。事实上,写到最后我认为只有2种实现方式值得被使用:1.原型继承2.中间件

只有当需要对装饰链进行更多控制的时候才使用中间件的方式,否则,原型继承似乎对我来说就是最终赢家。它具有所有的优点:

1.不需要修改基础对象

2.不需要复制每个成员到新的对象

3.不是猴子补丁

4.不支持差的代码

5.相对简单的装饰方法

我们谈论的是装饰器,然而装饰器很难和 ES6 class 语法一起工作。正因为此有项提议在 ES7 中应当解决装饰器的问题。然而,正如我希望你看到这篇文章,如果你继续使用函数语法,通过简单的 JavaScript 语法就能创建功能强大的装饰器。

无论是 new 关键字还是 class 关键字在 ES6 中都让人迷惑。把它们加进 JavaScript 中看上去会让从传统语言,像 Java,转过来的人感觉更舒适,但结果是笨重的,只会掩盖原型的真正能力和简单。

装饰器模式关注于在一个对象上动态的添加方法,然而代理模式关注于控制对对象的访问。

用代理模式,代理类(proxy class)可以对它的客户隐藏一个对象的具体信息。因此,当使用代理模式的时候,我们常常在一个代理类中创建一个对象的实例。

并且,当我们使用装饰器模 式的时候,我们通常的做法是将原始对象作为一个参数传给装饰者的构造器。 使用代理模式,代理和真实对象之间的的关系通常在编译时就已经确定了,而装饰者能够在运行时递归地被构造。