团队中对面向对象的理论研究已经做了很长时间,大家对接口,封装,继承,多态以及设计模式什么的似乎都能说出点东西来,但当看代码时发现大家其实并不十分清楚具体怎么做,所以我就想了个题目让大家来做,然后进行了一次头脑风暴,过程记录如下:
题目内容:
需要处理三种产品图书,数码,消费,需要计算产品的税率,图书的税率为价格的0.1,数码和消费类产品为价格的0.11,需要获得三种产品的信息,图书和消费类产品的信息为:"名字:" + Name;,数码产品的信息为:"数码类名字:" + Name;
要求:符合ocp原则(不懂ocp原则的到网上去查,变化点在计税的方式可能改变,信息打印方式可能改变)
这里我给大家一个面向过程的版本,方便大家理解需求
{
public string Name { get; set; }
public double Price { get; set; }
public int Type { get; set; }
public string GetProductInfo()
{
switch (Type)
{
case 1:
case 2:
return "名字:" + Name;
case 3:
return "数码类名字:" + Name;
default:
return string.Empty;
}
}
public double ComputeTax()
{
switch (Type)
{
case 1:
return Price * 0.1;
case 2:
case 3:
return Price * 0.11;
default:
return Price;
}
}
}
测试代码
public void GetProductInfoTest()
{
Product book = new Product() { Name = "C#编程", Price = 50, Type = 1 };
Product consume = new Product() { Name = "桌子", Price = 100, Type = 2 };
Product digital = new Product() { Name = "数码相机", Price = 1000, Type = 3 };
Assert.AreEqual("名字:C#编程", book.GetProductInfo());
Assert.AreEqual("名字:桌子", consume.GetProductInfo());
Assert.AreEqual("数码类名字:数码相机", digital.GetProductInfo());
}
这个过程化的版本的问题如下:
1、当更改税率或获得信息时需要修改product类,这不符合ocp原则
2、product类的职责也太多了
当然如果就只是这么简单的需求的话,这个过程化的版本也不错,至少简单。
第一个方案:
{
public string Name
{
get { return name; }
set { name = value; }
}
private string name;
public double Price
{
get { return price; }
set { price = value; }
}
private double price;
public double TaxRate
{
get { return taxRate; }
set { taxRate = value; }
}
private double taxRate;
public int Type
{
get { return type; }
set { type = value; }
}
private int type;
public Product(string name,double price,double taxRate,int type)
{
this.Name = name;
this.Price = price;
this.TaxRate = taxRate;
this.Type = type;
}
Tax tax = new Tax();
public double GetTax()
{
return tax.ComputeTax(price,taxRate);
}
ProductInfo productInfo = new ProductInfo();
public string GetProductInfo()
{
return productInfo.GetProductInfo(type, name);
}
}
Tax类
{
public double ComputeTax(double price, double taxRate)
{
return price * taxRate;
}
}
{
public string GetProductInfo(int type,string name)
{
switch (type)
{
case 1:
return "名字:" + name;
case 2:
return "数码类名字:" + name;
default:
return string.Empty;
}
}
}
这个方案其实完全没有解决问题,而且还比原来的方案更复杂了。新的税率仍然要改代码,新的信息仍然要改代码。
第二个方案:
{
public delegate double GetTax(double price);
public GetTax gt;
public string Name
{
get;
set;
}
public int type
{
get;
set;
}
public double Price
{
get;
set;
}
public double ComputeTax()
{
return gt(this.Price);
}
public override string ToString()
{
return base.ToString();
}
}
{
static Product pp;
public static ITax Tax
{
get;
set;
}
public static IPrint print
{
set;
get;
}
public static Product PProduct
{
get
{
return pp;
}
set { pp = value; pp.gt = Tax.CoumputeTax; }
}
}
{
double CoumputeTax(double price);
}
{
Product book = new Product();
book.Name = "图书";
book.Price = 1;
book.type = 1;
CreateProduct.Tax = new BookTax();
CreateProduct.PProduct = book;
Console.Write("this product name is {0}, price is {1}", book.Name,book.Price);
Console.Read();
}
这个方案似乎是解决了问题,新的税率变化添加一个新类继承ITax接口,在客户端用即可。但这种方案显然属于知识学太多了,混用了委托与接口,无端的增加的复杂度,CreateProduct完全没有存在的必要。
第三种方案:
{
public delegate double DComputeTax(double price);
public DComputeTax dComputeTax;
public string Name { get; set; }
public double Price { get; set; }
public int Type { get; set; }
public string GetProductInfo()
{
return "";
}
public double ComputeTax()
{
if (dComputeTax!=null)
{
return dComputeTax(Price);
}
return 0;
}
}
public void ComputeTaxTest()
{
ProductN.DComputeTax d = new ProductN.DComputeTax(c=>c*0.1);
ProductN target = new ProductN() { Price = 100 }; // TODO: 初始化为适当的值
target.dComputeTax = d;
double expected = 10; // TODO: 初始化为适当的值
double actual;
actual = target.ComputeTax();
Assert.AreEqual(expected, actual);
}
这是使用委托来实现的方案,我觉得这个方案符合了需求,也符合了ocp原则,但和接口方案还是有区别的,我们先看看接口组合方案:
第四种方案:
{
public string Name { get; set; }
public double Price { get; set; }
IGetInfo getInfo;
IGetTax getTax;
public Product(IGetInfo getInfo, IGetTax getTax)
{
this.getInfo = getInfo;
this.getTax = getTax;
}
public string GetInfo()
{
return getInfo.GetInfo(Name);
}
public double GetTax()
{
return getTax.GetTax(Price);
}
}
interface IGetTax
{
double GetTax(double price);
}
interface IGetInfo
{
string GetInfo(string name);
}
class BookTax:IGetTax
{
#region IGetTax 成员
public double GetTax(double price)
{
return price*0.1;
}
#endregion
}
class ConsumTax:IGetTax
{
#region IGetTax 成员
public double GetTax(double price)
{
return price*0.11;
}
#endregion
}
class DigitalInfo:IGetInfo
{
#region IGetInfo 成员
public string GetInfo(string name)
{
return "数码类名字:" + name;
}
#endregion
}
class SampleInfo:IGetInfo
{
#region IGetInfo 成员
public string GetInfo(string name)
{
return "名字:"+name;
}
#endregion
}
测试代码
public void GetInfoTest()
{
Product book = new Product(new SampleInfo(), new BookTax()) { Name = "C#编程", Price = 50 };
Assert.AreEqual("名字:C#编程", book.GetInfo());
Assert.AreEqual(5, book.GetTax());
Product consume = new Product(new SampleInfo(), new ConsumTax()) { Name = "桌子", Price = 100 };
Assert.AreEqual("名字:桌子", consume.GetInfo());
Assert.AreEqual(11, consume.GetTax());
Product digital = new Product(new DigitalInfo(), new ConsumTax()) { Name = "数码相机", Price = 1000 };
Assert.AreEqual("数码类名字:数码相机", digital.GetInfo());
Assert.AreEqual(110, digital.GetTax());
}
我觉得对于这个需求来说方案三和方案四都应该算是符合要求的比较好的解决方案,这两种方案各有优缺点。
方案三(委托方案)的优点:
1、足够灵活
2、代码简单,类少
缺点:
1、缺乏限制,只要符合计税委托签名的方法就可以计算税率,往往会造成已实现的业务代码职责不够清晰。
方案四(接口方案)的优点:
1、职责明确
2、也足够灵活
缺点:
1、使用的类往往过多
接口和委托的区别:
接口(interface)用来定义一种程序的协定。实现接口的类或者结构要与接口的定义严格一致。接口(interface)是向客户承诺类或结构体的行为方式的一种合同,当实现某个接口时,相当于告诉可能的客户:“我保证支持这个接口的方法,属性等”,接口不能实例化,接口只包含成员定义,不包含成员的实现,成员的实现需要在继承的类或者结构中实现。
C#中的委托是一种引用方法的类型,一旦为委托分配了方法,委托将与该方法具有完全相同的行为,委托方法的使用可以像其他任何方法一样具有参数和返回值。委托对象能被传递给调用该方法引用的代码而无须知道哪个方法将在编译时被调用。
从定义上来看似乎委托和接口没什么相似之处,但从隔离变化这个角度来看他们倒是有些相似之处,所以这里我们把他们放到一起来比较一番。
委托和接口都允许类设计器分离类型声明和实现。给定的接口可由任何类或结构继承和实现;可以为任何类中的方法创建委托,前提是该方法符合委托的方法签名。接口引用或委托可由不了解实现该接口或委托方法的类的对象使用。既然存在这些相似性,那么类设计器何时应使用委托,何时又该使用接口呢?
在以下情况中使用委托:
当使用事件设计模式时。委托是事件的基础,当需要某个事件触发外界响应时,使用委托事件比较合适。
当调用方不需要访问实现该方法的对象中的其他属性、方法或接口时。
需要方便的组合,使用委托可以利用+=,-=方便的组合方法。
当类可能需要该方法的多个实现时,使用多播委托。
在以下情况中使用接口:
当存在一组可能被调用的相关方法时。
当类只需要方法的单个实现时。
当使用接口的类想要将该接口强制转换为其他接口或类类型时。
当正在实现的方法链接到类的类型或标识时:例如比较方法。
使用单一方法接口而不使用委托的一个很好的示例是 IComparable 或 IComparable。IComparable 声明 CompareTo 方法,该方法返回一个整数,以指定相同类型的两个对象之间的小于、等于或大于关系。IComparable 可用作排序算法的基础,虽然将委托比较方法用作排序算法的基础是有效的,但是并不理想。因为进行比较的能力属于类,而比较算法不会在运行时改变,所以单一方法接口是理想的。