前言
23种设计模式很早就想留篇笔记了,不然总感觉学的不透彻,而且容易忘。下面我们来帮助小唐——一个类minecrfat游戏开发者,来学习一下设计模式吧。设计模式可以分为创建型模式,结构型模式与行为型模式,所以我们可能要分3篇博客来描述,时间跨度可能很短,也可能极长
创建型模式
1.工厂方法模式
小唐在写一个实体生成算法,需要大量的创建对象并初始化。比如说:
1 2 3 4
| System.out.println("模拟创建前大初始化"); Creeper creeper = new Creeper(); System.out.println("模拟向模型加载器类传入对象"); System.out.println("模拟向贴图加载器类传入对象");
|
可以发现对象的创建是一个过程而不仅仅是一个操作。于是每当想写Creeper实体的生成逻辑时,就会有大量的代码重复
这其中对象的使用者需要做的应该只是拿到对象,仅此而已,所以我们引出了工厂模式来降低对象创建与使用的耦合
简单工厂模式
简单工厂模式很简单,建立一个实体的接口,令所有实体实现它。再建立一个工厂类,根据用户的选择执行相对应的创建逻辑,并把结果向上转型为接口类型进行返回。用户只需要调用创建方法即可拿到对象,无需关注创建的细节。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| interface Entity{} class Zombie implements Entity{ public Zombie() {} } class Creeper implements Entity{ public Creeper() {} } class MinecraftEntityFactory{ public Entity summonEntity(String entityType){ if (Objects.equals(entityType, "minecraft:creeper")){ System.out.println("模拟创建前大初始化"); Creeper creeper = new Creeper(); System.out.println("模拟向模型加载器类传入对象"); System.out.println("模拟向贴图加载器类传入对象"); return creeper; } else if (Objects.equals(entityType, "minecraft:zombie")){ return new Zombie(); } else return null; } } public class Test { public static void main(String[] args){ MinecraftEntityFactory minecraftEntityFactory = new MinecraftEntityFactory(); Entity newEntity = minecraftEntityFactory.summonEntity("minecraft:creeper"); } }
|
简单工厂模式解决了对象创建与使用耦合性高的问题。但缺点在于不符合“开闭原则”,每次添加新实体就需要修改工厂类。在实体类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展维护,并且工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。
开闭原则(Open/Closed Principle,OCP) 是面向对象设计中的另一个重要原则,其核心思想是:软件实体(类、模块、函数等)应该对扩展开放,对修改(源代码)关闭。
工厂方法模式
对简单工厂模式的缺陷进行举例,假设需要创建的实体越来越多,并且逻辑不同时,维护原来的实体工厂就变得复杂了
比如Villager在沙漠群系和树林群系的模型和贴图不同,但是实际上使用者不在乎身处什么群系,而是只关注Villager的创建
这个时候我们需要定义一套工厂自身的规范,即加入一个工厂的接口,用户使用接口提供的方法创建实体。但是接口的具体实现则可以是多个诸如沙漠实体生成工厂,树林实体生成工厂的工厂组成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| interface Entity{} class Villager implements Entity{ public Villager() {} }
interface MinecraftEntityFactory{ Entity summonEntity(String entityType); }
class MinecraftDesertEntityFactory implements MinecraftEntityFactory{ @Override public Entity summonEntity(String entityType){ if (Objects.equals(entityType, "minecraft:villager")){ System.out.println("模拟创建前大初始化"); Villager villager = new Villager(); System.out.println("模拟向沙漠模型、贴图加载器类传入对象"); return villager; } else return null; } }
class MinecraftForestEntityFactory implements MinecraftEntityFactory{ @Override public Entity summonEntity(String entityType){ if (Objects.equals(entityType, "minecraft:villager")){ System.out.println("模拟创建前大初始化"); Villager villager = new Villager(); System.out.println("模拟向树林模型、贴图加载器类传入对象"); return villager; } else return null; } } public class Test { public static void main(String[] args){ MinecraftEntityFactory minecraftEntityFactory = new MinecraftForestEntityFactory(); Entity newEntity = minecraftEntityFactory.summonEntity("minecraft:villager"); } }
|
这样哪怕有更多的实体加入,哪怕逻辑不同,我们也只要创立新工厂即可,符合开闭原则
2.抽象工厂模式
现在小唐的想法改变了,想写一个生态生成算法,要创建的对象不仅仅是刚刚的实体,还要有方块对象。
你可以将之前实体的工厂方法模式复制一份,但那样会导致两个问题:
1.使用者在创建方块与实体时可以选择不同群系的工厂,丢失了群系的一致性,这是缺乏接口定义的规范性导致的
2.明明生态可以看作一个整体,现在却要分别使用到方块工厂与实体工厂两个工厂,不如直接定义一个生态工厂
于是抽象工厂模式诞生了:
我们创建生态工厂接口,提供2个方法分别创建方块与实体,能被沙漠群系工厂或树林群系工厂等所实现。
用户还是只用关心方块与实体的创建,群系是由实现的工厂决定的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| interface Entity{} class Villager implements Entity{ public Villager() {} } interface Block{} class Leaves implements Block{ public Leaves() {} }
interface MinecraftEcologyFactory{ Entity summonEntity(String entityType); Block summonBlock(String blockType); }
class MinecraftDesertEcologyFactory implements MinecraftEcologyFactory{ @Override public Entity summonEntity(String entityType){ if (Objects.equals(entityType, "minecraft:villager")){ System.out.println("模拟创建前大初始化"); Villager villager = new Villager(); System.out.println("模拟向沙漠模型、贴图加载器类传入对象"); return villager; } else return null; } @Override public Block summonBlock(String blockType){ if (Objects.equals(blockType, "minecraft:leaves_block")){ System.out.println("模拟创建前大初始化"); Leaves leaves = new Leaves(); System.out.println("模拟向沙漠模型、贴图加载器类传入对象"); return leaves; } else return null; } } class MinecraftForestEcologyFactory implements MinecraftEcologyFactory{ @Override public Entity summonEntity(String entityType){ return null; } @Override public Block summonBlock(String blockType){ return null; } } public class Test { public static void main(String[] args){ MinecraftEcologyFactory minecraftEcologyFactory = new MinecraftDesertEcologyFactory(); Entity newEntity = minecraftEcologyFactory.summonEntity("minecraft:villager"); Block newBlock = minecraftEcologyFactory.summonBlock("minecraft:leaves_block"); }
|
这里还是得介绍两个概念
产品族: 具有共同主题的一组相关产品,如方块,实体
产品等级:在同一产品族内,由主题划分的不同产品,如:树林方块,沙漠方块
所以我们发现,不论是工厂方法模式还是抽象工厂模式,不同的工厂实现类之间的区别其实是主题(群系)上的区别。
正因为主题的区别(即有不同的工厂实现类),才有了产品等级的区分
而正因为有了抽象工厂模式,才可以规范化地增加产品族
这便是整个工厂模式的基本原理
3.单例设计模式
写完了生态生成算法,小唐要给玩家写一个行为控制器类,这个行为控制器类在全局一定只有一个,不然就可能出现同时开启手柄、键鼠的情况下,相互干扰控制的情况。此外,行为控制器类中的属性首先一定是private的,要通过对象访问,不然玩家就能通过自由修改来达成某些不符合游戏设计初衷的目的了。
于是这就能说明单例设计模式的重要性了,单例模式确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。
懒汉式单例模式
最简单的单例模式,类中设置一个静态对象,类型为本身,将构造方法私有化并提供一个公共的静态方法供外部生成对象。
但外部调用静态方法时,如果没有实例化对象则新建对象,否则就把对象返回出去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class PlayerControl { private static PlayerControl controlInstance; private PlayerControl (){} public static PlayerControl getControlInstance(){ if (controlInstance == null) { controlInstance = new PlayerControl(); } return controlInstance; } } public class Test { public static void main(String[] args){ PlayerControl playerControl = PlayerControl.getControlInstance(); System.out.println(playerControl.hashCode()); playerControl = PlayerControl.getControlInstance(); System.out.println(playerControl.hashCode()); } }
|
经过测试发现两次执行PlayerControl.getControlInstance()后对象的hash码都是一样的,确实只产生了一个对象
小唐使用同样的方法来实现了游戏日志文件的记录,但他发现了一个严重的问题!
“之前写的控制器线程A申请拿到日志类的实例时,游戏内信息记录线程B也同时在申请拿到实例。当线程A判断loggerInstance == null
后并且实例化完成之前,由于loggerInstance此时确实是null,所以线程B也成功判到了loggerInstance == null并申请创建实例。
于是出现了两个实例,并没有实现所需要的单例设计,不具备线程安全性。”
不过解决方法也很简单,加上synchronized关键字即可实现线程安全
1
| public static synchronized Logger getLoggerInstance()
|
饿汉式单例模式
这种单例模式在类装载时即实现了实例化,但是由于不是懒加载,所以有很大可能创建了从始至终都没人用的实例,浪费了内存空间。但同时它又绝对是线程安全的。在不考虑懒加载的情况建议使用
1 2 3 4 5 6 7
| class PlayerControl { private static PlayerControl controlInstance = new PlayerControl(); private PlayerControl (){} public static PlayerControl getControlInstance() { return controlInstance; } }
|
登记式
虽然懒汉模式加上synchronized关键字即可实现线程安全,但对getInstance
调用频繁的应用环境下可能会导致效率的低下
但还有别的办法达到同样的目的
1 2 3 4 5 6 7 8 9
| class PlayerControl { private static class PlayerControlHolder { private static final PlayerControl controlInstance = new PlayerControl(); } private PlayerControl (){} public static PlayerControl getControlInstance() { return PlayerControlHolder.controlInstance; } }
|
此时就算PlayerControl类被装载了,但是其内部类PlayerControlHolder并没有被显式装载,也就并没有一个PlayerControl实例诞生。直到调用getControlInstance()时PlayerControlHolder才被显式装载,完成实例化
这个方法非常适合懒加载的情况,并且与饿汉模式一致,利用了 classloader 机制来保证初始化 instance 时只有一个线程,具有线程安全性。不足就是只能在静态域使用
枚举
这应该是最简洁的一种模式,JVM 保证每个枚举类型只会加载一次,并且是在第一次使用时被加载,所以保证了线程安全,不过无法实现懒加载
1 2 3 4 5 6 7 8
| enum PlayerControl { controlInstance; } public class Test { public static void main(String[] args){ PlayerControl playerControl = PlayerControl.controlInstance; } }
|
4.建造者模式
小唐觉得是时候有些穿着装备的实体了,一个正常的实体应该可以配置头盔,胸甲,裤子,靴子,武器,副手等栏位。于是他着手给实体添加这些属性并建立了构造方法。
但是写着写着小唐便发现了问题:
1.每次新建一个实体都需要向构造方法传参,如
1
| new Zombie("iron_helmet","iron_chestplate","iron_leggings","iron_boots","iron_sword","cooked_beef");
|
从工厂模式的原理来看,工厂模式区分的是类层面上的不同,而不是区分同一个类属性上的不同,这种实例化没法用工厂模式实现
2.不同的栏位配置会导致new语句与构造方法内过于冗长,并且如果想专门生成全铁甲的Zombie还需要多次书写new语句,没有复用性
3.也可以在Zombie类中预定了很多固定的栏位搭配,只要调用响应的get就能获取相应搭配的对象。这确实提高了复用性,但Zombie类的简单结构被严重破坏,并且每次想新加入搭配时都需要修改Zombie类的代码,严重违反了开闭原则。
于是一种能遵守开闭原则,并提高搭配复用性的,且能简单配置属性的创建模式是被需要的
这便是建造者模式
下面是一个标准的建造者模式的代码结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| interface Entity{} abstract class EntityAb implements Entity{ protected String helmet; protected String chestPlates; protected String leggings; protected String boots; protected String weapon; protected String second; } class Zombie extends EntityAb { public Zombie(){}; } interface EntityBuilder { EntityBuilder setHelmet(); EntityBuilder setChestPlates(); EntityBuilder setLeggings(); EntityBuilder setBoots(); EntityBuilder setWeapon(); EntityBuilder setSecond(); Entity build(); }
class IronZombieBuilder implements EntityBuilder{ protected Zombie zombie = new Zombie(); @Override public EntityBuilder setHelmet() { zombie.setHelmet("iron_helmet"); return this; } @Override public EntityBuilder setChestPlates() { zombie.setChestPlates("iron_chestPlates"); return this; } @Override public EntityBuilder setLeggings() { zombie.setLeggings("iron_leggings"); return this; } @Override public EntityBuilder setBoots() { zombie.setBoots("iron_boots"); return this; } @Override public EntityBuilder setWeapon() { zombie.setWeapon("iron_sword"); return this; } @Override public EntityBuilder setSecond() { zombie.setSecond("cooked_beef"); return this; } @Override public Entity build() { System.out.println("特定Zombie已经生成"); return zombie; } } class DiamondZombieBuilder implements EntityBuilder{ } class Director { public Director() {}
public Entity constructIronZombie() { return (new IronZombieBuilder().setHelmet().setBoots().setChestPlates().setLeggings().setWeapon().setSecond().build(); } public Entity constructDiamondZombie() { return (new DiamondZombieBuilder().setHelmet().setBoots().setChestPlates().setLeggings().setWeapon().setSecond().build(); } } public class Test { public static void main(String[] args){ Zombie zombie = (Zombie) new Director().constructIronZombie(); System.out.println(zombie); } }
|
建造者模式首先需要一个抽象建造者,如接口EntityBuilder
抽象建造者可以有许多具体实现建造者,如ConcreteBuilder1,ConcreteBuilder2,用于提供一种特定搭配。于是当你想增加一种新的固定搭配时只需要添加新的具体建造者即可,符合开闭原则并具有复用性
最后还需要一个指挥者,用建造者提供的搭配与build方法进行组装,用户只需要简单调用指挥者提供的construct方法,即可完成相应搭配的实体的生成。
与工厂模式合并
具体实现类有一处
1
| protected Zombie zombie = new Zombie();
|
此处的new Zombie()
在很多情况下并不是只有一个new操作,正如前面所说,还有加载贴图、模型等操作
所以可以将此处用工厂模式获取,如
1
| protected Zombie zombie = minecraftForestEntityFactory.summonEntity("minecraft:zombie");
|
5.原型模式
小唐想在游戏里给Rabbit加上繁殖的效果,如Rabbit繁殖后,会产生1到3只Rabbit,它们与父亲或者母亲完全相同,新的Rabbit也会继续繁殖,直到达到Rabbit群体的数量上限。
我们可以发现此时Rabbit的实例话是一个频繁的过程,并且每个Rabbit的对象都完全来自父亲或母亲。为了提高性能并达到上述的效果,我们可以提供一种模式,使得新的对象以某一对象为原型创建。
为方便演示模型结构,以下代码先脱离其他模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| interface Entity{}; abstract class EntityClone implements Cloneable { public Object clone() { Entity clone = null; try { clone = (Entity) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return clone; } } class Rabbit extends EntityClone implements Entity{ private String color; private String tail_len;
public Rabbit(String color, String tail_len) { this.color = color; this.tail_len = tail_len; } } public class Test { public static void main(String[] args) { Rabbit mom = new Rabbit("white", "long"); Rabbit dad = new Rabbit("black", "short"); Rabbit kid1 = (Rabbit) mom.clone(); Rabbit kid2 = (Rabbit) dad.clone(); } }
|
首先我们要有一个实现java的Cloneable接口的抽象类EntityClone,使其可以使用clone方法来返回其本身。
再用具体的实体类如Rabbit,Cat等继承EntityClone,使它们也具有clone方法,于是用户就可以通过原型对象调用clone来创建一样属性的对象了