开发者

OO Programming Style - virtual property, constructor parameter or protected setter

If I have a base class (MyBase in the example below) that needs subclasses to provide/override a value (Value), is there a preferred way of implementing this? I can think of several ways of doing it, but don't have much of a feel for why I would choose one over another. I've got some examples (in C#, but they could be any OO language).

Concrete property in base class - Subclasses can set this value if they wish.

public class MyBase
{
    protected string Value { get; set; }

    protected MyBase()
    {
        Value = "Default Value";
    }

    private DoSomethingWithValue()
    {
         Console.WriteLine(Value);
    }
}

public class MySub : MyBase
{
    public MySub()
    {
        Value = "Overridden Value";
    }
}

Virtual Property - Subclasses can override this if they wish.

public class MyBase
{
    protected virtual string Value { get { return "Default Value"; } }

    protected MyBase()
    {
    }

    private DoSomethingWithValue()
    {
         Console.WriteLine(Value);
    }
}

public class MySub : MyBase
{
    protected override string Value { get { return "Overridden Value"; } }

    public MySub()
    {
    }
}

Property set in base class constructor - Subclasses may provide a value开发者_如何学JAVA.

public class MyBase
{
    protected string Value { get; private set; }

    protected MyBase(string value)
    {
         Value = value;
    }

    protected MyBase() : this("Default Value")
    {
    }

    private DoSomethingWithValue()
    {
         Console.WriteLine(Value);
    }
}

public class MySub : MyBase
{
    public MySub() : base("Overridden Value")
    {
    }
}

Obviously some of these allow varying the value or computing it dynamically. However, in the situation that the value is known at compile-time, which way is preferable and why?


If the value is known at compile-time for all potential derived classes, why on earth are you making it stored at runtime?

More importantly, it's a total waste to invoke virtual lookup here when you know that they'll all just return "constant";, so I'd go for the concrete property in base.


Ignore all the OO gibberish and start off with a simple structure with public data members. This is the least work, simplest to understand, simplest to use, and most powerful. When you better understand the context of use and are sure the class actually represents an abstraction, then apply conservative restrictions and observe carefully where your code breaks (if anywhere).

Unless you have absolute proof backed by decades of heavy mathematics, do not use abstractions initially. You must be totally convinced your abstraction is sound and complete before you isolate the representation from the client. Make sure to state, in assertions or at least comments, the invariants of the representation. If you cannot do this you do not understand enough to create an abstraction.

For example (in C++, sorry):

class Rational {
  int numerator; int denominator;
public: 
  Rational (int x, int y) { 
    if (y == 0) { throw DivisionByZero; }
    assert (y != 0);
    if (y<0) { x = -x; y = -y; }
    assert (y>0);
    int d = gcd(x,y);
    assert(d>0); // proof, from specs of gcd
    // assert: if there exists an integer q>0 which divides both x/d and y/d
    // then q == 1 (from definition of gcd)

    assert (x % d == 0); // d divides x exactly (from defn of gcd)
    assert (y % d == 0); // d divides y exactly (from defn of gcd)

    numerator = x/d;
    denominator = y/d;

    // assert: provided y != 0, numerator / denominator (math div, not C div)
    // equals input x/ input y (math div, not C div).

    // invariant: denominator > 0, gcd (numerator, denominator) == 1
    // Theorem: representation is unique
  }

  bool eq(Rational other) { 
    numerator == other.numerator && denominator == other.denominator;
    // sufficient by uniqueness theorem
  }
}:

In this particular case you should note the computational cost of ensuring the representation invariant is preserved, particularly when implementing, say, addition. Perhaps this isn't a good idea. It all depends on what your interface looks like and how you're going to use it. Preserving the invariant has one useful property computationally, it prevents avoidable overflows on multiplication.

In this case the representation is a standard canonical form, with the following type:

R: int * int - { x,y | y<0 or Exists d. d divides y and d divides x }

It is typical, that a representation consists of a cartesian product minus a subset to produce a set R such that

R <==> A

where A is the abstract type, is a bijection.

My point: it is not so easy to get an abstraction right. Programmers overuse strong abstraction creating devices like classes, particularly in OO languages when they have no idea that, for example, a simple stucture, which is a cartesian product, is already formally abstract (represented by functions, in this case projections).

Repairing a faulty abstraction is a lot more work in terms of adjusting not only the interface of the class, but also every usage thereof, than if you used a simple structure and systematically abstract it in a refactoring phase when you have evidence of what the abstraction really should be.

Yttrill's rule: if you are thinking of an abstraction ..dont!

You need evidence, not thought!

0

上一篇:

下一篇:

精彩评论

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

最新问答

问答排行榜