Я думаю о разработке метода, который возвратил бы объект, который реализует интерфейс, но чей конкретный тип не будет, знают до времени выполнения. Например, предположите:
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();
}
Действительно ли это - хороший дизайн? Существует ли запах кода? Существует ли лучший способ сделать это?
Все хорошо с методом, который возвращает интерфейс, и если бы клиентский код называл методы интерфейса, очевидно, это было бы прекрасно. Но мое беспокойство - является ли клиентский бросок кода к конкретным типам хорошим дизайном.
Использование ключевого слова is
в C # (как вы продемонстрировали выше) почти всегда является запахом кода. И воняет.
Проблема в том, что что-то, что должно было знать только об 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 - Foo)
, поскольку теперь он разбросан по всей кодовой базе - внутри Driver
, внутри Garage
, внутри ParkingLot
... (Я говорю здесь, исходя из своего опыта работы над устаревшим кодом.)
Когда вы обнаружите, что делаете такое утверждение, как if (instance is SomeObject)
, остановитесь и спросите себя, почему это особое поведение нужно обрабатывать здесь. В большинстве случаев это может быть новый метод в классе interface / abstract, и вы можете просто предоставить реализацию по умолчанию для классов, которые не являются «специальными».
Это не значит, что вы абсолютно никогда не должны проверять типы с is
; тем не менее, вы должны быть очень осторожны в этой практике, потому что она имеет тенденцию выходить из-под контроля и подвергаться насилию, если ее не контролировать.
Теперь предположим, что вы определили, что должны окончательно проверить тип своего ICar
. Проблема с использованием заключается в
в том, что инструменты статического анализа кода дважды предупреждают вас о приведении типов, когда вы выполняете
if (car is Bmw)
{
((Bmw)car).ShiftLanesWithoutATurnSignal();
}
Снижение производительности, вероятно, незначительно, если только оно не находится во внутреннем цикле, но предпочтительный способ написания этого is
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();
}
. Вы можете быстро переключить
на перечисление, и это позволит вам знать (до некоторой степени) конкретный предел того, какие автомобили могут быть использованы.
Одним из недостатков использования перечисления является то, что CarType
высечен в камне; если другая (внешняя) сборка зависит от ICar
и они добавили новый автомобиль Tesla
, они не смогут добавить тип Tesla
в CarType
. Перечисления также плохо подходят для иерархий классов: если вы хотите, чтобы Chevy
был CarType.Chevy
и CarType.GM
, вам либо нужно использовать перечисление в качестве флагов (в данном случае уродливо), либо проверить наличие Chevy
до GM
, либо иметь много ||
s в ваших проверках по перечислениям.
Лучшим решением было бы объявить в ICar метод GenericCarMethod(), а Bmw и Toyota переопределить его. В общем, не стоит полагаться на нисходящую передачу, если вы можете этого избежать.
Вам нужен только виртуальный метод SpecificationMethod
, который реализован в каждом классе. Я рекомендую прочитать содержание FAQ Lite о наследовании. Упомянутый им метод проектирования может быть применен и к .Net.
Это классическая проблема двойной диспетчеризации, и у нее есть приемлемый паттерн для решения (паттерн Visitor).
//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);
}
}