[记]一个逐步“优化”的范例程序
一个逐步“优化”的范例程序
引言
本文是《Object-Oriented Analysis and Design》一书第一章和第五章的读书笔记。我对书中提供的一个范例程序进行了总结和整理,通过逐步优化这个乐器管理的范例程序,分析了进行程序设计时需要注意到的一些问题。
1.简单直接的实现
这个程序起初的需求很简单:我们需要创建一个吉他管理程序,它能够保存所有的吉他信息,并且可以通过输入吉他的参数来进行查询,返回查询结果。我们知道一个优良的软件应该从两个角度去衡量:
- 从用户的角度,软件应该是符合用户期望的,也就是满足了用户的需求,可以完成用户期望它完成的工作。
- 从开发者的角度,软件应该是易于维护的、可扩展的,以及可重用的。
这两个方面应该是递进的,也就是说软件首先要能满足用户的需求。所以我们先看如何完成用户的需求,我们定义一个类Guitar,它代表了吉他;以及一个Inventory类,它用于维护现有吉他的信息,可以进行添加、查找等操作:
然后我们来看下实现:
// 吉他类
public class Guitar {
private string serialNumber; // 序列号
private string builder; // 厂商
private string model; // 型号
private string type; // 类型
private string backWood; // 后部材质
private string topWood; // 前面材质
private double price; // 价格
public Guitar(string serialNumber, string builder, string model, string type, string backWood, string topWood, double price) {
this.backWood = backWood;
this.builder = builder;
this.model = model;
this.price = price;
this.serialNumber = serialNumber;
this.topWood = topWood;
this.type = type;
}
// 公有属性省略
}
public class Inventory {
// 维护现有的所有吉他
private List<Guitar> guitarList;
public Inventory() {
guitarList = new List<Guitar>();
}
// 向列表中添加 吉他
public void AddGuitar(string serialNumber, string builder, string model, string type, string backWood, string topWood, double price) {
Guitar guitar = new Guitar(serialNumber, builder, model, type,backWood, topWood, price);
guitarList.Add(guitar);
}
// 搜索吉他列表,寻找满足searchGuitar参数的吉他
// 如果searchGuitar的参数为null或者"",则忽略此参数
public Guitar Search(Guitar searchGuitar) {
List<Guitar>.Enumerator it = guitarList.GetEnumerator();
// 价格Price和序列号SerialNumber不参与查询
Guitar result = null;
while (it.MoveNext()) {
Guitar guitar = it.Current;
string builder = searchGuitar.Builder;
if(!String.IsNullOrEmpty(builder) &&
!builder.Equals(guitar.Builder))
continue;
string model = searchGuitar.Model;
if(!String.IsNullOrEmpty(model) &&
!model.Equals(guitar.Model))
continue;
string type = searchGuitar.Type;
if (!String.IsNullOrEmpty(type) &&
!type.Equals(guitar.Type))
continue;
string backWood = searchGuitar.BackWood;
if (!String.IsNullOrEmpty(backWood) &&
!backWood.Equals(guitar.BackWood))
continue;
string topWood = searchGuitar.TopWood;
if (!String.IsNullOrEmpty(topWood) &&
!topWood.Equals(guitar.TopWood))
continue;
result = guitar; // 找到第一个匹配结果就返回
return result;
}
return result;
}
}
接下来我们向Inventory中添加一些Guitar,然后来测试下查找功能:
class Program {
static void
Inventory inventory = new Inventory();
initializeInventory(inventory);
// 想要查找的Guitar
Guitar wanted = new Guitar("", "fender", "Stratocastor", "electric", "Alder", "Alder", 0);
// 返回符合条件的结果
Guitar guitar = inventory.Search(wanted);
if (guitar != null) { // 找到符合条件的结果
Console.WriteLine("You might like this {0} {1} {2} guitar:\n {3} back and sides,\n {4} top.\n You can have it for only ${5} !", guitar.Builder, guitar.Model, guitar.Type, guitar.BackWood, guitar.TopWood, guitar.Price);
} else {
Console.WriteLine("Sorry, nothing found.");
}
}
private static void initializeInventory(Inventory item) {
item.AddGuitar("V95693", "Fender", "Stratocastor", "electric", "Alder", "Alder", 1499.95D);
item.AddGuitar("B95315", "Gibson", "SpecialKind", "electric", "Maple", "Cedar", 2134.30D);
item.AddGuitar("V95694", "Fender", "Stratocastor", "electric", "Alder", "Alder", 1599.95D);
}
}
结果我们发现并未返回搜索结果,但是我们看下InitializeInventory()方法,确实存在一个Guitar,它的属性完全符合要查找的Guitar实例wanted。为什么查询却找不到呢?仔细查看一下,我们发现添加到Inventory中的Guitar的制造商Builder是"Fender",而输入的searchGuitar的Builder属性为"fender"。我们知道,在C#中字符串的大小写是敏感的,即是说 "a"=="A"返回的是false,所以"Fender"不等于"fender"。所以我们遇到的问题是:在不要求严格匹配大小写的情况下,对于字符串的比较,我们应该先全部转换为大写或者小写,然后再进行比较。但是这样做就没有问题了么?我们看一下Guitar类的定义,除了price为double类型以外,其余均为string。而某些属性,比如说吉他的发声类型type,只有两种可能,一种是传统的、通过震动发声的(Acoustic),一种是电子发声的(Electric);对于制造商Builder,可能只有有限的几个厂家。但使用string类型时,我们无法对于这些属性的取值进行限制,此时,我们应该考虑:如果对象的属性是由有限个项目构成的集合,我们最好定义一个枚举,并设置对象的属性为这个枚举类型。
所以对于上面程序可以进行的第一个改进,就是定义枚举,并将部分属性的值,由string改为枚举类型:
// 发生类型
public enum SoundType {
Acoustic, Electric
}
// 制造商
public enum Builder {
Fender, Martin, Gibson, Collings, Olson
}
// 木料
public enum Wood {
IndianRoseWood, BrazilianRoseWood, Mahogany, Maple, Cocobolo, Cedar, Alder, Sitka
}
同时修改Guitar类和Inventory类,让它们使用这些枚举作为字段类型:
public class Guitar {
private string serialNumber; // 序列号
private Builder builder; // 厂商
private string model; // 型号
private SoundType type; // 类型
private Wood backWood; // 后部材质
private Wood topWood; // 前面材质
private double price; // 价格
// 构造函数、属性做相应修改,此处略
}
此时,我们发现上面例子Inventory中符合搜索条件的有两项,而Search()方法只能返回查询到的第一个结果,所以第二处改进就是对Inventory的Search()方法进行修改,让它返回一个查询结果列表:
public class Inventory {
private List<Guitar> guitarList; // 维护现有的所有吉他
public Inventory() {
guitarList = new List<Guitar>();
}
// AddGuitar()方法略...
// 搜索吉他列表,寻找满足searchGuitar参数的吉他
public List<Guitar> Search(Guitar searchGuitar) {
List<Guitar>.Enumerator it = guitarList.GetEnumerator();
List<Guitar> list = new List<Guitar>(); // 保存满足搜索条件的吉他
while (it.MoveNext()) {
Guitar guitar = it.Current;
if (guitar.Builder!=searchGuitar.Builder)
continue;
string model = searchGuitar.Model.ToLower();
if (!String.IsNullOrEmpty(model) &&
!model.Equals(guitar.Model.ToLower()))
continue;
if (guitar.Type != searchGuitar.Type)
continue;
if (guitar.BackWood != searchGuitar.BackWood)
continue;
if (guitar.TopWood != searchGuitar.TopWood)
continue;
list.Add(guitar); // 添加到列表中
}
return list; // 返回结果
}
}
然后我们进行一下测试,可以看到它返回了两个结果。
static void
Inventory inventory = new Inventory();
initializeInventory(inventory);
// 想要查找的Guitar
Guitar wanted = new Guitar("", Builder.Fender, "Stratocastor", SoundType.Electric, Wood.Alder, Wood.Alder, 0);
// 返回符合条件的结果
List<Guitar> list = inventory.Search(wanted);
if (list.Count > 0) {
foreach (Guitar guitar in list) {
Console.WriteLine("You might like this {0} {1} {2} guitar:\n {3} back and sides,\n {4} top.\n You can have it for only ${5} !", guitar.Builder, guitar.Model, guitar.Type, guitar.BackWood, guitar.TopWood, guitar.Price);
}
} else {
Console.WriteLine("Sorry, not found.");
}
}
这里仍然需要注意一个问题:上面我们将Guitar的字段类型由string改为了枚举,虽然我们限制了输入,字段只能接受有限的数值,但是我们在调用Search()方法时,必须明确的指定一个枚举值。而有时候,我们并不希望指明数值(我们希望忽略此查询条件),比如说,我们不希望限制吉他的木料(任何木料的吉他都满足查询条件),在使用string类型时,我们只需要传递null或者空字符串("")进去就可以了,但使用枚举后却必须指定一个数值。此时,可以向枚举中添加一个字段,NotSet,这个值相当于string为null或空字符串("")时的情况。然后将Search()方法中的判断语句进行一下修改就可以了:
if (searchGuitar.TopWood != Wood.NotSet &&
guitar.TopWood != searchGuitar.TopWood)
continue;
2.属性分离和解耦
属性分离
我们再对上面的程序稍微进行一下分析,发现对于Guitar来说,SerialNumber和Price属性是一定会有的,而其他的属性以后可能会添加,比如说我们可能会再添加一个NumStrings属性,代表吉他有多少根玄;也可能会删除某个属性,比如我们可能以后会觉得model属性多余,然后把它删除掉。除此以外,我们发现Inventory类的Search()方法只需要Guitar的部分属性,而我们传递了整个Guitar进去。
此时,我们可以将不变的部分(SerialNumber和Price)仍保留在Guitar类中,将可能会变化的部分(Guitar类的其他属性),封装为另一个类型,我们称为GuitarSpec,并在Guitar中保存一个GuitarSpec类型实例:
public class GuitarSpec {
private Builder builder; // 厂商
private string model; // 型号
private SoundType type; // 类型
private Wood backWood; // 后部材质
private Wood topWood; // 前面材质
public GuitarSpec(Builder builder, string model, SoundType type, Wood backWood, Wood topWood) {
this.backWood = backWood;
this.builder = builder;
this.model = model;
this.topWood = topWood;
this.type = type;
}
// 属性略
}
解耦
由于GuitarSpec成为了一个独立的对象,所以,我们的Guitar类型只需要保存一个GuitarSpec对象就可以了:
public class Guitar {
private string serialNumber; // 序列号
private double price; // 价格
private GuitarSpec spec; // 吉他属性集
// 略...
}
此处有一个地方值得注意, Guitar的构造函数通常会有下面两种写法:
public Guitar(string serialNumber, Builder builder, string model, SoundType type, Wood backWood, Wood topWood, double price) {
this.price = price;
this.serialNumber = serialNumber;
this.spec = new GuitarSpec(builder, model, type, backWood, topWood);
}
public Guitar(string serialNumber, double price, GuitarSpec spec) {
this.price = price;
this.serialNumber = serialNumber;
this.spec = spec;
}
采用第一种写法时,我们在Guitar类的构造函数中创建GuitarSpec类型实例,第二种在Guitar类外部先行创建好,然后再传入。那么采用那种方式好呢?我们回想一下,创建GuitarSpec的目的就是为了将易变化的部分从Guitar类中隔离出去,而采用第一种方式时,无异于再次将变化重新引入Guitar类,因为当我们向GuitarSpec类添加或删除属性时,必须同时修改Guitar类的构造函数!所以,这里我们采用第二种方式的构造函数。
类似的我们修改Inventory类的AddGuitar()方法和Search()方法:
// 向列表中添加 吉他
public void AddGuitar(string serialNumber, double price, GuitarSpec spec) {
Guitar guitar = new Guitar(serialNumber, price, spec);
guitarList.Add(guitar);
}
// 搜索吉他列表,寻找满足searchSpec参数的吉他
public List<Guitar> Search(GuitarSpec searchSpec) {
List<Guitar>.Enumerator it = guitarList.GetEnumerator();
List<Guitar> list = new List<Guitar>(); // 保存满足搜索条件的吉他
while (it.MoveNext()) {
GuitarSpec guitarSpec = it.Current.Spec;
if (guitarSpec.Builder != searchSpec.Builder)
continue;
string model = searchSpec.Model.ToLower();
if (!String.IsNullOrEmpty(model) &&
!model.Equals(guitarSpec.Model.ToLower()))
continue;
if (guitarSpec.Type != searchSpec.Type)
continue;
if (guitarSpec.BackWood != searchSpec.BackWood)
continue;
if (guitarSpec.TopWood != searchSpec.TopWood)
continue;
list.Add(it.Current); // 添加到列表中
}
return list; // 返回结果
}
现在看上去程序已经完善的差不多了,我们上面做得这些都是为了能够在Guitar的属性变化的时候,尽可能的少做修改。检验程序是否经得起变化的一个方法就是我们现在假设删除一个属性model,看看需要改变哪些地方:我们得出Guitar类是不需要进行修改的,GuitarSpec类需要删除model属性,然而,我们发现Inventory类也需要进行修改,因为它的Search方法依赖于guitarSpec类的Model属性,因为要对它进行判断。此时,我们说Inventory类与GuitarSpec类是耦合在一起的。那么如何才能使得修改GuitarSpec类不需要改动Inventory类呢?我们可以将对GuitarSpec进行判等的操作,委托给GuitarSpec类型本身来完成,我们让GuitarSpec类实现IEquatable<T>接口:
public class GuitarSpec :IEquatable<GuitarSpec> {
// 其余略...
public bool Equals(GuitarSpec other) {
if (builder != other.Builder)
return false;
string model = other.Model.ToLower();
if (!String.IsNullOrEmpty(model) &&
! model.Equals(this.model.ToLower()))
return false;
if (type != other.Type)
return false;
if (backWood != other.BackWood)
return false;
if (topWood != other.TopWood)
return false;
return true;
}
}
现在判断两个GuitarSpec是否相等的逻辑转移到了GuitarSpec类型本身,我们再次修改Inventory的Search()方法,让它将对GuitarSpec的判等操作委托出去。
// 搜索吉他列表,寻找满足searchSpec参数的吉他
public List<Guitar> Search(GuitarSpec searchSpec) {
List<Guitar>.Enumerator it = guitarList.GetEnumerator();
List<Guitar> list = new List<Guitar>(); // 保存满足搜索条件的吉他
while (it.MoveNext()) {
GuitarSpec guitarSpec = it.Current.Spec;
if (guitarSpec.Equals(searchSpec)) // 进行两个对象的判等
list.Add(it.Current); // 将结果添加到列表中
}
return list; // 返回结果
}
经过现在的修改之后,不仅Search()方法的实现变得更为简单,各个类的职责也更加清晰,我们修改GuitarSpec类型也不会影响到Inventory类和Guitar类。
3.抽象和继承
接下来我们来对上面的程序进行一下扩展,假如我们的程序不仅需要对吉他(Guitar)进行管理和维护,还需要对曼陀林(Mandolin,一种琵琶乐器)进行管理,它的属性与吉他是类似的,但是多了一个Style属性,有"A"和"F"两种取值;同时我们为吉他再加入一个NumStrings属性,代表玹的数量,那么该如何改进程序呢?
首先我们创建一个Style枚举,它只包含A、F两个枚举值。接下来,我们可以将Guitar类和Mandolin的公共部分抽象出来,建立一个Instrument基类,这个Instrument基类包含Guitar和Mandolin公有的部分,然后让Guitar和Mandolin继承自Instrument。因为我们实际上并不需要创建一个Instrument的实例,所以我们将它声明为抽象的。类似的,我们将GuitarSpec也抽象为InstrumentSpec,并且再为Mandolin创建一个MandolinSpec类,让GuitarSpec和MandolinSpec继承自InstrumentSpec:
// Instrument乐器基类
public abstract class Instrument {
private string serialNumber; // 序列号
private double price; // 价格
private InstrumentSpec spec; // 乐器属性集
// 构造函数和属性略
}
// 吉他类
public class Guitar:Instrument {
public Guitar(string serialNumber, double price, GuitarSpec spec):base(serialNumber, price, spec) {
}
}
// 曼陀林类
public class Mandolin:Instrument {
public Mandolin(string serialNumber, double price, MandolinSpec spec)
: base(serialNumber, price, spec) {
}
}
以及InstrumentSpec和GuitarSpec、MandolinSpec类:
public abstract class InstrumentSpec : IEquatable<InstrumentSpec> {
private Builder builder; // 厂商
private string model; // 型号
private SoundType type; // 类型
private Wood backWood; // 后部材质
private Wood topWood; // 前面材质
// 构造函数和属性略
public bool Equals(InstrumentSpec other) {
string model = other.Model.ToLower();
if (!String.IsNullOrEmpty(model) &&
! model.Equals(this.model.ToLower()))
return false;
if (builder != other.Builder)
return false;
if (type != other.Type)
return false;
if (backWood != other.BackWood)
return false;
if (topWood != other.TopWood)
return false;
return true;
}
}
public class GuitarSpec:InstrumentSpec, IEquatable<GuitarSpec> {
private int numStrings;
public GuitarSpec(Builder builder, string model, SoundType type, Wood backWood, Wood topWood, int numStrings)
:base(builder, model,type,backWood, topWood) {
this.numStrings = numStrings;
}
public int NumStrings{
get { return numStrings; }
}
public bool Equals(GuitarSpec other) {
if(!base.Equals(other))
return false;
if (numStrings != other.NumStrings)
return false;
return true;
}
}
public class MandolinSpec : InstrumentSpec, IEquatable<MandolinSpec> {
private Style style;
public MandolinSpec(Builder builder, string model, SoundType type, Wood backWood, Wood topWood, Style style)
: base(builder, model, type, backWood, topWood) {
this.style = style;
}
public bool Equals(MandolinSpec other) {
if (!base.Equals(other))
return false;
if (style != other.style)
return false;
return true;
}
}
最后,我们需要修改Inventory类:
public class Inventory {
private List<Instrument> instrumentList;// 维护现有的所有乐器
public Inventory() {
instrumentList = new List<Instrument>();
}
// Search() 和 AddInstrument()方法见下
}
我们通过抽象和继承完成了程序的扩展。现在来看一下上面实现的扩展性如何,为了更简单地对问题进行描述,我们设想如果再加入一种乐器,班卓琴(Banjo),程序需要做哪些改动?
1、我们需要再定义一个继承自Instrument的类Banjo;
2、以及一个继承自InstrumentSpec的类BanjoSpec;此时,如果BanjoSpec拥有InstrumentSpec没有定义的属性,那么很好办,我们在BanjoSpec中添加新增的属性即可;如果BanjoSepc不需要InstrumentSpec中定义的属性,比如说Model,那么就麻烦了,我们需要从InstrumentSpec中删掉此属性,然后再在InstrumentSpec除了BanjoSpec以外的所有子类中添加刚才删去的Model属性。
3、我们还需要修改Inventory的AddInstrument()方法:
// 向列表中添加 乐器
public void AddInstrument(string serialNumber, double price, InstrumentSpec spec) {
Instrument instrument = null;
if (spec is GuitarSpec){
instrument = new Guitar(serialNumber, price, (GuitarSpec)spec);
} else if (spec is MandolinSpec) {
instrument = new Mandolin(serialNumber, price, (MandolinSpec)spec);
}
instrumentList.Add(instrument);
}
这里,因为Instrument是抽象类,所以我们无法创建Instrument的实例,只能创建其子类的实例,而Guitar和Mandolin的构造函数,分别需要InstrumentSpec的子类(GuitarSpec和MandolinSpec),所以我们需要先进行向下转换((GuitarSpec)spec),才能创建对象。
4、类似地,我们也需要修改Search()方法:
// 搜索列表,寻找满足SearchSpec参数的乐器
public List<Instrument> Search(InstrumentSpec searchSpec) {
List<Instrument>.Enumerator it = instrumentList.GetEnumerator();
List<Instrument> list = new List<Instrument>();
MandolinSpec mandolinSpec;
GuitarSpec guitarSpec;
while (it.MoveNext()) {
if (it.Current is Guitar && searchSpec is GuitarSpec) {
guitarSpec = (GuitarSpec)it.Current.Spec;
if (guitarSpec.Equals((GuitarSpec)searchSpec))
list.Add(it.Current);
} else if (it.Current is Mandolin && searchSpec is MandolinSpec) {
mandolinSpec = (MandolinSpec)it.Current.Spec;
if (mandolinSpec.Equals((MandolinSpec)searchSpec))
list.Add(it.Current);
}
}
return list;
}
我们看到,尽管只是添加一种乐器,不仅需要对多处进行修改,而且还要再添加两个新类Banjo和BanjoSpec。设想如果有10多种乐器,那么改动及类的数量都会是非常多的,维护起来也会像是噩梦一般。那么下来该再如何改进呢?我们接着往下看。
4.动态属性
首先我们看一下Guitar、Mandolin和Banjo类,它们除了构造函数不同以外其余完全相同。而一般情况下,我们定义一个抽象类和子类这种继承体系,目的是为了在基类中实现一种行为,然后在各个子类中对其进行重写,以实现多态的效果。所以,此处我们可以考虑另外一种方式,将Instrument声明为实例的,并且在其中加入一个枚举类型的属性InstrumentType,由这个属性来标识乐器的类别。以后我们需要添加新的类型,只需要在这个枚举中添加就可以了:
// 乐器类型
public enum InstrumentType {
Guitar = 0, Mandolin, Banjo
}
因为InstrumentType和SerialNumber、Price一样,属于每种乐器都有的属性,所以我们将它定义在Instrument类中,而非InstrumentSpec中,此时Instument我们也声明为一般类,而非抽象类:
public class Instrument {
private InstrumentType type; // 乐器类型
// 其余略...
}
对于InstrumentSpec类及其子类而言,由于属性是多变的,而基类并没有定义抽象或者虚拟方法供子类覆盖,所以我们可以使用一个Hashtable将乐器的属性值按照 key/value 的形式保存起来,其中 key是属性名称,value是属性值。这样就可以删去所有的InstrumentSpec的子类(GuitarSpec、MandolinSepc等),同时,我们将InstrumentSpec声明为一般类:
public class InstrumentSpec : IEquatable<InstrumentSpec> {
private Hashtable properties;
public InstrumentSpec(Hashtable properties) {
if (properties == null)
properties = new Hashtable();
else
this.properties = properties;
}
public Hashtable Properties {
get { return properties; }
}
public Object GetProperty(object propertyName) {
return properties[propertyName];
}
public bool Equals(InstrumentSpec other) {
IEnumerator it = other.properties.Keys.GetEnumerator();
while (it.MoveNext()) {
if (properties[it.Current] != other.properties[it.Current])
return false;
}
return true;
}
}
通过上面的改变,我们添加新乐器时,只需要改变枚举就可以了,而不需要再添加大量的诸如Guitar和GuitarSpec这样的子类。
最后我们再看一下Inventory类的实现:
public class Inventory {
private List<Instrument> instrumentList;// 维护现有的所有乐器
public Inventory() {
instrumentList = new List<Instrument>();
}
// 向列表中添加 乐器
public void AddInstrument(string serialNumber, double price, InstrumentSpec spec) {
Instrument instrument = new Instrument(serialNumber, price, spec);
instrumentList.Add(instrument);
}
// 搜索列表,寻找满足SearchSpec参数的乐器
public List<Instrument> Search(InstrumentSpec searchSpec) {
List<Instrument>.Enumerator it = instrumentList.GetEnumerator();
List<Instrument> list = new List<Instrument>();
while (it.MoveNext()) {
if (it.Current.Spec.Equals(searchSpec))
list.Add(it.Current);
}
return list;
}
}
可以看到Inventory类也变得清爽了许多。那么采用这种方式是不是就最好了呢?我们仍然要看到它的问题:
- 尽管将属性和属性值保存在Hashtable中极大的增加了灵活性,但是我们每次构建对象,为对象添加属性值也会变得非常繁琐。
- Hashtable返回的是一个Object类型的对象,所以我们在获得到属性之后,还需要再进行一次向下转换才行。
- 同样,因为Hashtable可以接收任何类型的对象,所以我们也就丧失了类型安全,比如说,对于一个只可以接受int类型的属性,我们可以输入任意值而在编译时不会报错,只有在运行时,我们将值取出进行向下转化时才会抛出异常。
所以说,设计并没有最好,只有最合适的,本文讨论的也是一样,我们只能根据实际情况,选择最合适的解决方案。对于 只有一种乐器、支持多种乐器、乐器属性变化不大、属性变化很大等各种不同情况,我们需要做出权衡,选择合适的解决方案。另外在实现时还要做出一定的预见,考虑以后某方面的变更会不会很大,然后再考虑需不需要留出扩展的余地。对于一个系统,我们很可能 设计不足,也有可能 过度设计。我觉得,我们应该首先具备了 过度设计 的能力,然后再去考虑哪些地方不需要过度“灵活”,因为通常每种设计都有着自身的优点和缺陷,很难找到一种绝对正确的方案。
感谢阅读,希望这篇文章对你有所帮助!