Плюсы и минусы «новых» свойств в C # / .Net? - PullRequest
9 голосов
/ 03 июня 2009

Учитывая следующий пример кода:

// delivery strategies
public abstract class DeliveryStrategy { ... }
public class ParcelDelivery : DeliveryStrategy { ... }
public class ShippingContainer : DeliveryStrategy { ... }

и следующий образец Класс заказа:

// order (base) class
public abstract class Order
{
    private DeliveryStrategy delivery;

    protected Order(DeliveryStrategy delivery)
    {
        this.delivery = delivery;
    }

    public DeliveryStrategy Delivery
    {
        get { return delivery; }
        protected set { delivery = value; }
    }
}

Когда я получу новый тип класса заказа, он наследует свойство Delivery типа DeliveryStrategy.

Теперь, когда указано, что CustomerOrders должны быть доставлены с использованием стратегии ParcelDelivery, мы можем рассмотреть « new » со свойством Delivery в классе CustomerOrder:

public class CustomerOrder : Order
{
    public CustomerOrder()
        : base(new ParcelDelivery())
    { }

    // 'new' Delivery property
    public new ParcelDelivery Delivery
    {
        get { return base.Delivery as ParcelDelivery; }
        set { base.Delivery = value; }
    }
}

(Очевидно, что CustomerOrder должен убедиться, что он совместим (полиморфно) с Order)

Это позволяет напрямую использовать стратегию ParcelDelivery в CustomerOrder без необходимости приведения.

Рассматриваете ли вы использовать этот шаблон? почему / почему нет?

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

Ответы [ 8 ]

11 голосов
/ 03 июня 2009

Я бы предпочел сделать тип универсальным:

public abstract class Order<TDelivery> where TDelivery : Delivery
{
    public TDelivery Delivery { ... }
    ...
}

public class CustomerOrder : Order<ParcelDelivery>
{
    ...
}

Это обеспечивает безопасность типов во время компиляции, а не оставляет ее до времени выполнения. Это также предотвращает ситуацию:

CustomerOrder customerOrder = new CustomerOrder();
Order order = customerOrder;
order.Delivery = new NonParcelDelivery(); // Succeeds!

ParcelDelivery delivery = customerOrder.Delivery; // Returns null

Уч.

Я считаю new как последнее средство. Это вносит дополнительную сложность как с точки зрения реализации, так и использования.

Если вы не хотите идти по общему маршруту, я бы ввел действительно новое свойство (с другим именем).

7 голосов
/ 03 июня 2009

Я думаю, что это хорошая модель. Это облегчает явное использование производных типов, устраняя необходимость в приведении результата, и это не «нарушает» поведение базового класса. На самом деле, подобный шаблон используется в некоторых классах в BCL, например, посмотрите на иерархию классов DbConnection:

  • DbConnection.CreateCommand () возвращает DbCommand
  • SqlConnection.CreateCommand () скрывает базовую реализацию, используя 'new', для возврата SqlCommand.
  • (другие реализации DbConnection делают то же самое)

Итак, если вы манипулируете объектом соединения через переменную DbConnection, CreateCommand вернет DbCommand; если вы манипулируете им через переменную SqlConnection, CreateCommand вернет SqlCommand, избегая приведения, если вы назначаете его переменной SqlCommand.

5 голосов
/ 03 июня 2009

Вы можете использовать дженерики.

// order (base) class
public abstract class Order<TDeliveryStrategy> where TDeliveryStrategy : DeliveryStrategy
{
    private TDeliveryStrategy delivery;

    protected Order(TDeliveryStrategy delivery)
    {
        this.delivery = delivery;
    }

    public TDeliveryStrategy Delivery
    {
        get { return delivery; }
        protected set { delivery = value; }
    }
}

public class CustomerOrder : Order<ParcelDelivery>
{
    public CustomerOrder()
        : base(new ParcelDelivery())
    { }
}
3 голосов
/ 03 июня 2009

Использование ключевого слова 'new' для скрытия доступных для записи свойств от базового класса, на мой взгляд, плохая идея. Ключевое слово new позволяет скрыть член базового класса в производном классе, а не переопределять его. Это означает, что вызовы к таким членам, использующим ссылку на базовый класс, все еще обращаются к коду базового класса, а не к производному коду класса. C # имеет ключевое слово «virtual», которое позволяет производным классам фактически переопределять реализацию, а не просто скрывать ее. Есть довольно хорошая статья здесь , в которой говорится о различиях.

В вашем случае похоже, что вы пытаетесь использовать скрытие метода как способ введения ковариации свойства в C #. Однако у этого подхода есть проблемы.

Часто ценность наличия базового класса состоит в том, чтобы позволить пользователям вашего кода полиморфно обрабатывать типы. Что происходит с вашей реализацией, если кто-то устанавливает свойство Delivery, используя ссылку на базовый класс? Будет ли производный класс ломаться, если свойство Delivery НЕ является экземпляром ParcelDelivery? Такого рода вопросы вам нужно задать себе при выборе варианта реализации.

Теперь, если свойство Delivery в базовом классе не предоставляет установщик, у вас немного другая ситуация. Пользователи базового класса могут только получить свойство и не устанавливать его. Поскольку вы перенаправляете доступ к свойству обратно в базовый класс, доступ через базовый класс продолжает работать. Однако, если ваш производный класс не запечатан, классы, которые наследуют его, могут создавать проблемы такого же типа, скрывая свойство Delivery с собственной версией.

Как уже упоминалось в некоторых других постах, вы можете использовать дженерики как способ достижения различных типов свойств Delivery. Пример Джона довольно хорош в демонстрации этого. Существует одна проблема с подходом обобщений, если вам нужно извлечь из CustomerOrder и изменить свойство Delivery на новый тип.

Есть альтернатива дженерикам. Вы должны решить, действительно ли вы хотите установить свойство в вашем случае. Если вы избавитесь от установщика в свойстве Delivery, проблемы, возникшие при использовании класса Order, сразу исчезнут. Поскольку вы устанавливаете свойство доставки с помощью параметров конструктора, вы можете гарантировать, что все заказы имеют правильный тип стратегии.

1 голос
/ 22 октября 2010

Ваше решение не делает то, что вы думаете, оно делает. Кажется, он работает, но не вызывает ваш «новый» метод. Рассмотрите следующие изменения в своем коде, чтобы добавить вывод, чтобы увидеть, какой метод вызывается:

// order (base) class
public abstract class Order
{
    private DeliveryStrategy delivery;

    protected Order(DeliveryStrategy delivery)
    {
        this.delivery = delivery;
    }

    public DeliveryStrategy Delivery
    {
        get 
        {
            Console.WriteLine("Order");
            return delivery; 
        }
        protected set { delivery = value; }
    }
}

public class CustomerOrder : Order
{
    public CustomerOrder()
        : base(new ParcelDelivery())
    { }

    // 'new' Delivery property
    public new ParcelDelivery Delivery
    {
        get 
        {
            Console.WriteLine("CustomOrder");
            return base.Delivery as ParcelDelivery; 
        }
        set { base.Delivery = value; }
    }
}

Затем следующий фрагмент кода для фактического использования вашего класса CustomOrder:

Order o = new CustomerOrder();
var d = o.Delivery;

Будет выводить «Заказ». Новый метод специально нарушает полиморфизм. Он создает новое свойство Delivery в CustomOrder, которое не является частью базового класса Order. Поэтому, когда вы используете свой CustomOrder, как если бы он был Order, вы не вызываете новое свойство Delivery, поскольку оно существует только в CustomOrder и не является частью класса Order.

То, что вы пытаетесь сделать, это переопределить метод, который нельзя переопределить. Если вы имеете в виду свойство, которое нужно переопределить, сделайте его абстрактным:

// order (base) class
public abstract class Order
{
    private DeliveryStrategy delivery;

    protected Order(DeliveryStrategy delivery)
    {
        this.delivery = delivery;
    }

    public abstract DeliveryStrategy Delivery
    {
        get { return delivery; }
        protected set { delivery = value; }
    }
}

public class CustomerOrder : Order
{
    public CustomerOrder()
        : base(new ParcelDelivery())
    { }

    public override ParcelDelivery Delivery
    {
        get { return base.Delivery as ParcelDelivery; }
        set { base.Delivery = value; }
    }
}
1 голос
/ 03 июня 2009

Рассмотрим этот подход:

public interface IOrder
{
    public DeliveryStrategy Delivery
    {
        get;
    }
}

// order (base) class
public abstract class Order : IOrder
{
    protected readonly DeliveryStrategy delivery;

    protected Order(DeliveryStrategy delivery)
    {
        this.delivery = delivery;
    }

    public DeliveryStrategy Delivery
    {
        get { return delivery; }
    }
}

затем используйте

public class CustomerOrder : Order
{
    public CustomerOrder()
        : base(new ParcelDelivery())
    { }

    public ParcelDelivery Delivery
    {
        get { return (ParcelDelivery)base.Delivery; }
    }


    DeliveryStrategy IOrder.Delivery
    {
        get { return base.Delivery}
    }
}

Это все еще далеко от совершенства (ваш пример не показывает , почему базовому классу вообще нужно знать о стратегии доставки, и было бы гораздо разумнее быть обобщенным с ограничением, но это по крайней мере, позволяет использовать одно и то же имя для свойства и получить тип безопасности.

То, что в вашем примере было бессмысленным, если что-то когда-либо не правильного типа, вы не должны маскировать его нулем, которое вы должны бросить, поскольку ваш инвариант был нарушен.

поля только для чтения всегда предпочтительнее, где это возможно. Они проясняют неизменность.

1 голос
/ 03 июня 2009

Есть ли причина, по которой вам нужно изменить тип возвращаемого значения? Если этого не произойдет, я бы предложил сделать свойство Delivery виртуальным, чтобы вместо него его определяли унаследованные классы:

public abstract class Order
{
    protected Order(DeliveryStrategy delivery)
    {
        Delivery = delivery;
    }

    public virtual DeliveryStrategy Delivery { get; protected set; }
}

public class CustomerOrder : Order
{
    public CustomerOrder()
        : base(new ParcelDelivery())
    { }

    public DeliveryStrategy Delivery { get; protected set; } 
}

Если вам требуется изменить тип возвращаемого значения, тогда я бы удивился, почему вам понадобится такое резкое изменение поведения возвращаемого типа. В любом случае, если это так, то это не сработает для вас.

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

0 голосов
/ 25 февраля 2012

Использование new для теневого копирования виртуальных членов базового класса - плохая идея, потому что дочерние производные типы не смогут должным образом переопределить их. Если существуют члены класса, которые требуется скрыть в производных классах, члены базового класса не должны объявляться как abstract или virtual, а должны просто вызывать член protected abstract или protected virtual. Производный тип может скрывать метод базового класса тем, который вызывает соответствующий элемент protected и соответствующим образом приводит к результату.

...