面向对象分析设计学习与探索(六):好的设计=软件的灵活程度(good design=flexible software)继续
永远希望你的软件更容易扩展?当你的程序遇到一些问题需要修改的时候,可能意味着你的软件需要更容易扩展。为了很好的扩展你的软件,你需要一些分析,整体的设计,并且研究面向对象原则如何能解耦你的软件。最后你会发现高内聚帮助你解耦合。
回到查询工具的类图,我们看看有哪些地方不合理:
1、 对于addInstrument方法每次添加一种乐器类型就要添加对这个方法进行修改
2、 对于search方法每次添加一种乐器类型就要添加一个相对应的search方法
首先看看search方法的修改:
解决每次添加一种乐器类型就要添加一个相对应的search方法,这个问题的关键在于InstrumentSpec类型是一个抽象类型,我们不能实例化它。但是如果把它变为实际类型就能够解决问题了吗?
把InstruemntSpec变为实际类型后给我们带来了什么?
首先是search方法的变化:
public List<Instrument> Search(InstrumentSpec Spec)
{
List<Instrument> list = new List<Instrument>();
foreach (Instrument instrument in inventory)
{
if(instrument.GetSpec().matches(Spec))
{
list.Add(instrument);
}
}
return list;
}
我们来分析一下:
在这个程序中为什么需要一个Instrument类型?
大多数乐器至少有一小部公共属性,如:serial number和price。Instrument类型存储这些公共属性,并且特殊的乐器类型可以从Instrument类型延伸。
所有乐器的公共部分是什么?
Serial Number,price和一些规格虽然说在不同的乐器类型中那些规格可能不同
乐器之间的不同是什么?
规格:每一种乐器都包含一些不同的属性,有不同的乐器类型和不同的构造函数。
即使现在search方法看起来好一些了,但是Inventory中的addInstrment方法还有些问题。原先的程序因为每一个Instrument type有一个相对应的扩展类型,所以把把Instrument抽象化。
通常建立扩展类型的原因是扩展类型与超级类型的行为是不同的。但是在查询工具中,Guitar的行为和其他的乐器类型不同吗?他们仅仅是有一些不同的属性,而没有不同的行为。由此产生一个问题:我们真的需要那些扩展类型吗?
在这个查询工具中所有的乐器都有相同的行为。因此instrument type扩展类型存在只有两个原因:
1、因为Instrument反映了一个概念,不是实际的对象。我们必须为每一个Instrument type建立一个扩展类型。
2、 每一个不同的乐器类型有不同的属性,并且使用不同的InstrmentSpec扩展类型,所以我们需要为每一个instrument建立特殊的构造函数。
看起来是两条很好的理由,但是结果是使软件很难变化,还记得写好软件的第二步吗?应有面向对象原则使其容易扩展(Apply basic OO principles to add flexibility)
在原来的程序中使用了继承、多态、抽象。但是现在应该考虑一下封装变化。如何把变化的部分从相对稳定的部分中分离出来。
现在来改变一下原先的设计:
根据上面的分析,实际上Instrument的扩展类型只是属性不同,类型中的行为没有什么变化。所以可以考虑去掉所有的扩展类型。
我们现在需要一个新的属性InstrumentType来表示Instrument的类型。对于instrument类型中的不同属性,我们可以试着有动态的方式得到他们。这个问题的关键在于我们用什么方式去存储这些属性。实际上属性无非两部分组成:1、属性名2、属性值,只要把这两部分对应起来就可以。试想一下,可以用Hashtable对象去存储这些属性。
当你的对象中有些变化的属性,或者说不确定的属性,使用集合动态的存储这些属性(When you have a set of properties that vary across you objects, use a collection, like a Map, to store those properties dynamically.)原书使用Java编写,在DotNet中的可以使用Hashtable类型。
这样你可以从原先的类型中移除很多方法,并且当需要添加属性的时候避免修改代码在你的程序中(You’ll remove lots of methods from your classes, and avoid having to change your code when new properties are added to your app.)
那么现在看看在程序中应该如何设计Instrument和InstrumentSpec类型
下面去具体到代码上,看看程序怎么写:
public class InstrumentSpec
{
private Hashtable _properties;
public InstrumentSpec(Hashtable properties)
{
if (properties != null)
{
_properties = properties;
}
else
{
_properties = new Hashtable();
}
}
public Object GetProperty(String PropertyName)
{
return _properties[PropertyName];
}
public Hashtable GetProperties()
{
return _properties;
}
public Boolean matches(InstrumentSpec OtherSpec)
{
Boolean Flag = true;
Hashtable OtherProperties = OtherSpec.GetProperties();
foreach (DictionaryEntry DE in OtherProperties)
{
String PropertyName = DE.Key.ToString();
if (_properties[PropertyName] != DE.Value)
{
Flag = false;
break;
}
}
return Flag;
}
}
public class Inventory
{
public Inventory()
{
_instruments = new List<Instrument>();
}
private List<Instrument> _instruments;
public List<Instrument> instruments
{
get { return _instruments; }
set { _instruments = value; }
}
public void addInstrument(String serialNumber, Double price, InstrumentSpec spec)
{
instruments.Add(new Instrument(serialNumber, price, spec));
}
public List<Instrument> Search(InstrumentSpec Spec)
{
List<Instrument> list = new List<Instrument>();
foreach (Instrument instrument in _instruments)
{
if (instrument.Spec.matches(Spec))
{
list.Add(instrument);
}
}
return list;
}
}
public enum InstrumentType
{
GUITAR, BANJO, DOBRO, FIDDLE, BASS, MANDOLIN
}
Instrument类型和以前差不多:
public class Instrument
{
public Instrument(String serialNumber, Double price, InstrumentSpec spec)
{
_SerialNumber = serialNumber;
_Price = price;
_Spec = spec;
}
private String _SerialNumber;
private Double _Price;
private InstrumentSpec _Spec;
public String SerialNumber
{
get { return _SerialNumber; }
set { _SerialNumber = value; }
}
public Double Price
{
get { return _Price; }
set { _Price = value; }
}
public InstrumentSpec Spec
{
get { return _Spec; }
set { _Spec = value; }
}
}
用客户端程序测试一下:
class Program
{
private static Inventory inventory;
static void Main(string[] args)
{
inventory = new Inventory();
InventoryInit();
FindInstrument();
Console.Read();
}
static void FindInstrument()
{
Hashtable FindProps = new Hashtable();
FindProps.Add("builder", "Collings");
FindProps.Add("model", "CJ1");
InstrumentSpec FindSpec = new InstrumentSpec(FindProps);
List<Instrument> list = inventory.Search(FindSpec);
if (list.Count > 0)
{
foreach (Instrument instrument in list)
{
Console.WriteLine("Find the SerialNumber is " + instrument.SerialNumber.ToString());
}
}
else
{
Console.WriteLine("Can NOT find");
}
}
static void InventoryInit()
{
Hashtable properties = new Hashtable();
properties.Add("instrumentType", InstrumentType.GUITAR);
properties.Add("builder", "Collings");
properties.Add("model", "CJ");
properties.Add("type", "Acoustic");
properties.Add("numStrings", 6);
properties.Add("topWood", "Indian");
properties.Add("backWood", "Sitka");
inventory.addInstrument("11277", 4000, new InstrumentSpec(properties));
Hashtable properties2 = new Hashtable();
properties2.Add("instrumentType", InstrumentType.MANDOLIN);
properties2.Add("builder", "Collings");
properties2.Add("model", "CJ1");
properties2.Add("type", "Acoustic");
properties2.Add("numStrings", 6);
properties2.Add("topWood", "Indian");
properties2.Add("backWood", "Sitka");
inventory.addInstrument("11278", 4000, new InstrumentSpec(properties2));
}
}
输出结果:
对这个查询工具已经作了很多的工作,但是不要忘记我们要做什么!现在再回来看看这个程序的类图:
再回来看看那几个问题:
1、 如果要加入一种新的乐器类型,有多少现有类型需要添加?
现有的程序不需要添加新的类型,现在我们已经把所有的乐器规格从Instrument和InstrumentSpec类型中脱离。
2、 如果变换一种乐器类型,有多少现有类型需要变化?
一个,修改InstrumentType,加入新的乐器类型
3、 如果说客户要求记录每个乐器的生产时间,有多少类型需要变化?
不需要,可以把它加入InstrumentSpec的properties中
4、 如果说客户给某种乐器修改属性,有多少类型需要变化?
如果说属性值与其他类型无关,则不需要变化,但如果属性值是一个枚举类型,那么就有一个类型需要变化
现在看起来程序比以前更容易扩展了,但是什么是内聚(cohesive)?内聚类型只做一件事情并且不做其他的事情(A cohesive class does really well and does not try to do or be something else)
内聚表现元素之间连接的程度。包括模块、类型或者对象。软件高内聚描述应用程序的单个类型的职责。还记得在上一篇文章中(OO Catastrophe)中有这样一句话:“Every class should attempt to make sure that it has only one reason to this, the death of many a badly designed piece of software”。
现在我们再回来说说内聚,还记得上一章提到过的“变化”吗?Every class should attempt to make sure that it has only one reason to this, the death of many a badly designed piece of software.实际上它的意思就是每一个类型尽可能仅有一个变化点或者说仅有一个变化原因。内聚表现在程序中类型之间的紧密关系程度。还记得那个汽车的例子吗?
每一个类型方法被相对较好的定义。每一个类型都是高内聚类型,并且很容易变化,但不影响其他的类型。回来看看我们所作的改变是否使得乐器的查询工具高内聚?程序中的对象是否解耦合?是否可以很容易的应对变化?
从这课当中我们又能总结出一些有关面向对象分析设计的东西。看看我们已经知道了多少。