团队中对面向对象的理论研究已经做了很长时间,大家对接口,封装,继承,多态以及设计模式什么的似乎都能说出点东西来,但当看代码时发现大家其实并不十分清楚具体怎么做,所以我就想了个题目让大家来做,然后进行了一次头脑风暴,过程记录如下:
题目内容:
需要处理三种产品图书,数码,消费,需要计算产品的税率,图书的税率为价格的0.1,数码和消费类产品为价格的0.11,需要获得三种产品的信息,图书和消费类产品的信息为:"名字:" + Name;,数码产品的信息为:"数码类名字:" + Name;
要求:符合ocp原则(不懂ocp原则的到网上去查,变化点在计税的方式可能改变,信息打印方式可能改变)
这里我给大家一个面向过程的版本,方便大家理解需求
代码
public
class
Product
{
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;
}
}
}
测试代码
代码
[TestMethod()]
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类的职责也太多了
当然如果就只是这么简单的需求的话,这个过程化的版本也不错,至少简单。
第一个方案:
Product类
class
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类
class
Tax
{
public
double
ComputeTax(
double
price,
double
taxRate)
{
return
price
*
taxRate;
}
}
ProductInfo
class
ProductInfo
{
public
string
GetProductInfo(
int
type,
string
name)
{
switch
(type)
{
case
1
:
return
"
名字:
"
+
name;
case
2
:
return
"
数码类名字:
"
+
name;
default
:
return
string
.Empty;
}
}
}
这个方案其实完全没有解决问题,而且还比原来的方案更复杂了。新的税率仍然要改代码,新的信息仍然要改代码。
第二个方案:
Product
public
class
Product
{
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();
}
}
CreateProduct
class
CreateProduct
{
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; }
}
}
interface
ITax
{
double
CoumputeTax(
double
price);
}
Main
static
void
Main(
string
[] args)
{
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完全没有存在的必要。
第三种方案:
Product类
class
Product
{
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
;
}
}
代码
[TestMethod()]
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原则,但和接口方案还是有区别的,我们先看看接口组合方案:
第四种方案:
代码
class
Product
{
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
}
测试代码
代码
[TestMethod]
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 可用作排序算法的基础,虽然将委托比较方法用作排序算法的基础是有效的,但是并不理想。因为进行比较的能力属于类,而比较算法不会在运行时改变,所以单一方法接口是理想的。