01.设计原则 & 设计模式
一、七大设计原则:
1、单一职责原则(Single Responsibility Principle):一个类只负责一项职责。一个类(或者大到模块,小到方法)职责越多,它被复用的可能性越小。类的职责有:数据职责和行为职责。数据职责通过属性体现,行为职责通过方法体现。具体处理方法就是高内聚、低耦合。把不同的职责分离成不同的类。类不要过于庞大。
2、开闭原则(Open close Principle):开放对外扩展,关闭对内修改。设计模块时,使得该模块可以在不被修改的前提下进行扩展。一般使用抽象化的设计原则。扩展时,仅仅是对抽象类的一个实现与扩展。抽象化是开闭原则的关键,或者说是可变性封装原则,即把经常改动的模块抽象化,并封装成抽象类,如果拓展功能的话,仅仅继承抽象类并实现即可。
3、里氏替换原则(Liskov Substitution Principle):所有能使用基类的地方,必须可以透明地使用派生类。派生类的功能是对基类的一个拓展,派生类包含了基类的所有特征,并在者基础上进行拓展。即软件中如果能够使用基类对象,那么一定能够使用其子类对象。它是实现开闭原则的一个重要手段。程序中尽量使用基类或者抽象类实现对对象的定义,运行时根据需要使用派生类对基类进行替换。
4、依赖倒置原则(Dependence Inversion Principle):高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象定义。针对接口或抽象类编程而不是实现或具体类,即代码要依赖于抽象的类而不是具体的类。该原则也是实现开闭原则的一个重要手段。子类型能够替换掉它们的父类型。
5、接口隔离原则(Interface Segregation Principle):大接口应该分解成小接口,客户端不必依赖于它不需要的接口。类似于单一职责原则。不要让接口臃肿。一个接口只代表一个角色,接口仅仅提供客户端需要的行为。类之间的相互依赖应建立在最小的接口上。
6、合用复用原则(Composite/Aggregate Reuse Principle):新对象中,尽量使用组合或聚合方式实现功能,而非继承。继承会使得类之间的耦合度过高!必须使用继承时,要严格的遵循里氏代换原则。即尽量使用组合和聚合少用继承的关系来达到复用的原则。
7、迪米特法则(Principle Of Least Knowledge):最少知识原则。一个对象应尽可能少的与其他对象发生相互作用,对象间保持最少的了解。即:如果两个类间无需直接通信,则这两个类就不应当发生直接的相互作用。如果一个类需要调用另一个类的某一方法,需要通过第三者转发这个调用。相互通信的对象必须是:
①、当前对象本身(this);
②、以参数形式传入到当前对象方法中的对象;
③、当前对象的成员对象;
④、如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;
⑤、当前对象所创建的对象。
二、24种设计模式:
1、设计模式是一套被反复使用,多数人知晓的经过分类的、代码设计经验的总结。它提高了代码可重用性,让代码更容易被他人理解,保证代码可靠性。
2、种类与分类:
种类创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
3、设计模式简述:
1、策略模式:定义了算法家族并分别封装,让它们之间可以互相替换,此模式让算法的变化,不会影响到使用算法的客户。
2、装饰模式:动态给一个对象添加有一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活。
3、代理模式:为其他对象提供一种代理以控制对这个对象的访问。
4、工厂模式:定义一个拥有创建对象的接口,让子类决定实例化哪一个类,工厂方法使一个类的实例化延迟到其子类。
5、抽象工厂模式:提供了一个创建一些列相关或相互依赖对象的接口,而无需指定它们具体的类。
6、模板模式:定义一个操作中算法的骨架,延迟一些步骤到子类中。子类不改变算法的结构,即可重定义该算法的某些特定步骤。
7、外观模式:为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
8、建造者模式:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
9、观察者模式:定义一对多的依赖关系,让多个观察者对象同时监听某一主题对象。主题对象状态变化时,会通知所有观察者对象,使它们能够自动更新自己。
10、原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
11、状态模式:当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。
12、适配器模式:将一个类的接口转换成客户希望的另外一个接口。使原本由于接口不兼容而不能一起工作的类可以一起工作。
13、备忘录模式:不破坏封装性的同时,捕获一个对象的内部状态,并在该对象之外保存此状态。方便之后对象恢复到那个状态。
14、组合模式:将对象组合成树形结构以表示“部分--整体”的层次结构。令用户对单个对象和组合对象的使用具有一致性。
15、迭代器模式:提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。
16、单例模式:一个类仅有一个实例,使它们都可以独立地变化。
17、桥接模式:将抽象部分与它的实现部分分离,使它们都可以独立地变化。
18、命令模式:将一个请求封装为一个对象,用不同的请求对客户进行参数化。对请求排队,或记录请求日志,以及支持可撤销的操作。
19、职责链模式:使多个对象都有机会处理请求。避免发送者和接受者之间耦合,将这个对象连成一条链传递该请求,直到有对象处理它。
20、中介者模式:用中介对象封装一系列的对象交互。对象间无需显式地相互引用。耦合松散,可以独立地改变它们间的交互。
21、享元模式:使用共享技术有效地支持大量细粒度的对象。
22、解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示中解释语言汇中的句子。
23、访问者模式:表示一个作用于某对象结构中的各元素的操作。不改变各元素的类的前提下,可以定义作用于这些元素的新操作。
三、常见设计模式:
1、单例模式:一个程序中有且只能有一个对象的设计模式。它有 懒汉模式、饿汉模式 两种模式。
①、饿汉模式与实现:在类被加载时,自动创建单例的实例对象。不管用户是否会去调用该成员。
Ⅰ、私有化类的构造方法。private
Ⅱ、提供静态成员并初始化来保存唯一的实例。static
Ⅲ、提供静态获取成员的方法来获取实例。static
②、懒汉模式与实现:首次调用类的方法时,会创建单例实例对象。后面再重复调用时,直接获取对象。
Ⅰ、私有化类的构造方法。private
③、两者区别:
饿汉模式:加载类时慢,运行时获取对象速度快。且是线程安全的。
懒汉模式:加载类时快,运行时获取对象速度慢。但非线程安全的,有可能创建多个实例对象。所以需要使用线程锁。
2、工厂模式:实例化对象,用工厂代理new操作。
①、几种工厂模式:
Ⅰ、简单工厂模式:一个模块仅需一个工厂类,没有必要把它产生出来,使用静态的方法。
Ⅱ、 多个工厂类:每个具体的产品类都对应一个创建者,每个创建者独立负责创建对应的产品对象,非常符合单一职责原则。
Ⅲ、代替单例模式:单例模式的核心要求就是在内存中只有一个对象,通过工厂方法模式也可以只在内存中生产一个对象。
Ⅳ、延迟初始化:ProductFactory 负责产品类对象的创建工作,并且通过prMap变量产生一个缓存,对需要再次被重用的对象保留。
②、抽象工厂模式:抽象工厂 => 实体工厂 => 抽象产品 => 实体产品。为创建一组相关或相互依赖的对象提供接口,无须指定具体类。
Ⅰ、定义一个接口创建对象,让子类决定哪些类需要实例化。
Ⅱ、工厂方法把实例化的工作推迟到了子类中去实现。
Ⅲ、调用者直接调用工厂,由工厂创建对象,调用者不需要知道工厂内部的实现机制。
Ⅳ、延迟初始化。
3、代理模式:为其他对象提供代理,以控制对这个对象的访问。代理对象起中介作用,可去掉或增加一些服务。它可分为:
●、抽象主题类:可以是抽象类也可以是接口,是一个最普通的业务类型定义,无特殊要求。
●、具体类:是被委托、被代理的角色。是业务逻辑的具体执行者。
●、代理类:即做委托类。它负责对真实角色的应用,把所有抽象主题类定义的方法限制委托给真实主题角色实现,并在真实主题角色处理完毕前后做预处理和善后。
①、应用场景:
Ⅰ、远程代理: 为不同地址空间的服务器提供局域网代理。(FQ,监控多个店铺的摄像头应用)
Ⅱ、虚拟代理: 根据需要将一个资源消耗很大的对象进行延迟,真正需要时再进行创建。(网页中的图片或者视频异步加载)
Ⅲ、保护代理: 增加一些权限,提供对原有对象的保护。(权限控制)
Ⅳ、智能代理:提供目标对象额外的一些服务。(火车票代售点,中介)
②、代理实现的方式:
Ⅰ、静态代理:已知代理类的情况下,代理和被代理的对象在代理前是确定的,都是实现相同接口或者继承相同的类。调用者直接调用真实角色,不用关心代理是否存在,其代理的产生由真实角色决定。即 要从真实角色查找到代理角色,不允许直接访问真实角色。 代理的管理由真实角色自己完成。使用方式如下:
1、继承方式:
①、添加一个功能接口,提供一个功能方法。
②、使用目标类实现功能接口,实现功能方法。
③、使用代理类继承目标类并重写功能方法,使用 super.父类功能方法。
2、包含/合成/聚合方式:
①、添加一个功能接口,提供一个功能方法。
②、使用目标类,实现功能接口,并实现功能方法。
③、添加一个代理类,增加一个带参的构造方法,增加一个方法,使用目标类对象调用功能方法。
3、两种实现方式的对比:
①、继承方式:代理类和被代理类耦合度高,不便于扩展。每次增加新的代理都必须重新添加新的代理类。成本较高。
②、聚合方式:代理类和被代理类耦合度低,便于替换被代理类的对象,更易于复用。
③、面向对象的基本原则中,推荐多使用聚合而不是继承。聚合方式更适合代理模式。
Ⅱ、动态代理:(JDK 动态代理)根据被代理的接口生成所有的方法,被代理的类必须要实现一个接口。两条线路独立发展。动态代理实现代理的职责,业务逻辑Subject实现相关的逻辑功能,两者之间没有必然的相互耦合的关系。通知Advice从另一个切面切入,最终在高层模块也就是Client进行耦合,完成逻辑的封装任务。它是横切面编程,在不改变我们已有代码结构的情况下增强或控制对象的行为。该模式下,调用者只知道代理而不用知道真实的角色。真实角色的修改对高层模块没有影响,只需要实现接口对应的方法。适用于对扩展性要求较高的场合。 使用方式如下:
1、添加一个功能接口,提供功能方法。
2、使用目标类,实现目标功能接口,并实现功能方法。
3、添加动态代理类,实现 InvocationHandler 接口,添加带有功能接口对象的构造方法,重写 InnovationHandler 中 invoke 方法:
Object obj = method.invoke(this.功能对象,args);
4、获取功能接口对象,获取动态代理类对象:
①、功能对象 = Proxy.newProxyInstance(代理类对象.getClass().getClassLoader(),被代理类对象.getClass().getInterfaces(),动态代理类对象);
②、功能对象 = Proxy.newProxyInstance(代理类对象.getClass().getClassLoader(),new Class[]{被代理类.class} ,动态代理类对象);
5、使用动态代理,须实现 InvocationHandler 接口,InvocationHandler 接口位于 java.lang.reflect 包下。
InvocationHandler 接口中只有一个方法:Object invoke(Object proxy,Method method,Object[] args)。
proxy : 被代理类的对象。method : 被代理类的方法。args : 被代理的方法的参数。
6、调用动态代理,需使用 Proxy.newProxyInstance()静态方法:
Proxy.newProxyInstance(loader,interfaces,handler)
loader : 代理类类加载器。interfaces : 被代理类实现的接口。handler : 动态代理类的实现类
7、动态代理的好处:减少编程的工作量,加入要实现多个代理的逻辑,只要写多个代理处理器就可以了,无需每种方式都写一个代理类。系统扩展性和维护性增强。
4、适配器模式:将一个类的接口转换为客户希望的另外一个接口。即让原本接口不匹配,不能一起工作的类可以一起工作。
①、组合方式(对象适配器):
Ⅰ、添加一个元接口,增加功能方法 添加实现类,并重写功能方法。
Ⅱ、添加一个目标接口,增加功能方法 添加实现类,并重写其功能方法。
Ⅲ、添加适配器类 实现元接口,添加带有目标接口对象的构造方法,重写元方法,使用目标接口对象调用目标方法。
②、继承方式(类适配器):
Ⅰ、添加一个元接口,增加功能方法 添加实现类,并重写功能方法。
Ⅱ、添加一个目标接口,增加功能方法 添加实现类,并重写其功能方法。
Ⅲ、添加适配器类 继承目标接口的实现类 并实现元接口,重写元接口中的方法,采用super.目标方法()调用。
③、区别:
Ⅰ、组合方式(对象适配器):把"被适配器"组一个对象组合到适配器中,以修改目标接口包装被适配器。
Ⅱ、继承方式(类适配器):通过多重继承不兼容的接口,实现对目标接口的匹配,单一为某个类的实现而适配。
Ⅲ、特点: 透明、重用、低耦合。用一个已经存在类的接口时,如果接口或方法与你的要求不同时,可以采用适配器模式。
5、策略模式:定义算法家族并分别封装,让它们之间可以互相替换。算法变化不会影响调用算法的用户。策略算法是相同行为的不同实现。
①、实现步骤:
Ⅰ 、添加抽象的策略父类 Strategy。
Ⅱ 、添加实现抽象类的子类。
Ⅲ 、定义 Factory 工厂生产策略对象。
Ⅳ、定义 Content 调用功能。
②、优点:
Ⅰ 、提供了管理相关的算法家族的办法。
Ⅱ 、提供了可以替换继承关系的办法。
Ⅲ 、避免使用多重条件转移的语句。
③、应用场景:多个类只区别在表现行为不同,可以使用 Strategy 模式,在运行时动态选择具体要执行的行为。
6、模板模式:定义一个操作中算法的骨架,步骤在子类中实现。子类不改变算法结构,即可重定义该算法的某些特定步骤。为防止恶意操作,一般模板方法都加上final关键字,不允许被覆写。
①、基本方法:
Ⅰ 、在模板方法中添加可被子类重写,方法返回值为 Boolean,来判断是否需要执行特定的操作。子类实现方法并在模板调用。
Ⅱ 、具体步骤:
1、多个子类有公有的方法,并且逻辑基本相同时。
2、重要、复杂的算法,把核心算法设计为模板方法,其余功能则由各个子类实现。
3、重构时,把相同的代码抽取到父类中,然后通过钩子函数(见Ⅰ)约束其行为。
②、优缺点:
Ⅰ 、在一个类中形式化地定义算法,而由它的子类实现细节的处理。
Ⅱ 、是一种代码复用的基本技术。
Ⅲ 、反向的控制结构。“好莱坞法则”,即父类调用子类,通过对子类的扩展增加新的行为,符合“开闭原则”。
Ⅳ、但会导致类的个数增加,但更符合“单一职责原则”。
③、适用场景:一次实现一个算法的不变部分,并将可变的行为留给子类来实现,需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。
7、建造者模式:将复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
①、组成部分:
Ⅰ、产品类:通常实现了模板模式,即有模板方法和基本方法。
Ⅱ、抽象建造者:父类方法等。一般由子类实现并组建规范的产品。
Ⅲ、具体建造者:实现抽象类定义的所有方法,并返回组建好的对象。
Ⅳ、导演类:安排已有模块的顺序,然后告诉Builder开始建造。
②、使用场景:
Ⅰ、相同的方法,不同的执行顺序,产生不同的事件结果时,可以采用建造者模式。
Ⅱ、多个部件或零件,都可以装配到一个对象中。但产生的运行结果又不相同时,可以使用该模式。
Ⅲ、产品类非常复杂,或类中的调用顺序不同产生了不同的效能,适合用建造者模式。
③、建造者模式与工厂模式的不同:
Ⅰ、建造者模式:基本方法的调用顺序安排,这些基本方法已经实现了,顺序不同产生的对象也不同;
Ⅱ、工厂方法:主要用于创建。组装顺序则不是它关心的。
8、原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。实际上就是实现Cloneable接口,重写clone()方法。
①、特点:
Ⅰ、性能优良。原型模式是内存二进制流的拷贝,比new对象性能好很多,尤其在循环体内产生大量对象的情况下。
Ⅱ、逃避构造函数的约束:既是优点也是缺点,直接在内存中拷贝,不会执行构造函数。
②、使用场景:
Ⅰ、资源优化:类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。
Ⅱ、性能和安全要求:new产生对象需要非常繁琐的数据准备或访问权限。
Ⅲ、一个对象多个修改者:一个对象要供其他对象访问,调用者们可能都需要修改其值。原型模式拷贝多个对象供调用者使用。
③、浅拷贝和深拷贝:
Ⅰ、浅拷贝:Object类提供的方法clone只拷贝本对象,不拷贝对象内部的数组、引用对象等。它们还是指向原生对象的内部元素地址。其他原始类型(int、long、char、string)会被拷贝。
Ⅱ、深拷贝:对私有的类变量进行独立的拷贝。如:thing.arrayList = (ArrayList<String>)this.arrayList.clone();
④、原型模式下,引用的成员变量两种情况下不会被拷贝:
Ⅰ、类的成员变量,而非方法内变量。
Ⅱ、必须是一个可变的引用对象,而非一个原始类型或不可变对象。
9、中介者模式:用中介对象封装一系列的对象交互,适用于多个对象紧密耦合的情况。紧密耦合的标准是:在类图中出现了蜘蛛网状结构,即每个类都与其他的类有直接的联系。
①、组成部分:
Ⅰ、抽象中介者:定义统一的接口,用于各同事角色之间的通信。
Ⅱ、具体中介者:通过协调各同事角色实现协作行为。它必须依赖于各个同事角色。
Ⅲ、同事:同事间通信时,需通过中介者角色协作。同事类行为分为两种:
1、同事本身的行为(自发行为),与其他的同事类或中介者没有任何依赖。比如改变对象本身的状态,处理自己的行为等。
2、必须依赖中介者才能完成的行为,叫做依赖方法。
②、ps:
Ⅰ、使用同事类注入而非抽象注入的原因:抽象类中不具有每个同事类必须要完成的方法。即每个同事类中的方法各不相同。
Ⅱ、同事类要使用构造函数注入中介者,因为同事类必须有中介者。中介者使用getter/setter方式注入同事类,因为中介者却可以只有部分同事类。