开发者

C#: Object having two constructors: how to limit which properties are set together?

Say you have a Price object that accepts either an (int quantity, decimal price) or a string containing "4/$3.99". Is there a way to limit which properties can be set together? Feel free to correct me in my logic below.

The Test: A and B are equal to each other, but the C example should not be allowed. Thus the question How to enforce that all three parameters are not invoked as in the C example?

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

The class:

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

The constructors:

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

The Methods:

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

The Accessors:

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


Hmmmm.... instead of fighting the compiler, maybe you just need to rethink your API. Have you considered the following:

  • No setters. Your class should be immutable, so that its fully initialized through the constructor and can't ever be initialized in an invalid state.

  • If you insist on setters, then your PriceText property can be readonly while the others are read/write. At least in doing this, you don't need to validate text passed into that property.

  • Maybe remove the PriceText property entirely, override the string representation of your object in the .ToString method.

The last option is the best approach in my opinion. I don't think users should be passing in pseudo-serialized strings to your class, because it requires parsing and validating -- and the burden should really be on the client using your class than your class itself.


How about making the setters in the properties private and add methods to change the prices or even without methods so the prices would only be set while instantiating new object.


For the string part, it will be good to have a static method which parses the string & returns a Price instance.

e.g.

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

Here, FromString is a static method.
This is same as Enum.Parse if you'd like to see an example.


If you are just going to be able to assign values directly to the properties as you are doing and have a default constructor why bother with constructors at all?

If the priceText and price/qty both need to be mutable externally then under what conditions would you consider object initialization to be complete:-

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

The above is the same code as you are using just without the "syntatic sugar". In order to prevent your example from occuring you would need to be able to prevent the above.

My suggestion would be to make the priceText property private. To initialise the object using a string one would be required to use the appropriate constructor.


I suggest to make the property setters private and just provide two constructors - one accepting a quantity and a price and one accepting the textual representation. In my opinion your price type has a value type semantics and should therefore be immutable.

Further I believe object initializers are heavily overused. After calling a constructor a object should be fully initialized and in a consistent state satisfying all invariants. You can enforce this by providing a well designed set of constructors but you usually can not enforce this by providing a default constructor and relying on the usage of object initializers. The consumer is free to just call the default constructor and do nothing else.

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

In my opinion this is really bad design. What is the semantics of the following code?

var employee = new Employee();

It is just an object without any attributes. Useless. I believe it would be much better to enforce that an employee has at very least an id by not providing a default constructor. If every employee instance requires a value for other properties depends on the actual context but if they have this properties should of course become constructor arguments.

var employee = new Employee(42);
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜