C#: объект, имеющий два конструктора: как ограничить, какие свойства задаются вместе?


Предположим, у вас есть объект Price, который принимает либо (int quantity, decimal price), либо строку, содержащую "4/$3.99". Есть ли способ ограничить, какие свойства могут быть установлены вместе? Не стесняйтесь поправить меня в моей логике ниже.

Тест: A и B равны друг другу, но пример C не должен быть разрешен. Таким образом, вопрос Как обеспечить, чтобы все три параметра не вызывались, как в Примере C?

AdPrice A = new AdPrice { priceText = "4/$3.99"};                        // Valid
AdPrice B = new AdPrice { qty = 4, price = 3.99m};                       // Valid
AdPrice C = new AdPrice { qty = 4, priceText = "2/$1.99", price = 3.99m};// Not

Класс:

public class AdPrice {
    private int _qty;
    private decimal _price;
    private string _priceText;

В конструкторы:

    public AdPrice () : this( qty: 0, price: 0.0m) {} // Default Constructor
    public AdPrice (int qty = 0, decimal price = 0.0m) { // Numbers only
        this.qty = qty;
        this.price = price; }

    public AdPrice (string priceText = "0/$0.00") { // String only
        this.priceText = priceText; }

Методы:

    private void SetPriceValues() {
       var matches = Regex.Match(_priceText, 
           @"^s?((?<qty>d+)s?/)?s?[$]?s?(?<price>[0-9]?.?[0-9]?[0-9]?)");
       if( matches.Success) {
           if (!Decimal.TryParse(matches.Groups["price"].Value, 
                                 out this._price))
               this._price = 0.0m;
           if (!Int32.TryParse(matches.Groups["qty"].Value, 
                                 out this._qty))
               this._qty = (this._price > 0 ? 1 : 0);
           else
               if (this._price > 0 && this._qty == 0)
                   this._qty = 1; 
    }  }

    private void SetPriceString() {
        this._priceText = (this._qty > 1 ? 
                               this._qty.ToString() + '/' : "") +
            String.Format("{0:C}",this.price);
    }

Методы Доступа:

    public int qty { 
        get { return this._qty; } 
        set { this._qty = value; this.SetPriceString(); } }
    public decimal price { 
        get { return this._price; } 
        set { this._price = value; this.SetPriceString(); } }
    public string priceText { 
        get { return this._priceText; } 
        set { this._priceText = value; this.SetPriceValues(); } }
}
5 4

5 ответов:

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

Если priceText и price/qty оба должны быть изменяемыми внешне, тогда при каких условиях вы считаете инициализацию объекта завершенной: -

AdPrice C = new AdPrice();
C.qty = 4;
C.priceText = "2/$1.99";
C.price = 3.99m

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

Мое предложение состояло бы в том, чтобы сделать собственность priceText частной. Для инициализации объекта с помощью строки потребуется использовать соответствующий конструктор.

Хммм.... вместо того чтобы бороться с компилятором, возможно, Вам просто нужно переосмыслить свой API. Вы рассмотрели следующее:

  • Никаких сеттеров. Ваш класс должен быть неизменяемым, чтобы он полностью инициализировался через конструктор и никогда не мог быть инициализирован в недопустимом состоянии.

  • Если вы настаиваете на сеттерах, то ваше свойство PriceText может быть прочитано только тогда, когда другие читаются/пишутся. По крайней мере, при этом вам не нужно проверять текст, переданный в это собственность.

  • Возможно, полностью удалить свойство PriceText, переопределить строковое представление вашего объекта .метод toString.

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

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

Для Строковой части будет полезно иметь статический метод, который анализирует строку и возвращает экземпляр Price.

Например

Price newPrice = Price.FromString("4/$3.99");
Console.WriteLine("{0} qty for {1}", newPrice.Quantity, newPrice.Price);

Здесь FromString является статическим методом.
Это то же самое, что и Enum.Parse, Если вы хотите увидеть пример.

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

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

var employee = new Employee { Id = 42, FirstName = "John", LastName = "Doe" };

На мой взгляд, это действительно плохой дизайн. Какова семантика следующего кода?

var employee = new Employee();
Это просто объект без каких-либо атрибутов. Бесполезный. Я считаю, что было бы гораздо лучше обеспечить, чтобы у сотрудника было по крайней мере удостоверение личности. предоставление конструктора по умолчанию. Есликаждый экземпляр сотрудника требует значения для других свойств, зависит от фактического контекста, но если они имеют эти свойства, конечно, должны стать аргументами конструктора.
var employee = new Employee(42);