C#: Метод к эхо-сигналу, конкретный тип которого определяется во времени выполнения?

Я думаю о разработке метода, который возвратил бы объект, который реализует интерфейс, но чей конкретный тип не будет, знают до времени выполнения. Например, предположите:

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

Действительно ли это - хороший дизайн? Существует ли запах кода? Существует ли лучший способ сделать это?

Все хорошо с методом, который возвращает интерфейс, и если бы клиентский код называл методы интерфейса, очевидно, это было бы прекрасно. Но мое беспокойство - является ли клиентский бросок кода к конкретным типам хорошим дизайном.

5
задан User 4 June 2010 в 01:57
поделиться

4 ответа

Использование ключевого слова 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 в ваших проверках по перечислениям.

11
ответ дан 18 December 2019 в 07:53
поделиться

Лучшим решением было бы объявить в ICar метод GenericCarMethod(), а Bmw и Toyota переопределить его. В общем, не стоит полагаться на нисходящую передачу, если вы можете этого избежать.

0
ответ дан 18 December 2019 в 07:53
поделиться

Вам нужен только виртуальный метод SpecificationMethod , который реализован в каждом классе. Я рекомендую прочитать содержание FAQ Lite о наследовании. Упомянутый им метод проектирования может быть применен и к .Net.

0
ответ дан 18 December 2019 в 07:53
поделиться

Это классическая проблема двойной диспетчеризации, и у нее есть приемлемый паттерн для решения (паттерн 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);
  }
}
9
ответ дан 18 December 2019 в 07:53
поделиться
Другие вопросы по тегам:

Похожие вопросы: