设计原则
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版本:
image.png
将粗粒度的IAnimalAction拆分为三个接口,分别实现eat、swim、fly方法:
public class Dog implements IEatAnimalAction,ISwimAnimalAction{
@Override
public void eat() {
}
@Override
public void swim() {
}
}
这样我们的实现类就可以按需实现对应接口。
这个接口隔离和单一职责的区别在于:
后者指的是类、接口职责是单一的,强调的是职责,只要职责单一,方法可以很多,注重的是细节和实现。
前者注重的是接口依赖的隔离,约束的是接口,框架的构建。
接口一定要适度,不要太小。
五、迪米特法则
定义:
一个对象应该对其他对象保持最少的了解。又叫最少知道原则。
尽量降低类和类之间的耦合
优点:
降低类之间的耦合
强调只和朋友交流,不和陌生人说话。
朋友:
出现在成员变量、方法的输入、输出参数中的类称为成员朋友类,而出现在方法体内部的类不属于朋友类。
多使用private/protected 权限符,凡是都要有个度,要反复权衡。
一、简单工厂
定义:
由一个工厂对象决定创建出哪一种产品类的实例
(这句话有两个信息:①工厂类一般只有一个 ②产品由工厂创建)
类型:创建型,但不属于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里对应的:
image.png
二、工厂方法
定义:
定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类,工厂方法让类的实例化推迟到子类中进行
类型:创建型
适用场景:
1.创建对象需要大量重复的代码
2.客户端(应用层)不依赖与产品类实例如何被创建、实现等细节
3.一个类通过其子类来指定创建哪个对象
优点:
用户只需要关心所需产品对应的工厂,无需关心创建细节,只需要关注工厂类即可
加入新产品符合开闭原则,提高可扩展性
缺点:
类的个数容易过多,增加复杂度(还好,一般并不会太多,真的会很多,就抽象工厂)
增加了系统的抽象性和理解难度
工厂方法和抽象工厂的区别在于理解两个概念:
产品等级和产品族
产品等级:
比如视频:java视频,python视频,android视频,这三个产品都是同一个等级的,都是视频,所以是同一个产品等级
比如电视:华为电视、小米电视、海信电视,也是同一个产品等级,都是电视
产品族:
比如java的视频、手记,这是同一产品族
比如海尔的手机、冰箱、空调是同一产品族
demo:
1.一个产品抽象类:
public abstract class Video {
abstract void produce();
}
2.一个抽象工厂:
public abstract class VideoFactory {
abstract Video getVideo();
}
3.具体产品:
public class PythonVideo extends Video {
@Override
void produce() {
System.out.println("PythonVideo");
}
}
4.具体工厂:
public class JavaFactory extends VideoFactory{
@Override
Video getVideo() {
return new JavaVideo();
}
}
扩展性:
当有新的产品的时候,可以新增一个产品类继承抽象产品基类,新增一个工厂类继承抽象工厂类,符合了开闭原则。
Tips:
这里抽象产品基类和抽象工厂类,也可以使用接口,因为产品或者工厂可能会有一些默认的实现,所以这里使用抽象类而不是接口,这样做更加方便。但是因为当下java接口支持default方法的实现,所以采用接口也好。
工厂方法的关键:
1.抽象产品类(或接口)
2.抽象工厂类(或接口)
3.作用范围是同一产品等级的产品
4.新增一个产品,需要新增产品类(继承抽象产品类)和工厂类(继承抽象工厂类)
5.产品的具体创建过程交由每个产品对应的产品类实现,而不是向简单工厂那样交给一个工厂基类实现
三、抽象工厂
定义:抽象工厂模式提供一个创建一系列相关或相互依赖的接口
无需指定他们具体的类
类型:创建型
抽象工厂-适用场景
客户端不依赖与产品类实例如何被创建、实现等细节
强调一系列相关的产品对象(属于同一个产品族)一起适用创建对象需要大量重复的代码
抽象工厂-优点
具体产品在应用层隔离,无需关心创建细节
将一个系列的产品族统一到一起创建
抽象工厂-缺点
规定了所有可能被创建的产品集合,产品族中扩展新的产品困难,需要修改抽象工厂的接口
增加了系统的抽象性和理解难度
image.pngimage.png
这里一个具体工厂不再只创建一个产品,而是创建一个产品族。
ex:
小米、华为、苹果 三者都生产 电视、手机、电脑
这里,小米电视、华为电视、华为电视是 同一产品等级
电视、手机、电脑是同一产品族
具体实现:
一个抽象工厂:IProductFactory
三个抽象产品:Tv、MobilePhone、Computer
三个具体工厂:HWProductFactory、MiProductFactory、IPhoneProductFactory
九个具体产品:三个Tv,三个MobilePhone,三个Computer
UML类图:
image.png
依赖关系:
image.png
具体调用:
image.png
抽象工厂有个缺点就是新增产品很麻烦。
//然后新增工厂类,在IProductFactory新增产品方法,然后新增的工厂类继承此
//问题是,比如小米新增一个智能家居的产品,如果抽象工厂里新增了一个产品方法,
// 其他两家没有的话
//如果用抽象方法来搞,就会导致其他两家的工厂里有些空实现.....
而且新增产品的话,也不符合开闭原则。
Tips:
只适用于很固定的,如果很频繁改动的话,不好用抽象工厂,不然维护成本很高很高。考虑用工厂方法最好,新增产品成本不高
四、建造者模式
定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示
用户只需指定需要建造的类型就可以得到它们,建造过程及细节不需要知道
类型:创建型
适用场景:
①如果一个对象有非常复杂的内部结构(很多属性)
⑥想把复杂对象的创建和使用分离
优点:
封装性好,创建和使用分离
扩展性好,建造类之间独立,一定程度上解耦
缺点:
产生多余的Builder对象
产品内部发生变化,建造者都要修改,成本较大
Tips:建造者模式和工厂模式的区别
①建造者模式更注重方法的调用顺序,
工厂模式注重于创建产品
②创建力度不同:建造者模式对于创建一些复杂的产品由各种构件构成,而工厂模式将这个产品创建出来即可
一定的顺序决定了产出的产品不同,工厂不关心顺序
个人认为:可以举个简单例子,比如,我们想造一个汽车,如果说关注面在于汽车这个整体,比如宝马这边需要造一个车子,大众这边也需要,还有其他汽车厂商也需要造车子,这时候对于业务来说,我们对于各个厂商都需要车子这么一个产品的关注度超过了创建车子的流程,这时候,最好使用工厂模式。工厂模式更注重的是多个产品,而不是流程。
而如果我们的业务是要造一个车子,关注的是创建流程,比如我这个车子产品,他的轮胎有哪些选择,怎么造,它的车窗有哪些选择,它的发动机又有哪些选择怎么造,那这种时候,建造者模式就再适合不过了。
五、原型模式
定义:指原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象
不需要知道任何创建的细节,不调用构造函数
类型:创建型
适用场景:
1.类初始化消耗较多资源
2.new产生的一个对象需要非常繁琐的过程(数据准备、访问权限等)
3.构造函数比较复杂
4.在循环体中生产大量对象
简而言之,如果一个类的属性过多,需要创建多个,就可以考虑使用原型模式。
优点
原型模式性能比new一个对象性能高
简化创建过程
缺点
1.必须配备克隆方法(如果还有其他对象,那这些对象也得实现!这就...)
2.对克隆复杂对象或对克隆出的对象进行复杂改造时,容易引入风险
3.深拷贝、浅拷贝要运用得当
原型--扩展
深克隆
浅克隆
由于看视频有点懵逼,所以查了查资料,其中一篇博客很详尽:
原型模式详解
先上UML图(盗的上面博客的图哈,因为参考了好几篇,UML图都是一样的)
主要就三个:抽象原型类、具体原型类、客户类
抽象原型类:声明克隆方法的接口,是所有具体类型类的公共父类。可以是接口、抽象类、具体类
具体原型类:实现抽象原型类中声明的克隆方法,在克隆方法中返回一个自己的克隆对象
客户类:调用方。
Tips:实际上,可以更加简便,将抽象原型类和具体实现类合并成一个,只需要实体类实现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);
}
image.png
如同预期:
修改克隆对象,对于列表、实体类只复制了索引,并没有真正的复制了对象。对于数组、八大基本类型之外的数据,复制的对象和原型对象指向的同一个索引,自然值也会相同,修改一方,另一方也会改变。
以上是浅拷贝,对于列表和实体类,如果我们需要深拷贝的话,就要对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.原因进到这个类里就发现了:
image.png
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();
}
}
输出结果:
开始购物...
商品已上架
----有库存
----开始送货....
image.png
根据UML类图我们可以看出,客户端只和外观对象有关系,跟子系统之间并无直接交互,遵循了迪米特法则。
由于每新增一个子系统都需要修改外观类,违背最基本的开闭原则,所以可以对外观模式进行优化,引入抽象外观类:
https://blog.csdn.net/LoveLion/article/details/7798064
依葫芦画瓢画了一下引入抽象类之后的外观模式:
image.png
这里引入了一个抽象外观类,在这个类中定义外观类应该具备的行为;然后两个具体外观类实现该方法,将具体实现在内部处理,然后暴露给客户端一个外观方法。
如果我们新增了一个子系统,逻辑随之变动,我们就不用修改原有的外观类,而是新增抽象外观类的实现,在新的外观类中进行新逻辑的处理。
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图:
image.png
分析一下代码:新建了一个订单的抽象类,定义订单所必须的相关方法。然后新建了一个抽象的装饰者类,具体的装饰者实现此类。(当装饰者没有必须的方法时,不用抽象类也行)这里有个必须十分要注意的事情!
抽象的装饰者需要继承抽象对象,并持有抽象对象!
为什么要继承?
因为继承了就说明装饰者实际上也是一个对象,也是一个订单,加了运费后,他还是一个订单,只不过是加了运费的订单,而不是一个其他的对象!
为什么要持有抽象对象?
因为我们需要对原有对象进行扩展!通过这个对象,我们就可以实现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
额外思考
在项目中,有发现过另一种 ,嗯,怎么说呢,就是符合代理模式的定义,但其实现并不像代理模式的实现。
就是在已有的接口中再定义一个接口。在管理者模式以及后面的装饰模式中都这么用,主要是减少系统之间的耦合性,使得功能变化时不需要修改代码或者只修改少量代码就能解决问题
实际上,开发中确定很容易遇到这种一种情况,就是某个接口里的方法不符合当下的需求,需要扩展接口,但是呢,又不能直接在这个接口中新增方法,因为这么做,其他实现该接口的类都是实现这个方法,这肯定是不行的。需要新增的方法业务上跟已有的这个接口一致,但又因为该接口已有多个实现,我们只是想修改几个实现,不想全部修改。这时候就可以考虑在接口中新增一个接口,添加扩展方法,这样已实现接口的地方不需要全部修改,只需要在所需的地方实现新增的接口。内部接口实现的意义,个人认为:在于可以将业务内敛,不会增加更多的接口碎片。代码紧凑 避免碎片化。
十、观察者模式
定义:定义了对象之间的一对多依赖,让多个观察者对象同时监听某一个主体对象,当主体对象发生改变时,它的所有依赖者(观察者)都会受到通知并更新。
类型:行为型
适用场景
关联行为场景,建立一套触发机制
优点
观察者和被观察者之间建立一个抽象的耦合
观察者模式支持广播通信
缺点
观察者之间有过多的细节依赖、提高时间消耗及程序复杂度
使用要得当,千万不要循环调用
coding:
//1.抽象观察者
public interface LoginObserver {
void loginSuccess(String personalInfo);
}
//2.抽象主题
public interface ObservableManager {
void addListener(LoginObserver observable);
void removeListener(LoginObserver observable);
void notifyObserver(String personalInfo);
}
//3.具体主题
public class LoginActionManager implements ObservableManager {
private static List<LoginObserver> observers;
private LoginActionManager() {
}
static {
observers = new CopyOnWriteArrayList<>();
}
private static LoginActionManager mInstance = null;
public static LoginActionManager getInstance() {
if (mInstance == null) {
synchronized (LoginActionManager.class) {
if (mInstance == null) {
mInstance = new LoginActionManager();
}
}
}
return mInstance;
}
@Override
public void addListener(LoginObserver observer) {
observers.add(observer);
}
public synchronized void addListeners(List<LoginObserver> obs){
for (LoginObserver observer:obs){
observers.add(observer);
}
}
@Override
public void removeListener(LoginObserver observer) {
observers.remove(observer);
}
/**
* 登陆成功,通知所有监听者
* @param personalInfo
*/
@Override
public void notifyObserver(String personalInfo) {
for (LoginObserver observer : observers) {
observer.loginSuccess(personalInfo);
}
}
}
//4.具体观察者
public class MainPage implements LoginObserver {
@Override
public void loginSuccess(String personalInfo) {
System.out.println("MainPage收到用户登录信息:"+personalInfo);
}
}
public class PersonalPage implements LoginObserver {
@Override
public void loginSuccess(String personalInfo) {
System.out.println("PersonalPager收到登陆信息:"+personalInfo);
}
}
5.测试
public class Test {
public static void main(String[] args) {
PersonalPage personalPage = new PersonalPage();
MainPage mainPage = new MainPage();
List<LoginObserver> observers = new CopyOnWriteArrayList<>();
observers.add(personalPage);
observers.add(mainPage);
LoginActionManager.getInstance()
.addListeners(observers);
LoginActionManager.getInstance()
.notifyObserver("用户:JackChen");
}
}
输出:
PersonalPager收到登陆信息:用户:JackChen
MainPage收到用户登录信息:用户:JackChen
如上。
这里的场景是我们登陆成功后,比如个人中心页,主页都需要拿到我们登陆成功后的用户个人信息去更新界面或者进行其他逻辑操作。这里的被观察者就是登陆账号这一行为,观察者就是需要获取用户信息的界面。
从上我们也可以看出实现观察者模式所需的因素:
①抽象主题角色:
抽象主题一般是个接口,可以增加和删除观察者角色
②具体主题角色:
把所有观察者对象的引用保存在一个集合中,一般需要管理添加、删除观察者,最好做成一个管理类
③抽象观察者:
为所有的具体观察者定义一个接口,在收到信息更新后,通知具体观察者
④具体观察者
观察者模式应用场景很多,必须得掌握才行!
十一、责任链模式
https://www.liaoxuefeng.com/wiki/1252599548343744/1281319474561057
定义:
为请求创建一个接收此次请求对象的链
类型:行为型
适用场景
一个请求的处理需要多个对象当中的一个或几个协作处理
优点
请求的发送者和接收者(处理者)解耦
责任链可以动态组合(这点很重要)
缺点
责任链太长或者处理时间过长,影响性能
责任链有可能过多
可以使用责任链的场景有很多,比如android Okhttp里的拦截器处理就是责任链的变形,还有比如我们的一个流程需要多层审批,哪一层满足条件就哪一层处理。常见的,很多人都会接触到的就是这个费用报销了,不同级别的领导有不同的报销审批资格。当然,如果一个设计模式我们只能想出一种使用场景,那么绝对就是个菜比。这里我承认我是个菜比...
Demo:
* @description: 抽象处理器
*/
public interface Handler {
boolean process(CostInformation costInformation);
}
*
* @description: 报销流程 --责任链
*/
public class ReimbursementProcessChain {
private List<Handler> handlers = new ArrayList<>();
public void addHandler(Handler handler) {
this.handlers.add(handler);
}
public void removeHandler(Handler handler) {
if (handlers.size() > 0 && handlers.contains(handler)) {
handlers.remove(handler);
}
}
public void process(CostInformation costInformation) {
if (handlers.isEmpty()) return;
for (int i = 0; i < handlers.size(); i++) {
Handler handler = handlers.get(i);
boolean result = handler.process(costInformation);
System.out.println(costInformation.getApplicant() + "申请报销:" + costInformation.getAmount()
+ (result ? " dealt by:" : " transmit by:")
+ handler.getClass().getSimpleName());
if (result) {
System.out.println("----------finish--------");
break;
}
}
}
}
* @description: 参数
*/
public class CostInformation {
private String applicant;
private int amount;
public CostInformation(String applicant,int amount){
this.applicant =applicant;
this.amount =amount;
}
public String getApplicant() {
return applicant;
}
public void setApplicant(String applicant) {
this.applicant = applicant;
}
public int getAmount() {
return amount;
}
public void setAmount(int amount) {
this.amount = amount;
}
}
* @description: 三级处理人 :经理
*/
public class ManagerHandler implements Handler {
@Override
public boolean process(CostInformation costInformation) {
if (costInformation.getAmount()<1000){
return true;
}
return false;
}
}
* @description: 二级处理人 :主管
*/
public class DirectorHandler implements Handler {
@Override
public boolean process(CostInformation costInformation) {
if (costInformation.getAmount() >= 1000 && costInformation.getAmount() < 10000) {
return true;
}
//---
return false;
}
}
* @description: 一级处理人 :CEO
*/
public class CEOHandler implements Handler{
@Override
public boolean process(CostInformation costInformation) {
//ceo can deal any amount of money ..
return true;
}
}
//客户端
public class Test {
public static void main(String[] args) {
ManagerHandler managerHandler = new ManagerHandler();
DirectorHandler directorHandler = new DirectorHandler();
CEOHandler ceoHandler = new CEOHandler();
ReimbursementProcessChain chain = new ReimbursementProcessChain();
chain.addHandler(managerHandler);
chain.addHandler(directorHandler);
chain.addHandler(ceoHandler);
CostInformation costInformation = new CostInformation("老李",1200);
CostInformation costInformation1 = new CostInformation("老张",19999);
chain.process(costInformation);
chain.process(costInformation1);
}
}
先简单说一下上面的实现:
就是报销审核我们一共有三层领导:经理,主管,老板。经理可以批准1000以下的报销,主管可以批准1000到10000的报销,老板可以审批无限金额的报销。 经理能审批的经理审,审不了的给主管,主管能审的主管审,审不了的给老板,老板能审的审,不能审的找老板娘。
代码里表现的流程就如上所说。
这里Handler,就是抽象处理者。其中的process方法,就是我们每个具体处理者必须实现的处理流程,这里返回布尔值,是说明此层的处理者是否已经处理了这个流程。
ManagerHandler/DirectorHandler/CEOHandler,就是具体处理者,都实现了自身的处理流程。
ReimbursementProcessChain:责任链,负责管理具体处理者以及多层处理者的处理顺序,可以说是责任链的核心所在。责任链中的处理顺序十分重要!!一定要依序处理!具体的来说就是handlers的add顺序贼吉尔重要!不然一个100块的报销,你跳过了经理和主管,直接给老板,社会性死亡...
廖学峰的一篇博客
可以参照他这个里面的责任链处理
我们来分析一下责任链模式实现所需要的内容:
①抽象处理者(一般为接口,返回值一般为bool,代表是否处理)
②具体处理者(会有多个,就是责任链上每一个点上的角色)
③责任链(持有所有处理者,处理方法是关键)
这里必须说一句,上面提到的只是我们常规使用的时候所需的必要元素,每种设计模式都不是一个简单demo能详尽的,可能存在多种变形,我这里只是以一个小白的视角来看此模式。能更方便我们使用此设计模式,最后能够根据具体场景自由变换,而不要被套在表面认知的壳子上。
中介者模式
是用来降低多个对象和类之间的通信复杂性。这种模式提供了一个中介类,该类通常处理不同类之间的通信,并支持松耦合,使代码易于维护。
类型:行为型
适用场景
多个类相互耦合,形成了网络结构(个人理解,这里的多个类可能是一个类的多个实例对象,比如人和人聊天,国家和国家之间贸易)
优点
降低了类的复杂度,将一对多转化成了一对一
各个类之间解耦
符合迪米特法则
缺点
中介者会庞大,变得复杂难以维护(一个项目中感觉能用到中介者的机会并不多,就算很多,也尽量控制个数在个级别,以上是我以为)
Demo:
比如多国贸易,买石油,如果直接A to B,B to C,可能会导致使用方业务逻辑繁杂,可以提供一个中介者类,进行贸易往来的管理。
public class Country {
public String countryName;
public Country(String countryName){
this.countryName = countryName;
}
public void buyOil(Country producer){
System.out.println(this.countryName +"买了" +producer.countryName +"的石油");
}
}
//中介者类
public class TradeManager {
public static void buyOil(Country producer,Country consumer){
//do much thing
System.out.println(consumer.countryName +"购买了"+producer.countryName+"的石油");
}
}
public class Test {
public static void main(String[] args) {
method1();
}
private static void method1(){
Country america = new Country("美国");
Country china = new Country("中国");
Country japan = new Country("小日本");
Country england = new Country("英国");
//---do much thing
america.buyOil(china);
//---do much thing
china.buyOil(england);
//---do much thing
japan.buyOil(america);
//---do much thing
england.buyOil(japan);
}
private static void mediator(){
Country america = new Country("美国");
Country china = new Country("中国");
Country japan = new Country("小日本");
Country england = new Country("英国");
TradeManager.buyOil(america,china);
TradeManager.buyOil(china,england);
TradeManager.buyOil(japan,america);
}
}
享元模式
定义:享元模式提供了减少对象数量从而改善应用所需的对象结构的方式
运用共享技术有效地支持大量细粒度的对象
类型:结构型
适用场景
常用于系统底层的开发,以便解决系统的性能问题(比如String,int的缓冲池,缓冲池里有则直接返回,没有则创建)
系统中有大量相似对象、需要缓冲池的场景,需要大量复用的场景,只是少量复用的话,没必要用享元。
优点
①减少对象的创建,减低内存中对象的数量,降低系统的内存,提高效率
②减少内存之外的其他资源占用
缺点
关注内/外部状态、关注线程安全问题(感觉这个很重要哎)
使系统、程序的逻辑复杂化
扩展
内部状态:相当于属性,不随外部改变而改变
外部状态:随着环境改变而改变
享元模式的实现
实现享元模式需要三个部分:
1.抽象享元:定义需要共享的对象业务接口,非必须,但是为了保证代码容易理解,最好定义一下
2.具体享元类:实现抽象享元类的接口,完成具体逻辑,这个就是我们需要被分享的元。
3.享元工厂:用于创建具体享元类,维护相同的享元对象。内部使用了类似单例模式的方法,当请求对象已经存在时,直接返回对象,不存在时,再创建对象。享元工厂是享元模式的核心,它需要确保系统可以共享相同的对象。
关键代码:
用HashMap存储这些对象。
demo:
public interface IBookAction {
void sell(); //销售
void sealOff();//拆封
}
/**
* Create by rye
* at 2020-09-09
*
* @description: 具体享元实现
*/
public class Book implements IBookAction {
private String bookName;
private String author;
public Book(String bookName) {
this.bookName = bookName;
}
@Override
public void sell() {
System.out.println(bookName + "is sold one");
}
@Override
public void sealOff() {
System.out.println(bookName + "is sealOff one");
}
}
/**
* Create by rye
* at 2020-09-09
*
* @description: 享元模式 ->工厂类
*/
public class BookFactory {
private static final HashMap<String, Book> books = new HashMap<>();
public static Book getBook(String name) {
if (books.containsKey(name)) {
System.out.println("取出书籍:"+name);
return books.get(name);
} else {
Book book = new Book(name);
//日志
System.out.println("创建书籍:"+name);
books.put(name,book);
return book;
}
}
}
//调用
public class Test {
public static void main(String[] args) {
String[] bookNames =new String[]{"《时间简史》","《物种起源》","《麦田守望者》"};
for (int i =0;i<20;i++){
int pos = new Random().nextInt(3);
BookFactory.getBook(bookNames[pos]);
}
}
}
输出结果:
创建书籍:《时间简史》
创建书籍:《物种起源》
取出书籍:《时间简史》
取出书籍:《时间简史》
创建书籍:《麦田守望者》
取出书籍:《时间简史》
取出书籍:《麦田守望者》
取出书籍:《麦田守望者》
取出书籍:《麦田守望者》
取出书籍:《物种起源》
取出书籍:《时间简史》
取出书籍:《麦田守望者》
取出书籍:《时间简史》
取出书籍:《物种起源》
取出书籍:《时间简史》
取出书籍:《时间简史》
取出书籍:《时间简史》
取出书籍:《时间简史》
取出书籍:《麦田守望者》
取出书籍:《物种起源》
源码解析
image.png如图,Integer的缓存机制就是享元模式的最佳实践。Integer缓存-127~128的整数,超过这个数就需要new了。也是一个常见的面试题。
组合模式
桥接模式
定义:将抽象部分与它的具体实现部分分离,使他们都可以独立地变化
通过组合的方式建立两个类之间的联系,而不是继承
类型:结构型
使用场景
抽象和具体实现之间增加更多的灵活性
一个类存在两个或多个独立变化的维度,且这个两个或多个维度需要独立进行扩展
不希望使用继承,或因为多层继承导致系统类的个数剧增
优点
分离抽象部分及其具体实现部分
提高了系统的可扩展性
符合开闭原则
符合合成复用原则
缺点
增加了系统的理解与设计难度
需要正确地识别出系统汇中两个独立变化的维度。
案例
比如我们平常发消息,消息类型有普通消息、紧急消息,而发送消息的方式也有很多:邮件、微信、短信。试想一下,如果我们新增一个消息类型,如果不采用设计模式,那么需要一一实现其支持的发送方式。同理如果新增一个发送方式,比如qq,那么针对上面不同的消息类型我们也需要修改他们内部的实现,以支持新增的发送方式。
实际上,这是两个维度的变化,而且,这两个各自的变化互不影响!当然有人说,我新增了一个发送方式,但是某一个消息类型不支持,这不也是有所影响吗?实际上是理解有出入,新增的方式并不会影响消息的类型,这两个之间没有必然的联系,不能说新增了一种消息类型,就一定会新增一种发送方式,也不是说多了一种发送方式就多一个新的发送类型。两者是两个维度独立变化的因素,可以采用组合的方式处理,用桥梁模式把抽象部分和具体部分分离出来,也就是抽象和抽象组合,具体和具体组合的方式来进行设计:
Demo:
/**
* Create by rye
* at 2020-09-10
*
* @description: 维度一 : 发送方式
*/
public interface SendAction {
void send(String message,String toUser);
}
/**
* Create by rye
* at 2020-09-10
*
* @description: 维度二 : 消息类型 ,可用接口
*/
public abstract class AbstractMessage {
private SendAction mSendAction;
public AbstractMessage(SendAction action) {
this.mSendAction = action;
}
public void sendMessage(String message,String toUser){
this.mSendAction.send(message,toUser);
}
}
/**
* Create by rye
* at 2020-09-10
*
* @description: 消息类型具体实现
*/
public class NormalMessage extends AbstractMessage {
public NormalMessage(SendAction action) {
super(action);
}
}
/**
* Create by rye
* at 2020-09-10
*
* @description: 紧急消息
*/
public class UrgencyMessage extends AbstractMessage {
public UrgencyMessage(SendAction action) {
super(action);
}
@Override
public void sendMessage(String message, String toUser) {
super.sendMessage(message, toUser);
}
//扩展功能
public void notifyUser(){
}
}
/**
* Create by rye
* at 2020-09-10
*
* @description:
*/
public class EmailChannel implements SendAction {
@Override
public void send(String message, String toUser) {
System.out.println("邮件发送消息:"+message+" 给"+toUser);
}
}
/**
* Create by rye
* at 2020-09-10
*
* @description: 微信渠道
*/
public class WechatChannel implements SendAction {
@Override
public void send(String message, String toUser) {
System.out.println("微信发送消息:"+message+" 给"+toUser);
}
}
/**
* Create by rye
* at 2020-09-10
*
* @description: 测试维度实现方式不同情况下的现象:抽象类+接口 && 接口+接口
*/
public class Test {
public static void main(String[] args) {
testMessage();
}
private static void testAbstractWay(){
NormalMessage message = new NormalMessage(new EmailChannel());
message.sendMessage("哈喽,WDNMD","李二狗");
UrgencyMessage urgencyMessage = new UrgencyMessage(new WechatChannel());
urgencyMessage.sendMessage("唯独你不懂","李二狗");
}
private static void testMessage(){
XXMessage xxMessage = new XXMessage("你在哪啊","李二狗");
xxMessage.sendMessage(new WechatChannel());
}
}
从上面可以看出,桥接模式的实现必须部分:
1.维度一 抽象类或接口
2.维度二 抽象类或接口
3.维度一抽象 以组合方式持有 维度二抽象
菜鸟教程里的解释很清晰:
使用场景: 1、如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
2、对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
3、一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
注意事项:对于两个独立变化的维度,使用桥接模式再适合不过了。
模板方法模式
定义:定义了一个算法的框架,并允许子类为一个或多个步骤提供实现
模板方法使得子类可以不改变算法结构的情况下,重新定义算法的某些步骤
类型:行为型
适用场景
一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现
各子类中公共的行为被提取出来并集中到一个公共父类中,从而避免代码重复
优点
提供复用性
提供扩展性
符合开闭原则
缺点
类数目增加
增加了系统实现的复杂度
继承关系自身缺点,如果父类添加新的抽象方法,所有子类都要重写(这个就有点坑了)
这个暂且保留,使用场景还需要确定。
迭代器模式
定义:提供一种方法,顺序访问一个集合对象中的各个元素,而又不暴露该对象的内部表示
类型:行为型
适用场景
①访问一个集合对象的内容而又无需暴露它的内部表示(个人理解是对一些重要的数据集合进行隔离保护,最好不好让外部知晓其内部是如何实现的)
②为遍历不同的集合结构提供一个统一的接口
优点
分离了集合对象的遍历行为
缺点
类的个数成对增加,新增一个集合,需要配对实现其迭代器,一定程度上增加了系统的复杂度
虽然使用范围很广,用的很多,但是自己定义的基本上很少。
关键代码
定义迭代器接口,提供hasNext,next方法
Demo:
/**
* 实体类
*/
public class Course {
private String name;
public Course(String name){
this.name = name;
}
public String getName(){
return name;
}
}
/**
* 抽象迭代器,可全局使用
*/
public interface RIterator {
boolean hasNext();
Object next();
boolean isLast();
}
/**
* 获取具体的迭代器,数据仓库需要实现
*/
public interface RContainer {
RIterator getIterator();
//除了获取迭代器外,此接口也可以实现其他操作数据类的方法
void addCourse(Course course);
void deleteCourse(Course course);
}
/**
* 具体迭代器
*/
public class CourseIterator implements RIterator {
private List<Course> dataList;
private int position;
private Course course;
public CourseIterator(List<Course> dataList) {
this.dataList = dataList;
}
@Override
public boolean hasNext() {
return position < dataList.size();
}
@Override
public Object next() {
course = dataList.get(position);
position++;
return course;
}
@Override
public boolean isLast() {
if (position < dataList.size()) {
return false;
}
return true;
}
}
/**
* 获取具体的迭代器,数据仓库需要实现
*/
public interface RContainer {
RIterator getIterator();
//除了获取迭代器外,此接口也可以实现其他操作数据类的方法
void addCourse(Course course);
void deleteCourse(Course course);
}
/**
* 保存数据的仓库
*/
public class CourseRepository implements RContainer {
private List<Course> dataList;
public CourseRepository() {
dataList = new ArrayList<>();
}
@Override
public RIterator getIterator() {
return new CourseIterator(dataList);
}
@Override
public void addCourse(Course course) {
dataList.add(course);
}
@Override
public void deleteCourse(Course course) {
if (dataList.contains(course)) {
dataList.remove(course);
}
}
}
public class Test {
public static void main(String[] args) {
Course course = new Course("Java");
Course course2 = new Course("Python");
Course course3 = new Course("C++");
Course course4 = new Course("Flutter");
RContainer repository = new CourseRepository();
repository.addCourse(course);
repository.addCourse(course2);
repository.addCourse(course3);
printCourses(repository);
repository.addCourse(course4);
printCourses(repository);
}
private static void printCourses(RContainer repository){
RIterator iterator = repository.getIterator();
while (!iterator.isLast()){
Course course = (Course) iterator.next();
System.out.println(course.getName());
}
}
}
由上可以看出,实现迭代器模式的几个必须类和接口:
①实体类
②抽象迭代器
③抽象仓库(主要是为了获取具体的迭代器)
④数据仓库(实现抽象仓库接口,拿到具体的迭代器)
⑤具体迭代器
网友评论