23种设计模式总结与分析override1
1 单例模式
有些时候,我们只需要新建实例,该实例通过继承与多态创建各种不一样的场景实例对象,该类提供了一种访问其唯一的实例对象的方式,可以直接访问,不需要实例化该类的对象,这样也可以保证JVM线程安全。单例模式属于创建型模式,它提供了一种创建对象的最佳方式在Spring的Bean工厂和Manager中,单例模式就得到了广泛使用
1.1 饿汉式
饿汉式是单例模式中最简单也是最实用的方法,故名起义,该方法就像一个饿汉一样,只要有食物(新的类加载至内存),饿汉就要吃进去(实例化),宝成了1
/**
* 饿汉式
* 类加载到内存后,就实例化一个单例,JVM保证线程安全
* 简单实用,推荐使用!
* 唯一缺点:不管用到与否,类装载时就完成实例化
* Class.forName("")
* (话说你不用的,你装载它干啥)
*/
public class Mgr01 {
private static final Mgr01 INSTANCE = new Mgr01();
private Mgr01() {};
public static Mgr01 getInstance() {
return INSTANCE;
}
public void m() {
System.out.println("m");
}
// 通过主方法验证是不是只实例化了一个对象
public static void main(String[] args) {
Mgr01 m1 = Mgr01.getInstance();
Mgr01 m2 = Mgr01.getInstance();
System.out.println(m1 == m2);
}
}
两个实例对象相等,是一个相同的实例。
1.2 静态方法
public class SingletonModule02 {
private static final SingletonModule02 INSTANCE;
static {
INSTANCE = new SingletonModule02();
}
private SingletonModule02(){};
public static SingletonModule02 getInstance(){return INSTANCE;}
public void m(){
System.out.println("m");
}
public static void main(String[] args) {
SingletonModule02 m1 = SingletonModule02.getInstance();
SingletonModule02 m2 = SingletonModule02.getInstance();
System.out.println(m1 == m2);
}
}
和饿汉式一样, 只是实例值的初始化变成了静态代码块
1.3 懒汉式
懒汉式就是此方法并不主动初始化实例对象,同时将构造方法设置为private,只有当前类可以访问,只有当调用获取实例的方法判断实例对象为空时,才初始化该实例对象。但是,此方法在多线程的时候会出现较大问题,有多个线程执行时,会一起判断实例是否为空,这时就会新建出不同的实例,一下代码可以验证
public class SingletonModule03 {
private static SingletonModule03 INSTANCE;
private SingletonModule03(){};
public static SingletonModule03 getInstance() throws InterruptedException {
if (INSTANCE == null){
Thread.sleep(1);
INSTANCE = new SingletonModule03();
}
return INSTANCE;
}
public void m(){
System.out.println("m");
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++){
new Thread(() ->
{
try {
//获取当前实例的哈希码,不一样的实例哈希码不同
System.out.println(SingletonModule03.getInstance().hashCode());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
).start();
}
}
}
可以看到实例的哈希码并不同,不是一个实例,所以懒汉式在多个线程运行实例时是有缺陷的
1.4 懒汉式加锁
应对上面的多线程初始化实例问题,最容易想到的方法就是加锁
public class Mgr04 {
private static Mgr04 INSTANCE;
private Mgr04() {
}
public static synchronized Mgr04 getInstance() {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr04();
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr04.getInstance().hashCode());
}).start();
}
}
}
通过给getInstance方法加锁,达到线程安全的目的,但上锁会带来线程阻塞,导致程序运行效率下降
1.5 给类加锁(错误方法)
public class Mgr05 {
private static Mgr05 INSTANCE;
private Mgr05() {
}
public static Mgr05 getInstance() {
if (INSTANCE == null) {
//妄图通过减小同步代码块的方式提高效率,然后不可行
synchronized (Mgr05.class) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr05();
}
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr05.getInstance().hashCode());
}).start();
}
}
}
在判空方法里 只有当没有实例对象时,给该类加锁
但是测试发现,创建的都不是同一个实例对象。这是为何??? 主要原因还是在判断实例为空后,后面的操作都不是原子操作,有可能不同的线程拿到不同的锁创建不同的实例对象
1.6 双重检查
针对上述问题,又对程序进行改进,加入双重检测机制
public class Mgr06 {
private static volatile Mgr06 INSTANCE; //JIT
private Mgr06() {
}
public static Mgr06 getInstance() {
if (INSTANCE == null) {
//双重检查
synchronized (Mgr06.class) {
if(INSTANCE == null) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr06();
}
}
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr06.getInstance().hashCode());
}).start();
}
}
}
在给类上锁后,再进行一次判空操作,避免创建新的实例对象
1.7 静态内部类方法(完美)
比饿汉式还要完美、精辟的代码🤭 (强迫症的福音)
public class Mgr07 {
private Mgr07() {
}
private static class Mgr07Holder {
private final static Mgr07 INSTANCE = new Mgr07();
}
public static Mgr07 getInstance() {
return Mgr07Holder.INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr07.getInstance().hashCode());
}).start();
}
}
}
主要由JVM保证只加载了一个类,内部的静态内部类Mgr07Holder 也只加载了一次,不用上锁耗费额外资源。并且在加载外部类时不会加载内部类,这样可以实现懒加载
1.8 枚举类
由JAVA创始人(大牛)在Effective java一书中(果然 人*还是要多读书,特别是大牛的书🤭)提出
/**
* 不仅可以解决线程同步,还可以防止反序列化。
*/
public enum Mgr08 {
INSTANCE;
public void m() {}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr08.INSTANCE.hashCode());
}).start();
}
}
}
等于直接在枚举类中定义一个INSTANCE,创建时调用
2 Factory(工厂)模式
所谓工厂,任何可以产生对象的方法/类,都可以称之为工厂。所以说单例也是一种工厂模式,也可以叫做静态工厂。工厂可以灵活控制"生产"(new)的过程,如设置权限、修饰、加日志等。
简单生动理解工厂模式可以用女娲造人的典故来理解:
女娲嬢孃补了天后,下到凡间一看,凡间太美了,但总感觉少了点什么,于是女娲就架起了八卦炉(技术术语:建立工厂)开始创建人
先定义什么是人类
public interface Human {
//人是愉快的,会笑的
public void laugh();
//人类还会哭,代表痛苦
public void cry();
//人类会说话
public void talk();
}
然后定义具体人类
//黄种人
public class YellowHuman implements Human {
public void cry() {
System.out.println("黄色人类会哭");
}
public void laugh() {
System.out.println("黄色人类会大笑,幸福呀!");
}
public void talk() {
System.out.println("黄色人类会说话,主要生活在亚洲");
}
}
//白种人
public class WhiteHuman implements Human {
public void cry() {
System.out.println("白色人类会哭");
}
public void laugh() {
System.out.println("白色人类会大笑,侵略的笑声");
}
public void talk() {
System.out.println("白色人类会说话,主要说外语");
}
}
//黑种人
public class BlackHuman implements Human {
public void cry() {
System.out.println("黑人会哭");
}
public void laugh() {
System.out.println("黑人会笑");
}
public void talk() {
System.out.println("黑人可以说话,说的话黄种人一般都听不懂!");
}
}
人类定义完毕,需要把烧制泥人的炉子也定义出来
public class HumanFactory {
//定一个烤箱,泥巴塞进去,人就出来,很NICE
public static Human createHuman(Class c){
Human human = null;
try {
human = (Human)Class.forName(c.getName()).newInstance(); //产生一个
人类
} catch (InstantiationException e) {
System.out.println("必须指定人类的颜色");
} catch (IllegalAccessException e) { //定义的人类有问题,那就烤不出来了,这是...
System.out.println("人类定义错误!");
} catch (ClassNotFoundException e) {
System.out.println("混蛋,你指定的人类找不到!");
}
return human;
}
}
然后再声明女娲
/**
**以下注释属于玩笑
*/
public class NvWa {
public static void main(String[] args) {
//女娲第一次造人,试验性质,少造点,火候不足,缺陷产品
System.out.println("------------造出的第一批人是这样的:白人
-----------------");
Human whiteHuman = HumanFactory.createHuman(WhiteHuman.class);
whiteHuman.cry();
whiteHuman.laugh();
whiteHuman.talk();
//女娲第二次造人,火候加足点,然后又出了个次品,黑人
System.out.println("\n\n------------造出的第二批人是这样的:黑人
-----------------");
Human blackHuman = HumanFactory.createHuman(BlackHuman.class);
blackHuman.cry();
blackHuman.laugh();
blackHuman.talk();
//第三批人了,这次火候掌握的正好,黄色人类
System.out.println("\n\n------------造出的第三批人是这样的:黄色人类
-----------------");
Human yellowHuman = HumanFactory.createHuman(YellowHuman.class);
yellowHuman.cry();
yellowHuman.laugh();
yellowHuman.talk();
}
}
如果女娲造人太累了,直接活进去一团泥巴进炉子,随机出来一群人,管他是黑人、白人、黄人,只要是人就成,这样先修改HumanFactory.java,**增加了 createHuman()**方法
public static Human createHuman(){
//定义一个类型的人类
Human human = null;
try{
human = (Human)Class.forName(c.getName()).newInstance(); //产生一个
人类
} catch ()(InstantiationException e) {//你要是不说个人类颜色的话,没法烤,要白的
黑,你说话了才好烤
System.out.println("必须指定人类的颜色");
} catch (IllegalAccessException e) { //定义的人类有问题,那就烤不出来了,这是...
System.out.println("人类定义错误!");
} catch (ClassNotFoundException e) { //你随便说个人类,我到哪里给你制造去?!
System.out.println("混蛋,你指定的人类找不到!");
}
return human;
}
//女娲偷懒了,把一团泥巴塞到八卦炉,哎产生啥人类就啥人类
public static Human createHuman(){
Human human=null; //定义一个类型的人类
//首先是获得有多少个实现类,多少个人类
List<Class> concreteHumanList = ClassUtils.getAllClassByInterface(Human.class); //定义了多少人类
//八卦炉自己开始想烧出什么人就什么人
Random random = new Random();
int rand = random.nextInt(concreteHumanList.size());
human = createHuman(concreteHumanList.get(rand));
return human;
}
}
接下来是ClassUtils类的实现
public class ClassUtils {
//给一个接口,返回这个接口的所有实现类
public static List<Class> getAllClassByInterface(Class c){
List<Class> returnClassList = new ArrayList<Class>(); //返回结果
//如果不是一个接口,则不做处理
if(c.isInterface()){
String packageName = c.getPackage().getName(); //获得当前的包名
try {
List<Class> allClass = getClasses(packageName); //获得当前包下以
及子包下的所有类
//判断是否是同一个接口
for(int i=0;i<allClass.size();i++){
if(c.isAssignableFrom(allClass.get(i))){ //判断是不是一个接口
if(!c.equals(allClass.get(i))){ //本身不加进去
returnClassList.add(allClass.get(i));
}
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return returnClassList;
}
//从一个包中查找出所有的类,在jar包中不能查找
private static List<Class> getClasses(String packageName)
throws ClassNotFoundException, IOException {
ClassLoader classLoader = Thread.currentThread()
.getContextClassLoader();
String path = packageName.replace('.', '/');
Enumeration<URL> resources = classLoader.getResources(path);
List<File> dirs = new ArrayList<File>();
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
dirs.add(new File(resource.getFile()));
}
ArrayList<Class> classes = new ArrayList<Class>();
for (File directory : dirs) {
classes.addAll(findClasses(directory, packageName));
}
return classes;
}
private static List<Class> findClasses(File directory, String packageName)
throws ClassNotFoundException {
List<Class> classes = new ArrayList<Class>();
if (!directory.exists()) {
return classes;
}
File[] files = directory.listFiles();
for (File file : files) {
if (file.isDirectory()) {
assert !file.getName().contains(".");
classes.addAll(findClasses(file, packageName + "." + file.getName()));
} else if (file.getName().endsWith(".class")) {
classes.add(Class.forName(packageName + '.' + file.getName().substring(0, file.getName().length() - 6)));
}
}
return classes;
}
这个 ClassUtils是个魔法类,既可以通过一个接口查找到所有的实现类,也可
以由父类查找到所有的子类,可以好好钻研钻研
增加了 createHuman()后,Human工厂类的拓展性变得更好了, 如果需要再添加一种人(蓝种人),只要继续集成 Human 接口成了,然后啥都不用修改就可以生产新的对象了。接下来,我们再引入一个问题:人是有性别的呀,有男有女,这个怎么实现呢?
抽象工厂
上面我们讲到女娲造人,人是造出来了,世界变得热闹了,但是总少了一什么,女娲想了想,嗯嗯都是清一色一个类型的对象,缺少个性、关爱、情绪,突然顿悟,忘记给人类定义性别了,于是准备重新"造人"。由于之前已经做了大量的工作了,(好好的炉子砸了重做也太可惜了),那就先从人(product产品)类开始修改,给每个人类都添加一个性别,再回炉重造,但是造人的炉子怎么改造,有了!可以使用"拷贝术",变成两个炉子,一个女性炉,一个男性炉,整个过程类图如下
其中这三个抽象(黑、黄、白)类在抽象工厂模式中是叫做产品等级,六个实现类是叫做产品族,然后是工厂类的类图:
抽象工厂HumanFactory只实现了一个 createHuman 的方法,目的是简化实现类的代码工作量。
代码如下:
public interface Human {
//首先定义什么是人类
public void laugh();
//人类还会哭,代表痛苦
public void cry();
//人类会说话
public void talk();
//定义性别
public void sex();
}
/**
**人类的接口定义好,然后根据接口创建三个抽象类,也就是三个产品等级,实现 laugh()、cry()、 * talk()三个方法,以 AbstractYellowHuman 为例:
**/
public abstract class AbstractYellowHuman implements Human {
public void cry() {
System.out.println("黄色人种会哭");
}
public void laugh() {
System.out.println("黄色人类会大笑,幸福呀!");
}
public void talk() {
System.out.println("黄色人类会说话,一般说的都是双字节");
}
}
/**
* 其它两个抽象类也是直接实现Human接口就行
**/
// 三个抽象类都实现了,然后就是实现类了
//女性黄种人实现类
public class YellowFemaleHuman extends AbstractYellowHuman {
public void sex() {
System.out.println("该黄种人的性别为女...");
}
}
//男性黄种人实现类
public class YellowMaleHuman extends AbstractYellowHuman {
public void sex() {
System.out.println("该黄种人的性别为男....");
}
}
// 其他四个男女性白种、黑种人代码也都是继承相应的抽象类实现
/**
*抽象工厂模式下的产品等级和产品族都已经完成,也就是人类以及产生出的人类是什么样子的都已经
*定义好了,下一步就等着工厂开工创建了,那我们来看工厂类。
**/
//定义人类枚举类
public enum HumanEnum {
//把世界上所有人类型都定义出来
YelloMaleHuman("com.xxx.yellowHuman.YellowMaleHuman"),
YelloFemaleHuman("com.xxx.yellowHuman.YellowFemaleHuman"),
WhiteFemaleHuman("com.xxx.whiteHuman.WhiteFemaleHuman"),
WhiteMaleHuman("com.xxx.whiteHuman.WhiteMaleHuman"),
BlackFemaleHuman("com.xxx.blackHuman.BlackFemaleHuman"),
BlackMaleHuman("com.xxx.blackHuman.BlackMaleHuman");
private String value = "";
private HumanEnum(String value){
this.value = value;
}
public String getValue(){
return this.value;
}
运用枚举类有一个很大的好处就是,,Enum 类型作为一个参
数传递到一个方法中时,在 Junit 进行单元测试的时候,不用判断输入参数是否为空、长度为 0 的边界异常条件,如果方法传入的参数不是 Enum 类型的话,根本就传递不进来。然后就是工厂类
public interface HumanFactory {
//制造黄色人类
public Human createYellowHuman();
//制造一个白色人类
public Human createWhiteHuman();
//制造一个黑色人类
public Human createBlackHuman();
}
//抽象类
public abstract class AbstractHumanFactory implements HumanFactory {
/*
* 给定一个性别人类,创建一个人类出来 专业术语是产生产品等级
*/
protected Human createHuman(HumanEnum humanEnum) {
Human human = null;
//如果传递进来不是一个Enum中具体的一个Element的话,则不处理
if (!humanEnum.getValue().equals("")) {
try {
//直接产生一个实例
human = (Human) Class.forName(humanEnum.getValue()).newInstance();
} catch (Exception e) {
//因为使用了enum,这个种异常情况不会产生了,除非你的enum有问题;
e.printStackTrace();
}
}
return human;
}
}
/*
* 我们可以看到这就是使用枚举类的好处,createHuman(HumanEnum humanEnum)这个方法定义了输入
* 参数必须是 HumanEnum 类型,然后直接使用 humanEnum.getValue()方法就能获得具体传递进来的值
*/
//接下来是实现类,男性工厂,只生产男性
public class MaleHumanFactory extends AbstractHumanFactory {
//创建一个男性黑种人
public Human createBlackHuman() {
return super.createHuman(HumanEnum.BlackMaleHuman);
}
//创建一个男性白种人
public Human createWhiteHuman() {
return super.createHuman(HumanEnum.WhiteMaleHuman);
}
//创建一个男性黄种人
public Human createYellowHuman() {
return super.createHuman(HumanEnum.YelloMaleHuman);
}
}
//女性工厂,只创建女性
public class FemaleHumanFactory extends AbstractHumanFactory {
//创建一个女性黑种人
public Human createBlackHuman() {
return super.createHuman(HumanEnum.BlackFemaleHuman);
}
//创建一个女性白种人
public Human createWhiteHuman() {
return super.createHuman(HumanEnum.WhiteFemaleHuman);
}
//创建一个女性黄种人
public Human createYellowHuman() {
return super.createHuman(HumanEnum.YelloFemaleHuman);
}
}
/*
* 接下里就是女娲要造人了,女娲建立了两条生产线,分别就是男性生产线,女性生产线,代码不在此po出
* /
抽象工厂符合OCP(开闭)原则以及高内聚低耦合的设计原则,可以从产品生产线(维度)或者产品一族两个方向拓展。但如果我们两个都想要呢?即能够在产品维度(男性,女性,有可能有双性人呢😂)也能在产品族(黑种人白种人黄种人还可能有蓝种人呢)拓展产品,该怎么做呢。 这里就可以提到Spring的Bean工厂了。Spring中既可以通过配置文件(yml)灵活配置类,也可以通过SpringIOC反射机制注入类,可以说非常神奇~这里就不深入讲解Spring了。
3 Strategy(策略)模式(接口与实现)
其实,通过总结与观察,所有的设计模式不过都是**封装、继承、多态**的组合与实现,为了解耦合和高效运行程序,并提高程序的健壮性和鲁棒性。
策略模式就是接口与实现+容器(Context),这里为更好理解设计模式,可以参考设计模式之禅这本"圣经"之书列举的诸多生动形象的例子让我们从各种不同的活灵活现的场景中理解各种设计模式的运行机制,底层逻辑
在设计模式之禅中,作者在策略模式中举了刘备在到江东娶老婆时, 诸葛亮给赵云三个锦囊妙计的例子:
妙计其实时同一种类型的东西,完全可以用接口实现
public interface IStrategy {
//每个锦囊妙计都是一个可执行的算法
public void operate(){};
}
接着就可以写三个实现类(三个锦囊嘛),三个锦囊有了,还需要有地方放这些锦囊啊,就可以写一个类的,在构造方法里放入类接口
public class Context {
//构造函数,你要使用那个妙计
private IStrategy straegy;
public Context(IStrategy strategy){
this.straegy = strategy;
}
//使用计谋了,看我出招了
public void operate(){
this.straegy.operate();
}
}
接下来就是在不同的场景,赵云(类),拆开(new Context)锦囊使用妙计了。这样设计的程序,既符合高内聚低耦合的特点,还具备拓展性也就是 OCP 原则,策略类可以一直添加下去,使用的时候只要修改Context.java文件就行了
在Java中,Comparator接口是策略模式最直接的引用。
对于任意类型的对象如果我们需要相互比较 ,可以自定义一个泛型的Comparable接口
public interface Comparable<T> {
int compareTo(T o);
}
这种接口要是实现一个对象的一种属性比较还适用(比如我们想比较一组学生的身高,一组猫的重量大小),但如果我们想灵活指定比较大小的方式,可以随时拓展,这时就需要策略模式了,Comparator****就是如此
public interface Comparator<T> {
int compare(T o1, T o2);
}
使用时只需要实现一个特定类型的Compatator就行
public class CatWeightComparator implements Comparator<Cat> {
@Override
public int compare(Cat c1, Cat c2) {
if(c1.weight < c2.weight) return -1;
else if (c1.weight > c2.weight) return 1;
else return 0;
}
}
4 Facade 门面(外观)模式
生活中我们很多人都会到政府部门办事,开各种证明以及办理手续。但我们知道有时候往往要办一件简单到不行的事情,甚至有些没必要的手续(比如证明我是我,我老妈是我的老妈😓),需要去不同的部门来回跑,非常耽误时间,影响效率(一份光阴一份金啊)。现在为了简介高效办事流程,这时就需要一个统一协调管理的部门,我们要办什么事,在门口先找这个门面部门,由这个部门调度到对应的职能部门办理,省了中间大量不必要的手续以及排队等候。现在比较火的数据和业务中台也差不多是这个职能。
下面来通过代码举个栗子,
/*
*以前在互联网不那么发达的年代,大家通信往往都会通过书信,写信通常有四个步骤:写信的内容,写信封,把信放进信封,将信封送去邮局邮递
* 虽然简单,这四个步骤都是要跑的呀,信多了还是麻烦,比如我们想要发一个大规模的传单或者印单,一下要发一千万封信,这样做不就玩球累死了
* 所以,现在邮局开发了一个新业务,你只要把信件的必要信息高速我,我给你发,我来做这四个过程,你就不要管了,只要把信件交给我就成了
*/
//定义一个写信的过程
public interface LetterProcess {
//首先要写信的内容
public void writeContext(String context);
//其次写信封
public void fillEnvelope(String address);
//把信放到信封里
public void letterInotoEnvelope();
//然后邮递
public void sendLetter();
}
//写信具体实现类
public class LetterProcessImpl implements LetterProcess {
//写信
public void writeContext(String context) {
System.out.println("填写信的内容...." + context);
}
//在信封上填写必要的信息
public void fillEnvelope(String address) {
System.out.println("填写收件人地址及姓名...." + address);
}
//把信放到信封中,并封好
public void letterInotoEnvelope() {
System.out.println("把信放到信封中....");
}
//塞到邮箱中,邮递
public void sendLetter() {
System.out.println("邮递信件...");
}
}
//然后就是用该过程开始写信,具体实现就是通过创建对象,输入内容,main代码就忽略了
但整个过程离高内聚的要求相差甚远,因为知道这四个步骤,而且还要知道这四个步骤的顺序,一旦出错,信就不可能邮寄出去,那么该如何改进呢;
通过门面模式,处理信件的子系统比较复杂,就对 Sub System进行了封装,增加了一个门面,Client 调用时,直接调用门面的方法就可以了,不用了解具体的实现方法以及相关的业务顺序。程序只是添加了一个ModenPostOffice类
public class ModenPostOffice{
private LetterProcess letterProcess = new LetterProcessImpl();
//写信,写信封,装信,邮寄一体化
public void sendLetter(String context,String address) {
//帮你写信
letterProcess.writeContext(context);
//写好信封
letterProcess.fillEnvelope(address);
//把信放到信封中
letterProcess.letterInotoEnvelope();
//邮递信件
letterProcess.sendLetter();
}
}
这相当于邮局又提供了一个新的服务,客户只要把信的内容以及收信地址给他们,他们就会把信写好,封好,并发送出去,这种服务提出时大受欢迎呀,这简单呀,客户减少了很多工作。使用这种模式后,系统的拓展性能大大增加了,比如说既往不同省份的邮件需要以不同的等级标记(人口规模越大的城市等级越高,因为物流压力大嘛),这时我们只需要在
ModenPostOffice类下添加一个classify类。只是增加了一个 letterPolice 变量的声明以及一个方法的调用,那这个写信的过程就变成了这样:先写信,然后写信封,然后物流开始检查分类,然后才把信放到信封,然后发送出去,那这个变更对客户来说,是透明的,他根本就看不到有人在检查他的邮件,他也不用了解,反正现代化的邮件都帮他做了,这也是他乐意的地方。
这就是门面模式,这是一个很好的封装方法,一个子系统比较复杂的实话,比如算法或者业务比较复杂,就可以封装出一个或多个门面出来,项目的结构简单,而且扩展性非常好。使用门面模式后,对门面进行单元测试,约束项目成员的代码质量,对项目整体质量的提升也是一个比较好的帮助。
5 Mediator(中介/调停者)模式
上节我们讲到的门面模式主要时对外的业务处理的,但是对内业务呢,内部部门与部门之间工作总也需要常常打交道啊,这时我们就需要一个类似居委会大妈职能的角色了,有什么事通过一个中介来调停解决,也是统一管理协调。
我们都知道公司是一个以盈利挣钱为目的的组织机构,盈利方法则不尽相同,但是作为公司都有相同三个环节:采购、销售和库存。这个怎么说呢?比如一个软件公司,要开发软件,需要开发环境吧, Windows 、Linux操作系统,数据库产品等这你得买吧,那就是采购,开发完毕一个产品还要把产品推销出去,推销出去了大家才有钱赚,不推销出
去大家都去喝西北风呀,既然有产品就必然有库存,软件产品也有库存,你总要拷贝吧,虽然是不需要占用库房空间,那也是要占用光盘或硬盘,这也是库存,再比如做咨询服务的公司,它要采购什么?采购知识,采购经验,这是这类企业的生存之本,销售的也是知识和经验,库存同样是知识和经验,库存、采购、销售三个模块之间示意图如下:
三个模块是相互依赖的,基本上是你中有我,我中有你的,互相影响。我们可以先实现一个进销存
Purchase 负责采购管理,buyIBMComputer 是指定了采购 IBM 电脑,refuseBuyIBM 是不再采购 IBM 了,实现代码如下
public class Purchase {
//采购IBM型号的电脑
public void buyIBMcomputer(int number){
//访问库存
Stock stock = new Stock();
//访问销售
Sale sale = new Sale();
//电脑的销售情况
int saleStatus = sale.getSaleStatus();
if(saleStatus>80){ //销售情况良好
System.out.println("采购IBM电脑:"+number + "台");
stock.increase(number);
} else { //销售情况不好
int buyNumber = number/2; //折半采购
System.out.println("采购IBM电脑:"+buyNumber+ "台");
}
}
//不再采购IBM电脑
public void refuseBuyIBM(){
System.out.println("不再采购IBM电脑");
}
}
//接下来是库存类
public class Stock {
//刚开始有100台电脑
private static int COMPUTER_NUMBER =100;
//库存增加
public void increase(int number){
COMPUTER_NUMBER = COMPUTER_NUMBER + number;
System.out.println("库存数量为:"+COMPUTER_NUMBER);
}
//库存降低
public void decrease(int number){
COMPUTER_NUMBER = COMPUTER_NUMBER - number;
System.out.println("库存数量为:"+COMPUTER_NUMBER);
}
//获得库存数量
public int getStockNumber(){
return COMPUTER_NUMBER;
}
//存货压力大了,就要通知采购人员不要采购,销售人员要尽快销售
public void clearStock(){
Purchase purchase = new Purchase();
Sale sale = new Sale();
System.out.println("清理存货数量为:"+COMPUTER_NUMBER);
//要求折价销售
sale.offSale();
//要求采购人员不要采购
purchase.refuseBuyIBM();
}
}
/*
*库房中的货物数量肯定有增加和减少了,同时库房还有一个容量显示,达到一定的容量后就要求对一
* 些商品进行折价处理,腾出更多的空间容纳新产品,于是就有了 clearStock 方法,既然是清仓处理肯定就要折价销售了,于是在 Sale 这个类中就有了 offSale 方法
*/
public class Sale {
//销售IBM型号的电脑
public void sellIBMComputer(int number){
//访问库存
Stock stock = new Stock();
//访问采购
Purchase purchase = new Purchase();
if(stock.getStockNumber()<number){ //库存数量不够销售
purchase.buyIBMcomputer(number);
}
System.out.println("销售IBM电脑"+number+"台");
stock.decrease(number);
}
//反馈销售情况,0——100之间变化,0代表根本就没人卖,100代表非常畅销,出一个卖一个
public int getSaleStatus(){
Random rand = new Random(System.currentTimeMillis());
int saleStatus = rand.nextInt(100);
System.out.println("IBM电脑的销售情况为:"+saleStatus);
return saleStatus;
}
//折价处理
public void offSale(){
//库房有多少卖多少
Stock stock = new Stock();
System.out.println("折价销售IBM电脑"+stock.getStockNumber()+"台");
}
}
/*
* 销售情况当然只有销售人员才能反馈出来了,通过百分制的机制衡量销售情况
*/
public class Client {
public static void main(String[] args) {
//采购人员采购电脑
System.out.println("------采购人员采购电脑--------");
Purchase purchase = new Purchase();
purchase.buyIBMcomputer(100);
//销售人员销售电脑
System.out.println("\n------销售人员销售电脑--------");
Sale sale = new Sale();
sale.sellIBMComputer(1);
//库房管理人员管理库存
System.out.println("\n------库房管理人员清库处理--------");
Stock stock = new Stock();
stock.clearStock();
}
}
我们在场景类中模拟了三种人员类型的活动:采购人员采购电脑,销售人员销售电脑,库管员管理库存,运行结果也是我们期望的,三个不同类型的参与者完成了各自的活动。但是,重点中的重点就是:这三个类间是彼此关联的每个类都与其他两个类产生了关联关系,迪米特法则教育我们“每个类只和朋友类交流”,这个朋友类可不是越多越好,越多耦合性越大。那么改怎么重新设计协调三者之间的关系呢,我们可以借鉴网络结构的星型拓扑
星型网络拓扑中每个计算机通过交换机和其他计算机进行数据交换,各个计算机之间并没有直接出现交互的情况,结构简单,而且稳定,只要中间那个交换机不瘫痪,整个网络就不会发生大的故障,公司和网吧一般都采用星型网络,足以证明此结构的稳定与食用性,由此可以借鉴到我们的库存销量采购管理中,运用Mediator中介模式
类图如下:
建立两个抽象类AbstractMediator 和 AbstractColeague,每个对象只是与中介者 Mediator 之间产生依赖,与其他对象之间没有直接的关系,来看代码:
public abstract class AbstractMediator {
protected Purchase purchase;
protected Sale sale;
protected Stock stock;
//构造函数
public AbstractMediator(){
purchase = new Purchase(this);
sale = new Sale(this);
stock = new Stock(this);
}
//中介者最重要的方法,叫做事件方法,处理多个对象之间的关系
public abstract void execute(String str,Object...objects);
}
//具体实现的中介者,中介者可以根据业务的要求产生多个中介者(一般情况只有一个中介者),划分各个终结者的职责
public class Mediator extends AbstractMediator {
//中介者最重要的方法
public void execute(String str,Object...objects){
if(str.equals("purchase.buy")){ //采购电脑
this.buyComputer((Integer)objects[0]);
}else if (str.equals("sale.sell")){ //销售电脑
this.sellComputer((Integer)objects[0]);
}else if (str.equals("sale.offsell")){ //折价销售
this.offSell();
}else if (str.equals("stock.clear")){ //清仓处理
this.clearStock();
}
}
//采购电脑
private void buyComputer(int number){
int saleStatus = super.sale.getSaleStatus();
if(saleStatus>80){ //销售情况良好
System.out.println("采购IBM电脑:"+number + "台");
super.stock.increase(number);
}else { //销售情况不好
int buyNumber = number/2; //折半采购
System.out.println("采购IBM电脑:"+buyNumber+ "台");
}
}
//销售电脑
private void sellComputer(int number){
if(super.stock.getStockNumber()<number){ //库存数量不够销售
super.purchase.buyIBMcomputer(number);
}
super.stock.decrease(number);
}
//折价销售电脑
private void offSell(){
System.out.println("折价销售IBM电脑"+stock.getStockNumber()+"台");
}
//清仓处理
private void clearStock(){
//要求清仓销售
super.sale.offSale();
//要求采购人员不要采购
super.purchase.refuseBuyIBM();
}
}
/*
* 中介者 Mediator 有定义了多个 Private 方法,其目标是处理各个对象之间的依赖关系,即是说原有一个对象要依赖多个对象的情况
* 移到中介者的 Private 方法中实现
*/
public abstract class AbstractColleague {
protected AbstractMediator mediator;
//AbstractColleague的构造方法
public AbstractColleague(AbstractMediator _mediator){
this.mediator = _mediator;
}
}
//采购类代码实现
public class Purchase extends AbstractColleague{
public Purchase(AbstractMediator _mediator){
super(_mediator);
}
//采购IBM型号的电脑
public void buyIBMcomputer(int number){
super.mediator.execute("purchase.buy", number);
}
//不在采购IBM电脑
public void refuseBuyIBM(){
System.out.println("不再采购IBM电脑");
}
}
/**
* Purchase 类简化了很多,看着也清晰了很多,处理自己的职责,与外界有关系的事件处理交给了中介者来完成。再来看 Stock 类
**/
public class Stock extends AbstractColleague{
public Purchase(AbstractMediator _mediator){
super(_mediator);
}
//初始库存
private static int COMPUTER_NUMBER =100;
//库存增加
public void increase(int number){
COMPUTER_NUMBER = COMPUTER_NUMBER + number;
System.out.println("库存数量为:"+COMPUTER_NUMBER);
}
//库存降低
public void decrease(int number){
COMPUTER_NUMBER = COMPUTER_NUMBER - number;
System.out.println("库存数量为:"+COMPUTER_NUMBER);
}
//获得库存数量
public int getStockNumber(){
return COMPUTER_NUMBER;
}
//存货压力大了,就要通知采购人员不要采购,销售人员要尽快销售
public void clearStock(){
System.out.println("清理存货数量为:"+COMPUTER_NUMBER);
super.mediator.execute("stock.clear");
}
}
// Sale 类实现
public class Sale extends AbstractColleague{
public Sale(AbstractMediator _mediator) {
super(_mediator);
}
//销售IBM型号的电脑
public void saleIBMComputer(int quantity) {
super.mediator.execute("sale.sell", number);
System.out.println("销售IBM电脑"+number+"台");
}
//反馈销售情况,0——100之间变化,0代表根本就没人卖,100代表非常畅销,出1一个卖一个
public int getSaleStatus(){
Random rand = new Random(System.currentTimeMillis());
int saleStatus = rand.nextInt(100);
System.out.println("IBM电脑的销售情况为:"+saleStatus);
return saleStatus;
}
//折价处理
public void offSale(){
super.mediator.execute("sale.offsell");
}
}
// 场景类
public class Client {
public static void main(String[] args) {
AbstractMediator mediator = new Mediator();
//采购人员采购电脑
System.out.println("------采购人员采购电脑--------");
Purchase purchase = new Purchase(mediator);
purchase.buyIBMcomputer(100);
//销售人员销售电脑
System.out.println("\n------销售人员销售电脑--------");
Sale sale = new Sale(mediator);
sale.sellIBMComputer(1);
//库房管理人员管理库存
System.out.println("\n------库房管理人员清库处理--------");
Stock stock = new Stock(mediator);
stock.clearStock();
}
}
/*
1. 在场景类中增加了一个中介者,然后分别传递到三个同事类中,三个类都具有相同的特性:只负责处
2. 理自己的活动(行为),与自己无关的活动就丢给中介者处理,程序运行的结果是相同的。从项目设计上来看,加入了中介者,设计结构清晰了很多,而且类间的耦合性大大减少
3. 代码质量也得到了提升
*/
我们可以总结出,中介者模式通常由一下几部分组成:
- 抽象中介者角色: 定义统一的接口处理各个角色之间的通信
- 具体中介者角色: 具体实现协调行为,因此它必须依赖同事(关联类)角色
- 同事角色: 每一个同事角色都知道中介者角色,而且与其他的同事角色通信的时候,一定要通过中介者角色协作。
为什么同事类要使用构造函数注入中介者而中介者使用 getter/setter 方式注入同事类呢?想过没有?那是因为同事类必须有中介者,而中介者可以只有部分同事类
中介者模式的优点就是减少类间的依赖,把原有的一对多的依赖变成了一对一的依赖,同事类只依赖中介者,减少了依赖,当然也同时减低了类间的耦合。它的缺点呢就是中介者会膨胀的很大,而且逻辑会很复杂,因为所有的原本 N 个对象直接的相互依赖关系转换为中介者和同事类的依赖关系,同事类越多,中介者的逻辑就复杂。
中介者模式也不代表一定简单适用所有体系结构,假设有一个类,与其他类没有任何的依赖关系,其他类也不依赖这个类,是完全独立逻辑的"孤岛",你可以说这个类完全没有存在的必要吗?答案是否定的,这时候使用中介者模式,反而增加了系统的复杂度。中介者模式适用于多个对象之间紧密耦合,耦合的标准可以这样来衡量:在类图中出现了蜘蛛网状结构,在这种情况下一定要考虑使用中介者模式,有利于把蜘蛛网梳理为一个星型结构,使原本复杂混乱关系变得清晰简单。
中介者模式在很多系统中都有应用体现: 如SpringMVC,大家都应该使用过 Struts 吧,MVC 框架,其中的 C(Controller)就是一个中介者,叫做前端控制器(Front Controller),它的作用就是把 M(Model,业务逻辑)和 V(View,视图)隔离开,协调 M
和 V 协同工作,把 M 运行的结果和 V 代表的视图融合成一个前端可以展示的页面,减少 M 和 V 的依赖关系。MVC 框架已经成为一个非常流行、成熟的开发框架,这也是中介者模式优秀的一个体现。
C/S 结构。C/S 结构的应用也是一个典型的中介者模式,比如 MSN,张三发一个消息给四,其过程应该是这样的:张三发送消息,MSN 服务器(中介者)接受到消息,查找李四,把消息发送到李四,同时通知张三,消息已经发送,在这里 MSN 服务器就是一个中转站,负责协调两个客户端的信息交流,与此相反的就是 IPMSG(也叫飞鸽)没有使用中介者,直接使用了 UDP 广播的方式,每个客户端既是客户端也是服务端。
MQ(Message Queue) 消息中间件。市面上现在我们常用的中间件Redis、Kafka、RabbitMQ等都类似于中介者的机制,生产者生产消息\线程,放入中间队列,由中间队列处理消息,并给消费者消费掉。
当然,使用中介模式就必然会带来中介者的膨胀问题,这在一个项目中时很不恰当的,那到底在什么情况下使用中介者模式呢?大家可以在如下的情况下尝试使用中介者模式:
- N 个对象之间产生了相互的依赖关系,其中 N 大于 2,注意是相互的依赖;
- 多个对象有依赖关系,但是依赖的行为尚不确定或者有发生改变的可能,在这种情况下一般建议采用中介者模式,降低变更引起的风险扩散;
- 产品开发。其中一个明显的例子就是 MVC 框架,把这个应用到产品中,可以提升产品的性能和扩展性,但是作为项目开发就未必,项目是以交付投产为目标,而产品以稳定、高效、扩展为宗旨。
6 Decorator(装饰)模式
装饰模式,顾名思义,就是给类上"装饰",赋予类更加灵活、拓展性更强的功能,不用写多的继承类,以一变应万变,装饰模式的通用类图如图:
举个栗子,如果我们想写个坦克游戏,想在中途给坦克加一个坦克血条显示,或者给坦克加一个装甲,亦或是给坦克射出的子弹加上尾焰效果。难道我们要重新写一个NewTank类继承Tank父类和TankBlood类在这个实例中同时new出这两个类吗?显然不合适耦合度太高而且低效,这还只是添加一个效果,如果以后各种各样的效果功能需要添加进来怎么办呢?这时候就可用到装饰器模式
所有的游戏物体,坦克、子弹、障碍物或者坦克装甲都是GameObject的具体实现,Decorator不再是装饰具体的某一个类,而是装饰GameObject,这样就可以灵活组合了,子弹不仅可以和ObstacleDeco聚合,更可以和ArmDeco聚合。
装饰模式可以替代继承,解决我们类膨胀的问题,你要知道继承是静态的给类增加功能,而装饰模式则是动态的给增加功能,你看上面的那个例子,我不想要 SortDecorator 这层的封装也很简单呀,直接在 Father 中去掉就可以了,如果你用继承就必须修改程序。
装饰模式还有一个非常好的优点,扩展性非常好,在一个项目中,你会有非常多因素考虑不到,特别是业务的变更,时不时的冒出一个需求,特别是提出一个令项目大量延迟需求时候,那种心情是…,真想骂娘!装饰模式可以给我们很好的帮助,通过装饰模式重新封装一个类,而不是通过继承来完成,简单点说,三个继承关系 Father,Son,GrandSon 三个类,如果我要给Son 类上强行增强一些功能呢?我想你会坚决的顶回去!不允许,对了,为什么呢?你增强的功能是修改 Son 类中的方法吗?增加方法吗 ?对 GrandSon的影响呢?特别是 GrandSon 有多个的情况,你该怎么解决?这个评估的工作量就是够你受的,所以这个是不允许的,那还是要解决问题的呀,怎么办?通过建立 SonDecorator 类来修饰 Son,等于说是创建了一个新的类,这个对原有程序没有变更,通过扩充很好的完成了这次变更。