设计(原则、模式)

136 阅读14分钟

设计模式

复习

1、反射的应用场景通常出现在底层实现当中,往往是工具类或框架平台的底层。比如:我们已经学习过的FastJson,以及即将要学到的Spring框架,当然也有后面课程要教给大家的其他常用框架。

对于工具类或框架的实现人员,他们会在他们的代码中完成要实现效果的基本操作。但是,不同的应用场景,具体要实现的业务他们是不知道的,只能是调用后续开发人员的代码。而后续人员的代码在框架开发人员来说是不知情的,所以他们只能采用反射的方式,达到“编译时未知,运行时使用”的效果。

作为我们同学们来说,我们大部分情况下都是属于后续的业务开发人员。我们是用框架、用工具的人,所以很多同学在一开始的时候是不会操作到反射API的,除非你后面升级了。

所以,对反射API的学习要求:

a、在学习或使用框架的过程中,要能够体会到人家底层的调用方式,特别是为什么要我们做配置。

b、针对面试而言,反射也是面试官的最爱之一。因此,能够掌握得越多越有利。

2、注解(Annotation),它的地位是以后你们谁都离不开的。后面的框架,服务器当中一定会用注解!

注解到底是干什么的?它就是用来做配置的,就是用来替换XML的。而对大家的要求就是要学会配置框架中已经设计好了的注解。

3、我们昨天学习了注解的3个内容:

3-1、定义注解;

3-2、使用注解;

3-3、在运行时通过反射解析注解,然后做出相应的实现。 实际工作当中,我们用得最多的是3-2。

3-1、定义注解

@interface

Annotation接口

注解当中有哪些内容?--- 类型元素 --- 语法特殊既像方法又像属性 --- 赋值的时候像属性,取值的时候像方法

能够定义哪些数据类型的类型元素?--- 5种以及这5种的一纬数组。

元注解 -- 修饰注解的注解 -- 4个

3-2、使用注解

标准语法:

@注解名(属性名=属性值,数组属性名={元素1,元素2,元素3},.....)

简洁语法:

@注解名

@注解名()

@注解名(值)

@注解名(属性名=属性值,数组属性名={元素1})

@注解名(属性名=属性值,数组属性名=元素1)

3-3、在运行时使用注解

前提:这个注解的Retention必须是RUNTIME级别的。

操作方式是通过反射去进行获取;你把注解写在哪个地方,就用这个地方的反射类型。

定义在类头上,找Class对象要注解

定义在构造头上,找Constructor对象要注解

定义在属性上,找Field要

定义在方法上,找Method要

设计原则

面向对象是一种编程思维,是我们分析问题设计模型的一种思路。从本质上看,是没有绝对的对与错的。但是,不同的设计方式还有高低之分的。而判断的标准是什么呢?

1、最基本的标准是功能实现;

2、在都能够实现功能的前提下,我们的判断标准就叫做“高内聚、低耦合”。

“内聚度” --- 体现的是每一个基本模块自己本身的功能完整度。高内聚指的是一个模块本身的任务就应该在它内部完成,不要分散到别的地方。

基本模块根据粒度,可以是一个子系统,也可以是一个包,也可以是一个类,还可以是一个方法。

“耦合度” --- 体现的是模块与模块的关联关系。低耦合指的是尽量关联需要使用到的其他模块,无关模块不要关联;关联的时候尽量采用松散易解除或易修改的关联方式。

“高内聚”和“低耦合”仍然是一个范围比较大的概念,对代码设计的具体指导意义不大。所以,软件业又提出了一些设计原则,供我们在具体分析和设计的时候作为指导性原则:

单一职责

模块的职责要单一,不能将太多的职责放在一个模块中。

具体到我们能够理解的范围内:

1、一个方法只应该完成一个功能,方法名就应该是对这个功能的完整描述;

2、一个类只应该描述一种对象;

3、一个包只应该包含跟这个包描述的功能相符的功能类和接口;

开闭原则

开闭原则是其他各种原则的核心,或者说其他的原则都是为了在某一方面达到开闭效果而提出来具体原则。

Software should be opened for Extendtion, but closed for Modification。

软件应该对扩展进行开放,对修改进行关闭。

里氏替换

这是一个专门用来判断该不该做继承的原则。

我们之前判断该不该做继承,我们用的是一种叫做"is-a"(是一个)的判断方式。

可惜,is-a方式是基于我们的生活场景提出来的。但是,在软件系统中,这种经验有可能是错误的。

她提出来判断两个类是否应该做继承的标准是:凡是父类对象在本系统中能够正确工作的位置,都能够替换成子类对象且不引起额外的错误。

依赖倒转

在设计类与类的关联的时候,尽量关联对方的抽象(抽象类与接口),不要直接去关联对象的实现类。

这样做的好处是为了能够达到多态的效果。因为抽象类(接口)的引用可以指向它所有的实现子类对象。那么,我们如果需要更改实现,只需要增加新的子类或新的实现类,而不需要更改关联处的代码。

接口隔离

尽量定义职责单一的小接口,少定义职责过多的大接口。

愿义:上层接口的设计不应该污染下层接口(或实现类)的职责,要做好职责隔离。

比如:上层接口有三个行为,下层子接口或实现类只需要用到其中的两个或1个。但是,通过接口实现的语法,导致下层的接口或实现类也有3个行为,多了他们不需要的,这就是“污染”。那么在这种情况下,我们最好做三个上层接口,各自拥有一个行为,让子接口或实现类根据自己的业务需要可以自主选择。

误区一:有人会认为为了不违反接口隔离原则,那每个接口都只定义一个行为。

如果我们判断出某些行为是同时出现或同时不出现的,完全可以把它们写在一个接口当中。

误区二:很多同学到了面试阶段后,会把接口隔离原则和依赖倒转原则搞混。

在三层架构当中,我们的表示类关联业务层接口,然后通过配置去产生业务类对象交给这个接口的引用;同理,我们的业务类关联持久层接口,然后通过配置去产生持久类对象交给这个接口引用。这是典型的“上层不要关联下层的具体实现,而应该关联下层的抽象”---依赖倒转。

这个过程,我们后面还要继续通过实践来进一步讲解和领悟。

组合聚合原则

少用继承,多用组合和聚合。

继承的方式是一种硬代码的关联,一旦发生变化,我们必须去修改extends 后面的父类

组合和聚合是支持多态的,然后再配合我们的反射技术,通过配置文件和注解,可以在不改变java代码的情况下,直接绑定子类对象。

所以从动态性,以及开闭原则来看,组合聚合比继承更好。

迪米特法则

也叫“最少知识原则”。

类与类之间的绑定关系,尽量的简洁,只绑定跟你相关的类,无关的别去绑定。

因为你绑定的越多,那么引起这类变化的原因也就越多。

模式

在上面讲解当中所涉及到的设计原则,这些原则非常重要,特别是对于一个设计人员来说。但是编码实现的开发人员来说,往往局限于眼界或经验并不能完全体会到它们的实用性,或者在处理具体问题域的时候不知道该如何来设计或实现自己代码从而满足上面的原则要求。由此,产生了“模式”。

我们在编程的过程中,随着参与的项目越来越多,大家会发现:有时候很多问题并不是某一个项目中独有的,而是反反复复在不同的项目中出现。于是先人们就把这些问题总结出来,起了名字,给出了统一的标准的经过印证的解决“套路”。

所以,“模式”就是“套路”。模式在学习过程中有如下几个关键点:

1、模式要用在什么地方

2、模式的名字 --- 名字的主要目的是为了更好的在同行中进行交流 --- 名字也是对模式的精炼描述

3、当然是模式的具体解决方案

4、这个模式使用后的优缺点,特别是一个模式有多种解决方案,那更要去了解这多种方案的优缺点,才能在合适的时候选择使用合适的方案。

在软件工程当中,模式有两大类:

1、设计模式;

更偏向于某个具体的问题域的解决,是代码级别的解决方案;-- 微观

2、架构模式; 更偏向于工程的组织架构的设计,对于一个项目中拥有的众多的类和接口,如何进行分工,如何进行关联;-- 宏观

设计模式

一共有23种,分为三大类:

1、创建模式 -- 主要创建某种特殊的对象,或则在某种特殊要求下创建对象

2、结构模式 -- 主要是利用组合/聚合,或者是继承,让类与类能够形成某种关联结构,从而更适应某个问题域的解决。

3、行为模式 -- 探讨的是在某种问题域当中如何设计一个行为来适应这个场景。

单例模式 --- Singleton

当在一些问题域当中,需要我们去创建一种类,这个类能且只能产生一个对象。

单例模式有几种:

1、饿汉模式;

单例模式 -- 饿汉模式

优点:
线程绝对安全

缺点:
   由于对象的产生是写在静态属性的初始位置;
   那么只要一加载这个类,不管是否调用到getInstance(),
   就算是调用别的静态方法,也会把实例对象产生出来。--- 专业叫法:预加载
   我们更多的时候是希望当我们真正需要获取实例对象的时候,
   你才给我产生。什么时候是我真正需要?是当我调用getInstance()
   的时候。

2、懒汉模式;

单例模式 --- 懒汉模式

优点:
它是延迟加载的,只有当第一次我们真正调用getInstance(),表明
外部真的需要它的实例对象了,它才会在内部产生对象。

缺点:
如果不加控制,那么在多线程情况下,这个方式是线程不安全的。
要想让这个懒汉模式做到线程安全,那么就要给getInstance()
加synchronized关键字,让它能够做到线程同步。
而做到了线程同步,那么在多线程的情况下,它的效率就会下降。

3、DCL模式;

单例模式 --- Double Check Lock 机制
优点:
1、延迟加载
2、线程安全
3、性能提升 -- 只有当instance还没有产生的时候,就有多个线程
             同时调用getInstance(),那么只在这种情况下线程会排队;
             一旦instance已经产生了,就算有多个线程同时调用
             getInstance(),这些线程是不会排队的。
缺点:
JDK5之前,Java程序员只能看,不能用。当时的JVM的内存模型,不支持这种
语法,因为在Java当中new 对象()其实不是一条指令,这些指令不具有原子性,
还是可能在多线程的情况下被被的线程插入。
JDK5之后,我们可以用了,但是需要增加一个关键字,告知JVM,这个对象的产生
需要原子性,是不可插入的。而加了这个关键字,会让JVM多做一些工作,会部分丢
失性能。

4、内部类模式;

单例模式 --- 静态内部类实现
1、延迟加载
   只有调用Singleton4的getInstance()的时候,才会用到
   InnerClass,也才会加载InnerClass,在加载的时候才会
   产生InnerClass的静态属性instance,也才会产生Singleton4
   的实例对象。

2、线程安全
   在InnerClass的加载期产生instance对象,运行期所有的线程都
   只是获取到这个对象,不存在安全性问题。

3、支持高并发
   整个代码中没有使用同步锁,所有线程都可以同时并发运行,不存在
   排队。

 4、唯一的不友好就是对初学者,因为使用了内部类的语法。

5、枚举的方式实现单例 --- 有兴趣的同学可以自己去看看

前面4种常常在面试中出现,要求分辨各自的优缺点,以及手工书写要求的实现方式(DCL和内部类的方式最多)。

工厂模式 --- Factory

使用场景是:将对象的使用者和对象的生产者进行分离。

在所有的工厂模式当中,都有三种角色的类,我们把它们称之为"消费者","生产者",“产品”。消费者需要用到产品对象,但是不用自己去构建这个产品对象,而是找生产者要即可;

生产者提供方法,专门返回产品对象,无需关心这个产品对象被拿来做什么。

1、简单工厂模式

这种模式考虑的变化点是,同一个工厂,可以根据不同的情况,产生不同的产品对象。

由于变化点是在产品对象上,所以这个时候我们往往会抽象产品,然后给它提供不同的实现类。

2、工厂方法模式

这种模式同一个产品,不同的工厂生产出来会有差异性。

这个时候的变化点是在工厂上,所以我们往往会抽象工厂。

3、抽象工厂

工厂和产品都是变化点,所以我们既要抽象出工厂父类,也要抽象出产品父类。

原型模式 --- prototype

场景是:根据一个已经存在的对象,创建一个新的跟它一摸一样的对象。 --- 克隆

利用Object中的clone()

1、首先要让被克隆的类实现Cloneable接口;

2、然后要重写clone方法,提升它的访问修饰符为public;

3、最后的效果,是浅克隆的效果。

所谓的“浅克隆”是指,被clone的对象会产生一个新的,但是它的属性对象不产生。也就是说,clone方法只克隆一层。

要解决这个问题:只能一层层去克隆,需要让我们自己去克隆每一个关联对象,然后再把它们组装起来,才能够完成“深克隆”。很明显,这么做很麻烦。

直接利用对象的序列化和反序列化实现深克隆

1、让所有的自定义类型实现Serializable接口;

2、在最外层的类身上增加一个自定义方法,完成序列化和反序列化就可以了。

public BasketballStar deepClone(){
        BasketballStar newStar = null;
        try(ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);){
            oos.writeObject(this);
            
            try(ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
                ObjectInputStream ois = new ObjectInputStream(bis)){
                newStar = (BasketballStar) ois.readObject();
            }catch(Exception ex1){
                ex1.printStackTrace();
            }
        }catch(Exception ex2){
            ex2.printStackTrace();
        }
        return newStar;
    }

总结一下

原型模式也是后面使用比较多的一种,同时也是面试官的常用问题。

1、深克隆和浅克隆的区别

2、clone方法的使用

3、能够实现对象序列化和反序列化方式的深克隆(代码级别)