Favoring composition over inheritance...

Favoring composition over inheritance...

Inheritance is meant to reduce boilerplate. What if I have an object A and I want an identical object B, except that the write method of a property should do something slightly different.

A prime example here is a data table. Imagine I have a TTable class, which is technically just a wrapper for a 2-dimensional array. I want a special case of the implementation of TTable variant, which has a property Decimals that specifies how many decimal places I wish to preserve.

For example, if LTable.Decimals was equal to 2, then LTable.Values[Col,Row] := 0.231231 would be equivalent to LTable.Values[Col,Row] := 0.23.

Using composition is quite innefficient in this case, as it would require me to hold a TTable as a private field, and then delegate an analog collection of methods and properties on the new class to that field, with the one exception of the write method of the Values property.

What are your thoughts? Is this a "necessary evil", or am I missing something?

Comments

  1. In fact, it is a "grey area", depending of what you want to achieve.
    If you look at inheritance in terms of abstraction, your example is breaking the LSP: you should be able to rely on the parent class, and not introduce any property or behavior (like truncation) which is not defined at parent level.
    If you look at inheritance in terms of implementation (i.e. let children re-use parents code), your example does make sense.
    Composition allows to uncouple the classes. And it is perhaps not so inefficient as you expect, since you just create several class instances, but the actual data is still stored within an array.

    ReplyDelete
  2. I'm no expert, but here's my take on using composition to solve your case. You'd have TTable and  TTableStorage for holding the actual data. In your case the TTableStorage implementation is the one that should expose the Decimals property. So you'd have something like

    TTableStorage  = class(...)
    protected
       function GetData(const RowIdx, ColIdx: integer): T; virtual; abstract;
       procedure SetData(const RowIdx, ColIdx: integer; const Value: T); virtual; abstract;
    public
      property Data[const RowIdx, ColIdx: integer]: T read GetData write SetData;
    end;

    TRoundedDoubleTableStorage = class(TTableStorage)
    private
       ...
    protected
       function GetData(const RowIdx, ColIdx: integer): double; override;
       procedure SetData(const RowIdx, ColIdx: integer; const Value: double); override;
    public
       property Decimals: integer read FDecimals write SetDecimals;
    end;

    TTable = class(...)
    private
       FStorage: TTableStorage;
       ...
    public
      constructor Create(const Storage: TTableStorage);

      property Values[const RowIdx, ColIdx: integer]: T read GetValue write SetValue;
    end;

    where

    procedure TRoundedDoubleTableStorage.SetData(const RowIdx, ColIdx: integer; const Value: double);
    begin
      // store value rounded to target number of decimals
      FData[RowIdx][ColIdx] := RoundDecimals(Value, Decimals);
    end;

    procedure TTable.SetValue(const RowIdx, ColIdx: integer; const Value: T);
    begin
      // forward to storage implementation
      FStorage.Data[RowIdx, ColIdx] := Value;
    end;

    ReplyDelete
  3. When you want to have different implementations of the Write method then you could leave the TTable class almost the same. The only difference is that you create the interface ITableWriter that defines a procedure Write(const AllValuesItneeds: TSomething). You create two implementations for that interface performing the different ways of writing as you described above.
    The constructor of TTable expects an implementation of ITableWriter which will be stored as a private member. The method TTable.Write performs a call to FTableWriter.Write.

    This way you compose the TTable objects. You are open for extensions without needing to modify existing units when you want to introduce a third version of writing a table.

    ReplyDelete
  4. Christopher Wosinski So basically, this ITableWriter is an interface for a Controller style class? As per the MVC model I mean.

    ReplyDelete
  5. The interface has nothing to do with MVC since we have no view here. It's just an abstraction to the writing method. The TTable class has no need to know more about the writer than that it implements the tiny ITableWriter interface.

    ReplyDelete
  6. The Single Responsibility Principle (1 class = 1 function) is a good criteria to resolve the Inheritance Or Composition dilemma

    ReplyDelete
  7. I agree that "holding table data" and "formatting value to 2 decimal places" are different responsibilities and so they should be in differrent classes.

    Create something like IValueFormatter and pass it to TTable in the constructor. Whenever you set or read the value (depending if you need to store the value already formatted), pass it through the formatter. Default formatter implementation will simply return the provided value and can be created by TTable implementations if no other formatter is provided. But one specific implementation of this interface for double values TDoubleValueDecimalsFormatter will have Decimals property and will truncate the value to specified decimal places.

    Take a look at talk "Nothing is Something" by Sandi Metz: https://www.youtube.com/watch?v=OMPfEXIlTVE
    She explains this idea WAY better than I just did.

    ReplyDelete
  8. Andris Klaipins That was an excellent video!

    I'm really glad I asked this question. The answers have been really helpful!

    ReplyDelete

Post a Comment