标签:ret 开发人员 throw 总结 optional 完成 工具属性 ict 组合
建造者模式(Builder)将复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
(1)抽象建造者角色(Builder):为创建一个Product对象的各个部件指定抽象接口,以规范产品对象的各个组成成分的建造。一般而言,此角色规定要实现复杂对象的哪些部分的创建,并不涉及具体的对象部件的创建。
(2)具体建造者(ConcreteBuilder)
? 1)实现Builder的接口以构造和装配该产品的各个部件。即实现抽象建造者角色Builder的方法。
? 2)定义并明确它所创建的表示,即针对不同的商业逻辑,具体化复杂对象的各部分的创建
? 3) 提供一个检索产品的接口
? 4) 构造一个使用Builder接口的对象即在指导者的调用下创建产品实例
(3)指导者(Director):调用具体建造者角色以创建产品对象的各个部分。指导者并没有涉及具体产品类的信息,真正拥有具体产品的信息是具体建造者对象。它只负责保证对象各部分完整创建或按某种顺序创建。
(4)产品角色(Product):建造中的复杂对象。它要包含那些定义组件的类,包括将这些组件装配成产品的接口。
(1) 发现最终加工的产品类型并没有统一抽象为Product接口,主要的原因是:经过不同ConcreteBuilder加工后的产品差别相对较大,给它一个公共的基准抽象对象意义不大,而且可以看到GetResult()方法仅仅位于具体ConcreteBuilder类型中。
(2)实际项目中每个创建者加工的产品类型其实一般很明确,进而Builder接口和每个ConcreteBuilder的各个BuildPart()步骤一般也都是针对这个特定类型产品设计的。
/// <summary>
/// 目标产品类
/// 交通工具类
/// </summary>
public class Vehicle
{
/// <summary>
/// 车轮属性
/// </summary>
public IEnumerable<string> Wheels { get; set; }
/// <summary>
/// 灯属性
/// </summary>
public IEnumerable<string> Lights { get; set; }
}
/// <summary>
/// abstract builder (抽象建造者)
/// 规定了构建者的类型
/// </summary>
public abstract class VehicleBuilderBase
{
/// <summary>
/// 交通工具属性
/// </summary>
public Vehicle Vehicle { get; set; }
/// <summary>
/// 创建一种交通工具
/// </summary>
public virtual void Create()
{
Vehicle = new Vehicle();
}
/// <summary>
/// 车轮属性
/// </summary>
public abstract void AddWheels();
/// <summary>
/// 灯属性
/// </summary>
public abstract void AddLights();
}
/// <summary>
/// concrete builder (具体建造者)
/// 具体的建造者类型
/// </summary>
public class CarBuilder : VehicleBuilderBase
{
#region VehicleBuilderBase 成员
public override void AddWheels()
{
Vehicle.Wheels = new[] {"front", "front", "back", "back"};
}
public override void AddLights()
{
Vehicle.Lights = new[] { "front", "front", "back", "back" };
}
#endregion
}
/// <summary>
/// concrete builder (具体建造者)
/// 具体的建造者类型
/// </summary>
public class BicycleBuilder : VehicleBuilderBase
{
#region VehicleBuilderBase 成员
public override void AddWheels()
{
Vehicle.Wheels = new[] { "front", "back" };
}
public override void AddLights()
{
Vehicle.Lights = null;
}
#endregion
}
/// <summary>
/// Director(指挥者)
/// 定义实际指导创建进行产生的Director
/// </summary>
public class VehicleMaker
{
/// <summary>
/// 建造者
/// </summary>
public VehicleBuilderBase Builder { get; set; }
/// <summary>
/// 建造出来的具体交通工具
/// </summary>
public Vehicle Vehicle => Builder.Vehicle;
/// <summary>
/// 指导 Builder 如何加工的抽象工序描述
/// </summary>
public void Construct()
{
Builder.Create();
Builder.AddWheels();
Builder.AddLights();
}
}
public void BuildUp()
{
//创建指挥者对象
var maker = new VehicleMaker();
//创建一个构建者
maker.Builder = new CarBuilder();
//调用指挥者加工
maker.Construct();
Assert.AreEqual<int>(4, maker.Vehicle.Wheels.Count());
Assert.AreEqual<int>(4, maker.Vehicle.Lights.Count());
//创建第二个构建者
maker.Builder = new BicycleBuilder();
maker.Construct();
Assert.AreEqual<int>(2, maker.Vehicle.Wheels.Count());
Assert.IsNull(maker.Vehicle.Lights);
}
? 1)构建者模式 将复杂的每个组成创建步骤暴露出来,借助 Director(或者客户程序)既可以选择其执行次序,也可以选择执行哪些步骤。上述过程可以在应用中动态完成,相比较工厂方法和抽象工厂模式的一次性创建过程而言,创建者模式合适创建“更为复杂且每个组成裱花较多”的类型。
? 2)向客户程序屏蔽了对象创建过程的多变性,同样是经过“一系列”创建过程。如上述:前面创建了一辆汽车,后面造了一辆自行车。
? 3)如上面的例子可见:构造过程的最终成果可以根据实际变化的情况,选择使用一个统一的接口,或者不同类的对象,给客户类型更大的灵活性。
? 创建者模式会暴露出更多的执行步骤,需要 Director (或者客户程序)具有更多的领域知识,如果使用不慎很容易造成更为紧密的耦合。原因是:Director 很可能成为瓶颈,它无法稳定,最后影响相关类型也难以稳定。
(1)当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时。
(2)相同的方法,不同的执行顺序,产生不同的事件结果时。
(3)多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不相同时。
(4)产品类非常复杂,或者产品类中的调用顺序不同产生了不同的效能。
(5)创建一些复杂的对象时,这些对象的内部组成构件间的建造顺序是稳定的,但是对象的内部组成构件面临着复杂的变化。
// <summary>
/// 指导每个具体类型BuildPart过程目标方法和执行情况的属性
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple=false)]
public sealed class BuildStepAttribute : Attribute
{
int sequence;
int times;
public BuildStepAttribute(int sequence, int times)
{
if((sequence <= 0) || (times <= 0 ))
throw new ArgumentOutOfRangeException();
this.sequence = sequence;
this.times = times;
}
public BuildStepAttribute(int sequence) : this(sequence, 1) { }
/// <summary>
/// 执行的次序
/// </summary>
public int Sequence { get { return this.sequence; } }
/// <summary>
/// 执行的次数
/// </summary>
public int Times { get { return this.times; } }
}
? 借助这个属性,Builder可以获得执行各个 BuildPart() 步骤的必要信息,接着设计一个工具类,完成相关信息的提取。
设计思路:
? 1.可以基于反射针对既定实例执行某个方法,这些特性用于创建者模式,其原因是:Builder 甚至可以在完全不知道待创建类型自身结构的前提下,仅根据它的“标签”就依次调用各 BuilderPart() 完成装配工作。
? 2.在实际工程中,由于反射的效率较低,因此在这个工具类设计中我们还要本着“能省就省”的思路,尽量将目标类型的反射结果通过缓存保存下来。
/// <summary>
/// 实体
/// </summary>
public class BuildStep
{
public MethodInfo Method { get; set; }
public int Times { get; set; }
public int Sequence { get; set; }
}
/// <summary>
/// 通过反射获得某个类型相关BuildPart()步骤指导信息的工具类型
/// </summary>
public class BuilderStepDiscovery
{
/// <summary>
/// 缓冲已经解析过的类型信息
/// 优化措施:登记已经反射确认过的类型
/// </summary>
static IDictionary<Type, IEnumerable<BuildStep>> cache =
new Dictionary<Type, IEnumerable<BuildStep>>();
/// <summary>
/// 登记那些已经认定没有Build Step 属性的类型
/// 优化措施:登记那些不具有 BuildStepAttribute 属性的类型
/// </summary>
static IList<Type> errorCache = new List<Type>();
/// <summary>
/// 借助反射获得类型 T 所需执行BuildPart()的自动发现机制
/// </summary>
/// <returns></returns>
public IEnumerable<BuildStep> DiscoveryBuildSteps(Type type)
{
if (type == null) throw new ArgumentNullException("type");
if (errorCache.Contains(type)) return null;
if (!cache.ContainsKey(type))
{
//借助 LINQ 获取每一个执行步骤信息的查询
var aType = typeof(BuildStepAttribute);
var methods = from item in
(from method in type.GetMethods()
where method.IsDefined(aType, false)
select new
{
M = method,
A = (BuildStepAttribute)method.GetCustomAttributes(aType, false).First()
}
)orderby item.A.Sequence
select new BuildStep
{
Method = item.M,
Times = item.A.Times,
Sequence = item.A.Sequence
};
if (!methods.Any())
{
errorCache.Add(type); // register invalidate type
return null;
}
else
{
cache.Add(type, methods); // register validate type
return methods;
}
}
else
return cache[type];
}
}
public interface IBuilder<T> where T : class, new()
{
T BuildUp();
}
public class Builder<T> : IBuilder<T> where T : class, new()
{
public virtual T BuildUp()
{
var steps = new BuilderStepDiscovery().DiscoveryBuildSteps(typeof(T));
// 没有BuildPart步骤,退化为Factory模式
if (steps == null) return new T();
var target = new T();
foreach (var step in steps)
for (var i = 0; i < step.Times; i++)
step.Method.Invoke(target, null);
return target;
}
}
#region 采用BuildStep定义装配过程的测试类型
/// <summary>
/// 第一个实现测试类
/// </summary>
class Car
{
public IList<string> Parts { get; private set; }
public Car()
{
Parts = new List<string>();
}
/// <summary>
/// 为汽车添加轮胎
/// </summary>
/// <remarks>
/// Attributed Builder第二个执行的Setp
/// 执行4次,即为每辆汽车装配增加4个轮胎
/// </remarks>
[BuildStep(2, 4)]
public void AddWheel()
{
buildPartHandler("wheel", Parts.Add);
}
/// <summary>
/// 为汽车装配引擎
/// </summary>
/// <remarks>
/// 没有通过Attribute标注的内容,因此不会被Attributed Builder执行
/// </remarks>
public void AddEngine()
{
buildPartHandler("engine", Parts.Add);
}
/// <summary>
/// 为汽车装配车身
/// </summary>
/// <remarks>
/// Attributed Builder第一个执行的Setp
/// 执行1次,即为每辆汽车装配增加1个车身
/// </remarks>
[BuildStep(1)]
public void AddBody()
{
buildPartHandler("body", Parts.Add);
}
}
/// <summary>
/// 用于显示测试结果的委托
/// </summary>
/// <remarks>
/// Action<string>是实际处理Build Step操作内容的委托
/// </remarks>
static Action<string, Action<string>> buildPartHandler = (x, y) =>
{
Trace.WriteLine("add " + x);
y(x);
};
/// <summary>
/// 第二个实现测试类
/// </summary>
class Computer
{
public string Bus { get; private set; }
public string Monitor { get; private set; }
public string Disk { get; private set; }
public string Memory { get; private set; }
public string Keyboard { get; private set; }
public string Mouse { get; private set; }
/// <summary>
/// 缓存Computer类型的所有Property
/// </summary>
static PropertyInfo[] properties = typeof (Computer).GetProperties();
/// <summary>
/// 获得Computer类型指定名称Property的Setter方法委托
/// </summary>
/// <param name="target">Computer类型实例</param>
/// <param name="name">Property名称</param>
/// <returns>指定名称Property的Setter方法委托</returns>
static Action<string> GetSetter(Computer target, string name)
{
var property = properties.Where(x => string.Equals(x.Name, name)).FirstOrDefault();
return x => property.SetValue(target, x, null);
}
[BuildStep(1)]
public void LayoutBus()
{
buildPartHandler("bus", GetSetter(this, "Bus"));
}
[BuildStep(2)]
public void AddDiskAndMemory()
{
buildPartHandler("disk", GetSetter(this, "Disk"));
buildPartHandler("16G memory", GetSetter(this, "Memory"));
}
[BuildStep(3)]
public void AddUserInputDevice()
{
buildPartHandler("USB mouse", GetSetter(this, "Mouse"));
buildPartHandler("keyboard", GetSetter(this, "Keyboard"));
}
[BuildStep(4)]
public void ConnectMonitor()
{
buildPartHandler("monitor", GetSetter(this, "Monitor"));
}
}
#endregion
/// <summary>
/// 测试电脑类
/// </summary>
[TestMethod]
public void BuildComputerByAttributeDirection()
{
Trace.WriteLine("\nassembly computer");
var computer = new Computer();
Assert.IsNull(computer.Keyboard);
Assert.IsNull(computer.Memory);
computer = new Builder<Computer>().BuildUp();
Assert.IsNotNull(computer.Bus);
Assert.IsNotNull(computer.Monitor);
Assert.IsNotNull(computer.Disk);
Assert.IsNotNull(computer.Memory);
Assert.IsNotNull(computer.Keyboard);
Assert.IsNotNull(computer.Mouse);
}
/// <summary>
/// 测试汽车类
/// </summary>
[TestMethod]
public void BuildCarByAttributeDirection()
{
Trace.WriteLine("build car");
var car = new Builder<Car>().BuildUp();
Assert.IsNotNull(car);
Assert.IsFalse(car.Parts.Contains("engine")); // 不会被执行的内容
Assert.AreEqual<string>("body", car.Parts.ElementAt(0));
for (var i = 1; i <= 4; i++)
Assert.AreEqual<string>("wheel", car.Parts.ElementAt(i));
}
在项目中,属性到底“贴”在 Builder ,ConcreteBuilder 上,还是目标类型上视项目组的需要而定:
? ☆目标类型导向型:如果待构造的目标类型本身就很复杂。例如,它的每个成员本身又是一个复杂的对象,这时将属性直接贴在目标类型上,然后设计一个很通用的 Director 也许比较明智,从而以不变应万变。
? ☆创建者导向型:如果创建过程仅涉及某个 Builder 接口比较固定的几个方法,目标类型抽象后差异不大,这时可以考虑将属性贴在 Builder 或者 ConcreteBuilder 上.
无论是 “标类型导向型” 还是 “创建者导向型”,采用标注方式在实现上性对复杂一些,下游开发人员使用时却非常方便,优势:
? 不必反反复复地编写创建者甲,创建者乙,只要贴标签即可。
? 客户程序使用简洁、一致。
? 可以将标签和配置文件相结合,进一步提高在生产环境中标签指示内容的灵活性。
/// <summary>
/// 产品类型
/// </summary>
public class Product
{
public int Count;
public IList<int> Items;
}
/// <summary>
/// 创建者接口
/// 描述具有闭合操作的抽象创建者
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IBuilder<T>
{
T BuildUp();
T TearDown();
}
public class ProductBuilder : IBuilder<Product>
{
Product product = new Product();
Random random = new Random();
public Product BuildUp()
{
product.Count = 0;
product.Items = new List<int>();
for (int i = 0; i < 5; i++)
{
product.Items.Add(random.Next());
product.Count++;
}
return product;
}
public Product TearDown()
{
while (product.Count > 0)
{
int val = product.Items[0];
product.Items.Remove(val);
product.Count--;
}
return product;
}
}
[TestMethod]
public void Test()
{
IBuilder<Product> builder = new ProductBuilder();
var product = builder.BuildUp();
Assert.AreEqual<int>(5, product.Count);
Assert.AreEqual<int>(5, product.Items.Count);
product = builder.TearDown();
Assert.AreEqual<int>(0, product.Count);
Assert.AreEqual<int>(0, product.Items.Count);
}
class Entry
{
public string A { get; set; } // essential
public int B { get; private set; } // essential
public double C { get; set; } // optional
public float D { get; private set; } // optional
public DateTime E { get; private set; } // optional
public IList<string> F { get; set; } // optional
/// <summary>
/// 对外封闭Entry的构造过程
/// </summary>
/// <param name="builder"></param>
private Entry(Builder builder)
{
A = builder.AValue;
B = builder.BValue;
C = builder.CValue;
D = builder.DValue;
E = builder.EValue;
F = builder.FValue;
}
public class Builder
{
public string AValue { get; private set; } // essentail
public int BValue { get; private set; } // essential
public double CValue { get; private set; } // optional
public float DValue { get; private set; } // optional
public DateTime EValue { get; private set; } // optional
public IList<string> FValue { get; private set;}// optional
public Builder(string a, int b)
{
AValue = a;
BValue = b;
// C、D 默认为0, F 默认为 null
EValue = DateTime.Now;
}
#region 连贯接口方法
public Builder C(double c)
{
this.CValue = c;
return this;
}
public Builder D(float d)
{
this.DValue = d;
return this;
}
public Builder E(DateTime e)
{
this.EValue = e;
return this;
}
public Builder F(IList<string> f)
{
this.FValue = f;
return this;
}
#endregion
/// <summary>
/// 可以对Entry实例的构造过程进行管理
/// 比如:重用既有的实例
/// </summary>
/// <returns></returns>
public Entry BuildUp()
{
return new Entry(this);
}
}
}
[TestMethod]
public void TestInnerFluentBuildUp()
{
var e1 = new Entry.Builder("a", 20)
.C(30)
.D(40)
.F(new List<string> {"A", "B", "C"}).BuildUp();
Output(e1);
var e2 = new Entry.Builder("b", 18).BuildUp();
Output(e2);
}
void Output(Entry entry)
{
Trace.WriteLine("\nEntry [" + entry.A + "]");
Trace.WriteLine(entry.B.ToString());
Trace.WriteLine(entry.C.ToString());
Trace.WriteLine(entry.D.ToString());
Trace.WriteLine(entry.E.ToString());
Trace.WriteLine(entry.F == null ? "NULL" : string.Join(",", entry.F));
}
实现过程得到意义:
? 1)可以由内部 Biulder 管理外部 Entity 的实例化过程,如果需要,还可以管理 Entity 实例的缓存,提高系统资源重用率(参考享元模式)。
? 2)类似 Lamada ,可以解决不确定数量构造参数排列组合的问题。
? 3)可以支持 private Setter 的结构函数参数。
? 4)便于调试跟踪。
? 5)实际构造过程仅在 Entity 的 private 构造函数中一次完成,整个构造过程保持一致性。
/// <summary>
/// BuildUp()适用的EventArgs
/// </summary>
/// <typeparam name="T"></typeparam>
class BuilderEventArgs<T> : EventArgs
{
/// <summary>
/// 还没有进行装配的对象实例
/// </summary>
public T Target { get; set; }
}
/// <summary>
/// BuildPart()适用的EventArgs
/// </summary>
/// <typeparam name="T"></typeparam>
class BuilderStepEventArgs<T> : BuilderEventArgs<T>
{
/// <summary>
/// 该BuildStep的操作信息
/// </summary>
public B.BuildStep Step { get; set; }
/// <summary>
/// 该BuildStep正在执行第几轮循环
/// </summary>
public int CurrentLoop { get; set; }
}
class Builder<T> where T : class, new()
{
public event EventHandler<BuilderEventArgs<T>> BeforeBuildUp;
public event EventHandler<BuilderEventArgs<T>> AfterBuildUp;
public event EventHandler<BuilderStepEventArgs<T>> BeforeBuildStepExecuting;
public event EventHandler<BuilderStepEventArgs<T>> AfterBuildStepExecuted;
public virtual T BuildUp()
{
var steps = new B.BuilderStepDiscovery().DiscoveryBuildSteps(typeof(T));
if (steps == null) return new T(); // 没有BuildPart步骤,退化为Factory模式
var target = new T();
// BuildUp() 执行前的切入点
BeforeBuildUp?.Invoke(this, new BuilderEventArgs<T>()
{ Target = target });
foreach (var step in steps)
for (var i = 0; i < step.Times; i++)
{
// BuildPart() 执行前的切入点
BeforeBuildStepExecuting?.Invoke(this,
new BuilderStepEventArgs<T>()
{
Target = target,
Step = step,
CurrentLoop = i
});
step.Method.Invoke(target, null);
// BuildPart() 执行后的切入点
AfterBuildStepExecuted?.Invoke(this,
new BuilderStepEventArgs<T>()
{
Target = target,
Step = step,
CurrentLoop = i
});
}
// BuildUp() 执行后的切入点
AfterBuildUp?.Invoke(this, new BuilderEventArgs<T>() { Target = target });
return target;
}
}
/// <summary>
/// 这里模拟一个可能制造出次品的汽车对象
/// 每个部件都可能在装配过程中出现错误
/// </summary>
class Car
{
public const string WheelItem = "wheel";
public const string EngineItem = "engine";
public const string BodyItem = "body";
const int WheelDefectiveCycle = 9;
const int BodyDefectiveCycle = 11;
static int wheelDefectiveIndexer;
static int bodyDefectiveIndexer;
public IList<string> Parts { get; private set; }
public Car() { Parts = new List<string>(); }
/// <summary>
/// 为汽车添加轮胎
/// </summary>
/// <remarks>
/// Attributed Builder第二个执行的Setp
/// 执行4次,即为每辆汽车装配增加4个轮胎
/// </remarks>
[B.BuildStep(2, 4)]
public void AddWheel()
{
wheelDefectiveIndexer++;
// 模拟会出现装配错误的情况
if (wheelDefectiveIndexer % WheelDefectiveCycle != 0)
Parts.Add(WheelItem);
}
/// <summary>
/// 为汽车装配引擎
/// </summary>
/// <remarks>
/// 没有通过Attribute标注的内容,因此不会被Attributed Builder执行
/// </remarks>
public void AddEngine()
{
Trace.WriteLine(EngineItem);
Parts.Add(EngineItem);
}
/// <summary>
/// 为汽车装配车身
/// </summary>
/// <remarks>
/// Attributed Builder第一个执行的Setp
/// 执行1次,即为每辆汽车装配增加1个车身
/// </remarks>
[B.BuildStep(1)]
public void AddBody()
{
bodyDefectiveIndexer++;
// 模拟会出现装配错误的情况
if (bodyDefectiveIndexer % BodyDefectiveCycle != 0)
Parts.Add(BodyItem);
}
}
[TestClass]
public class AopAttributedBuilderFixture
{
int totalBuildUpCars; // 装配车辆总数
int qualifiedCars; // 装配合格车辆总数
int defectiveCars; // 装配次品车辆总数
int partsBeforeBuild; // 当前BuildPart()执行前已经装配合格的零件数量
Builder<Car> builder;
[TestInitialize]
public void Initialize()
{
totalBuildUpCars = 0;
qualifiedCars = 0;
defectiveCars = 0;
builder = new Builder<Car>();
// BuildUp()执行前记录总共装配车辆数量
builder.BeforeBuildUp += (x, y) => { totalBuildUpCars++; };
// BuildUp()执行后记录装配的产品是否合格
builder.AfterBuildUp +=
(x, y) =>
{
// 合格的车辆是4个轮子一个车身
if ((y.Target.Parts.Count(part => string.Equals(part, Car.WheelItem)) == 4)
&& (y.Target.Parts.Count(part => string.Equals(part, Car.BodyItem)) == 1))
{
qualifiedCars++;
Trace.WriteLine("qualified\n");
}
else
{
defectiveCars++;
Trace.WriteLine("defective\n");
}
};
// BuildPart()执行前,记录当前装配的工序信息
// 同时登记之前已经合格完成装配的零件数量
builder.BeforeBuildStepExecuting +=
(x, y) =>
{
partsBeforeBuild = y.Target.Parts.Count;
if (y.Step.Times <= 1)
Trace.Write(string.Format("step {0} sequence\t[{1}] \t",
y.Step.Method.Name,
y.Step.Sequence));
else
Trace.Write(string.Format("step {0} sequence\t[{1}] current loop [{2}/{3}]\t",
y.Step.Method.Name,
y.Step.Sequence,
y.CurrentLoop + 1,
y.Step.Times));
};
// 通过对比每个BuildPart()过程前后实际已装配的零件数量,可以记录缺陷具体出现在哪个步骤
builder.AfterBuildStepExecuted +=
(x, y) =>
{
Trace.WriteLine(y.Target.Parts.Count > partsBeforeBuild
? "qualified" : "defective");
};
}
/// <summary>
/// 单元测试中对于整车和每个部件的检测工作通过AOP手段嵌入BuildUp()过程中
/// 合格车辆的标准是每个整车和每个部件(1个车身和4个轮子)都合格
/// </summary>
[TestMethod]
public void TestAopDuringBuildUp()
{
for (var i = 0; i < 7; i++ )
builder.BuildUp();
Trace.WriteLine("\n\n ---------------------------------------");
Trace.WriteLine("total build up " + totalBuildUpCars);
Trace.WriteLine("qualified " + qualifiedCars);
Trace.WriteLine("defective " + defectiveCars);
}
}
标签:ret 开发人员 throw 总结 optional 完成 工具属性 ict 组合
原文地址:https://www.cnblogs.com/King2019Blog/p/11219740.html