Skip to content

jacksplwxy/DesignPatterns_TypeScript

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

57 Commits
 
 
 
 
 
 
 
 

Repository files navigation


* 编程语言的本质都是字符,当我们为字符赋予特性含义时,便产生了lisp、js、java等不同的程序语言

* 什么是设计模式?
  · 狭义的设计模式:指的是GoF四人组在《Design Patterns: Elements of Reusable Object-Oriented Software》一书中提出的23种设计模式。
  · 广义的设计模式:最早的设计模式是美国著名建筑大师克里斯托夫·亚历山大在他的著作《建筑模式语言:城镇、建筑、构造》中描述了一些常见的建筑设计问题,并提出了253种关于对城镇、邻里、住宅、花园和房间等进行设计的基本模式。后来软件界也开始论坛设计模式的话题,因为这也是类似的。所以设计模式指的是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,是可以反复使用的。


* 为什么要学习设计模式?
  · 什么是高质量代码:
    在弄清楚为什么要学习设计模式之前,我们先思考什么才是高质量的代码?高质量代码的特点:
    -- 代码bug少
    -- 代码够健壮
    -- 代码易测试
    -- 代码易阅读
    -- 代码易扩展
    -- 代码性能高
    更多高质量代码的内容可查看我的这篇总结:https://github.com/jacksplwxy/Good-Code
  · 如何开发出高质量代码呢?一个重要的方式就是遵守前人总结的7大设计原则:
    -- 开闭原则:要对扩展开放,对修改关闭
    -- 里氏替换原则:不要破坏继承体系
    -- 依赖倒置原则:要面向接口编程
    -- 单一职责原则:实现类要职责单一
    -- 接口隔离原则:在设计接口的时候要精简单一
    -- 迪米特法则:要降低耦合度
    -- 合成复用原则:要优先使用组合或者聚合关系复用,少用继承关系复用
  · 设计原则与设计模式的关系:
    -- 好的程序需要靠7大设计原则完成,但是由于语言的缺陷导致程序需要按照一定复杂度的步骤去实现这些设计原则,而这些步骤通常都是固定的,就像武功中的套路招式一样,如果再给这些套路加上好听的名字,这就成了设计模式。也就是说23种设计模式就是7大设计原则在某些语言的具体实现的一种方式,每个设计模式的背后我们都能找到其依靠的一种或多种设计原则。换句话说就是,只要我们写代码遵循设计原则,代码就会自然而然变成了某些设计模式,这也在王垠的《解密“设计模式”》中得到证明。
    -- 由于现实问题的复杂性,往往导致代码不可能同时满足所有的设计原则,甚至要违背部分设计原则,这里就会有一个最佳实践的问题了。而设计模式就为解决特定问题提供了最佳实践方案,以至于学习了设计模式后,在遇到特定问题时,我们脑子很容易知道如何在满足设计原则最优解的基础上实现代码的编写。
    -- 虽然设计原则为开发出高质量代码指明了方向,但没有对程序的高性能等做出指示,而设计模式在这方面做了补充。
  · 结论:
    -- 设计模式通过写代码的形式帮助我们更好的学习理解设计原则,以实现高质量代码;
    -- 为实现设计原则的招式统一命名;
    -- 为特定场景问题的提供最优解决方案;
    -- 补充了设计原则在构建高性能程序等方面的内容。


* 解耦与高质量代码之间的关系:
  · 解耦(低耦合)在某个角度上说属于高质量代码的一个重要体现,但不是全部。高质量代码具体还体现在代码可测试性、代码健壮性、代码高性能等等许多细节方面。
  · 耦合性高低的衡量标准:https://www.zhihu.com/question/21386172/answer/54476702
    -- 依赖性:模块之间依赖尽量少
    -- 正交性:模块之间重复功能尽量少
    -- 紧凑性:模块暴露的方法和参数尽量少
  · 实际上这3个指标大概分别对应着7大设计原则中的迪米特法则、单一职责原则、接口隔离原则等原则,而7大设计原则也就是高质量软件的编写原则,所以也印证了解耦是属于高质量代码的一部分体现。
  · 解耦的本质:减少系统需要维护的状态总数
    -- 减少模块内部的状态数:例如一个vue文件中有a、b、c共3个状态变量,每个变量分别有3种状态,那么这个vue文件的状态总数就是3^3=27;可以通过减少状态数或者将利用vue的computed将状态归类映射为其他的少数状态
    -- 缩小状态的作用域:状态越难被改变,可能存在的状态数就越少,例如将全局变量尽可能的转为局部变量、使用闭包、只允许使用指定方法改变状态;
    -- 模块间方法调用的单向依赖:调用其他模块方法目的基本都是为了改变对方的状态,如此对方的状态被耦合到了本模块,状态数翻倍
    -- 模块间数据调用的单向依赖:即单向数据流,与方法单向依赖一样,可以减少状态在两个模块中都存在导致的状态数翻倍


* 学习设计模式的常见问题:
  · 学习设计模式的核心是掌握设计模式的意图是什么:不同的设计模式在不同的语言中会有不同表现形式,千万不要被模式中的各种概念迷惑,而只学到表面套路。我看到很多热门教程是示例代码甚至是错误的,例如这个超过2k star的设计模式项目https://github.com/torokmark/design_patterns_in_typescript ,他的工厂方法的实现就不对,如果要增加新的产品,他就必须必须修改createProduct代码,但这违背了开闭原则。导致这个错误的原因就是因为作者没有理解工厂方法的目的:对简单工厂模式的进一步抽象化,使系统在不修改原来代码的情况下引进新的产品,即满足开闭原则。又如这个项目的原型模式的实现也是错误的,原型模式的场景是因为复制一个对象比new一个对象更高效。所以学习设计模式的正确姿势应该是:掌握该设计模式的意图,并在遵守设计原则的情况下去实现它。
  · 分辨模式的关键是意图而不是结构:在设计模式的学习中,有人经常会发出这样的疑问:代理模式和装饰着模式,策略模式和状态模式,这些模式的类图看起来几乎一模一样,他们到底有什么区别?实际上这种情况是普遍存在的,许多模式的类图看起来都差不多,模式只有放在具体的环境下才有意义。比如我们的手机,用它当电话的时候它就是电话;用它当闹钟的时候它就是闹钟;用它玩游戏的时候他就是游戏机。有很多模式的类图和结构确实很相似,但这不太重要,辨别模式的关键是这个模式出现的场景,以及为我们解决了什么问题。
  · 设计模式一直在发展,例如现在逐渐流行起来的模块模式、沙箱模式等,但真正得到人们的认可还需要时间的检验。
  · 设计模式的合理使用:
    -- 总体来说,使用设计模式的必要性的程度是逐级递增的:应用程序(Application) < 工具包/类库(ToolKit/Library) < 框架(Framework)
    -- 具体来说,我们不必刻意为了设计模式而设计模式,例如我们当前只需要创建一个产品,而未来没有多大可能增加新产品时,我们就用简单工厂模式,而无需为了符合开闭原则而去选择工厂方法模式。引用轮子哥的话:“为了合理的利用设计模式,我们应该明白一个概念,叫做扩展点。扩展点不是天生就有的,而是设计出来的。我们设计一个软件的架构的时候,我们也要同时设计一下哪些地方以后可以改,哪些地方以后不能改。倘若你的设计不能满足现实世界的需要,那你就要重构,把有用的扩展点加进去,把没用的扩展点去除掉。这跟你用不用设计模式没关系,跟你对具体的行业的理解有关系。倘若你设计好了每一个扩展点的位置,那你就可以在每一个扩展点上应用设计模式,你就不需要去想到底这个扩展点要怎么实现他才会真正成为一个扩展点,你只需要按照套路写出来就好了。
  · 设计模式命名:为了他人能快速理解你使用的设计模式,建议参考模式英文名进行命名,例如:建造者模式——xxxBuilder,单例模式——xxxSingleton,适配器模式——xxxAdapter,状态模式——xxxState,策略模式——xxxStratege等等。
  · 对设计模式的误解:
    -- 习惯把静态语言的设计模式照搬到动态语言中
    -- 习惯根据模式名称去臆测该模式的一切


* 开闭原则:
  · 定义:一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。
  · 解释:
    当软件系统需要面对新的需求时,我们应该尽量保证系统的设计框架是稳定的。如果一个软件设计符合开闭原则,那么可以非常方便地对系统进行扩展,而且在扩展时无须修改现有代码,使得软件系统在拥有适应性和灵活性的同时具备较好的稳定性和延续性。
    为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。在Java、C#等编程语言中,可以为系统定义一个相对稳定的抽象层,而将不同的实现行为移至具体的实现层中完成。在很多面向对象编程语言中都提供了接口、抽象类等机制,可以通过它们定义系统的抽象层,再通过具体类来进行扩展。如果需要修改系统的行为,无须对抽象层进行任何改动,只需要增加新的具体类来实现新的业务功能即可,实现在不修改已有代码的基础上扩展系统的功能,达到开闭原则的要求。
    在软件开发中,一般不把对配置文件的修改认为是对系统源代码的修改。如果一个系统在扩展时只涉及到修改配置文件,而原有的代码没有做任何修改,该系统即可认为是一个符合开闭原则的系统。所以,配置文件 + 反射技术在java、c#、.net等各类后端语言中大量采用,以实现完全开闭原则。
  · 开闭原则的实现由里氏替换原则和依赖倒置原则来完成。


* 里氏替换原则:
  · 背景:
    继承的优点:代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性、提高代码的重用性、子类可以形似父类,但又异于父类、提高代码的可扩展性、提高产品或项目的开放性。
    继承的缺点:继承是侵入性的:只要继承,就必须拥有父类的所有属性和方法、降低代码的灵活性:子类必须拥有父类的属性和方法、增强了耦合性:当父类的常量、变量和方法被修改时,必需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果大片的代码需要重构
    里氏替换原则能够克服继承的缺点。
  · 定义:子类可以扩展父类的功能,但不能改变父类原有的功能。父类能出现的地方都可以用子类来代替,而且换成子类也不会出现任何错误或异常,而使用者也无需知道是父类还是子类,但反过来则不成立。
  · 解释:
    LSP的原定义比较复杂,我们一般对里氏替换原则 LSP的解释为:子类对象能够替换父类对象,而程序逻辑不变。
    里氏代换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。例如:我喜欢动物,那我一定喜欢狗,因为狗是动物的子类;但是我喜欢狗,不能据此断定我喜欢动物,因为我并不喜欢老鼠,虽然它也是动物。
    里氏替换原则有至少以下两种含义:
    ①里氏替换原则是针对继承而言的,如果继承是为了实现代码重用,也就是为了共享方法,那么共享的父类方法就应该保持不变,不能被子类重新定义。子类只能通过新添加方法来扩展功能,父类和子类都可以实例化,而子类继承的方法和父类是一样的,父类调用方法的地方,子类也可以调用同一个继承得来的,逻辑和父类一致的方法,这时用子类对象将父类对象替换掉时,当然逻辑一致,相安无事。
    ②如果继承的目的是为了多态,而多态的前提就是子类覆盖并重新定义父类的方法,为了符合LSP,我们应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法,当父类是抽象类时,父类就是不能实例化,所以也不存在可实例化的父类对象在程序里。也就不存在子类替换父类实例(根本不存在父类实例了)时逻辑不一致的可能。
    不符合LSP的最常见的情况是,父类和子类都是可实例化的非抽象类,且父类的方法被子类重新定义,这一类的实现继承会造成父类和子类间的强耦合,也就是实际上并不相关的属性和方法牵强附会在一起,不利于程序扩展和维护。
    如何符合LSP?总结一句话 —— 就是尽量不要从可实例化的父类中继承,而是要使用基于抽象类和接口的继承。
  · 最佳实践:
    ①子类必须完全实现父类的抽象方法,但不能覆盖父类的非抽象方法;
    ②子类中可以增加自己特有的方法;
    ③当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数要更宽松;
    ④当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
  · 里氏代换原则和依赖倒置原则一样,是开闭原则的具体实现手段之一。


* 依赖倒置原则:
  · 定义:
    高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。
  · 解释:
    高层模块不应该直接依赖于底层模块的具体实现,而应该依赖于底层的抽象。换言之,模块间的依赖是通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。
    接口和抽象类不应该依赖于实现类,而实现类依赖接口或抽象类。这一点其实不用多说,很好理解,“面向接口编程”思想正是这点的最好体现。
  · 解释:
    常规我们认为上层模块应该依赖下层,但是这也有个问题就是,下层变动将导致“牵一发动全身”。依赖倒置就是反常规思维,将原本的高层建筑依赖底层建筑“倒置”过来,变成底层建筑依赖高层建筑。高层建筑决定需要什么,底层去实现这样的需求,但是高层并不用管底层是怎么实现的。这样就不会出现前面的“牵一发动全身”的情况。当然,严格的来说上层不应该依赖下层,而依赖自身接口,通过注入的方式依赖其他接口。
  · 控制反转(Inversion of Control):
    就是依赖倒置原则的一种代码设计的思路。具体采用的实现方法就是所谓的依赖注入(Dependency Injection)。
  · 控制反转容器(IoC Container):
    因为采用了依赖注入,在初始化的过程中就不可避免的会写大量的new,并且还要要管理各个对象之间依赖关系,所以这里使用工厂方法还是比较麻烦。而IoC容器就解决以上2个问题。这个容器可以自动对你的代码进行初始化,你只需要维护一个Configuration(可以是xml可以是一段代码),而不用每次初始化一实例都要亲手去写那一大段初始化的代码。另外一个好处是:我们在创建实例的时候不需要了解其中的依赖细节。
  · java的反射机制:
    -- 实现依赖倒置原则的基础是代码的抽象化。java提供了抽象类、接口、泛型等方式进行抽象,但这些还是不够抽象,最抽象的状态是像js一样的鸭子类型。但是由于java在编译阶段存在类型检查系统,要避开类型检测,也就必须要绕过编译阶段。而反射的操作都是中编译成字节码以后的操作,这个阶段不存在类型检查,所以可以利用反射编写各种抽象代码。因为抽象具有更强兼容能力,所以各种框架会经常使用到反射技术。
    -- typescirpt的类型检查也只发生在编译阶段
    -- 类也是对象,类是java.lang.Class类的实例对象
    -- java类的静态加载和动态加载:
      -- 静态加载就是编译时加载,此时将进行类型检查,变量需满足理论的数据类型
      -- 动态加载就是运行时加载,发生在编译后,此时才真正能确定对象的确切类型
      -- 编译时刻加载类是静态加载类,运行时刻加载类是动态加载类。new创建对象是静态加载类,在编译时刻就要加载所有可能用到的类,这样导致某一个类(的类型)出现问题,其他所有类都将无法使用(无法通过编译)。通过反射可以实现动态加载类解决这个问题。动态加载的方法,即通过类的类类型创建该类的对象
    -- Java反射机制允许程序在运行时透过Reflection APIs取得任意一个已知名称的class的内部信息,包括modifiers(如public、static等)、superclass(如Object)、实现的interfaces(如Serializable)、fields(属性)和methods(方法)(但不包括methods定义),可于运行时改变fields的内容,也可调用methods
    -- 反射api:
      -- 获取类:Class c=Class.forName("Alunbar");c.newInstance();
      -- 获取类的方法信息,代码示例:Class c=obj.getClass();Method[] ms=c.getMethods();//参数列表获取省略
      -- 获取类的成员变量信息,代码示例:Class c=obj.getClass();Field[] fs=c.getFields();
      -- 获取类的构造方法信息信息,代码示例:Class c=obj.getClass();Constructor[] cs=c.getConstructors();//参数列表获取省略
      -- 方法的反射调用,操作步骤:
          1、根据方法的名称和参数列表获取唯一的方法;
          2、拿到方法后,method.invoke(对象,参数列表)。
    -- 配置文件 + 反射机制 实现开闭原则:在引入配置文件和反射机制后,需要更换或增加新的具体类将变得很简单,只需增加新的具体类并修改配置文件即可,无须对现有类库和客户端代码进行任何修改,完全符合开闭原则。在很多设计模式中都可以通过引入配置文件和反射机制来对客户端代码进行改进,如在抽象工厂模式中可以将具体工厂类类名存储在配置文件中,在适配器模式中可以将适配器类类名存储在配置文件中,在策略模式中可以将具体策略类类名存储在配置文件中等等。通过对代码的改进,可以让系统具有更好的扩展性和灵活性,更加满足各种面向对象设计原则的要求。
    -- 小结:代码是现实世界的抽象,由于世界复杂的本质,注定不会有银弹。而java的反射机制为java提供一种编写足够抽象代码的手段 
  · 注解:
    在编码过程中,为了更好的理解程序,我们通常会加上注释代码,这样可以方便程序员理解。而注解就是注释的一种,但是与一般的注释不同的是,注解需要按一定规则编写(见自定义注解),并且注解可以被程序解析,并根据解析结果进行相应处理。如果没有用来读取注解的方法和工作,那么注解也就不会比注释更有用处了。使用注解的过程中,很重要的一部分就是创建于使用注解处理器。Java SE5扩展了反射机制的API,以帮助程序员快速的构造自定义注解处理器。
  · 元数据:指用来描述数据的数据。
  · 元数据和注解:注解可以用来描述数据,所有注解是元数据的一种实现方式。
  · 注解的作用:通过解析注解可以拿到注解中的属性和方法,并实现各式功能。 例如可以搭配反射实现开闭原则,因为注解可以被反射解析出来,此时的注解相当于一个配置文件。
  · 在java中,除了注解充当配置文件,还可以用xml作为配置文件,但注解优点明显:
    1、在class文件中,可以降低维护成本,annotation的配置机制很明显简单;
    2、不需要第三方的解析工具,利用java反射技术就可以完成任务;
    3、编辑期可以验证正确性,查错变得容易;
    4、提高开发效率
  · ts中的注解:ts中其实没有注解的概念,但是前端界曾经还是有语言借鉴了注解:Angular2的AtScript语言,它能完完全全的单纯附加元数据。例如:
  @Component({
    selector: 'app'
  })
  class AppComponent {}
    等价于
  class AppComponent {} 
  AppComponent.annotations = [
    new Component({
      selector: 'app'
    })
  ]
  · java自定义注解:
    @Target({ElementType.METHOD,ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @Documented
    public @interface Description{
      String desc();
      String author();
      int age() default 18;
    }
    -- 使用@interface关键词定义注解
    -- 成员以无参无异常方式声明
    -- 可以用default为成员指定一个默认值
    -- 成员类型是受限制的,合法的类型包括原始类型及String,Class,Annotation,Enumeration
    -- 如果注解只有一个成员,则成员名必须取名为value(),在使用时可以忽略成员名和赋值号(=)
    -- 注解类可以没有成员,没有成员的注解成为标识注解
    -- @Target、@Retention、@Inherited、@Documented为注解Description的注解,即元注解
    -- @Target(options)中的options包括注解的作用域,该例中表示注解可以在方法和类中使用。完整的作用域项包括:CONSTRUCTOR(构造方法)、FIELD(字段)、LOCAL_VARIABLE(局部变量)、METHOD(方法)、PACKAGE(包)、PARAMETER(参数)、TYPE(类,接口)
    -- @Retention():注解生命周期
      -- 包括SOURCE(只在源码显示,编译时丢弃)
      -- CLASS(编译时会记录到class中,运行时忽略)
      -- RUNTIME(运行时存在,可以通过反射读取)
    -- @Inherited:允许子注解继承,子类只能继承父类的类上的注解,不会继承成员上的注解
    -- @Documented:生成javadoc时会包含注解信息
    --使用自定义注解:
      @Description(desc='I am eyeColor',author='Jacksplwxy',age=18)
      public String eyeColor(){
        return 'red';
      }
  · 注解和装饰器区别:
    -- 注解(Annotation):java中元数据的一种实现方式。仅提供附加元数据支持,并不能实现任何操作。需要另外的Scanner根据元数据执行相应操作。
    -- 装饰器(Decorator):ES6中增加的对装饰器模式的简单实现。其仅提供定义劫持,能够对类及其方法的定义并没有提供任何附加元数据的功能。
    他们语法相似,都是@符号。但注解仅仅为数据提供一些更加细节的属性描述,我们可以利用反射等方式来获取这些描述再进行函数操作。而装饰器可以相当于直接附加函数操作。实际上,两者在实现上都可以相互模拟。
  · ts的反射机制实现:
    -- 回顾反射:反射就是根据类名获取其更详细信息
    -- Function.prototype.toString实现反射:Function.prototype.toString这个原型方法可以帮助你获得函数的源代码,通过合适的正则, 我们可以从中提取出丰富的信息。但是并不方便,也不优雅。
    -- JavaScript本身为动态语言,天生具备反射能力,例如遍历对象内所有属性、判断数据类型。ES6中新增了新的api:Reflect,来把这些操作归结到一起。Reflect能够获取到类中的成员变量和方法,但是由于js迎合了web的压缩特点,所以其无法获取到参数名。解决方案就是通过在方法上添加定义了参数相关的装饰器,再解析装饰器即可获取参数名了。可惜ES6的Reflect也无法获取到究竟有哪些装饰器添加到这个类/方法上。为了获取到装饰器,Reflect Metadata应运而生,它是ES7的一个提案,它主要用来在声明的时候添加和读取装饰器的。
  · 依赖注入的三种方式:
    -- 构造函数注入
    -- 属性注入
    -- 接口注入
  · ts实现IoC容器:
    -- 《使用Typescript实现依赖注入(DI)》:https://blog.csdn.net/HaoDaWang/article/details/79776021
  · 里氏代换原则和依赖倒置原则一样,是开闭原则的具体实现手段之一。
  · 接口和抽象类:实际开发中90%的情况使用接口,因为其简洁、灵活。而抽象类只在既起约束作用又需要复用代码时才使用。
  · 最佳实践:
    -- 原则上每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备:这是依赖倒置的基本要求,接口和抽象类都是属于抽象的,有了抽象才可能依赖倒置。
    -- 任何类都不应该从具体类派生:如果一个项目处于开发状态,确实不应该有从具体类派生出的子类的情况,但这也不是绝对的,因为人都是会犯错误的,有时设计缺陷是在所难免的,因此只要不超过两层的继承都是可以忍受的。特别是做项目维护的同志,基本上可以不考虑这个规则,为什么?维护工作基本上都是做扩展开发,修复行为,通过一个继承关系,覆写一个方法就可以修正一个很大的Bug,何必再要去继承最高的基类呢?
    -- 尽量不要覆写基类的方法:如果基类是一个抽象类,而且这个方法已经实现了,子类尽量不要覆写。类间依赖的是抽象,覆写了抽象方法,对依赖的稳定性会产生一定的影响。
    -- 结合里氏替换原则使用:里氏替换原则要求父类出现的地方子类就能出现,再依赖倒置原则,我们可以得出这样一个通俗的规则: 接口负责定义public属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。
  · 文档:
    -- 《Spring IoC有什么好处呢?》:https://www.zhihu.com/question/23277575/answer/169698662
    -- 《依赖倒置原则》:https://www.cnblogs.com/cbf4life/archive/2009/12/15/1624435.html
    -- 《小话设计模式原则之:依赖倒置原则DIP》:https://zhuanlan.zhihu.com/p/24175489


* 单一职责原则:
  · 定义:一个类只负责一个功能领域中的相应职责,或者就一个类而言,应该只有一个引起它变化的原因。
  · 解释:一个类不能太“累”!在软件系统中,一个类(大到模块,小到方法)承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。


* 接口隔离原则:
  · 定义:一个类对另一个类的依赖应该建立在最小的接口上,即要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
  · 解释:
    -- 接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。
    -- 为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。
    -- 了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同深入了解业务逻辑。
    -- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。


* 迪米特法则:
  · 定义:一个软件实体应当尽可能少地与其他实体发生相互作用。
  · 解释:
    -- 从依赖者的角度来说,只依赖应该依赖的对象。
    -- 从被依赖者的角度说,只暴露应该暴露的方法。
    -- 在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
    -- 在类的结构设计上,尽量降低类成员的访问权限。
    -- 在类的设计上,优先考虑将一个类设置成不变类。
    -- 在对其他类的引用上,将引用其他对象的次数降到最低。
    -- 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。


* 合成复用原则:
  · 定义:尽量使用对象组合,而不是继承来达到复用的目的。
  · 解释:
    在面向对象设计中,可以通过两种方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承,但首先应该考虑使用组合/聚合,组合/聚合可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
    通过继承来进行复用的主要问题在于继承复用会破坏系统的封装性,因为继承会将基类的实现细节暴露给子类,由于基类的内部细节通常对子类来说是可见的,所以这种复用又称“白箱”复用,如果基类发生改变,那么子类的实现也不得不发生改变;从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性;而且继承只能在有限的环境中使用(如类没有声明为不能被继承)。
    由于组合或聚合关系可以将已有的对象(也可称为成员对象)纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能,这样做可以使得成员对象的内部实现细节对于新对象不可见,所以这种复用又称为“黑箱”复用,相对继承关系而言,其耦合度相对较低,成员对象的变化对新对象的影响不大,可以在新对象中根据实际需要有选择性地调用成员对象的操作;合成复用可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的其他对象。


* 设计模式:
  · 创建型模式(5种): 
    -- 单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。
    -- 原型(Prototype)模式:通过拷贝原型创建新的对象。
    -- 工厂方法(FactoryMethod)模式:定义一个用于创建产品的接口,由子类决定生产什么产品。
    -- 抽象工厂(AbstractFactory)模式:提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。
    -- 建造者(Builder)模式:将一个复杂的构建过程与其具表示细节相分离,使得同样的构建过程可以创建不同的表示。
  · 结构型模式(7种):
    -- 代理(Proxy)模式:为其他对象提供一种代理以控制对这个对象的访问:增加中间层(代理层),代理类与底层实现类实现共同接口,并创建底层实现类对象(底层实现类对象依赖注入代理类),以便向外界提供功能接口。
    -- 适配器(Adapter)模式:使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
    -- 桥接(Bridge)模式:两个维度独立变化,依赖方式实现抽象与实现分离:需要一个作为桥接的接口/抽象类,多个角度的实现类依赖注入到抽象类,使它们在抽象层建立一个关联关系。
    -- 装饰(Decorator)模式:动态地给对象增加一些职责,即增加其额外的功能。
    -- 外观(Facade)模式:在客户端和复杂系统之间再加一层,这一次将调用顺序、依赖关系等处理好。即封装底层实现,隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的高层接口。
    -- 享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用。
    -- 组合(Composite)模式:用户对单个对象和组合对象的使用具有一致性的统一接口。
  · 行为型模式(11种):
    -- 模板方法(Template Method)模式:将这些通用算法抽象出来,在一个抽象类中公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。
    -- 策略(Strategy)模式:策略对象依赖注入到context对象,context对象根据它的策略改变而改变它的相关行为(可通过调用内部的策略对象实现相应的具体策略行为)。
    -- 命令(Command)模式:将"行为请求者"与"行为实现者"解耦:调用者依赖命令,命令依赖接收者,调用者Invoker→命令Command→接收者Receiver。
    -- 职责链(Chain of Responsibility)模式:拦截的类都实现统一接口,每个接收者都包含对下一个接收者的引用。将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。
    -- 状态(State)模式:状态对象依赖注入到context对象,context对象根据它的状态改变而改变它的相关行为(可通过调用内部的状态对象实现相应的具体行为)。
    -- 观察者(Observer)模式:一对多的依赖关系,在观察目标类里有一个 ArrayList 存放观察者们。当观察目标对象的状态发生改变,所有依赖于它的观察者都将得到通知,使这些观察者能够自动更新(即使用推送方式)。
    -- 中介者(Mediator)模式:对象与对象之间存在大量的关联关系,将对象之间的通信关联关系封装到一个中介类中单独处理,从而使其耦合松散,可以独立地改变它们之间的交互。
    -- 迭代器(Iterator)模式:提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
    -- 访问者(Visitor)模式:在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。
    -- 备忘录(Memento)模式:在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。
    -- 解释器(Interpreter)模式:提供如何定义语言的文法,以及对语言句子的解释方法,即解释器。


* 设计模式的本质:对语言缺陷的弥补
  · 设计模式的招式并不是一成不变的,它在不同语言中会有不同的表现,但背后的思想和设计原则都是一样的,例如装饰器模式在java中需要定义抽象构件、抽象装饰,而ts只需一个@即可搞定。例如Java8加入lambda后,很多设计模式的实现都会改变。厉害的语言不需要那么多设计模式也能满足设计原则,参考:https://zhuanlan.zhihu.com/p/19835717
  · 设计模式在不同语言之间的实现原理:GoF的《设计模式》一书是针对面向对象语言提炼的技巧,但并不意味着设计模式只能用面向对象语言来写,实际上动态语言也是可以使用设计模式的。例如Java这种静态编译型语言中,无法动态给已存在的对象添加职责,所有一般通过包装类的方式来实现装饰者模式。但是js这种动态解释型语言中,给对象动态添加职责是再简单不过的事情。这就造成了js的装饰者模式不再关注给对象动态添加职责,而是关注于给函数动态添加职责。例如有人模拟js版本的工厂模式,而生硬地把创建对象延迟到子类中。实际上,在java等静态语言中,让子类来“决定”创建何种对象的原因是为了让程序迎合依赖倒置原则。在这些语言中创建对象时,先解开对象类型之间的耦合关系非常重要,这样才有机会在将来让对象表现出多态性。而在js这类类型模糊的语言中,对象多态性是天生的,一个变量既可以指向一个类,又可以随时指向另一个类。js不存在类型耦合的问题,自然也没有刻意去把对象“延迟”到子类创建,也就是说,js实际上是不需要工厂方法模式的。模式的存在首先是为了满足设计原则的。
  · js的语言特性与7大设计原则:
    -- js在设计模式的实现上与java有着很大的区别,这种区别主要源自于js语言特性的不同: 动态语言 和 一等公民函数
    -- 动态语言:
       强类型语言错误检测、友好提示等优势,但带来不够抽象的缺点,不够抽象将导致代码包容性差,不利于修改和拓展,虽然提供了抽象类和接口这样的抽象能力,但还是比不过js动态语言的鸭子类型。
       例如java的工厂模式的实现,为符合开闭原则,必须将产品和工厂抽象为接口。而js根本不用管这一套,要啥就new啥,你认为它的类型是公鸡就是公鸡,认为是鸭子就是鸭子。除了开闭原则,里氏替换原则和依赖导致原则这类为了抽象类型的原则对于js都是不适用的。虽然依赖导致原则不适用于js(无需强调进行抽象),但依赖倒置中的依赖注入和IOC容器思想仍是可以借鉴的,例如:(function ($, window) { ... })(jQuery, window)
    -- 函数是一等公民:
       虽然开闭原则、里氏替换原则和依赖导致原则不适用js,但单一职责原则、接口隔离原则、迪米特法则和合成复用原则仍然适用于js。
       由于java的面向对象范式和强制类型的限制,导致模块间很容易产生复杂的关联关系,而js的灵活性以及函数第一公民的特性使js代码非常容易满足迪米特法则,不仅如此,做到单向依赖也是完全没有问题的:当模块之间需要双向依赖时需应增加中间模块,模块之间通过中间模块通讯。
       中间模块的形式可以是:
         -- 第三方的实体:例如vue框架使用vuex进行状态存储,不同组件之间对数据的依赖改为第三方实体vuex的依赖
         -- 订阅发布:例如存在这个3个页面,一个是实时播放页面1,一个录像播放页面2,他们两个都依赖了一个播放器iframe页面3。播放器加载过程要展示loading层,而实时播放页面的loading和录像页面的不同。单向依赖是指播放时,页面1和页面2都单向依赖页面3,页面1和页面2都可以调用页面3的播放方法,但是页面3不允许直接调用页面1或页面2的loading方法,而必须要通过第三方模块订阅发布方式进行调用,例如页面1、页面2以通过window订阅loading消息,页面3通过window.postMessage发布loading消息
         -- 回调函数:本质上是订阅发布的一种实现
         -- 抽象层:js虽然不存在抽象层,但对于强类型语言,抽象层本质上也是一个中间模块。例如中介者模式中(http://c.biancheng.net/view/139.html ),同事类对中介者实例的依赖就放到了抽象同时类中,因为抽象就是意味着稳定的关系,代码变动可能性极小


* 主要参考文献:
  · 《Java设计模式:23种设计模式全面解析》,http://c.biancheng.net/design_pattern/
  · 《设计模式|菜鸟教程》,http://www.runoob.com/design-pattern/design-pattern-tutorial.html
  · 《刘伟技术博客》,https://blog.csdn.net/lovelion
  · Carson_Ho,《设计模式》,https://blog.csdn.net/carson_ho/column/info/14783
  · 刘伟,《设计模式Java版》,https://gof.quanke.name/
  · 曾探,《JavaScript设计模式与开发实践》,北京:人民邮电出版社,2015.5
  · Addy Osmani,《JavaScript设计模式》,徐涛 译,北京:人民邮电出版社,2013.6
  · codeTao,《23种设计模式全解析》,http://www.cnblogs.com/geek6/p/3951677.html
  · me115,《图说设计模式》,https://design-patterns.readthedocs.io/zh_CN/latest/index.html
  · vczh,《为什么我们需要学习(设计)模式》,https://zhuanlan.zhihu.com/p/19835717
  · 杨博,《代码耦合是怎么回事呢?》,https://www.zhihu.com/question/21386172/answer/54476702
  · 知乎问题,《为何大量设计模式在动态语言中不适用》,https://www.zhihu.com/question/63734103
  · Mingqi,《Spring IoC有什么好处呢?》,https://www.zhihu.com/question/23277575/answer/169698662
  · 阮一峰,《ECMAScript 6 入门》,http://es6.ruanyifeng.com/
  · 萧萧,《设计模式在实际开发中用的多吗》,https://www.zhihu.com/question/29477933/answer/586378235
  · DD菜,《浅析Typescript设计模式》,https://zhuanlan.zhihu.com/p/43283016
  · semlinker,https://github.com/semlinker/typescript-design-patterns