💪🏻 制定明确可量化的目标,坚持默默的做事。
原型模式是一种设计模式,属于创建型模式的一种,它用于创建重复的对象,同时又能保持性能。在原型模式中,通过复制现有对象的原型来创建新对象,而不是通过实例化类来创建对象。这样做可以避免耗费过多的资源开销,特别是在对象的创建过程比较复杂或耗时的情况下。
在原型模式中,原型对象实现一个克隆方法(Clone)用于复制自身,当需要创建新对象时,就可以通过克隆原型对象来得到一个新的对象副本。原型模式通常包括浅拷贝和深拷贝两种形式,浅拷贝只复制对象本身,而深拷贝则会连同对象引用的其他对象一起复制,因此能够得到完全独立的新对象。
原型模式可以在需要大量创建相似对象的场景中发挥作用,它能够提高对象的创建效率,同时也能够减少对类的直接依赖,使系统结构更灵活。
关键角色有两个:
- 原型(Prototype):定义用于复制现有对象的接口,通常包含一个克隆方法,用于返回一个克隆对象。
- 具体原型(Concrete Prototype):实现原型接口,实现克隆方法来复制自身。
优点:
- 减少对象创建时间:原型模式通过复制现有对象来创建新对象,避免了昂贵的对象创建过程,特别是在需要频繁创建相似对象时,可以大大减少对象创建的时间和开销。
- 简化对象创建过程:原型模式封装了对象的创建过程,客户端无需关心具体的创建细节,使得对象创建变得更加简单,提高了系统的可维护性和扩展性。
- 动态增加或减少原型:原型模式允许动态地添加或删除原型,使得系统更加灵活,能够根据需求动态创建新的原型对象。
缺点:
- 需要理解原型对象:在使用原型模式时,需要确保原型对象的克隆方法能够正确地复制对象的状态,有时需要深度复制而不是浅复制,这需要额外的处理和理解。
- 难以保持一致性:原型模式可能会造成系统中的一些对象克隆出来之后状态难以保持一致,特别是涉及到对象之间的引用关系时,需要特别小心处理。
本质体现了两个关键点:
- 原型:具备复制能力的对象,它是创建新对象的模板。原型可以是一个接口、抽象类或具体类,关键是它需要提供克隆自身的方法。
- 克隆:根据原型对象复制出来的新对象。克隆过程可以是浅复制(只复制对象本身)或者深复制(复制对象和其引用的对象)。
原型模式的本质是通过复制现有对象来创建新对象,从而封装了对象的创建过程,提供了一种灵活、高效的对象创建方式。
一、案例
场景:前段时间有一个考试,发现在一个教室里考试的试卷有ABCDEFG卷,座位上前后左右人的卷子跟我的都不一样。考完后听老师说,ABCDEFG卷的题目是一样的,意思就是题目是一样的,题目的顺序是打乱的甚至同一个题的选项的顺序也是打乱的。
下面我们从无到有来感受原型模式的设计艺术。
1.1 不用模式来实现
选择题类:
@Data public class ChoiceQuestion { /** * 选择题题目 */ private String title; /** * 选项 */ private Map<String, String> options; /** * 答案 */ private String key; public ChoiceQuestion(){} public ChoiceQuestion(String title, Map<String, String> options, String key) { this.title = title; this.options = options; this.key = key; } }
填空题类:
@Data public class Completion { /** * 填空题题目 */ private String title; /** * 答案 */ private List<String> key; public Completion() {} public Completion(String title, List<String> key) { this.title = title; this.key = key; } }
试卷类:
public class TestPaper { /** * 创建试卷 * @param name 考生姓名 * @param code 考生编号 * @return */ public String createPaper(String name, String code) { // 这里举例有两道选择题和两道填空题 List<ChoiceQuestion> choiceQuestionList = new ArrayList<>(2); List<Completion> completionList = new ArrayList<>(2); // 初始化选择题 Map<String, String> choiceMap1 = new HashMap<>(); choiceMap1.put("A", "1"); choiceMap1.put("B", "2"); choiceMap1.put("C", "3"); choiceMap1.put("D", "4"); Map<String, String> choiceMap2 = new HashMap<>(); choiceMap2.put("A", "1"); choiceMap2.put("B", "2"); choiceMap2.put("C", "3"); choiceMap2.put("D", "4"); choiceQuestionList.add(new ChoiceQuestion("1 + 1 = ", choiceMap1, "B")); choiceQuestionList.add(new ChoiceQuestion("3 + 1 = ", choiceMap2, "D")); // 初始化填空题 List<String> comList1 = new ArrayList<>(); comList1.add("富强"); comList1.add("民主"); List<String> comList2 = new ArrayList<>(); comList2.add("51"); comList2.add("21"); completionList.add(new Completion("任意列举出两个社会主义核心价值观", comList1)); completionList.add(new Completion("2008年中国获得金牌数___枚,银牌___枚", comList2)); String newLine = "\n"; // 组装试卷 StringBuffer strBuff = new StringBuffer(); strBuff.append("考生:").append(name).append(newLine) .append("考号:").append(code).append(newLine) .append("一、选择题").append(newLine); for (ChoiceQuestion choiceQuestion : choiceQuestionList) { strBuff.append(choiceQuestion.getTitle()).append(newLine); for (Map.Entry<String, String> entry : choiceQuestion.getOptions().entrySet()) { String key = entry.getKey(); String value = entry.getValue(); strBuff.append(key).append(":").append(value).append(newLine); } strBuff.append("答案:").append(choiceQuestion.getKey()).append(newLine); } for (Completion completion : completionList) { strBuff.append(completion.getTitle()).append(newLine); strBuff.append("答案:"); for (String anster : completion.getKey()) { strBuff.append(" ").append(anster); } } return strBuff.toString(); } }
测试客户端:
public class Client { public static void main(String[] args) { TestPaper testPaper = new TestPaper(); System.out.println(testPaper.createPaper("张三", "ACCDF0001")); System.out.println(testPaper.createPaper("李四", "ACCDF0002")); System.out.println(testPaper.createPaper("王五", "ACCDF0003")); } }
运行结果如下:
考生:张三 考号:ACCDF0001 一、选择题 1 + 1 = A:1 B:2 C:3 D:4 答案:B 3 + 1 = A:1 B:2 C:3 D:4 答案:D 任意列举出两个社会主义核心价值观 答案: 富强 民主 2008年中国获得金牌数___枚,银牌___枚 答案: 51 21 考生:李四 考号:ACCDF0002 一、选择题 1 + 1 = A:1 B:2 C:3 D:4 答案:B 3 + 1 = A:1 B:2 C:3 D:4 答案:D 任意列举出两个社会主义核心价值观 答案: 富强 民主 2008年中国获得金牌数___枚,银牌___枚 答案: 51 21 考生:王五 考号:ACCDF0003 一、选择题 1 + 1 = A:1 B:2 C:3 D:4 答案:B 3 + 1 = A:1 B:2 C:3 D:4 答案:D 任意列举出两个社会主义核心价值观 答案: 富强 民主 2008年中国获得金牌数___枚,银牌___枚 答案: 51 21
一坨坨代码很快就实现了,看上去很简单。
1.2 有何问题
- 以上呢就是三位考试的试卷; 线三 、李四和王五 ,每个⼈人的试卷内容是⼀一样的这没问题,但是三个人的题⽬以及选项顺序和答案都是一样的,但是没有达到我们的场景中“ABCDEFG卷题目顺序和选项顺序都不一样”的目的。
- 以上代码⾮非常难扩展,随着题⽬目的不不断的增加以及乱序功能的补充,都会让这段代码变 得越来越混乱。
1.3 原型模式重构代码
原型模式解决的痛点:创建⼤量重复的类,⽽我们的场景就需要给不不同的用户都创建相同的试卷,但这些试卷的题目不便于每次都从库中获取,甚至有时候需要从远程的RPC中获取。这样都是⾮常耗时的,⽽且随着创建对象的增多将严重影响效率。
用原型模式注意要点:
- 原型对象的接口:在原型模式中,原型对象需要实现一个克隆方法(Clone),该方法用于复制自身并返回一个新的对象副本。这需要确保原型对象具有适当的接口和克隆能力。
- 浅拷贝和深拷贝:在实现克隆方法时,需要考虑对象中是否存在引用类型的成员变量。浅拷贝只会复制对象本身,而不会复制对象包含的引用类型成员变量,这意味着原型对象和克隆对象会引用同一个引用类型的成员变量。深拷贝则会复制对象本身以及其引用类型的成员变量,使得原型对象和克隆对象完全独立。选择浅拷贝还是深拷贝取决于实际需求和设计考虑。
- 创建新对象的方式:在使用原型模式创建新对象时,可以通过调用原型对象的克隆方法,或者使用原型管理器(Prototype Manager)来获取新对象。原型管理器可以维护一组原型对象,并根据需要返回相应的克隆对象。
- 修改克隆对象:当获得克隆对象后,可以根据需要对克隆对象做进一步的修改。这使得每个克隆对象都可以根据需求进行个性化的定制。
代码改造
添加选项答案类:
@Data public class Option { /** * 选项 */ private Map<String, String> options; /** * 答案 */ private String key; public Option(Map<String, String> options, String key) { this.options = options; this.key = key; } }
添加选项乱序工具类:
public class RandomUtil { /** * 乱序Map元素,记录对应答案key * @param option 题⽬选项 * @param key 答案 * @return Option 乱序后 {A=c., B=d., C=a., D=b.} */ static public Option random(Map<String, String> option, String key) { Set<String> keySet = option.keySet(); ArrayList<String> keyList = new ArrayList<>(keySet); Collections.shuffle(keyList); HashMap<String, String> optionNew = new HashMap<>(); int idx = 0; String keyNew = ""; for (String next : keySet) { String randomKey = keyList.get(idx++); if (key.equals(next)) { keyNew = randomKey; } optionNew.put(randomKey, option.get(next)); } return new Option(optionNew, keyNew); } }
添加题库类:
@Data public class QuestionBank implements Cloneable { /** * 试卷序号 */ private String seq; /** * 考生姓名 */ private String name; /** * 考生考号 */ private String code; private ArrayList<ChoiceQuestion> choiceQuestionList = new ArrayList<>(2); private ArrayList<Completion> completionList = new ArrayList<>(2); /** * 实现中题目可能来自数据库中,这里示例便直接写死 */ public QuestionBank() { // 初始化选择题 Map<String, String> choiceMap1 = new HashMap<>(); choiceMap1.put("A", "1"); choiceMap1.put("B", "2"); choiceMap1.put("C", "3"); choiceMap1.put("D", "4"); Map<String, String> choiceMap2 = new HashMap<>(); choiceMap2.put("A", "1"); choiceMap2.put("B", "2"); choiceMap2.put("C", "3"); choiceMap2.put("D", "4"); this.choiceQuestionList.add(new ChoiceQuestion("1 + 1 = ", choiceMap1, "B")); this.choiceQuestionList.add(new ChoiceQuestion("3 + 1 = ", choiceMap2, "D")); // 初始化填空题 List<String> comList1 = new ArrayList<>(); comList1.add("富强"); comList1.add("民主"); List<String> comList2 = new ArrayList<>(); comList2.add("51"); comList2.add("21"); this.completionList.add(new Completion("任意列举出两个社会主义核心价值观", comList1)); this.completionList.add(new Completion("2008年中国获得金牌数___枚,银牌___枚", comList2)); } @Override public QuestionBank clone() { try { QuestionBank questionBank = (QuestionBank) super.clone(); questionBank.setChoiceQuestionList((ArrayList<ChoiceQuestion>) this.getChoiceQuestionList().clone()); questionBank.setCompletionList((ArrayList<Completion>) this.getCompletionList().clone()); // 打乱选择题 Collections.shuffle(questionBank.getChoiceQuestionList()); // 选择题的选项打乱 for (ChoiceQuestion choiceQuestion : questionBank.getChoiceQuestionList()) { Option option = RandomUtil.random(choiceQuestion.getOptions(), choiceQuestion.getKey()); choiceQuestion.setOptions(option.getOptions()); choiceQuestion.setKey(option.getKey()); } // 打乱填空题 Collections.shuffle(questionBank.getCompletionList()); return questionBank; } catch (Exception e) { throw new AssertionError(); } } @Override public String toString() { String newLine = "\r\n"; StringBuffer strBuff = new StringBuffer(); strBuff.append("考卷:").append(getSeq()).append(newLine) .append("考生:").append(getName()).append(newLine) .append("考号:").append(getCode()).append(newLine); strBuff.append("一、选择题").append(newLine); for (ChoiceQuestion choiceQuestion : getChoiceQuestionList()) { strBuff.append(choiceQuestion.getTitle()).append(newLine); for (Map.Entry<String, String> entry : choiceQuestion.getOptions().entrySet()) { String key = entry.getKey(); String value = entry.getValue(); strBuff.append(key).append(":").append(value).append(newLine); } strBuff.append("答案:").append(choiceQuestion.getKey()).append(newLine); } strBuff.append("一、填空题").append(newLine); for (Completion completion : getCompletionList()) { strBuff.append(completion.getTitle()).append(newLine); strBuff.append("答案:"); for (String anster : completion.getKey()) { strBuff.append(" ").append(anster); } } return strBuff.toString(); } }
试卷类改造:
@Data public class TestPaper { /** * 题库 */ QuestionBank questionBank = new QuestionBank(); /** * 创建试卷 * @param req 试卷序号 * @param name 考生姓名 * @param code 考生编号 * @return */ public String createPaper(String req, String name, String code) { QuestionBank questionBankClone = this.getQuestionBank().clone(); questionBankClone.setSeq(req); questionBankClone.setName(name); questionBankClone.setCode(code); return questionBankClone.toString(); } }
改造测试客户端:
public class Client { public static void main(String[] args) { // 创建试卷工具类并初始化题库 TestPaper testPaper = new TestPaper(); // 创建张三 System.out.println(testPaper.createPaper("A", "张三", "A0001")); System.out.println(testPaper.createPaper("B", "李四", "A0002")); System.out.println(testPaper.createPaper("C", "王五", "A0003")); } }
运行结果如下:
(考卷A)
考卷:A 考生:张三 考号:A0001 一、选择题 1 + 1 = A:3 B:1 C:2 D:4 答案:C 3 + 1 = A:3 B:1 C:4 D:2 答案:C 一、填空题 2008年中国获得金牌数___枚,银牌___枚 答案: 51 21 任意列举出两个社会主义核心价值观 答案: 富强 民主
(考卷B)
考卷:B 考生:李四 考号:A0002 一、选择题 3 + 1 = A:4 B:1 C:2 D:3 答案:A 1 + 1 = A:2 B:4 C:3 D:1 答案:A 一、填空题 2008年中国获得金牌数___枚,银牌___枚 答案: 51 21 任意列举出两个社会主义核心价值观 答案: 富强 民主
(考卷C)
考卷:C 考生:王五 考号:A0003 一、选择题 1 + 1 = A:1 B:4 C:3 D:2 答案:D 3 + 1 = A:4 B:3 C:1 D:2 答案:A 一、填空题 任意列举出两个社会主义核心价值观 答案: 富强 民主 2008年中国获得金牌数___枚,银牌___枚 答案: 51 21
1.4 完美实现
从上面结果看出,A卷、B卷和C卷的题目和选项都被打乱了。再添加DEF等卷,就完美实现了案例场景下我座位中前后左右人的卷子与我不同的情况。
三、模式讲解
本质:通过复制现有对象来创建新对象,从而封装了对象的创建过程,提供了一种灵活、高效的对象创建方式。
3.1 功能
原型模式 的功能 实际上包含两个方面:
- 一个是通过克隆来创建新的对象实例;
- 另一个是为克隆出来的对象实例复制原型实例属性的值。
3.2 原型模式的结构和说明
- Prototype:声明一个克隆自身的接口,用来约束想要克隆自己的类,要求它们都要实现这里定义的克隆方法。
- ConcretePrototype:实现Prototype接口的类,这些类真正实现了克隆自身的功能。
- Client:使用原型的客户端,首先获取到原型对象,然后通过原型实例克隆自身来创建新的对象实例。
3.3 几种工厂模式总结
工厂方法、抽象工厂、建造者模式和原型模式是常见的软件设计模式,它们各自解决了不同类型的设计问题。
- 工厂方法模式:
工厂方法模式是一种创建型设计模式,它提供了一种将对象的创建和使用分离的方式。核心在于定义一个创建对象的接口,但将具体的创建过程延迟到子类中进行实现。- 抽象工厂模式:
抽象工厂模式也是一种创建型设计模式,它提供了一种创建一系列相关或相互依赖对象的接口,而无需指定具体的类。核心在于提供一个能够创建一系列产品的工厂接口,而具体的工厂类负责实现这个接口并创建具体的产品。- 建造者模式:
建造者模式是一种创建型设计模式,它将一个复杂对象的构建过程与其表示相分离,并允许使用相同的构建过程来创建不同的表示。核心在于将对象的构建过程分解成多个步骤,并提供一个统一的接口来组合这些步骤,从而创建复杂对象。- 原型模式:
原型模式是一种创建型设计模式,它允许通过复制现有对象来创建新对象,而不是通过实现一个特定的工厂方法来创建对象。核心在于通过克隆已有的对象来创建新的对象,从而避免了通过构造函数创建对象时的复杂性。
区别:
- 工厂方法模式和抽象工厂模式都是用于对象创建,但工厂方法模式关注于单个对象的创建,而抽象工厂模式关注于一系列相关对象的创建。
- 建造者模式主要用于创建复杂对象,并且可以根据需要定制对象的不同表示,而工厂方法和抽象工厂模式更多地关注于对象的创建过程和对象的组合。
- 原型模式主要用于通过复制来创建新对象,避免了直接通过构造函数创建对象时的复杂性,与工厂方法、抽象工厂和建造者模式的区别在于其实现方式和目的不同。