设计原则
1.开闭原则
2.依赖倒置原则
3.单一职责原则
4.接口隔离原则
5.迪米特法则(最少知道原则)
6.里式替换原则
7.合成/复用原则(组合/复用原则)
设计原则要讲究取舍。
一、开闭原则【最重要】
定义:
一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
这里的对扩展开放,感觉就是接口里新增方法。对修改关闭就是尽量不要修改具体实现。
用抽象构建框架,用实现扩展细节
优点:提高软件系统的可复用性及可维护性
其核心就是面向抽象编程。
个人理解就是面向接口编程。
场景:
比如我们网络选课,有很多课程,比如英语、数学、java;每个课程都有其对应的课程名、价格、Id.
那我们在写的时候,一方面可以设置一个BaseCource类;另一方面也可以面向接口编程,设置一个ICource接口:
public interface ICourse {
Integer getId();
String getName();
Double getPrice();
}
然后就是具体实现:
public class JavaCourse implements ICourse {
private Integer id;
private String name;
private Double price;
public JavaCourse(Integer id, String name, Double price) {
this.id = id;
this.name = name;
this.price = price;
}
@Override
public Integer getId() {
return this.id;
}
@Override
public String getName() {
return this.name;
}
@Override
public Double getPrice() {
return this.price;
}
}
调用:
public class Test {
public static void main(String[] args){
ICourse javaSource = new JavaCourse(96,"Java设计模式精讲",299d);
System.out.println("课程ID:"+javaSource.getId()+"\n课程名:"+javaSource.getName());
}
}
基本的实现如上,没有什么难度。但是现在问题来了!突然有一天我们的java课程打折了,八折优惠。这时候我们就需要改变其价格了。
关于改变价格,有多种修改方案:
1.在ICourse里新增一个getDiscountPrice()方法,返回折扣价
2.在JavaCourse的getPrice乘上0.8返回
这两种方案实际上都有问题:
第一种,如果在接口里新增方法。那么JavaCourse需要实现方法自不必说,关键的问题是其他实现了该接口的类也得被动去实现这个新增的方法!(这里其实也可以继承ICourse新写个接口,不过后面有更优雅的方法)
接口作为一个契约,应该是稳定的,不能随意修改。(不到万一不要修改,比如这个需求只是java课程打折,没有涉及到全局,如果是全局折扣,倒是可以修改)
第二种,如果修改了其获取价格的实现,那如果有需求需要拿到原价呢?修改了原本的实现,已经导致了功能的缺失了。
3.继承JavaCourse,新增getDiscountPrice方法
ex:
public class JavaDiscountPrice extends JavaCourse {
public JavaDiscountPrice(Integer id, String name, Double price) {
super(id, name, price);
}
public Double getDiscountPrice(){
return getPrice()*0.8;
}
}
//调用
public class Test {
public static void main(String[] args){
ICourse source = new JavaDiscountPrice(96,"Java设计模式精讲",299d);
JavaDiscountPrice javaSource = (JavaDiscountPrice)source;
System.out.println("课程ID:"+javaSource.getId()+"\n课程名:"+javaSource.getName() +
"\n折后价:"+javaSource.getDiscountPrice());
}
}
这里需要强转是因为引用是ICourse,其实现是JavaCourse实现的,用父类声明的引用,拿不到getDiscountPrice,需要强转一下.引用和实现不同,需要向下转型,强转.
Tips:
父子对象之间的转换分为了向上转型和向下转型,它们区别如下:
向上转型 : 通过子类对象(小范围)实例化父类对象(大范围),这种属于自动转换
向下转型 : 通过父类对象(大范围)实例化子类对象(小范围),这种属于强制转换
这里父类声明(JavaCourse实现了ICourse),子类实现,需要向下转型。
总结:
这里我们实现了一个子类,没有修改父类(没有修改ICourse、JavaCourse),而是对功能进行了扩展,新建了一个子类,对上层几乎没有什么影响。
开闭原则是其他原则的基础,重中之重。一定要遵循开闭原则。
二、依赖倒置原则
定义:
高层模块不应该依赖底层模块,二者都应该依赖其抽象。
抽象不应该依赖细节;细节应该依赖抽象。
针对接口编程,不要针对实现编程。
在开发中还需要注意的是:
类最好实现接口或者继承抽象类,尽量不要继承具体实现类,不要覆盖已实现的方法。
优点:
可以减少类间的耦合性、提高系统稳定性,提高代码可读性和可维护性,可降低修改程序所造成的风险。
Example:
小明要学习一些课程,他目前要学java,前端、python,后期可能还要学习其他课程等。
最简单的实现:
面向实现编程:
public class Geely {
public void studyJavaCourse(){
System.out.println("Geely 学习java课程");
}
public void studyFECourse(){
System.out.println("Geely 学习FE课程");
}
public void studyPythonCourse(){
System.out.println("Geely 学习Python课程");
}
}
//高层调用
private static void dependencyInversion(){
Geely geely = new Geely();
geely.studyFECourse();
geely.studyJavaCourse();
}
这种实现可以看出来,如果我们后续要学习其他的课程,那么底层实现Geely是要经常改变的。(这也就违反了开闭原则)
根据依赖倒置原则的第一条原则,高层模块是不能依赖于低层模块的。这里Test是高层模块,Geely属于底层实现模块,这里的Test依赖于Geely的实现,这是依赖倒置原则绝对无法容忍的。
引入抽象
根据我们的依赖倒置原则,面向抽象编程,我们引入一个抽象接口:ICourseEx.
这是我们所有学习课程的抽象实现。
public interface ICourseEx {
void studyCourse();
}
然后需要学习的课程实现此接口即可:
public class JavaCourseEx implements ICourseEx {
@Override
public void studyCourse() {
System.out.println("学习Java课程");
}
}
public class FECourseEx implements ICourseEx {
@Override
public void studyCourse() {
System.out.println("学习FE课程");
}
}
**
那么在Geely这个底层实现中,我们就可以传入ICourseEx接口作为参数,在高层调用的时候传入具体实现即可。这样高层就不会依赖于底层了!而是我们在新增课程的时候,并不会修改Geely这个底层实现,只需要新增一个实现类,也不会碰到之前课程的逻辑。在遵循依赖倒置原则的时候,同时遵守了开闭原则。面向接口编程的世界,远比面向实现编程的世界可扩展性强。在抽象层面上搭建的架构,其稳定性与可靠性远比在细节上搭建强。
**
Geely中:
public void studyCourse(ICourseEx course){
course.studyCourse();
}
高层:
Geely geely = new Geely();
geely.studyCourse(new JavaCourseEx());
geely.studyCourse(new FECourseEx());
解耦的最终成效就在此刻。
Others:
除了方法中注入ICourseEx,也可以在Geely的构造器中注入ICourseEx
public Geely(ICourseEx course){
this.mCourse = course;
}
public void studyImoocCourse(){
mCourse.studyCourse();
}
最后来一句,此原则的核心就在于:面向接口编程。
三、单一职责原则
定义:
不要存在多于一个导致类变更的原因。
一个类/接口/方法只负责一项职责
优点:
降低类的复杂度、提高类的可读性
提高系统的可维护性、降低变更引起的风险
ex:
1.从具体实现来看
假设有个Bird类,里面有个method:moveType(String birdName).
众所周知,并不是所有鸟都是飞的,比如鸵鸟跑,企鹅游走,一般情况下我们都是在这个方法中加if-else.但是这样做可能会导致影响到之前的逻辑,毕竟实际业务中开发影响因素肯定不会那么简单,将两个不同品种的鸟类,毫不相关的移动方式,放在一个判断中,还是有较大的几率影响之前的逻辑的。这时候在Bird类的基础上引申出FlyBird和RunBird等,可以将Bird类的职责降低,扩展性也更强,同时也遵循了开闭原则。
2.从接口实现来看
* @description: 职责:1.获取课程信息、2.操作课程
*/
public interface ICourseX {
String getCourseName();
byte[] getCourseVideo();
/**课程操作 职责**/
void studyCourse();
//退课程
void refundCourse();
}
上面这个接口,虽然都是跟课程有关的,但是实际上有两个职责,见注释。
修改后:
* @description: 单一职责:获取课程信息
*/
public interface ICourseContent {
String getCourseName();
byte[] getCourseVideo();
}
* @description: 单一职责:操作课程
*/
public interface ICourseManager {
/**课程操作 职责**/
void studyCourse();
//退课程
void refundCourse();
}
实现:
* @description: 两个职责可以分别实现
*/
public class CourseImpl implements ICourseContent, ICourseManager {
@Override
public String getCourseName() {
return null;
}
@Override
public byte[] getCourseVideo() {
return new byte[0];
}
@Override
public void studyCourse() {
}
@Override
public void refundCourse() {
}
}
分离后的好处就是两个接口可以分别实现,可以不用让一个类有那么大的责任...最起码想给其降责任的时候,也好降一些
四、接口隔离原则
定义:
用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口
一个类对一个类的依赖应该建立在最小的接口上
建立单一接口,不要建立庞大臃肿的接口
尽量细化接口,接口中的方法尽量少
注意适度原则,一定要适度
优点:
符合高内聚低耦合的设计思想,从而使得类具有很好的可读性、可扩展性和可维护性
ex:
public interface IAnimalAction {
void eat();
void fly();
void swim();
}
//实现类
public class Dog implements IAnimalAction{
@Override
public void eat() {
}
@Override
public void fly() {
}
@Override
public void swim() {
}
}
这里狗会吃和游泳,但是不会飞,这就导致了狗这里面会有一些方法空实现。
对于一个接口声明的方法太多,且不是同一类型,就会导致这种空实现的存在。
这时候就得遵循接口隔离原则来做事了。
v2版本:
将粗粒度的IAnimalAction拆分为三个接口,分别实现eat、swim、fly方法:
public class Dog implements IEatAnimalAction,ISwimAnimalAction{
@Override
public void eat() {
}
@Override
public void swim() {
}
}
这样我们的实现类就可以按需实现对应接口。
这个接口隔离和单一职责的区别在于:
后者指的是类、接口职责是单一的,强调的是职责,只要职责单一,方法可以很多,注重的是细节和实现。
前者注重的是接口依赖的隔离,约束的是接口,框架的构建。
接口一定要适度,不要太小。
五、迪米特法则
定义:
一个对象应该对其他对象保持最少的了解。又叫最少知道原则。
尽量降低类和类之间的耦合
优点:
降低类之间的耦合
强调只和朋友交流,不和陌生人说话。
朋友:
出现在成员变量、方法的输入、输出参数中的类称为成员朋友类,而出现在方法体内部的类不属于朋友类。
多使用private/protected 权限符,凡是都要有个度,要反复权衡。
设计模式分类:
** 六个创建型模式**
- 简单工厂模式(Single Factory Pattern)
- 工厂方法模式(Factory Method Pattern)
- 抽象工厂(Abstract Factory Pattern)
- 单例模式(Singleton Pattern)
- 原型模式(Prototype Pattern)
- 建造者模式(Builder Pattern)
七个结构型模式 - 适配器模式(Adapter Pattern)
- 桥接模式(Bridge Pattern)
- 组合模式(Composite Pattern)
- 装饰模式(Decorator Pattern)
- 外观模式(Facade Pattern)
- 享元模式(Flyweight Pattern)
- 代理模式(Proxy Pattern)
十一个行为型模式 - 职责链模式(Chain of Responsibility)
- 命令模式(Command Pattern)
- 解释器模式(Interpreter Pattern)
- 迭代器模式(Iterator Pattern)
- 中介者模式(Mediator Pattern)
- 备忘录模式(Memento Pattern)
- 观察者模式(Observer Pattern)
- 状态模式(State Pattern)
- 策略模式(Strategy Pattern)
- 模板方法模式(Template Method Pattern)
- 访问者模式(Visitor Pattern)
一、简单工厂
定义:
由一个工厂对象决定创建出哪一种产品类的实例
(这句话有两个信息:①工厂类一般只有一个 ②产品由工厂创建)
类型:创建型,但不属于GOF23种设计模式
优点:
只需要传入一个正确的参数,就可以获取你所需要的对象而无需知道其创建细节
缺点:
工厂类的职责过重,增加新的产品需要修改工厂类的判断逻辑,违背开闭原则(缺点太大了,直接违背了最基本的设计原则,所以真的不推荐使用啦,但不影响了解)
Demo:
原场景:有个抽象类Video,其有多个子类:JavaVideo,AdultVideo等等,那我们在应用层调用的时候,可以:
Video video = new JavaVideo();
Video video2 = new AdultVideo();
如果采用这种方法,那么应用层难免会引入子类的依赖,违背了迪米特法则。所以我们希望能将创建子类具体实例的逻辑交给工厂类,尽量减少引入众多子类的依赖。(这样顶多引入一个工厂类的依赖,不会因为其他众多子类依赖),工厂类如下:
public class VideoFactory {
public static Video createVideo(String name) {
if (name.equalsIgnoreCase("JavaVideo")) {
return new JavaVideo();
} else if (name.equalsIgnoreCase("AdultVideo")) {
return new AdultVideo();
}
return null;
}
}
正如之前所说,只需要引入传入一个参数就可以获得我们想要的对象,但是缺点很明显,每次有新的子类实现,都需要修改工厂类,违反了最重要的开闭原则。
改善:
通过反射可以使简单工厂不违背开闭原则:
/**
* 通过反射 解决开闭原则
* @param c
* @return
*/
public static Video getVideo(Class c){
Video video = null;
try {
video = (Video) Class.forName(c.getName()).newInstance();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return video;
}
//使用
//改善简单工厂
Video av2 = VideoFactory.getVideo(AdultVideo.class);
if (av2 ==null) return;
av2.produce()
jdk里对应的:
二、工厂方法
定义:
定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类,工厂方法让类的实例化推迟到子类中进行
类型:创建型
适用场景:
1.创建对象需要大量重复的代码
2.客户端(应用层)不依赖与产品类实例如何被创建、实现等细节
3.一个类通过其子类来指定创建哪个对象
优点:
用户只需要关心所需产品对应的工厂,无需关心创建细节,只需要关注工厂类即可
加入新产品符合开闭原则,提高可扩展性
缺点:
类的个数容易过多,增加复杂度(还好,一般并不会太多,真的会很多,就抽象工厂)
增加了系统的抽象性和理解难度
工厂方法和抽象工厂的区别在于理解两个概念:
产品等级和产品族
产品等级:
比如视频:java视频,python视频,android视频,这三个产品都是同一个等级的,都是视频,所以是同一个产品等级
比如电视:华为电视、小米电视、海信电视,也是同一个产品等级,都是电视
产品族:
比如java的视频、手记,这是同一产品族
比如海尔的手机、冰箱、空调是同一产品族
demo:
//产品抽象类
public abstract class Video {
abstract void produce();
}
//抽象工厂(必须)
public abstract class VideoFactory {
abstract Video getVideo();
}
//具体工厂 (必须)
public class JavaFactory extends VideoFactory{
@Override
Video getVideo() {
return new JavaVideo();
}
}
//具体产品1,本例中可忽视
public class PythonVideo extends Video {
@Override
void produce() {
System.out.println("PythonVideo");
}
}
//具体产品2
public class JavaVideo extends Video {
@Override
void produce() {
System.out.println("JavaVideo");
}
}
//客户端调用
public static void main(String[] args) {
// VideoFactory videoFactory = new PythonFactory();
// Video video = videoFactory.getVideo();
// video.produce();
//---应用层需要更换实现的时候,只需要替换工厂即可,不过
//同一产品等级的产品,都需要各自的工厂类,但这也是工厂方法实现的必经之路
VideoFactory videoFactory = new JavaFactory();
Video video = videoFactory.getVideo();
video.produce();
}
扩展性:
当有新的产品的时候,可以新增一个产品类继承抽象产品基类,新增一个工厂类继承抽象工厂类,符合了开闭原则。
Tips:
这里抽象产品基类和抽象工厂类,也可以使用接口,因为产品或者工厂可能会有一些默认的实现,所以这里使用抽象类而不是接口,这样做更加方便。但是因为当下java接口支持default方法的实现,所以采用接口也好。
工厂方法的关键:
1.抽象产品类(或接口)
2.抽象工厂类(或接口)
3.作用范围是同一产品等级的产品
4.新增一个产品,需要新增产品类(继承抽象产品类)和工厂类(继承抽象工厂类)
5.产品的具体创建过程交由每个产品对应的产品类实现,而不是向简单工厂那样交给一个工厂基类实现
三、抽象工厂
定义:抽象工厂模式提供一个创建一系列相关或相互依赖的接口
无需指定他们具体的类
类型:创建型
抽象工厂-适用场景
客户端不依赖与产品类实例如何被创建、实现等细节
强调一系列相关的产品对象(属于同一个产品族)一起适用创建对象需要大量重复的代码
抽象工厂-优点
具体产品在应用层隔离,无需关心创建细节
将一个系列的产品族统一到一起创建
抽象工厂-缺点
规定了所有可能被创建的产品集合,产品族中扩展新的产品困难,需要修改抽象工厂的接口
增加了系统的抽象性和理解难度
这里一个具体工厂不再只创建一个产品,而是创建一个产品族。
ex:
小米、华为、苹果 三者都生产 电视、手机、电脑
这里,小米电视、华为电视、华为电视是 同一产品等级
电视、手机、电脑是同一产品族
具体实现:
一个抽象工厂:IProductFactory
三个抽象产品:Tv、MobilePhone、Computer
三个具体工厂:HWProductFactory、MiProductFactory、IPhoneProductFactory
九个具体产品:三个Tv,三个MobilePhone,三个Computer
UML类图:
依赖关系:
具体调用:
抽象工厂有个缺点就是新增产品很麻烦。
//然后新增工厂类,在IProductFactory新增产品方法,然后新增的工厂类继承此
//问题是,比如小米新增一个智能家居的产品,如果抽象工厂里新增了一个产品方法,
// 其他两家没有的话
//如果用抽象方法来搞,就会导致其他两家的工厂里有些空实现.....
而且新增产品的话,也不符合开闭原则。
Tips:
只适用于很固定的,如果很频繁改动的话,不好用抽象工厂,不然维护成本很高很高。考虑用工厂方法最好,新增产品成本不高
四、建造者模式
定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示
用户只需指定需要建造的类型就可以得到它们,建造过程及细节不需要知道
类型:创建型
适用场景:
①如果一个对象有非常复杂的内部结构(很多属性)
⑥想把复杂对象的创建和使用分离
优点:
封装性好,创建和使用分离
扩展性好,建造类之间独立,一定程度上解耦
缺点:
产生多余的Builder对象
产品内部发生变化,建造者都要修改,成本较大
Tips:建造者模式和工厂模式的区别
①建造者模式更注重方法的调用顺序,
工厂模式注重于创建产品
②创建力度不同:建造者模式对于创建一些复杂的产品由各种构件构成,而工厂模式将这个产品创建出来即可
一定的顺序决定了产出的产品不同,工厂不关心顺序
个人认为:可以举个简单例子,比如,我们想造一个汽车,如果说关注面在于汽车这个整体,比如宝马这边需要造一个车子,大众这边也需要,还有其他汽车厂商也需要造车子,这时候对于业务来说,我们对于各个厂商都需要车子这么一个产品的关注度超过了创建车子的流程,这时候,最好使用工厂模式。工厂模式更注重的是多个产品,而不是流程。
而如果我们的业务是要造一个车子,关注的是创建流程,比如我这个车子产品,他的轮胎有哪些选择,怎么造,它的车窗有哪些选择,它的发动机又有哪些选择怎么造,那这种时候,建造者模式就再适合不过了。
五、原型模式
定义:指原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象
不需要知道任何创建的细节,不调用构造函数
类型:创建型
适用场景:
1.类初始化消耗较多资源
2.new产生的一个对象需要非常繁琐的过程(数据准备、访问权限等)
3.构造函数比较复杂
4.在循环体中生产大量对象
简而言之,如果一个类的属性过多,需要创建多个,就可以考虑使用原型模式。
优点
原型模式性能比new一个对象性能高【通过实现Cloneable接口的原型模式在调用clone函数构造实例时并不一定比通过new操作速度快。实现原型模式也不一定非要实现Cloneable接口】
简化创建过程
缺点
1.必须配备克隆方法(如果还有其他对象,那这些对象也得实现!这就...)
2.对克隆复杂对象或对克隆出的对象进行复杂改造时,容易引入风险
3.深拷贝、浅拷贝要运用得当
原型--扩展
深克隆
浅克隆
由于看视频有点懵逼,所以查了查资料,其中一篇博客很详尽:
原型模式详解
先上UML图(盗的上面博客的图哈,因为参考了好几篇,UML图都是一样的)
主要就三个:抽象原型类、具体原型类、客户类
抽象原型类:声明克隆方法的接口,是所有具体类型类的公共父类。可以是接口、抽象类、具体类
具体原型类:实现抽象原型类中声明的克隆方法,在克隆方法中返回一个自己的克隆对象
客户类:调用方。
Tips:实际上,可以更加简便,将抽象原型类和具体实现类合并成一个,只需要实体类实现Cloneable,覆写clone方法即可。(有两点需要注意:Cloneable接口是个空接口,clone方法是Object里的。其次,Cloneable是个标识接口,它表明这个类是可拷贝的。如果没有实现Cloneable接口,但却调用了clone()函数,程序会抛出异常的!)
再开始敲之前,先说明一下深克隆和浅克隆的区别,这两者的区别,据我所闻,阿里腾讯好像都有人被问到过。
浅克隆:
①八大基本类型:byte/short/int/long/char/double/float/double/boolean;
②String
③String[]
如果是复杂的类型,比如枚举、实体类就只复制对应的内存地址。(相当于你复制的这些类型的对象跟你原对象指向同一个内存,改了后者的还是会影响你最初的对象!就很不行!)
深克隆:
全部复制,各自独立。修改克隆对象对于原型对象没有丝毫影响。
Demo:
public class InvoiceEx implements Cloneable {
public String invoiceHeader;
public int amountOfMoney;
//行业分类
public String[] industryTypes;
//纸质分类
public ArrayList<String> paperTypes;
//详情
public InvoiceExDetail detail;
public InvoiceEx(String header, int money, String[] industry, ArrayList<String> paperTypes, InvoiceExDetail detail) {
this.invoiceHeader = header;
this.amountOfMoney = money;
this.industryTypes = industry;
this.paperTypes = paperTypes;
this.detail = detail;
}
@Override
protected InvoiceEx clone() throws CloneNotSupportedException {
return (InvoiceEx) super.clone();
}
@NonNull
@Override
public String toString() {
return "invoiceHeader:" + invoiceHeader + "\n amountOfMoney:" + amountOfMoney +
"\n industryTypes" + Arrays.toString(industryTypes) + "\n paperTypes:" + paperTypes;
}
static class InvoiceExDetail implements Cloneable {
//数量
public int count;
//单价
public int unitPrice;
//开票日期
public Date invoiceDate;
public InvoiceExDetail(int count, int unitPrice, Date invoiceDate) {
this.count = count;
this.unitPrice = unitPrice;
this.invoiceDate = invoiceDate;
}
@Override
protected InvoiceExDetail clone() throws CloneNotSupportedException {
return (InvoiceExDetail) super.clone();
}
@NonNull
@Override
public String toString() {
return "count:" + count + "~~~unitPrice:" + unitPrice + "~~~invoiceDate:" + invoiceDate;
}
}
}
调用:
public static void testProto2() throws CloneNotSupportedException {
InvoiceEx.InvoiceExDetail invoiceExDetail = new InvoiceEx.InvoiceExDetail(1, 250, new Date());
ArrayList<String> paperTypes = new ArrayList<>();
paperTypes.add("普通纸");
paperTypes.add("压感纸");
String[] industries = new String[]{"工业", "商业"};
InvoiceEx proto = new InvoiceEx("ICBC", 250, industries,
paperTypes, invoiceExDetail);
System.out.println("原型:\n"+proto);
System.out.println("-------------------------------------");
//开始克隆和修改
InvoiceEx clone = proto.clone();
//修改克隆,看是否会影响之前
InvoiceEx.InvoiceExDetail exDetail2 = new InvoiceEx.InvoiceExDetail(2, 500, new Date());
ArrayList<String> paperTypes2 = new ArrayList<>();
paperTypes2.add("拷贝纸");
paperTypes2.add("打字纸");
String[] industries2 = new String[]{"收购业","水电业"};
//开始重新赋值,修改的是clone对象
clone.invoiceHeader ="BC";
clone.amountOfMoney = 500;
clone.industryTypes = industries2;
clone.paperTypes = paperTypes2;
clone.detail = exDetail2;
//开始打印信息
System.out.println("克隆:\n"+clone);
System.out.println("-------------------------------------");
System.out.println("修改克隆之后的原型:\n"+proto);
}
简要说一下:
这里就是克隆了一个对象,然后进行了修改。看看这些修改是否会影响到原型对象。可以看出,我们这里主要就是试验:
基本类型、数组、列表、实体类
那么激动人心的时刻终于到了噻,看输出:
结果竟然和我想的不一样。列表和实体类竟然没有变,仔细看了代码发现,虽然clone对象是复制的,本来只复制索引,不真的复制对象,修改改索引下的值,原型也应该变。但是这里我们的列表和实体类都是new出来的,赋了新的值,不是原来的索引了,自然不变了,改一下代码:
public static void testProto2() throws CloneNotSupportedException {
InvoiceEx.InvoiceExDetail invoiceExDetail = new InvoiceEx.InvoiceExDetail(1, 250, new Date());
List<String> paperTypes = new ArrayList<>();
paperTypes.add("普通纸");
paperTypes.add("压感纸");
String[] industries = new String[]{"工业", "商业"};
InvoiceEx proto = new InvoiceEx("ICBC", 250, industries,
paperTypes, invoiceExDetail);
System.out.println("原型:\n"+proto);
System.out.println("-------------------------------------");
//开始克隆和修改
InvoiceEx clones = proto.clone();
//修改克隆,看是否会影响之前
clones.setAmountOfMoney(25);
//新增
paperTypes.add("拷贝纸");
paperTypes.add("油墨纸");
clones.setPaperTypes(paperTypes);
InvoiceEx.InvoiceExDetail detail2 = clones.getDetail();
detail2.setCount(222);
clones.setDetail(detail2);
//开始打印信息
System.out.println("克隆:\n"+clones);
System.out.println("-------------------------------------");
System.out.println("修改克隆之后的原型:\n"+proto);
}
如同预期:
修改克隆对象,对于列表、实体类只复制了索引,并没有真正的复制了对象。对于数组、八大基本类型之外的数据,复制的对象和原型对象指向的同一个索引,自然值也会相同,修改一方,另一方也会改变。
以上是浅拷贝,对于列表和实体类,如果我们需要深拷贝的话,就要对clone进行一些修改了,在此之前说一下,实体类的深拷贝容易实现,主要是列表,网上有很多关于列表深拷贝的方法,但经过测试发现很多都是浅拷贝。
在某些特殊情况下,如果需要实现集合的深拷贝,那就要创建一个新的集合,然后通过深拷贝原先集合中的每个元素,将这些元素加入到新的集合当中。
靠谱的两个方法是:①序列化拷贝 ②转成Json拷贝
因为是android这块,第一种方式需要实现Seralizeable接口,但是android这块主要用Parceable接口,性能比前者好,所以这一种方法在android基本上行不通。那就可以看第二种了:
参考博客
/**
* 深克隆使用
* @param json
* @param clazz
* @param <T>
* @return
*/
public static <T> ArrayList<T> jsonToArrayList(String json, Class<T> clazz) {
Type type = new TypeToken<ArrayList<JsonPrimitive>>() {
}.getType();
ArrayList<JsonPrimitive> jsonObjects = new Gson().fromJson(json, type);
ArrayList<T> arrayList = new ArrayList<>();
for (JsonPrimitive jsonObject : jsonObjects) {
arrayList.add(new Gson().fromJson(jsonObject, clazz));
}
return arrayList;
}
这里博客里写的是JsonObject,但是对于List<String>而言是JsonPrimitive.原因进到这个类里就发现了:
String/基本类型/基本类型的包装类型 都是用JsonPrimitive,列表里是实体类可以用JsonObject。
所以,改造后的深拷贝:
@Override
protected InvoiceEx clone() throws CloneNotSupportedException {
InvoiceEx ex = (InvoiceEx)super.clone();
//List<String>
ex.setPaperTypes(deepCopyList(getPaperTypes()));
//实体类
ex.setDetail(getDetail().clone());
return ex;
}
private ArrayList<String> deepCopyList(List<String> sourceList){
Gson gson = new Gson();
String jsonTran = gson.toJson(sourceList);
ArrayList<String> deepCloneList = GsonUtils.jsonToArrayList(jsonTran,String.class);
return deepCloneList;
}
//JsonPrimitive 和JsonObject不同情况下使用,前者是String,基 //本类型及包装类型;后者实体类
public static <T> ArrayList<T> jsonToArrayList(String json, Class<T> clazz) {
Type type = new TypeToken<ArrayList<JsonPrimitive>>() {
}.getType();
ArrayList<JsonPrimitive> jsonObjects = new Gson().fromJson(json, type);
ArrayList<T> arrayList = new ArrayList<>();
for (JsonPrimitive jsonObject : jsonObjects) {
arrayList.add(new Gson().fromJson(jsonObject, clazz));
}
return arrayList;
}
Tips:原型模式,一定要注意深克隆,浅克隆!!
六、外观模式
定义:又叫门面模式,提供了一个统一的接口,用来访问子系统中的一群接口
外观模式定义了一个高层接口,让子系统更容易使用
类型:结构型
适用场景
①子系统越来越复杂,增加外观模式提供简单调用接口
②构建多层结构系统,利用外观对象作为每层的入口,简化层间调用
优点:
简化了调用过程,无需了解深入子系统,防止带来风险
减少系统依赖,松散耦合
更好的划分访问层次
符合迪米特法则,即最少知道原则
缺点:
增加子系统、扩展子系统容易引入风险
不符合开闭原则
相关的设计模式:
外观模式和中介者模式
前者关注外界和子系统交互,后者更关注子系统内部之间的交互
外观模式和单例模式
通常将外观模式中的外观对象做成单例模式。
https://blog.csdn.net/LoveLion/article/details/7798038?utm_source=blogxgwz9
Demo:
比如我们在淘宝网上买一件商品,我们只需要看有没有对应的商品,然后下单付款即可。
我们看一下后台都需要进行哪些逻辑处理,首先,肯定是店铺这边商品是否上架,这属于店铺子系统的逻辑;如果有上架了,再看仓库这边是否有库存,没有库存的话,自然也买不了此商品。最后就是物流系统这边取完货物后送货上门,当然还有付款,下订单这些,这些暂且不谈,我们只抽离整个流程的三个子系统:
店铺系统、仓库系统、物流系统;
处理也尽量简单一些,店铺系统用来判断是否上架,仓库系统判断是否有库存,物流系统负责送货上门。
public class StoreService {
public boolean isOntheShelf(){
//----店铺判断逻辑
System.out.println("商品已上架");
return true;
}
}
public class WareHouseService {
public boolean hasStock(){
//仓库判断逻辑---是否有库存
System.out.println("----有库存");
return true;
}
}
public class LogisticsService {
public void sendToConsumer(){
System.out.println("----开始送货....");
}
}
如果我们直接使用上面三个子系统的内容,那么Client端就会引入三个子系统的依赖,实际上,我们客户端要做的只有一件事,就是shopping,是否上架、有无库存、送货上门这些你们后台处理好,我只需要知道我此次购物是否成功即可。
所以,我们这里就可以提供一个外观对象,提供一个购物方法,在这个外观对象的方法里进行三个子系统的逻辑处理,客户端调用外观对象的这个shopping方法就足够了,没必要引入这么多依赖,也没必要知道子系统之间的处理。遵循迪米特法则,也就是最少知道原则即可。
/**
*消費系統,外觀對象
*/
public class ConsumeService {
//店铺系统
private StoreService mStoreService = new StoreService();
//仓库系统
private WareHouseService mWareHouseService =new WareHouseService();
//物流系统
private LogisticsService mLogisticsService =new LogisticsService();
//开始购物
public void startShopping(){
System.out.println("开始购物...");
if (mStoreService.isOntheShelf()){
if (mWareHouseService.hasStock()){
mLogisticsService.sendToConsumer();
}
}
}
}
客户端调用:
public class Test {
public static void main(String[] args) {
ConsumeService consumeService = new ConsumeService();
consumeService.startShopping();
}
}
输出结果:
开始购物...
商品已上架
----有库存
----开始送货....
根据UML类图我们可以看出,客户端只和外观对象有关系,跟子系统之间并无直接交互,遵循了迪米特法则。
由于每新增一个子系统都需要修改外观类,违背最基本的开闭原则,所以可以对外观模式进行优化,引入抽象外观类:
https://blog.csdn.net/LoveLion/article/details/7798064
依葫芦画瓢画了一下引入抽象类之后的外观模式:
这里引入了一个抽象外观类,在这个类中定义外观类应该具备的行为;然后两个具体外观类实现该方法,将具体实现在内部处理,然后暴露给客户端一个外观方法。
如果我们新增了一个子系统,逻辑随之变动,我们就不用修改原有的外观类,而是新增抽象外观类的实现,在新的外观类中进行新逻辑的处理。
demo:比如现在将支付系统PayService接进来,店铺有货,支付成功,仓库有货,才能送货上门。
//抽象外观类
public abstract class BaseConsumeService {
abstract void consume();
}
//新外观类
public class NewConsumeService extends BaseConsumeService {
private StoreService mStoreService = new StoreService();
private WareHouseService mWareHouseService = new WareHouseService();
private PayService mPayService = new PayService();
private LogisticsService mLogisticsService = new LogisticsService();
@Override
void consume() {
if (mStoreService.isOntheShelf()){
//----商铺系统逻辑处理
if (mPayService.isPaySuccess()){
//支付系统处理
if (mWareHouseService.hasStock()){
//仓库系统逻辑处理
mLogisticsService.sendToConsumer();
}
}
}
}
}
//Client
public class Test {
public static void main(String[] args) {
// ConsumeService consumeService = new ConsumeService();
// consumeService.startShopping();
newCousumeProcedure();
}
private static void newCousumeProcedure(){
BaseConsumeService consumeService = new NewConsumeService();
consumeService.consume();
}
}
输出:
商品已上架
支付成功....
----有库存
----开始送货....
可以看出,再不修改原有外观类的情况下,在遵循开闭原则的前提下,成功引入了新的子系统。
七、装饰者模式
定义:在不改变原有对象的基础上,将功能附加到对象上
提供了比继承更有弹性的替代方案(扩展原有对象功能)
类型:结构型
(kotlin中的扩展函数应该是一种很好的应用体现)
适用场景
①扩展一个类的功能或给一个类添加附加职责(扩展函数...)
②动态的给一个对象添加功能,这些功能可以再动态的撤销
优点:
①继承的有力补充,比继承灵活,不改变原有对象的情况下给一个对象扩展功能(本身实际上也是继承)
②通过使用不同装饰类以及这些装饰类的排列组合,可以实现不同效果
③符合开闭原则
缺点:
会出现更多的代码,更多的类,增加程序复杂性
动态装饰时,多层装饰会更复杂
Demo:
老师讲解视频的时候,使用的场景是鸡蛋灌饼加蛋和香肠;
一个鸡蛋灌饼可以是简单的单纯灌饼,也可以是进阶的加再加一个蛋的鸡蛋灌饼,也会有加蛋再加香肠的超进阶版;当然也有土豪他会加辣条等等。可以给一个加蛋添加N多组合方式去构建一个鸡蛋灌饼Ex版。例子简单明了,而且场景十分吻合。但是因为实际上我们开发的时候,接触卖鸡蛋灌饼的场景很少,如果有个现有的,真正的能看到的功能去展示这个设计模式,可能会更好一些理解这个模式。
经过一番思考,我觉得在淘宝上下订单这个行为也十分适合装饰者模式:
一个订单包括物品本身的价格,不包邮的情况下可以加运费,运险费,有活动的时候,还有店铺红包等。
这里要注意,订单被装饰的时候,不一定被谁装饰!装饰很自由!
比如一个订单可以加运费,也可以加运费再加运险,也可以只加运险,或者只加店铺红包和运费,等等。
这时候,如果我们不用装饰者,那实现起来就很麻烦了;我们需要创建一个订单类,然后有其他装饰的时候,就会出现N多继承:
订单加运费,订单加运费加运险,订单只加店铺红包等等。
ex:
public class Order {
public String getDesc(){
return "一笔订单价格:";
}
public int getCost(){
return 110;
}
}
public class OrderWithPremium extends Order {
@Override
public String getDesc() {
return super.getDesc()+" 加运险费";
}
@Override
public int getCost() {
return super.getCost()+10;
}
}
public class OrderWithPremiumAndCarriage extends OrderWithPremium {
@Override
public String getDesc() {
return super.getDesc()+"再加运费";
}
@Override
public int getCost() {
return super.getCost()+30;
}
}
public class Test {
public static void main(String[] args) {
Order order = new OrderWithPremium();
System.out.println(order.getDesc());
System.out.println(order.getCost());
Order order1 = new OrderWithPremiumAndCarriage();
System.out.println(order1.getDesc());
System.out.println(order1.getCost());
}
}
多年之后,人们又想起被继承支配的恐惧....
而采用了装饰者模式后,我们就可以对一个订单进行多种自由组合的装饰,实现如下:
//抽象订单
public abstract class ABOrder {
abstract String getDesc();
abstract int getCost();
}
//具体订单
public class Order extends ABOrder {
@Override
String getDesc() {
return "订单价格:";
}
@Override
int getCost() {
return 110;
}
}
//抽象装饰类,这里没有用抽象类,是因为当下没有必须要实现的方法,所以用一般的类即可
public class OrderDecorator extends ABOrder {
private ABOrder mABOrder;
public OrderDecorator(ABOrder order){
this.mABOrder = order;
}
@Override
String getDesc() {
return mABOrder.getDesc();
}
@Override
int getCost() {
return mABOrder.getCost();
}
}
//具体装饰1
public class OrderWithCarriage extends OrderDecorator {
public OrderWithCarriage(ABOrder order) {
super(order);
}
@Override
String getDesc() {
return super.getDesc() + "加运费";
}
@Override
int getCost() {
return super.getCost() + 30;
}
}
//具体装饰2
public class OrderWithPremium extends OrderDecorator {
public OrderWithPremium(ABOrder order) {
super(order);
}
@Override
String getDesc() {
return super.getDesc()+"加运险费";
}
@Override
int getCost() {
return super.getCost()+10;
}
}
//调用
public static void main(String[] args) {
ABOrder order = new Order();
System.out.println(order.getDesc());
System.out.println(order.getCost());
//加上运费
ABOrder orderWithCarriage = new OrderWithCarriage(order);
System.out.println(orderWithCarriage.getDesc());
System.out.println(orderWithCarriage.getCost());
//再加上保险费
ABOrder orderWithCarriageAndPremium = new OrderWithPremium(orderWithCarriage);
System.out.println(orderWithCarriageAndPremium.getDesc());
System.out.println(orderWithCarriageAndPremium.getCost());
//自由组合:只加运费(包邮);加店铺红包(价格反向增长);---比继承好在可以自由组合、装饰,不用建更多
//的组合类
}
}
UML图:
分析一下代码:新建了一个订单的抽象类,定义订单所必须的相关方法。然后新建了一个抽象的装饰者类,具体的装饰者实现此类。(当装饰者没有必须的方法时,不用抽象类也行)这里有个必须十分要注意的事情!
抽象的装饰者需要继承抽象对象,并持有抽象对象!
为什么要继承?
因为继承了就说明装饰者实际上也是一个对象,也是一个订单,加了运费后,他还是一个订单,只不过是加了运费的订单,而不是一个其他的对象!
为什么要持有抽象对象?
因为我们需要对原有对象进行扩展!通过这个对象,我们就可以实现N多装饰,拿到扩展后的对象,我们还可以进行再次的其他扩展,被运费装饰了,还可以被运险费、店铺红包等再次装饰。
如上面的例子:
ABOrder order = new Order();
ABOrder orderWithCarriage = new OrderWithCarriage(order);
ABOrder orderWithCarriageAndPremium = new OrderWithPremium(orderWithCarriage);
可以看到,我们可以拿到一般的订单,传入这个订单后,就会获得订单+运费后的订单,拿到这个订单+运费的订单后,我们可以再用运险进行装饰,就获得了 订单+运费+运险的订单。对象还是之前的对象,只不过加了格外的功能(加钱)。
比之前的好处在于:
新增场景后,比如需要两份运险,之前的就会再建一个类;而现在我们只需要获得订单+运费+运险的订单,再用运费进行装饰一次即可。验证了:通过使用不同装饰类以及这些装饰类的排列组合,可以实现不同效果。
Other:
①除了订单场景,还有很多场景可以用装饰者模式,比如买电脑,需不需要再加数据线、耳机等。
②源码中可以参考BufferedReader
③看到一个类以Decorator或者Wrapper,就可以猜测它使用的是装饰者模式了。
这里就可以总结一下装饰者模式几个必须点了:
①抽象对象(抽象类或者接口)
②抽象装饰者类(也可以是非抽象类,根据业务场景来,就是一个基类)
③抽象装饰者类继承抽象对象,并持有抽象对象
八、适配器模式
定义:讲一个类的接口转换成客户期望的另一个接口
使原本不兼容的类可以一起工作
类型:结构性
适用场景:
已经存在的类,它的方法和需求不匹配时(方法结果相同或类似)
不是软件设计阶段考虑的设计模式,是随着软件维护,由于不同产品、不同厂家造成 功能类似而接口不相同情况下的解决方案。
(是对老系统功能的扩展)
优点:
能提高类的透明性和复用,现有的类复用但不需要改变
目标类和适配器类解耦,提高程序扩展性
符合开闭原则
(客户端只需要扩展适配器类)
缺点:
适配器编写过程需要全面考虑,可能会增加系统的复杂性
增加系统可读难度
扩展:
对象适配器:符合组合复用原则,使用委托机制
类适配器:通过集成实现
相关设计模式:
适配器模式和外观模式:
都是对现有的类、现有的功能进行封装。外观定义了新的接口(抽象对象),适配器是复用一个原有的接口,适配器是使两个已有的接口协同工作,而外观是在现有的系统中提供一个更方便的访问入口。
适配力度不同:外观力度更大,是针对整个系统。
适配器一般有两种实现方式:类适配器、对象适配器
两种方式中的角色都一样:
1.目标接口(Target):客户所期待的接口。目标可以是具体的或抽象的类,也可以是接口。
2.需要适配的类(Adaptee):需要适配的类或适配者类。
3.适配器(Adapter):通过包装一个需要适配的对象,把原接口转换成目标接口。
视频中,以及多个博客都是以充电器电流转换为例。为了联系到真正的业务开发中,这里使用另外一种场景:
常见的播放模式一般有三种:竖屏小屏播放、竖屏全屏播放、横屏全屏播放。如果现在产品需要一个横屏半屏播放的需求,那么我们就可以复用横屏全屏播放的部分逻辑,通过适配器,将横屏全屏播放转变成横屏半屏播放。
因为上面提到适配器有两种实现方法,所以我们一一实验,对比一样两种实现的优劣,demo如下:
//Adaptee 源角色
public class HorizontalFullScreen {
public String resolvePlayerParams(){
//...解析参数
return "params";
}
public void fullScreenPlay(){
System.out.println("--全屏横屏播放");
}
}
/**
* Target
*/
public interface HorizontalThumbPlay {
void thumbPlay();
}
/**
* 类适配模式
*/
public class HorizontalThumbScreen extends HorizontalFullScreen implements HorizontalThumbPlay{
@Override
public void thumbPlay() {
String params = resolvePlayerParams();
System.out.println("继承方式: 解析参数:"+params+",半屏播放");
}
}
/**
* 对象适配模式
*/
public class HorizontalThumbScreenEx implements HorizontalThumbPlay {
private HorizontalFullScreen fullScreen;
public HorizontalThumbScreenEx(HorizontalFullScreen fullScreen){
this.fullScreen = fullScreen;
}
@Override
public void thumbPlay() {
String param = fullScreen.resolvePlayerParams();
System.out.println("组合方式: 解析参数:"+param+",半屏播放");
}
}
public class Test {
public static void main(String[] args) {
//类适配模式
HorizontalThumbPlay thumbPlay = new HorizontalThumbScreen();
thumbPlay.thumbPlay();
//对象适配模式
HorizontalThumbPlay thumbPlayEx = new HorizontalThumbScreenEx(
new HorizontalFullScreen());
thumbPlayEx.thumbPlay();
}
}
输出:
继承方式: 解析参数:params,半屏播放
组合方式: 解析参数:params,半屏播放
可以看出,两种方式都实现了新老接口的转换。
类适配器模式需要采用继承,对象适配器是组合方式。在设计模式中,组合的优先级大于继承,能组合的就不要用继承!所以在不得已使用适配器模式的时候,也要注意采用对象适配器模式。
分析一下两种方式实现步骤:
①首先,都需要三个角色:
Adaptee,Target,Adapter
②类适配器,需要Adapter继承Adaptee,实现Target接口
③对象适配器,只需要Adapter持有Adaptee(方便复用其中的部分老逻辑),然后实现Target接口。
九、代理模式
定义与类型
定义:为其他对象提供一种代理,以控制对这个对象的访问
代理对象在客户端和目标对象之间起到中介的作用
适用场景
保护目标对象
增强目标对象
优点
代理模式能将代理对象与真实被调用的目标对象分离
一定程度上降低了系统的耦合度,扩展性好
保护目标对象
增强目标对象
缺点
代理模式会造成系统设计中类的数目增加
在客户端和目标对象增加了一个代理对象,会造成请求处理速度降低
增加系统的复杂度
扩展
静态代理
动态代理(只能针对实现了接口的类,不能对一般类实现)
CGLib代理(android这边不怎么用)
Tips
简单来说,就是在不改变源码的情况下,实现对目标对象的功能扩展。
比如说播放业务,我们最初的时候只支持均速播放,后面需要增加倍速播放功能,对原有的播放器进行扩展,这里就可以用代理模式进行处理。
Demo:
public class PlayController implements PlayExpand {
public void normalPlay(){
System.out.println("均速播放");
}
@Override
public void speedPlay() {
System.out.println("PlayController speedPlay....");
}
}
public interface PlayExpand {
void speedPlay();
}
public class PlayDelegate implements PlayExpand {
private PlayController mPlayController;
public PlayDelegate(PlayController playController){
this.mPlayController = playController;
}
@Override
public void speedPlay() {
//---扩展后的逻辑处理
//----
mPlayController.speedPlay();
//---逻辑处理
System.out.println("PlayDelegate speedPlay...");
}
}
public class Test {
public static void main(String[] args) {
PlayController mPlay = new PlayController();
PlayDelegate mDelegate = new PlayDelegate(mPlay);
mDelegate.speedPlay();
}
}
输出:
PlayController speedPlay....
PlayDelegate speedPlay...
上面是静态代理的代码,从上面我们需要总结一下,静态代理需要三个角色:
①目标对象(被代理的类,对应上面就是PlayController)
②代理对象(代理类,对应上面的PlayDelegate)
③代理接口(被代理类和代理类需要共同实现的接口)
此外,还有两点:
①代理对象和被代理对象均需要实现代理接口
②代理对象需要持有被代理对象。
//静态代理缺点:
代理对象必须提前写出,如果接口层发生改变,代理对象的代码也需要进行维护。
动态代理:
public class ProxyFactory {
private Object target;
public ProxyFactory(Object target){
this.target =target;
}
public Object getProxyInstance(){
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object returnValue = method.invoke(target,args);
return returnValue;
}
});
}
}
//动态代理
private static void dynamicProxy(){
//获取目标对象
PlayExpand target = new PlayController();
//获取代理对象
PlayExpand proxy = (PlayExpand) new ProxyFactory(target).getProxyInstance();
proxy.speedPlay();
}
从上面可以看出,动态代理工厂类:ProxyFactory是可以复用的。
动态代理原理可以参考:
https://www.jianshu.com/p/85d181d7d09a
额外思考
在项目中,有发现过另一种 ,嗯,怎么说呢,就是符合代理模式的定义,但其实现并不像代理模式的实现。
就是在已有的接口中再定义一个接口。在管理者模式以及后面的装饰模式中都这么用,主要是减少系统之间的耦合性,使得功能变化时不需要修改代码或者只修改少量代码就能解决问题
实际上,开发中确定很容易遇到这种一种情况,就是某个接口里的方法不符合当下的需求,需要扩展接口,但是呢,又不能直接在这个接口中新增方法,因为这么做,其他实现该接口的类都是实现这个方法,这肯定是不行的。需要新增的方法业务上跟已有的这个接口一致,但又因为该接口已有多个实现,我们只是想修改几个实现,不想全部修改。这时候就可以考虑在接口中新增一个接口,添加扩展方法,这样已实现接口的地方不需要全部修改,只需要在所需的地方实现新增的接口。内部接口实现的意义,个人认为:在于可以将业务内敛,不会增加更多的接口碎片。代码紧凑 避免碎片化。
代理模式和装饰者模式的区别:
** 装饰者模式是代理模式的一种特殊运用。装饰者模式更注重类的功能的增强或者减弱,代理模式更注重过程控制 **
动态代理
说到代理,就不得不提动态代理。
参考博客1
参考博客2
动态代理模式定义:
代理类在程序运行时创建的代理方式被称为动态代理。
上面提到的代理是静态代理,也就是代理类在程序运行前就已经存在(肯定存在,因为就是我们手动加的...)。静态代理模式有两个很大的缺点:
①当需要代理多个类时,由于代理对象要实现与目标一致的接口,所以由两种方式:
1.只维护一个代理类,由于这个代理类实现多个接口,会导致代理类过于庞大
2.新建多个代理类,每个目标对象对应一个代理类,这样会产生过多的代理类
②当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护。
以上两个缺点也就引出了动态代理模式。这里只提JDK动态代理。
JDK代理主要涉及两个类:java.lang.reflect.Proxy
和java.lang.reflect.Invocationhandler.
InvocationHandler
每一个动态代理类都必须要实现InvocationHandler这个接口,并且每个代理类的实例都关联到了一个handler,当我们通过代理对象调用 一个方法的时候,这个方法的调用就会被转发为由InvocationHandler这个接口的 invoke 方法来进行调用。我们来看看InvocationHandler这个接口的唯一一个方法 invoke 方法:
Object invoke(Object proxy, Method method, Object[] args) throws Throwable
proxy: 指代我们所代理的那个真实对象【但是从来么用过😌】
method: 指代的是我们所要调用真实对象的方法的Method对象
args: 指代的是调用真实对象某个方法时接受的参数
很清晰的动态代理原理解析
Demo:
/**
* Create by rye
* at 2021/1/16
*
* @description: 委托类【被代理类】
*/
public class LoginImpl implements ILoginIn, ILogout, ILoginControl {
@Override
public void login() {
System.out.println("login...");
}
@Override
public void logout() {
System.out.println("logout...");
}
@Override
public void control() {
System.out.println("control...");
}
@Override
public void control2() {
System.out.println("control2...");
}
@Override
public void control3() {
System.out.println("control3...");
}
}
interface ILoginIn {
void login();
}
interface ILogout {
void logout();
}
interface ILoginControl {
void control();
void control2();
void control3();
}
//测试类:
public class Test {
public static void main(String[] args) {
dynamicProxy();
}
private static void dynamicProxy() {
LoginImpl loginImpl = new LoginImpl();
ILoginControl loginControl = (ILoginControl) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
loginImpl.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before..."+proxy.getClass());
Object result = method.invoke(loginImpl, args);
System.out.println("after...");
return result;
}
});
loginControl.control();
loginControl.control2();
loginControl.control3();
}
}