我正在考虑设计一个方法,该方法将返回一个实现接口的对象,但在运行时才会知道其具体类型。例如:假设:
ICar
Ford implements ICar
Bmw implements ICar
Toyota implements ICar
public ICar GetCarByPerson(int personId)
我们不知道在运行之前我们会得到什么车。
a)我想知道这个人的汽车类型。
b)根据我们得到的具体车型,我们会调用不同的方法(因为有些方法只对类有意义)。所以客户端代码会做类似的事情。
ICar car = GetCarByPerson(personId);
if ( car is Bmw )
{
((Bmw)car).BmwSpecificMethod();
}
else if (car is Toyota)
{
((Toyota)car).ToyotaSpecificMethod();
}
这是一个好设计吗?有代码味吗?有更好的方法吗?
我很好用返回接口的方法,如果客户端代码显然调用接口方法,那就没问题了。但我关心的是客户端代码转换为具体类型是否是良好的设计。
答案 0 :(得分:11)
在C#中使用is
关键字(以上面演示的方式)几乎总是代码味道。它很臭。
问题在于,现在需要了解ICar
只能了解ICar
的几个不同的类来实现class Driver
{
private ICar car = GetCarFromGarage();
public void FloorIt()
{
if (this.car is Bmw)
{
((Bmw)this.car).AccelerateReallyFast();
}
else if (this.car is Toyota)
{
((Toyota)this.car).StickAccelerator();
}
else
{
this.car.Go();
}
}
}
。虽然这有效(因为它产生的代码可以运行),但它的设计很糟糕。你将从几辆汽车开始......
FloorIt
稍后,当你Driver
时,另一辆汽车会做一些特别的事情。并且您将该功能添加到if (car is Foo)
,您将考虑需要处理的其他特殊情况,并且您将浪费20分钟跟踪每个有Driver
的地方,因为它现在分散在整个代码库中 - Garage
内部,ParkingLot
内部,if (instance is SomeObject)
内部... (我说的是处理遗留代码的经验此处。)子>
当你发现自己发表is
这样的陈述时,请停下来问自己为什么需要在这里处理这种特殊行为。大多数情况下,它可以是接口/抽象类中的新方法,您可以简单地为非“特殊”的类提供默认实现。
这并不是说你绝对不应该用ICar
检查类型;但是,你必须在这种做法中非常小心,因为除非得到控制,否则它有失控和滥用的倾向。
现在,假设您已确定最终必须对is
进行类型检查。使用if (car is Bmw)
{
((Bmw)car).ShiftLanesWithoutATurnSignal();
}
的问题在于静态代码分析工具会在您执行此操作时向您发出两次警告
var bmw = car as Bmw;
if (bmw != null) // careful about overloaded == here
{
bmw.ParkInThreeSpotsAtOnce();
}
除非在内循环中,性能命中率可能微不足道,但是首选的方式是
enum CarType
{
Bmw,
Toyota,
Kia
}
interface ICar
{
void Go();
CarType Make
{
get;
}
}
这只需要一次施法(内部)而不是两次。
如果你不想走这条路,另一种干净的方法就是简单地使用枚举:
if (car.Make == CarType.Kia)
{
((Kia)car).TalkOnCellPhoneAndGoFifteenUnderSpeedLimit();
}
接着是
switch
你可以在枚举上快速CarType
,它可以让你知道(在某种程度上)可能使用的汽车的具体限制。
使用枚举的一个缺点是ICar
一成不变;如果另一个(外部)程序集依赖于Tesla
并且他们添加了新的Tesla
汽车,则他们将无法向CarType
添加Chevy
类型。枚举也不适合类层次结构:如果您希望CarType.Chevy
成为CarType.GM
而成为Chevy
,则必须使用枚举为标志(在这种情况下很难看)或确保在GM
之前检查||
,或者在对照枚举的检查中有很多{{1}}。
答案 1 :(得分:8)
这是一个经典的双重调度问题,它有一个可接受的解决模式(访客模式)。
//This is the car operations interface. It knows about all the different kinds of cars it supports
//and is statically typed to accept only certain ICar subclasses as parameters
public interface ICarVisitor {
void StickAccelerator(Toyota car); //credit Mark Rushakoff
void ChargeCreditCardEveryTimeCigaretteLighterIsUsed(Bmw car);
}
//Car interface, a car specific operation is invoked by calling PerformOperation
public interface ICar {
public string Make {get;set;}
public void PerformOperation(ICarVisitor visitor);
}
public class Toyota : ICar {
public string Make {get;set;}
public void PerformOperation(ICarVisitor visitor) {
visitor.StickAccelerator(this);
}
}
public class Bmw : ICar{
public string Make {get;set;}
public void PerformOperation(ICarVisitor visitor) {
visitor.ChargeCreditCardEveryTimeCigaretteLighterIsUsed(this);
}
}
public static class Program {
public static void Main() {
ICar car = carDealer.GetCarByPlateNumber("4SHIZL");
ICarVisitor visitor = new CarVisitor();
car.PerformOperation(visitor);
}
}
答案 2 :(得分:0)
您只需要一个虚拟方法SpecificationMethod
,它在每个类中实现。我建议阅读FAQ Lite关于遗产的内容。他提到的设计方法也可以应用于.Net。
答案 3 :(得分:0)
更好的解决方案是ICar声明GenericCarMethod()并让Bmw和Toyota覆盖它。一般来说,如果你能避免它,那么依靠向下转换并不是一个好的设计实践。