【读书活动感悟分享】特能扛——《设计模式:可复用面向对象软件的基础》第3章 原型模式 - 3.4_文章

【读书活动感悟分享】特能扛——《设计模式:可复用面向对象软件的基础》第3章 原型模式 - 3.4

柳俊臣
发表于 2025-11-10 22:43:37

一、原型模式概述

原型模式(Prototype Pattern)是一种创建型设计模式,其核心思想是通过复制(克隆)已有对象(原型)来创建新对象,而非通过构造函数重新初始化。

在该模式中,新对象的初始状态与原型完全一致,后续可根据需求修改自身属性。这种方式绕开了复杂的初始化流程(如数据库查询、资源加载等),直接复用原型的状态,从而减少重复劳动,提高对象创建效率。

原型模式的核心角色包括:

  • 抽象原型(Prototype):定义克隆方法的接口(通常为 Clone() 方法);
  • 具体原型(Concrete Prototype):实现抽象原型的克隆方法,返回自身的副本;
  • 客户端(Client):通过调用原型的克隆方法创建新对象,无需关注对象的具体创建逻辑。



二、原型模式的使用场景

  • 对象创建成本高:当对象初始化需要消耗大量资源(如读取大文件、复杂计算、网络请求),且需频繁创建相似对象时(如游戏中批量生成同类型怪物)。
  • 动态生成对象变体:运行时需根据参数生成多个相似但略有差异的对象(如文档编辑器中格式一致但内容不同的段落)。
  • 避免构造函数限制:当构造函数访问权限受限,或初始化逻辑复杂且不希望暴露细节时(如封装第三方库的对象创建)。
  • 需要保存对象快照:需在修改对象前保留原始状态(如撤销操作、版本控制),通过克隆原型可快速生成快照。
  • 对象类型不确定:编译期无法确定具体类型,需在运行时动态创建(如根据配置文件生成不同子类对象)。
  • 三、原型模式的优缺点

    优点

  • 提高创建效率:跳过重复的初始化步骤,尤其适合大对象或复杂对象的创建。
  • 简化创建逻辑:客户端无需了解对象的创建细节,只需调用克隆方法,降低代码耦合度。
  • 灵活性强:可在运行时动态添加或删除原型,轻松扩展对象类型(如通过原型管理器切换不同原型)。
  • 便于状态管理:通过克隆快速保存对象快照,支持撤销、回滚等操作。
  • 缺点

  • 浅克隆的局限性:默认的浅克隆(MemberwiseClone)仅复制值类型和引用类型的引用,导致新对象与原型共享引用类型数据,修改时可能相互影响。
  • 深克隆实现复杂:若需完全独立的对象(深克隆),需手动处理多层引用类型的克隆,或依赖序列化(可能影响性能,且要求所有成员可序列化)。
  • 维护成本增加:当对象结构复杂(如嵌套多层引用),克隆逻辑的维护难度会上升,且新增成员时需同步更新克隆方法。
  • 四、使用原型模式的注意事项

  • 区分浅克隆与深克隆:
    • 若对象仅包含值类型成员,或引用类型成员无需独立修改,优先使用浅克隆(简单高效);
    • 若引用类型成员需要独立修改,必须实现深克隆(避免原型与克隆对象相互干扰)。
  • 谨慎使用序列化实现深克隆:
    • 序列化方式(如二进制序列化、JSON 序列化)是深克隆的常用手段,但需注意:
      • 所有成员(包括嵌套对象)必须支持序列化(如标记 [Serializable] 特性);
      • 序列化可能降低性能,不适合高频克隆场景;
      • 避免序列化循环引用对象(可能导致异常)。
  • 避免过度设计:
    • 若对象创建简单(如仅需设置几个值类型属性),直接使用构造函数更简洁,无需强行使用原型模式。
  • 原型的初始化与管理:
    • 原型对象本身需要正确初始化(如通过工厂方法创建原型),避免克隆未初始化的原型;
    • 对于多原型场景,可使用 “原型管理器” 统一管理原型(如字典存储不同类型的原型),简化客户端调用。
  • 警惕克隆中的特殊成员:
    • 对单例对象、静态成员、非序列化成员(如 [NonSerialized] 标记),克隆时需单独处理(可能导致状态不一致)。
  • 五、示例代码


    using System;
    using System.Collections.Generic;
    // 原型接口(实现ICloneable或自定义克隆方法)
    public interface IPrototype<T>
    {
    T Clone();
    }
    // 具体原型类(可克隆对象)
    public class Product : IPrototype<Product>
    {
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public List<string> Tags { get; set; } // 引用类型成员
    public Product(int id, string name, decimal price, List<string> tags)
    {
    Id = id;
    Name = name;
    Price = price;
    Tags = tags;
    }
    // 深克隆实现(复制值类型+引用类型深拷贝)
    public Product Clone()
    {
    // 对引用类型成员进行深拷贝
    List<string> clonedTags = new List<string>(Tags);
    return new Product(Id, Name, Price, clonedTags);
    }
    public override string ToString()
    {
    return $"Id: {Id}, Name: {Name}, Price: {Price}, Tags: [{string.Join(", ", Tags)}]";
    }
    }
    // 客户端调用示例
    public class Client
    {
    public static void Main()
    {
    // 创建原始对象
    var originalTags = new List<string> { "电子", "数码", "新品" };
    Product originalProduct = new Product(1, "智能手机", 3999.99m, originalTags);
    Console.WriteLine("原始对象:" + originalProduct);
    // 通过原型克隆新对象
    Product clonedProduct = originalProduct.Clone();
    Console.WriteLine("\n克隆对象(初始状态):" + clonedProduct);
    // 修改克隆对象的属性(验证深克隆:引用类型修改不影响原始对象)
    clonedProduct.Id = 2;
    clonedProduct.Name = "高端智能手机";
    clonedProduct.Price = 5999.99m;
    clonedProduct.Tags.Add("旗舰");
    Console.WriteLine("\n修改后的克隆对象:" + clonedProduct);
    Console.WriteLine("修改后原始对象:" + originalProduct);
    }
    }

    六、原型模式与使用new关键字创建对象的区别

    原型模式与通过 new关键字创建对象(即直接实例化)是两种不同的对象创建方式,核心区别体现在创建逻辑、效率、适用场景等方面。以下从多个维度对比两者的差异:

    (1)、核心思想不同

    • new 关键字创建对象
      基于类的构造函数初始化新对象,本质是“从无到有”创建:先分配内存,再执行构造函数中的初始化逻辑(如设置初始值、调用方法、依赖外部资源等),最终生成一个全新的对象。
      例如:var obj = new MyClass(param1, param2); 会严格按照 MyClass 的构造函数逻辑执行。
    • 原型模式
      基于已有对象(原型)的“克隆”创建新对象,本质是“复制已有”:直接复制原型对象的内存数据(或深度复制其状态),生成一个与原型初始状态一致的新对象,跳过构造函数的执行。
      例如:var obj = prototype.Clone(); 复用原型的状态,无需重新执行初始化逻辑。

    (2)、创建过程不同


    对比维度

    new 关键字创建对象

    原型模式

    执行逻辑

    调用类的构造函数,执行完整的初始化流程(如参数校验、资源加载、属性赋值等)。

    不执行构造函数,直接复制原型的内存数据或状态(浅克隆复制值类型和引用,深克隆递归复制所有成员)。



    对象状态来源




    完全依赖构造函数的参数和内部逻辑,初始状态由代码硬编码或外部参数决定。

    初始状态与原型完全一致,后续可通过修改属性生成变体。



    对类的依赖




    必须知道具体类名,编译期需确定类型(如 new MyClass() 依赖 MyClass的可见性)。

    无需知道具体类名,通过原型对象动态克隆(如 prototype.Clone(),原型可以是接口或抽象类的实现)。



    (3)、效率不同

    • new 关键字创建对象
      若对象初始化逻辑简单(如仅设置几个值类型属性),效率与克隆接近;
      若初始化逻辑复杂(如查询数据库、读取大文件、复杂计算),频繁 new 会重复执行这些操作,导致效率低下。
    • 原型模式
      克隆过程本质是内存数据的复制(浅克隆)或序列化/反序列化(深克隆),跳过了重复的初始化逻辑,因此在以下场景效率更高:
      • 需频繁创建相似对象(如游戏中批量生成同类型怪物);
      • 初始化成本高的对象(如包含大量配置的复杂对象)。

    (4)、灵活性不同

    • new 关键字创建对象
      灵活性较低:
      • 编译期需确定具体类型,无法动态切换(如 new A()不能在运行时改为 new B());
      • 若需创建多个相似但略有差异的对象,需重复编写初始化代码(如多次调用 new 并修改部分属性)。
    • 原型模式
      灵活性更高:
      • 运行时可动态切换原型(如通过“原型管理器”选择不同原型克隆),无需修改客户端代码;
      • 克隆后只需修改少量属性即可生成变体,减少重复代码(符合“开闭原则”)。

    (5)、适用场景不同


    new 关键字创建对象

    原型模式

    1. 对象初始化逻辑简单

    1. 对象初始化成本高

    2. 无需频繁创建相似对象,或对象间差异较大

    2. 需频繁创建相似对象(仅部分属性不同)

    3. 编译期可确定对象类型,且类型固定

    3. 运行时需动态生成对象(类型不确定或需灵活切换)

    4. 不需要保存对象快照

    4. 需要保存对象快照


    对象快照(Object Snapshot): 某一时刻内存中对象的 “静态副本”。

    (6)、代码示例对比

    场景:创建“用户订单”对象,包含订单信息和用户信息(引用类型)。

    1. 使用 new 关键字创建public class User
    {
    public string Name { get; set; }
    public int Age { get; set; }
    public User(string name, int age)
    {
    // 模拟复杂初始化(如查询用户等级)
    Console.WriteLine("执行User构造函数,查询用户等级...");
    Name = name;
    Age = age;
    }
    }
    public class Order
    {
    public int Id { get; set; }
    public decimal Amount { get; set; }
    public User Buyer { get; set; }
    public Order(int id, decimal amount, User buyer)
    {
    // 模拟复杂初始化(如校验订单金额)
    Console.WriteLine("执行Order构造函数,校验金额...");
    Id = id;
    Amount = amount;
    Buyer = buyer;
    }
    }
    // 客户端
    var user = new User("张三", 30);
    // 多次创建相似订单(每次都执行构造函数)
    var order1 = new Order(1, 100, user);
    var order2 = new Order(2, 200, user); // 重复执行校验逻辑,效率低

    输出(构造函数被多次执行):

    执行User构造函数,查询用户等级...
    执行Order构造函数,校验金额...
    执行Order构造函数,校验金额...
    2. 使用原型模式创建public class User
    {
    public string Name { get; set; }
    public int Age { get; set; }
    public User(string name, int age)
    {
    Console.WriteLine("执行User构造函数,查询用户等级...");
    Name = name;
    Age = age;
    }
    }
    public class Order : ICloneable
    {
    public int Id { get; set; }
    public decimal Amount { get; set; }
    public User Buyer { get; set; }
    public Order(int id, decimal amount, User buyer)
    {
    Console.WriteLine("执行Order构造函数,校验金额...");
    Id = id;
    Amount = amount;
    Buyer = buyer;
    }
    // 浅克隆(示例)
    public object Clone() => this.MemberwiseClone();
    }
    // 客户端
    var user = new User("张三", 30);
    var prototypeOrder = new Order(1, 100, user); // 仅执行一次构造函数
    // 克隆生成新订单(不执行构造函数)
    var order2 = (Order)prototypeOrder.Clone();
    order2.Id = 2;
    order2.Amount = 200;

    输出(构造函数仅执行一次):

    执行User构造函数,查询用户等级...
    执行Order构造函数,校验金额...

    • new 关键字是最基础的对象创建方式,适合初始化简单、类型固定、创建频率低的场景,直接依赖类的构造函数,逻辑直观但灵活性和效率有限。
    • 原型模式是对 new 的补充,适合初始化复杂、需频繁创建相似对象或动态生成对象的场景,通过克隆复用原型状态,效率更高、灵活性更强,但需处理浅克隆/深克隆的细节。


    实际开发中,两者并非互斥:简单对象用 new,复杂且重复创建的对象用原型模式,需根据具体场景权衡选择。

    七、原型模式在特来电业务中可应用的场景

    1.电站信息创建,目前电站有多种,普通电站、光伏电站、储能站、互联互通电站。 基本信息有很多是一样的,只是有个别属性不一致。在构建电站信息时,可使用原型模式。

    2.价格策略创建,与电站类似,价格策略也是有多种不同的策略,普通策略、接入电价策略、超时占用费策略、折扣策略等,在创建策略信息时,也可使用原型模式。

    八、总结思考

    原型模式通过 “克隆” 实现对象创建,是一种高效的 “以空间换时间” 的设计思想。其核心价值在于复用已有对象的状态,减少重复初始化操作,尤其适合处理创建成本高或需频繁生成相似对象的场景。

    从实践角度看,使用原型模式需注意以下几点:

  • 合理选择克隆方式:浅克隆适合简单对象,深克隆适合包含独立引用类型成员的对象,可根据业务需求灵活选择(如 JSON 序列化比二进制序列化更通用,但性能略低)。
  • 避免过度使用:若对象创建逻辑简单,直接使用构造函数更直观,无需为了 “设计模式” 而强行使用原型模式。
  • 关注可维护性:对于复杂对象,建议封装克隆逻辑(如单独的克隆方法),并在新增成员时同步更新,避免克隆后的状态不一致。
  • 总之,原型模式是对传统对象创建方式的有效补充,合理使用可显著提升系统性能和灵活性,但需权衡其实现复杂度与业务需求的匹配度,避免为了设计而设计。


    110 0

    评论
    

    意见反馈