C #: метод для возврата объекта, конкретный тип которого определяется во время выполнения? - PullRequest
5 голосов
/ 04 июня 2010

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

ICar
Ford implements ICar
Bmw implements ICar
Toyota implements ICar

public ICar GetCarByPerson(int personId)

Мы не знаем, какую машину мы вернем до времени выполнения.

а) Я хочу знать, какой тип машины у человека.

b) в зависимости от конкретного типа автомобиля, который мы получим, мы будем вызывать разные методы (поскольку некоторые методы имеют смысл только для класса). Таким образом, код клиента будет делать что-то вроде.

ICar car = GetCarByPerson(personId);

if ( car is Bmw )
{
  ((Bmw)car).BmwSpecificMethod();
}
else if (car is Toyota)
{
  ((Toyota)car).ToyotaSpecificMethod();
}

Это хороший дизайн? Есть ли запах кода? Есть ли лучший способ сделать это?

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

Ответы [ 4 ]

11 голосов
/ 04 июня 2010

Использование ключевого слова 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 is Foo), так как оно разбросанотеперь по всей базе кода - внутри 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 для перечисления, и это позволяет вам (в некоторой степени) узнать конкретный предел того, какие автомобили могут быть использованы.

Недостатком использования enum является то, что CarType установлен в камне;если другая (внешняя) сборка зависит от ICar и они добавили новую машину Tesla, они не смогут добавить тип Tesla к CarType.Перечисления также не очень хорошо подходят для иерархий классов: если вы хотите, чтобы Chevy были CarType.Chevy и a CarType.GM, вы должны либо использовать перечисление в качестве флагов (уродливо в этомили убедитесь, что вы проверили Chevy до GM или у вас есть много || в ваших чеках против перечислений.

8 голосов
/ 04 июня 2010

Это классическая проблема двойной отправки, и она имеет приемлемый шаблон для ее решения (шаблон посетителя).

//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);
  }
}
0 голосов
/ 04 июня 2010

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

0 голосов
/ 04 июня 2010

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

...