前端学设计模式
转自:https://github.com/jacksplwxy/DesignPatterns_TypeScript
*标题:
·前端开发者如何学习设计模式
·系统学习设计模式
·TypeScript版设计模式
*前言:
·背景:
①前端开发者普遍缺乏设计模式相关知识,但网上相关资料基本都是基于java或c#,不利于前端学习。
②经典23种设计模式来源于静态语言,部分基于js的设计模式书籍往往又只是单单通过动态语言js来实现,不利于真正理解设计模式。
③因为设计模式很难验证对错,网络上存在大量错误的设计模式教程误导新人。
④设计模式资料普遍只讲述模式的实现步骤,缺乏对理解这些步骤的基础知识进行讲解,导致对设计模式的学习只是表面招式。
·项目内容:
①帮助缺乏经验的前端开发者了解设计模式的来龙去脉,补充学习设计模式需要了解的基础知识,如:设计原则、解耦、控制反转、依赖注入、IoC容器、反射、注解、装饰器等。
②着重实现TypeScript版设计模式代码。
③补充实现Java/JavaScript版设计模式代码。
·理解有误处欢迎指出。
*什么是设计模式?
·狭义的设计模式:指的是GoF四人组在《Design Patterns: Elements of Reusable Object-Oriented Software》一书中提出的23种设计模式。
·广义的设计模式:最早的设计模式是美国著名建筑大师克里斯托夫·亚历山大在他的著作《建筑模式语言:城镇、建筑、构造》中描述了一些常见的建筑设计问题,并提出了253种关于对城镇、邻里、住宅、花园和房间等进行设计的基本模式。后来软件界也开始论坛设计模式的话题,因为这也是类似的。所以设计模式指的是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,是可以反复使用的。
*为什么要学习设计模式?
在弄清楚这个问题之前,我们先思考什么才是高质量的程序?高质量程序的特点:开发周期短、代码无bug、代码性能高、代码易阅读、代码够健壮、代码易扩展、代码易重用。
那我们如何开发出高质量程序呢?一个重要的方式就是遵守前人总结的7大设计原则:①开闭原则:总纲,要对扩展开放,对修改关闭②里氏替换原则:不要破坏继承体系③依赖倒置原则:要面向接口编程④单一职责原则:实现类要职责单一⑤接口隔离原则:在设计接口的时候要精简单一⑥迪米特法则:要降低耦合度⑦合成复用原则:要优先使用组合或者聚合关系复用,少用继承关系复用。
好的程序需要靠7大设计原则完成,但是由于语言的缺陷导致程序需要按照一定复杂度的步骤去实现这些设计原则,而这些步骤通常都是固定的,就像武功中的套路招式一样,如果再给这些套路加上好听的名字,这就成了设计模式。也就是说23种设计模式就是7大设计原则在某些语言的具体实现的一种方式,每个设计模式的背后我们都能找到其依靠的一种或多种设计原则。换句话说就是,只要我们写代码遵循设计原则,代码就会自然而然变成了某些设计模式,这也在王垠的《解密“设计模式”》中得到证明。
由于现实问题的复杂性,往往导致代码不可能同时满足所有的设计原则,甚至要违背部分设计原则,这里就会有一个最佳实践的问题了。而设计模式就为解决特定问题提供了最佳实践方案,以至于学习了设计模式后,在遇到特定问题时,我们脑子很容易知道如何在满足设计原则最优解的基础上实现代码的编写。
虽然设计原则为开发出高质量程序指明了方向,但没有对程序的高性能、易用性等做出指示,而设计模式在这方面做了补充。
总结:①设计模式通过写代码的形式帮助我们更好的学习理解设计原则;②为实现设计原则的招式统一命名;③为特定场景问题的提供最优解决方案;④补充了设计原则在构建高性能、易使用程序等方面的内容。
*解耦与高质量程序之间的是什么关系?
·解耦(低耦合)在某个角度上说属于高质量程序的一个重要体现,但不是全部。高质量程序具体还体现在代码易复用、代码安全稳定、代码层次结构清晰、代码易扩展、代码性能好等等许多细节方面。
·解耦是一个很模糊的定义,耦合性高低实际上可以用依赖性、正交性、紧凑性3个指标来衡量,具体参考:https://www.zhihu.com/question/21386172/answer/54476702 。实际上这3个指标大概分别对应着7大设计原则中的迪米特法则、单一职责原则、接口隔离原则等原则,而7大设计原则也就是高质量软件的编写原则,所以也印证了解耦是属于高质量程序的一部分体现。
*设计模式补充:
·学习设计模式的核心是掌握设计模式的意图是什么:不同的设计模式在不同的语言中会有不同表现形式,千万不要被模式中的各种概念迷惑,而只学到表面套路。我看到很多热门教程是示例代码甚至是错误的,例如这个超过2k star的设计模式项目https://github.com/torokmark/design_patterns_in_typescript ,他的工厂方法的实现就不对,如果要增加新的产品,他就必须必须修改createProduct代码,但这违背了开闭原则。导致这个错误的原因就是因为作者没有理解工厂方法的目的:对简单工厂模式的进一步抽象化,使系统在不修改原来代码的情况下引进新的产品,即满足开闭原则。又如这个项目的原型模式的实现也是错误的,原型模式的场景是因为复制一个对象比new一个对象更高效。所以学习设计模式的正确姿势应该是:掌握该设计模式的意图,并在遵守设计原则的情况下去实现它。
·设计模式的招式并不是一成不变的,它在不同语言中会有不同的表现,但背后的思想和设计原则都是一样的,例如装饰器模式在java中需要定义抽象构件、抽象装饰,而ts只需一个@即可搞定。例如Java8加入lambda后,很多设计模式的实现都会改变。厉害的语言不需要那么多设计模式也能满足设计原则,参考:https://zhuanlan.zhihu.com/p/19835717
·设计模式在不同语言之间的实现原理:GoF的《设计模式》一书是针对面向对象语言提炼的技巧,但并不意味着设计模式只能用面向对象语言来写,实际上动态语言也是可以使用设计模式的。例如Java这种静态编译型语言中,无法动态给已存在的对象添加职责,所有一般通过包装类的方式来实现装饰者模式。但是js这种动态解释型语言中,给对象动态添加职责是再简单不过的事情。这就造成了js的装饰者模式不再关注给对象动态添加职责,而是关注于给函数动态添加职责。例如有人模拟js版本的工厂模式,而生硬地把创建对象延迟到子类中。实际上,在java等静态语言中,让子类来“决定”创建何种对象的原因是为了让程序迎合依赖倒置原则。在这些语言中创建对象时,先解开对象类型之间的耦合关系非常重要,这样才有机会在将来让对象表现出多态性。而在js这类类型模糊的语言中,对象多态性是天生的,一个变量既可以指向一个类,又可以随时指向另一个类。js不存在类型耦合的问题,自然也没有刻意去把对象“延迟”到子类创建,也就是说,js实际上是不需要工厂方法模式的。模式的存在首先是为了满足设计原则的。
·分辨模式的关键是意图而不是结构:在设计模式的学习中,有人经常会发出这样的疑问:代理模式和装饰着模式,策略模式和状态模式,这些模式的类图看起来几乎一模一样,他们到底有什么区别?实际上这种情况是普遍存在的,许多模式的类图看起来都差不多,模式只有放在具体的环境下才有意义。比如我们的手机,用它当电话的时候它就是电话;用它当闹钟的时候它就是闹钟;用它玩游戏的时候他就是游戏机。有很多模式的类图和结构确实很相似,但这不太重要,辨别模式的关键是这个模式出现的场景,以及为我们解决了什么问题。
·设计模式一直在发展,例如现在逐渐流行起来的模块模式、沙箱模式等,但真正得到人们的认可还需要时间的检验。
·设计模式的合理使用:
-- 总体来说,使用设计模式的必要性的程度是逐级递增的:应用程序(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.lang.Class类的实例对象
-- 编译时刻加载类是静态加载类,运行时刻加载类是动态加载类。new创建对象是静态加载类,在编译时刻就要加载所有可能用到的类,这样导致某一个类(的类型)出现问题,其他所有类都将无法使用(无法通过编译)。通过动态加载类可以解决这个问题。动态加载的方法,即通过类的类类型创建该类的对象,示例:Class c=Class.forName(args[0]);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();//参数列表获取省略
-- 方法的反射,操作步骤:①根据方法的名称和参数列表获取唯一的方法;②拿到方法后,method.invoke(对象,参数列表)。
-- 反射的操作都是编译成字节码以后的操作
-- java中集合的泛型是防止错误输入的,只在编译阶段有效,绕过编译就无效了。(其实typescirpt也是这样的,类型检查只发生在编译时)
-- 因为反射的操作是编译后的,所以不存在类型检查问题了
-- 小结:Java反射机制允许程序在运行时透过Reflection APIs取得任意一个已知名称的class的内部信息,包括modifiers(如public、static等)、superclass(如Object)、实现的interfaces(如Serializable)、fields(属性)和methods(方法)(但不包括methods定义),可于运行时改变fields的内容,也可调用methods.
-- 配置文件+反射机制实现开闭原则:在引入配置文件和反射机制后,需要更换或增加新的具体类将变得很简单,只需增加新的具体类并修改配置文件即可,无须对现有类库和客户端代码进行任何修改,完全符合开闭原则。在很多设计模式中都可以通过引入配置文件和反射机制来对客户端代码进行改进,如在抽象工厂模式中可以将具体工厂类类名存储在配置文件中,在适配器模式中可以将适配器类类名存储在配置文件中,在策略模式中可以将具体策略类类名存储在配置文件中等等。通过对代码的改进,可以让系统具有更好的扩展性和灵活性,更加满足各种面向对象设计原则的要求。
·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';
}
·元数据:指用来描述数据的数据。
·元数据和注解:注解可以用来描述数据,所有注解是元数据的一种实现方式。
·注解的作用:注解有很多作用,其中最重要的一个就是搭配反射实现开闭原则。因为注解可以被反射解析出来,此时的注解相当于一个配置文件。另外,在java中,除了注解充当配置文件,还可以用xml作为配置文件,但注解优点明显:①在class文件中,可以降低维护成本,annotation的配置机制很明显简单;②不需要第三方的解析工具,利用java反射技术就可以完成任务;③编辑期可以验证正确性,差错变得容易;④ 提高开发效率
·ts中的注解:ts中其实没有注解的概念,但是前端界曾经还是有语言借鉴了注解:Angular2的AtScript语言,它能完完全全的单纯附加元数据。例如:
@Component({
selector: 'app'
})
class AppComponent {}
等价于
class AppComponent {}
AppComponent.annotations = [
new Component({
selector: 'app'
})
]
·注解和装饰器区别:
-- 注解(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
·里氏代换原则和依赖倒置原则一样,是开闭原则的具体实现手段之一。
·最佳实践:
-- 每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备:这是依赖倒置的基本要求,接口和抽象类都是属于抽象的,有了抽象才可能依赖倒置。
-- 变量的显示类型尽量是接口或者是抽象类:很多书上说变量的类型一定要是接口或者是抽象类,这个有点绝对化了,比如一个工具类,xxxUtils一般是不需要接口或是抽象类的。还有,如果你要使用类的clone方法,就必须使用实现类,这个是JDK提供一个规范。
-- 任何类都不应该从具体类派生:如果一个项目处于开发状态,确实不应该有从具体类派生出的子类的情况,但这也不是绝对的,因为人都是会犯错误的,有时设计缺陷是在所难免的,因此只要不超过两层的继承都是可以忍受的。特别是做项目维护的同志,基本上可以不考虑这个规则,为什么?维护工作基本上都是做扩展开发,修复行为,通过一个继承关系,覆写一个方法就可以修正一个很大的Bug,何必再要去继承最高的基类呢?
-- 尽量不要覆写基类的方法:如果基类是一个抽象类,而且这个方法已经实现了,子类尽量不要覆写。类间依赖的是抽象,覆写了抽象方法,对依赖的稳定性会产生一定的影响。
-- 结合里氏替换原则使用:里氏替换原则要求父类出现的地方子类就能出现,再依赖倒置原则,我们可以得出这样一个通俗的规则: 接口负责定义public属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。
·关于接口和抽象类:实际开发中90%的情况使用接口,因为其简洁、灵活。而抽象类只在既起约束作用又需要复用代码时才使用。
·文档:
-- 《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
*单一职责原则:
·定义:一个类只负责一个功能领域中的相应职责,或者就一个类而言,应该只有一个引起它变化的原因。
·解释:一个类不能太“累”!在软件系统中,一个类(大到模块,小到方法)承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。
*接口隔离原则:
·定义:一个类对另一个类的依赖应该建立在最小的接口上,即要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
·解释:
-- 接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。
-- 为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。
-- 了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同深入了解业务逻辑。
-- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
*迪米特法则:
·定义:一个软件实体应当尽可能少地与其他实体发生相互作用。
·解释:
-- 保证类之间单向依赖(jacksplwxy)
-- 从依赖者的角度来说,只依赖应该依赖的对象。
-- 从被依赖者的角度说,只暴露应该暴露的方法。
-- 在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
-- 在类的结构设计上,尽量降低类成员的访问权限。
-- 在类的设计上,优先考虑将一个类设置成不变类。
-- 在对其他类的引用上,将引用其他对象的次数降到最低。
-- 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
-- 谨慎使用序列化(Serializable)功能。
*合成复用原则:
·定义:尽量使用对象组合,而不是继承来达到复用的目的。
·解释:
在面向对象设计中,可以通过两种方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承,但首先应该考虑使用组合/聚合,组合/聚合可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
通过继承来进行复用的主要问题在于继承复用会破坏系统的封装性,因为继承会将基类的实现细节暴露给子类,由于基类的内部细节通常对子类来说是可见的,所以这种复用又称“白箱”复用,如果基类发生改变,那么子类的实现也不得不发生改变;从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性;而且继承只能在有限的环境中使用(如类没有声明为不能被继承)。
由于组合或聚合关系可以将已有的对象(也可称为成员对象)纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能,这样做可以使得成员对象的内部实现细节对于新对象不可见,所以这种复用又称为“黑箱”复用,相对继承关系而言,其耦合度相对较低,成员对象的变化对新对象的影响不大,可以在新对象中根据实际需要有选择性地调用成员对象的操作;合成复用可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的其他对象。
*设计模式:
·创建型模式(5种):
-- 单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。
-- 原型(Prototype)模式:将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。
-- 工厂方法(FactoryMethod)模式:定义一个用于创建产品的接口,由子类决定生产什么产品。
-- 抽象工厂(AbstractFactory)模式:提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。
-- 建造者(Builder)模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。
·结构型模式(7种):
-- 代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
-- 适配器(Adapter)模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
-- 桥接(Bridge)模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现的,从而降低了抽象和实现这两个可变维度的耦合度。
-- 装饰(Decorator)模式:动态地给对象增加一些职责,即增加其额外的功能。
-- 外观(Facade)模式:为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
-- 享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用。
-- 组合(Composite)模式:将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。
·行为型模式(11种):
-- 模板方法(Template Method)模式:定义一个操作中的算法骨架,将算法的一些步骤延迟到子类中,使得子类在可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
-- 策略(Strategy)模式:定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算法的客户。
-- 命令(Command)模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。
-- 职责链(Chain of Responsibility)模式:把请求从链中的一个对象传到下一个对象,直到请求被响应为止。通过这种方式去除对象之间的耦合。
-- 状态(State)模式:允许一个对象在其内部状态发生改变时改变其行为能力。
-- 观察者(Observer)模式:多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。
-- 中介者(Mediator)模式:定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。
-- 迭代器(Iterator)模式:提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
-- 访问者(Visitor)模式:在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。
-- 备忘录(Memento)模式:在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。
-- 解释器(Interpreter)模式:提供如何定义语言的文法,以及对语言句子的解释方法,即解释器。
*主要参考文献:
·《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
posted on 2019-05-03 18:20 jacksplwxy 阅读(299) 评论(0) 编辑 收藏 举报