views:

118

answers:

5

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(); } }
}
+5  A: 

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.

Itay
The person using the object won't use functions or methods. The accessors are nice because they are methods that work like properties.
Dr. Zim
+5  A: 

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.

shahkalpesh
Creating a new instance of the object wouldn't be useful to the end user, but may be useful within the class itself.
Dr. Zim
+11  A: 

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.

Juliet
+1, perhaps even `AdPrice.TryParse` return an `AdPrice` from the string value.
sixlettervariables
Problem is they will either set by numbers or set by strings after the initial values are accepted. They must be able to set numbers or strings after the creation of the objects.
Dr. Zim
+2  A: 

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.

AnthonyWJones
Maybe a state pattern? Where setting quantity would make price invalid until price is set and vice versa? I am liking your thinking.
Dr. Zim
Maybe a Price.IsValid()? LOL
Dr. Zim
You are right about the constructors. It executes the code twice for the constructor and then the property assignments. I ended up using a state pattern on the values. Works pretty well now.
Dr. Zim
I marked this correct as it is what lead to the final answer (in the comments.) Now I have an object where I can set the Qty, which makes the price invalid, then set the Price which makes the price valid.
Dr. Zim
+2  A: 

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);
Daniel Brückner
I would use var employee = new Employee() in the sense of a Factory to create a blank valid Employee for a form to edit, then persist the resulting object as a new Employee, which would update ID and give it a unique identity. The reason for the defaults is a sort of Null Object Pattern where based on business rules, a zero price is essentially null, and yet won't throw exceptions when used.
Dr. Zim
The prices are used in the sense of the new Employee() above, where they are not null, but known to be invalid based on business rules. The trick we are trying to employ is using Price.price = 3.99 without using methods so that we can gang these together in a larger object (eight of them). It may be a bad technique LOL.
Dr. Zim