5

我正在考虑设计一种方法,该方法将返回一个实现接口但其具体类型直到运行时才知道的对象。例如假设:

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();
}

这是一个好的设计吗?有代码味道吗?有一个更好的方法吗?

我对返回接口的方法很好,如果客户端代码正在调用接口方法,显然这很好。但我担心的是客户端代码转换为具体类型是否是好的设计。

4

4 回答 4

11

在 C# 中使用is关键字(以您上面演示的方式)几乎总是代码异味。而且很臭。

问题是,现在需要一些应该只知道 an 的东西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,你会考虑其他需要处理的特殊情况,你会浪费 20 分钟来追踪每个有 的地方if (car is Foo),因为它现在分散在整个代码库中-- inside Driver, inside Garage, inside 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. 枚举也不适合类层次结构:如果您希望 aChevy成为 aCarType.Chevy a CarType.GM,则必须将枚举用作标志(在这种情况下很难看)或确保检查ChevybeforeGM或有很多||s在您对枚举的检查中。

于 2010-06-04T02:07:12.130 回答
9

这是一个经典的双重分派问题,它有一个可接受的模式来解决它(访问者模式)。

//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);
  }
}
于 2010-06-04T02:23:53.110 回答
0

您只需要一个SpecificationMethod在每个类中实现的虚拟方法。我推荐阅读FAQ Lite关于继承的内容。他提到的设计方法也可以应用于.Net。

于 2010-06-04T02:01:26.617 回答
0

更好的解决方案是让 ICar 声明一个 GenericCarMethod() 并让 Bmw 和 Toyota 覆盖它。一般来说,如果可以避免的话,依赖向下转换并不是一个好的设计实践。

于 2010-06-04T02:01:45.323 回答