走进 Gang of Four 设计模式:原型模式

走进 Gang of Four 设计模式:原型模式
走进 Gang of Four 设计模式原型模式说明不仅告诉你怎么用更告诉你为什么这样设计前置知识Java 面向对象基础接口、抽象类、Cloneable 接口、浅拷贝与深拷贝概念 设计模式分类总览GoF 23 种设计模式可按两个维度交叉分类类/对象处理方式 ×创建型/结构型/行为型目的。维度创建型Creational结构型Structural行为型Behavioral类Class通过继承复用Factory Method工厂方法Adapter类适配器Interpreter解释器Template Method模板方法对象Object通过组合/聚合复用Abstract Factory抽象工厂Builder建造者Prototype原型Singleton单例Adapter对象适配器Bridge桥接Composite组合Decorator装饰Facade外观Flyweight享元Proxy代理Chain of Resp.责任链Command命令Iterator迭代器Mediator中介者Memento备忘录Observer观察者State状态Strategy策略Visitor访问者本文档聚焦创建型-对象模式中的Prototype原型其余创建型对象模式包括 Abstract Factory、Builder、Singleton。 目录模式概述经典实现Shape 缓存克隆示例Demo 分析角色在代码中如何体现深拷贝与浅拷贝Spring 中的原型模式其他生态中的应用总结1. 模式概述1.1 要解决什么问题核心矛盾某些对象的创建代价很高如数据库查询后组装、复杂计算、加载配置文件如果每次需要时都从头new一个性能开销不可接受。此外如果对象的构造函数有复杂的约束或权限控制直接new可能被限制。// ❌ 每次都要查数据库才能创建对象性能极差ShapeshapeshapeService.loadFromDatabase(1);// 如果下次还需要一个类似的 shape又要查一次库解决方案缓存一个原型对象后续需要新对象时直接克隆这个原型而不是从头创建。这就是细胞分裂的思路——一个细胞分裂成两个两者共享相同的 DNA。1.2 模式定义原型模式使用原型实例指定要创建对象的种类并通过拷贝这些原型创建新的对象。这是 GoF 《设计模式》中定义的创建型模式其本质是通过克隆代替构造避开高昂的初始化成本。1.3 适用场景场景说明创建代价高对象需要经过数据库查询、I/O 操作、复杂计算等才能创建运行时动态指定需要在运行时确定实例化的类如通过动态加载避免工厂类层次不想为每个产品都创建一个对应的工厂类对象状态组合有限只有几种固定的状态组合克隆原型比手工实例化更方便2. 经典实现Shape 缓存克隆示例我们以图形对象为例创建 Shape 对象需要从数据库加载数据代价较高。因此我们将已创建的 Shape 缓存起来后续请求通过克隆返回。2.1 创建抽象类实现 Cloneable// 抽象原型Prototype publicabstractclassShapeimplementsCloneable{privateStringid;protectedStringtype;abstractvoiddraw();publicStringgetType(){returntype;}publicStringgetId(){returnid;}publicvoidsetId(Stringid){this.idid;}publicObjectclone(){Objectclonenull;try{clonesuper.clone();}catch(CloneNotSupportedExceptione){e.printStackTrace();}returnclone;}}2.2 具体原型类// 具体原型Concrete Prototype publicclassRectangleextendsShape{publicRectangle(){typeRectangle;}Overridepublicvoiddraw(){System.out.println(Inside Rectangle::draw() method.);}}publicclassSquareextendsShape{publicSquare(){typeSquare;}Overridepublicvoiddraw(){System.out.println(Inside Square::draw() method.);}}publicclassCircleextendsShape{publicCircle(){typeCircle;}Overridepublicvoiddraw(){System.out.println(Inside Circle::draw() method.);}}2.3 缓存管理器提供克隆访问// 原型缓存Registry importjava.util.Hashtable;publicclassShapeCache{privatestaticHashtableString,ShapeshapeMapnewHashtable();// 返回克隆体而非原型本身publicstaticShapegetShape(StringshapeId){ShapecachedShapeshapeMap.get(shapeId);return(Shape)cachedShape.clone();}// 模拟从数据库加载数据创建原型并缓存publicstaticvoidloadCache(){CirclecirclenewCircle();circle.setId(1);shapeMap.put(circle.getId(),circle);SquaresquarenewSquare();square.setId(2);shapeMap.put(square.getId(),square);RectanglerectanglenewRectangle();rectangle.setId(3);shapeMap.put(rectangle.getId(),rectangle);}}2.4 客户端// 客户端Client publicclassPrototypePatternDemo{publicstaticvoidmain(String[]args){ShapeCache.loadCache();// 获取克隆对象而非原始对象ShapeclonedShape(Shape)ShapeCache.getShape(1);System.out.println(Shape : clonedShape.getType());ShapeclonedShape2(Shape)ShapeCache.getShape(2);System.out.println(Shape : clonedShape2.getType());ShapeclonedShape3(Shape)ShapeCache.getShape(3);System.out.println(Shape : clonedShape3.getType());}}2.5 运行结果Shape : Circle Shape : Square Shape : Rectangle3. Demo 分析角色在代码中如何体现角色对应代码职责Prototype原型接口Shape抽象类实现Cloneable定义克隆自身的方法clone()ConcretePrototype具体原型Circle、Square、Rectangle实现clone()方法提供具体类型的克隆能力Registry / Cache缓存ShapeCache维护原型注册表管理原型对象的存储和克隆分发Client客户端PrototypePatternDemo通过缓存获取克隆的新对象不再直接new原型模式的关键在于客户端拿到的是克隆体不是原型本身。ShapeCache.getShape(1)返回的是circle.clone()的结果原型circle仍然安全地留在shapeMap中不会被外部修改。4. 深拷贝与浅拷贝4.1 浅拷贝的隐患上述代码使用的是super.clone()—— Java 默认的浅拷贝。这意味着如果Shape类中有引用类型的成员变量如ListString、自定义对象克隆体和原型会指向同一个内存地址。// 如果 Shape 中有一个 List 字段publicabstractclassShapeimplementsCloneable{privateListStringtagsnewArrayList();// ...}// 浅拷贝后CircleoriginalnewCircle();original.getTags().add(big);Circlecloned(Circle)original.clone();cloned.getTags().add(small);System.out.println(original.getTags());// [big, small] ← 被改了修改克隆体的引用字段会连带影响原型这在多线程或共享缓存时是灾难性的。4.2 如何实现深拷贝在实际工程中有几种常见的深拷贝手段方式说明适用场景重写clone()在clone()中手动拷贝引用字段类结构简单、字段少时序列化流通过ByteArrayOutputStreamObjectInputStream读写对象图复杂、含循环引用时JSON 工具用 Jackson/Gson 序列化为 JSON 再解析回来业务开发中最常用拷贝构造函数提供一个public Xxx(Xxx other)构造方法推荐做法类型安全推荐做法 —— 拷贝构造函数publicclassUser{privateStringname;privateAddressaddress;// 拷贝构造函数实现深拷贝publicUser(Userother){this.nameother.name;this.addressnewAddress(other.address.getCity());}}在现代 Java 开发中更推荐拷贝构造函数或拷贝工厂方法而不是依赖Cloneable接口。因为Cloneable本身是标记接口clone()是protected的使用起来不够灵活。Josh Bloch 在《Effective Java》中也建议谨慎使用Cloneable优先考虑拷贝构造函数。5. Spring 中的原型模式5.1Scope(prototype)Spring 中除了默认的singleton作用域另一个常用的是prototypeComponentScope(prototype)publicclassTaskProcessor{privateStringtaskId;publicvoidprocess(){// 每个实例处理不同的任务}}每次getBean()时 Spring 都会创建一个新实例内部不是通过Object.clone()实现的而是通过反射重新走实例化、依赖注入、初始化的完整生命周期一个常见的面试陷阱如果一个singleton的 Service 注入了一个prototype的 Component这个 Component 会每次都变吗答案是不会。因为单例 Service 只会初始化一次依赖注入也只发生一次。要解决这个问题需要使用 Spring 的Lookup注解或手动实现ApplicationContextAware在运行时动态获取新实例。5.2 源码分析AbstractBeanFactory.doGetBeanSpring 处理原型 Bean 的核心代码在AbstractBeanFactory.doGetBean中// 原型作用域分支简化后的核心逻辑elseif(mbd.isPrototype()){ObjectprototypeInstancenull;try{// 1. 标记该 Bean 正在创建中用于检测循环依赖beforePrototypeCreation(beanName);// 2. 反射创建新实例走完整生命周期prototypeInstancecreateBean(beanName,mbd,args);}finally{// 3. 移除创建标记afterPrototypeCreation(beanName);}beanInstancegetObjectForBeanInstance(prototypeInstance,name,beanName,mbd);}三个核心设计要点要点说明每次创建新实例没有缓存每次调用都走createBean通过反射构造新对象ThreadLocal 防循环依赖beforePrototypeCreation将当前 beanName 存入ThreadLocal如果在创建过程中被再次请求说明发生循环引用直接抛出BeanCurrentlyInCreationException不管理销毁原型 Bean 创建完成后 Spring 就交出了引用不再调用PreDestroy或DisposableBean销毁完全依赖 JVM GC设计思想的差异课堂上的原型模式通过clone()复制已有对象来避免构造开销Spring 则通过配置元数据作为原型模板每次用反射重新创建。这样能保证每个 Bean 的依赖都绑定到最新的容器状态比单纯的字节码拷贝更安全、更灵活。6. 其他生态中的应用6.1 JavaScript 原型继承原型模式规模最大的应用不在 Java而在 JavaScript。ES6 之前 JS 没有类的概念整个对象体系建立在原型之上每个对象有一个隐藏的__proto__属性指向其原型对象访问属性时如果对象本身没有JS 引擎沿着原型链向上找10000 个按钮对象共享同一个原型上的方法极大节省内存6.2 AST 节点复制数据库 规则引擎在 SQL 解析器如 Druid SQL Parser或规则引擎如 Drools中一条 SQL 被解析为抽象语法树AST。查询优化器需要对 AST 进行各种变形测试常量折叠、谓词下推为了不破坏原始树结构会快速克隆一份表达式节点在克隆体上进行操作最后对比执行计划代价。6.3 JDK 与工具库场景说明java.util.Date/Calendar涉及复杂的时区和历法计算某些实现内部通过clone()快速复制新实例高性能对象映射工具MapStruct 等工具在字节码层面生成硬编码的拷贝代码本质是原型思想ArrayList/HashMap它们的clone()方法返回浅拷贝的容器实例6.4 游戏开发Prefab 预制体在 Unity 等游戏引擎中当需要每隔几秒生成一个怪物时不会每次都从磁盘加载模型和纹理。引擎在内存中预先加载一个兽人步兵的原型Prefab刷怪时直接Clone赋予新坐标和血量即可。这正是原型模式的经典应用——“细胞分裂”。6.5 识别原型模式的标志当你看到一个系统满足以下条件时它很可能使用了原型模式存在一个注册表/缓存来存放标杆对象获取新对象的方法是clone()/copy()/ 反序列化而不是new原型对象本身不被直接返回返回的都是它的副本7. 总结核心要点原型模式通过克隆替代构造适用于创建代价高或需要避免构造函数约束的场景浅拷贝是super.clone()的默认行为引用字段会被共享深拷贝需要手动处理推荐使用拷贝构造函数原型缓存Registry是原型模式的常见变体将原型存储在 Map 中客户端请求时返回克隆体Spring 的prototype作用域是原型思想的应用但实现方式反射与经典模式clone不同与工厂模式的配合原型模式常与工厂方法模式搭配使用工厂方法内部通过clone()创建对象而不是new。这样可以结合两者的优势——工厂负责选择哪个产品原型负责如何快速创建。一句话原型模式的核心是以空间换时间缓存一个原型对象后续通过克隆快速生成新实例避开高昂的初始化成本。