系列文章目录
一、前言
创建者模式提供创建对象的机制, 能够提升已有代码的灵活性和可复用性。
序号 | 类型 | 图稿 | 业务场景 | 实现要点 |
---|---|---|---|---|
1 | 工厂方法 | 多种类型商品不同接口,统一发奖服务搭建场景 | 定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。 | |
2 | 抽象工厂 | 替换Redis双集群升级,代理类抽象场景 | 提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。 | |
3 | 建造者 | 各项装修物料组合套餐选配场景 | 将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。 | |
4 | 原型 | 上机考试多套试,每人题目和答案乱序排列场景 | 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。 | |
5 | 单例 | 7种单例模式案例,Effective Java作者推荐枚举单例模式 | 保证一个类仅有一个实例,并提供一个访问它的全局访问点。 |
二、工厂方法模式
工厂模式又称工厂方法模式,是⼀种创建型设计模式,其在父类中提供⼀个创建对象的方法, 允许子类决定实例化对象的类型。
2.1 场景模拟代码实战
这里我们模拟积分兑换中的发放多种类型商品,假如现在我们有三种类型的商品接口分别为兑换卡、实物商品、优惠券。
2.1.1 不使用设计模式实现
传统写法我们一般会用一个字段如 type 来区分三种类型的奖品,然后用 if else 或者是switch case 的写法来进行判断,这样带来的问题就是,之后如果新增加一种类型的奖品我们就需要增加一层 if else 的判断或是 case 判断,不利于之后的扩展与维护
if (type == 1) {
// 兑换卡
} else if (type == 2) {
// 实物商品
} else if (type ==3) {
// 优惠券
}
switch (type) {
case 1:
// 兑换卡
break;
case 2:
// 实物商品
break;
case 3:
// 优惠券
break;
default:
// 没有匹配到对应的类型
break;
}
2.1.2 工厂写法
1. 定义一个发放奖品的统一接口
因为每一种奖品发放的传参也不同,所以我们把不同的部分用一个map来接收,这样一来不管是发放什么类型的奖品我们都是只调用这个接口就行,不关心它背后的逻辑
public interface PrizeService {
void sendPrize();
}
2. 每一个类型的奖品都去实现这个接口,完成各自奖品发放的逻辑
/**
* 兑换卡
*/
public class CardServiceImpl implements PrizeService {
public void sendPrize(){
// 兑换卡具体逻辑
}
}
/**
* 实物奖品
*/
public class GoodsServiceImpl implements PrizeService {
public void sendPrize(){
// 实物奖品具体逻辑
}
}
/**
* 优惠券
*/
public class CouponServiceImpl implements PrizeService {
public void sendPrize(){
// 优惠券具体逻辑
}
}
3. 编写奖品工厂
这里你可能会想,这样写后续如果要扩展的话不还是要增加一个 if 判断吗?但是这样写相对来说代码要更加干净整洁很多。如果你觉得这样还不够好可以参考 Java 工厂模式与反射结合,使你的代码更加优雅
public class PrizeFactory {
public static PrizeService getPrizeService(Integer type) {
if (null == type) {
return null;
}
if (1 == type) {
return new CardServiceImpl ();
}
if (2 == type) {
return new GoodsServiceImpl ();
}
if (3 == type) {
return new CouponServiceImpl ();
}
// 不存在的奖品类型
}
}
4. 测试
@Test
public void test() {
// 1. 兑换卡
PrizeService cardService = PrizeFactory.getPrizeService(1);
cardService.sendPrize();
// 2. 实物商品
PrizeService goodsService = PrizeFactory.getPrizeService(2);
goodsService.sendPrize();
// 3. 优惠券
PrizeService couponService = PrizeFactory.getPrizeService(2);
couponService.sendPrize();
}
2.2 总结
可以看出工厂方法模式避免创建者与具体的产品逻辑耦合 、 满足单⼀职责,每⼀个业务逻辑实现都在所属自己的类中完成 、 满足开闭原则,无需更改使用调用方就可以在程序中引入新的产品类型
三、抽象工厂模式
抽象工厂与工厂类似,不过抽象工厂是一个工厂中心,用于创建其他的工厂
3.1场景模拟代码实战
抽象工厂的场景模拟,可以参考下图。可以看到有两个工厂一个中国工厂一个外国工厂,两个工厂都可以生产苹果也可以生产手机。但是生产的苹果种类和手机种类都是不同的,抽象工厂的作用就是把其中同类的产品放到一个工厂中,也就是图中红色框圈出的部分。
抽象工厂就是为了兼容两个工厂,对外统一提供一个方法叫做 apple(),表示生产苹果
3.1.1 抽象工厂写法
1. 定义苹果工厂统一接口,根据type区分中国工厂和外国工厂
public interface AppleService {
public void apple(Integer type);
}
2. 创建中国苹果工厂实现类
public class ChinaApple implements AppleService{
public void apple(Integer type) {
System.out.println("china_apple");
}
}
3. 创建外国苹果工厂实现类
public class ForeignApple implements AppleService{
public void apple(Integer type) {
System.out.println("foreign_apple");
}
}
4. 创建抽象工厂
这里抽象工厂中我也写了手机工厂,但是具体关于手机的代码没有写,只写了苹果的部分。写出手机工厂是为了方便大家理解抽象工厂是多个工厂的整合
public abstract class AbstractFactory {
public abstract AppleService createApple(String type);
public abstract PhoneService createPhone(String type);
}
5. 创建苹果抽象工厂继承抽象工厂
public class AppleFactory extends AbstractFactory{
@Override public AppleService createApple(String type) {
if (type == null) {
return null;
}
if (type.equals("china")) {
return new ChinaApple();
}else if (type.equals("foreign")) {
return new ForeignApple();
}
return null;
}
@Override public PhoneService createPhone(String type) {
return null;
}
}
6. 创建一个工厂生成器
public class FactoryProducer {
public static AbstractFactory getFactory(String choice){
if(choice.equalsIgnoreCase("china")){
return new AppleFactory();
} else if(choice.equalsIgnoreCase("foreign")){
return new PhoneFactory();
}
return null;
}
}
7. 测试
public class ApiTest {
@Test
public void test() {
AbstractFactory china = FactoryProducer.getFactory("china");
AppleService appleService = china.createApple("china");
appleService.apple();
}
}
3.2 总结
抽象工厂模式满足了单一职责、开闭原则、解耦等优点,但随着业务的不断扩展可能会造成类实现上的复杂度。但也可以借助代理类以及自动生成加载的方式降低此项缺点
四、建造者模式
建造者模式可以想象成工厂的流水线,将一个个简单的对象,组合成一个复杂的对象。
4.1 场景模拟代码实战
结合生活中例子,我们想一下肯德基。在肯德基中有汉堡、薯条、可乐等食品,对应的就是建造者模式中的简单对象,汉堡+薯条可以组成一个A套餐、汉堡+可乐可以组成B套餐等等可以形成多种不同的组合,但是不同的套餐里其实都是这些简单对象组成的。这些套餐就是建造者模式中的复杂对象。
4.1.1 不使用设计模式实现
1. 创建食品统一接口
汉堡、薯条、饮料都实现此接口
public interface Food {
/**
* 名称
* @return
*/
String getName();
/**
* 价格
* @return
*/
float getPrice();
}
2. 创建每种食物对应的抽象父类
汉堡的抽象父类
public abstract class Hamburg implements Food {
public abstract String getName();
public abstract float getPrice();
}
薯条的抽象父类
public abstract class Chips implements Food {
public abstract String getName();
public abstract float getPrice();
}
可乐的抽象父类
public abstract class Cola implements Food {
public abstract String getName();
public abstract float getPrice();
}
3. 创建每个父类的具体子类
汉堡包括鸡肉汉堡和牛肉汉堡
public class ChickenHamburg extends Hamburg{
@Override public String getName() {
return "鸡肉汉堡";
}
@Override public float getPrice() {
return 15;
}
}
public class BeefHamburg extends Hamburg{
@Override public String getName() {
return "牛肉汉堡";
}
@Override public float getPrice() {
return 18;
}
}
薯条包括小份薯条和大份薯条
public class SmallChips extends Chips{
@Override public String getName() {
return "小份薯条";
}
@Override public float getPrice() {
return 8;
}
}
public class LargeChips extends Chips{
@Override public String getName() {
return "大份薯条";
}
@Override public float getPrice() {
return 12;
}
}
可乐包括小杯可乐和中杯可乐
public class SmallCola extends Cola{
@Override public String getName() {
return "小杯可乐";
}
@Override public float getPrice() {
return 6;
}
}
public class MiddleCola extends Cola{
@Override public String getName() {
return "中杯可乐";
}
@Override public float getPrice() {
return 8;
}
}
4. 组合套餐
这里具体的我就不写了,就是根据一个type字段来判断套餐的类型。这样的话如果有很多的套餐,我们这里的代码就会很长。
private List<Food> foodList = new ArrayList<Food>();
public void getMeal(String type){
if (type.equals("A")) {
// 套餐A(鸡肉汉堡 + 小杯可乐 + 小份薯条 )
foodList.add(new ChickenHamburg());
foodList.add(new SmallCola());
foodList.add(new SmallChips());
} else if (type.equals("B")) {
}
}
4.1.2 建造者模式实现
1. 定义组合套餐的接口及实现类
public interface Meal {
void addFood(Food food);
float getPrice();
void showFoods();
}
public class MealImpl implements Meal{
private List<Food> foodList = new ArrayList<Food>();
public void addFood(Food food) {
foodList.add(food);
}
public float getPrice() {
float price = 0.0f;
for (Food food : foodList) {
price += food.getPrice();
}
return price;
}
public void showFoods() {
for (Food food : foodList) {
System.out.print("食品 : "+ food.getName());
System.out.println(", 价格 : "+ food.getPrice());
}
}
}
2. 定义建造者类
这里我写了三个套餐A、B、C举例
public class MealBuilder {
/**
* 套餐A(鸡肉汉堡 + 小杯可乐 + 小份薯条 )
* @return
*/
public Meal mealA(){
MealImpl mealA = new MealImpl();
mealA.addFood(new ChickenHamburg());
mealA.addFood(new SmallCola());
mealA.addFood(new SmallChips());
return mealA;
}
/**
* 套餐B(Niue汉堡 + 中杯可乐 + 大份薯条 )
* @return
*/
public Meal mealB(){
MealImpl mealB = new MealImpl();
mealB.addFood(new BeefHamburg());
mealB.addFood(new MiddleCola());
mealB.addFood(new LargeChips());
return mealB;
}
/**
* 套餐C(鸡肉汉堡 + 中杯可乐 + 小份薯条 )
* @return
*/
public Meal mealC(){
MealImpl mealC = new MealImpl();
mealC.addFood(new ChickenHamburg());
mealC.addFood(new MiddleCola());
mealC.addFood(new SmallChips());
return mealC;
}
}
6. 测试
@Test
public void test(){
MealBuilder builder = new MealBuilder();
Meal meal = builder.mealA();
System.out.println("--------------套餐A-------------------");
meal.showFoods();
System.out.println("套餐价格:" + meal.getPrice());
}
4.2 总结
当一些基本元素不变但组合经常改变时我们可以使用建造者模式,该模式满足单一职责原则、以及可复用技术、建造者独立、易扩展、便于控制细节风险。如果基本元素很多组合很多,类的不断扩展也会难以维护,这个时候就可以借助数据库,把元素放到数据库中来减少大量重复的代码
五、原型模式
原型模式主要解决的问题是创建重复对象,因为对象本身的创建过程比较复杂(比如需要从数据库获取大量的数据),付出的代价比较大。因此原型模式就是在一个对象在第一次被创建后,将他缓存起来,下一次请求时返回它的克隆。
5.1 场景模拟代码实战
这里我们模拟一个生成考试试卷的情景,每一份试卷的题目都是一样的,但是题目的顺序以及选项的顺序是不一样的。这样就能保证考试的公平性,作弊也更难了。
5.1.1 不使用设计模式实现
1. 创建选择题类
public class Question {
// 题目
private String title;
// 选项
private Map<String, String> option;
// 正确答案
private String rightAnswer;
public Question() {
}
public Question(String title, Map<String, String> option, String rightAnswer) {
this.title = title;
this.option = option;
this.rightAnswer = rightAnswer;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Map<String, String> getOption() {
return option;
}
public void setOption(Map<String, String> option) {
this.option = option;
}
public String getRightAnswer() {
return rightAnswer;
}
public void setRightAnswer(String rightAnswer) {
this.rightAnswer = rightAnswer;
}
}
2. 构建试卷测试
可以看到试卷已经生成了,但是这样一来我们如果想要再这个的基础上增加一些功能就没有那么灵活了,比如我们还要增加题目的乱序和答案的乱序。
@Test
public void createTest(){
ArrayList<Question> questionList = new ArrayList<Question>();
Map<String, String> map01 = new HashMap<String, String>();
map01.put("A", "10");
map01.put("B", "12");
map01.put("C", "14");
map01.put("D", "16");
Map<String, String> map02 = new HashMap<String, String>();
map02.put("A", "9");
map02.put("B", "15");
map02.put("C", "21");
map02.put("D", "35");
Map<String, String> map03 = new HashMap<String, String>();
map03.put("A", "-1");
map03.put("B", "-2");
map03.put("C", "-3");
map03.put("D", "-4");
questionList.add(new Question("5 + 5 = ?", map01, "A"));
questionList.add(new Question("7 * 5 = ?", map02, "D"));
questionList.add(new Question("2 - 4 = ?", map03, "B"));
for (int i = 0; i < questionList.size(); i++){
Question question = questionList.get(i);
System.out.println("第" + (i + 1) + "题: " + question.getTitle());
Map<String, String> option = question.getOption();
// 遍历map(lambda表达式写法)
option.forEach((key, value) ->{
System.out.println(key + ":" + value);
});
System.out.println("正确答案:" + question.getRightAnswer());
System.out.println("----------------------------");
}
}
5.1.2 原型模式实现
1. 增加克隆对象处理类
注意:这里不能用 List 要用 ArrayList 因为 List 是没有 clone() 方法的
public class QuestionBank implements Cloneable {
private ArrayList<Question> questionList = new ArrayList<Question>();
public void addQuestion(Question question){
questionList.add(question);
}
@Override public Object clone() throws CloneNotSupportedException {
QuestionBank questionBank = (QuestionBank) super.clone();
questionBank.questionList = (ArrayList<Question>) questionList.clone();
// 题目乱序
Collections.shuffle(questionBank.questionList);
for (Question question : questionBank.questionList) {
// 答案乱序
random(question);
}
return questionBank.questionList;
}
public void random(Question question) {
Map<String, String> option = question.getOption();
String rightAnswer = question.getRightAnswer();
// 获取key的集合也就是 A、B、C、D
Set<String> keySet = option.keySet();
// 将set中的内容放入list
ArrayList<String> keyList = new ArrayList<String>(keySet);
// 使用工具类中的乱序
Collections.shuffle(keyList);
//创建一个新的选项集合
HashMap<String, String> optionNew = new HashMap<String, String>();
int index = 0;
String rightAnswerNew = "";
for (String key : keySet) {
// 按顺序获取乱序集合中的选项
String randomKey = keyList.get(index);
if (rightAnswer.equals(key)) {
rightAnswerNew = randomKey;
}
optionNew.put(randomKey, option.get(key));
index ++;
}
question.setOption(optionNew);
question.setRightAnswer(rightAnswerNew);
}
}
2. 测试
@Test
public void createTest() throws CloneNotSupportedException {
Map<String, String> map01 = new HashMap<String, String>();
map01.put("A", "10");
map01.put("B", "12");
map01.put("C", "14");
map01.put("D", "16");
Map<String, String> map02 = new HashMap<String, String>();
map02.put("A", "9");
map02.put("B", "15");
map02.put("C", "21");
map02.put("D", "35");
Map<String, String> map03 = new HashMap<String, String>();
map03.put("A", "-1");
map03.put("B", "-2");
map03.put("C", "-3");
map03.put("D", "-4");
QuestionBank questionBank = new QuestionBank();
questionBank.addQuestion(new Question("5 + 5 = ?", map01, "A"));
questionBank.addQuestion(new Question("7 * 5 = ?", map02, "D"));
questionBank.addQuestion(new Question("2 - 4 = ?", map03, "B"));
ArrayList<Question> questionList = (ArrayList<Question>) questionBank.clone();
for (int i = 0; i < questionList.size(); i++){
Question question = questionList.get(i);
System.out.println("第" + (i + 1) + "题: " + question.getTitle());
Map<String, String> option = question.getOption();
// 遍历map(lambda表达式写法)
option.forEach((key, value) ->{
System.out.println(key + ":" + value);
});
System.out.println("正确答案:" + question.getRightAnswer());
System.out.println("----------------------------");
}
}
对比上一次的输出结果我们可以发现,题目的顺序以及答案的顺序均已发生变化。
通过断点调试我们也可以看到克隆的过程,又生成了一个一模一样的questionList
5.2 总结
原型模式便于通过克隆⽅式创建复杂对象、也可以避免重复做初始化操作、不需要与类中所属的其他类耦合等
六、单例模式
单例模式涉及到一个单一的类,该类负责创建自己的对象,同时确保仅有一个实例哪怕多线程同时访问。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。单例模式主要解决的是,⼀个全局使用的类频繁的创建和销毁,从而提升提升整体的代码的性能。
6.1 懒汉模式(线程不安全)
单例模式要求是不允许外部直接创建,所以构造器是 private,但是这种写法没有加锁,多线程访问会有问题,所以并没有达到单例模式的要求
public class Singleton_01 {
private static Singleton_01 instance;
private Singleton_01() {
}
public static Singleton_01 getInstance(){
if (null != instance) {
return instance;
}
return new Singleton_01();
}
}
6.2 懒汉模式(线程安全)
此种模式虽然是安全的,但由于把锁加到方法上后,所有的访问都因需要锁占⽤导致资源的浪费。如果不是特殊情况下,不建议此种方式实现单例模式
public class Singleton_02 {
private static Singleton_02 instance;
private Singleton_02() {
}
public static synchronized Singleton_02 getInstance(){
if (null != instance) {
return instance;
}
return new Singleton_02();
}
}
6.3 饿汉模式(线程安全)
在程序启动的时候直接运行加载,和我们在类中直接定义一个静态变量类似。但是类加载时就初始化,浪费内存。
public class Singleton_03 {
private static Singleton_03 instance = new Singleton_03();
private Singleton_03() {
}
public static Singleton_03 getInstance() {
return instance;
}
}
6.4 使用类的内部类(线程安全)
使用类的静态内部类实现的单例模式,既保证了线程安全有保证了懒加载,同时不会因为加锁的方式耗费性能。这主要是因为JVM虚拟机可以保证多线程并发访问的正确性,也就是⼀个类的构造方法在多线程环境下可以被正确的加载。
public class Singleton_04 {
private static class SingletonHolder {
private static Singleton_04 instance = new Singleton_04();
}
private Singleton_04() {
}
public static Singleton_04 getInstance() {
return SingletonHolder.instance;
}
}
6.5 双重锁校验(线程安全)
双重锁的方式是方法级锁的优化,减少了部分获取实例的耗时。
同时这种方式也满足了懒加载。
public class Singleton_05 {
private static Singleton_05 instance;
private Singleton_05() {
}
public static Singleton_05 getInstance(){
if(null != instance) {
return instance;
}
synchronized (Singleton_05.class){
if (null == instance){
instance = new Singleton_05();
}
}
return instance;
}
}
6.6 CAS「AtomicReference」(线程安全)
使⽤CAS的好处就是不需要使⽤传统的加锁方式保证线程安全,⽽是依赖于CAS的忙等算法,依赖于底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞也就没有了额外的开销,并且可以支持较大的并发性。当然CAS也有⼀个缺点就是忙等,如果⼀直没有获取到将会处于死循环中。
public class Singleton_06 {
private static final AtomicReference<Singleton_06> INSTANCE = new AtomicReference<Singleton_06>();
private static Singleton_06 instance;
private Singleton_06() {
}
public static final Singleton_06 getInstance() {
for (; ; ) {
Singleton_06 instance = INSTANCE.get();
if (null != instance) {
return instance;
}
INSTANCE.compareAndSet(null, new Singleton_06());
return INSTANCE.get();
}
}
}
6.7 枚举(线程安全)
Effective Java 作者推荐使用枚举的方式解决单例模式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化
public enum Singleton_07 {
INSTANCE;
public void test(){
System.out.println("hi~");
}
}
七、参考资料
设计模式|菜鸟教程
《重学 Java 设计模式》 作者 小傅哥