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?
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
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
What are your thoughts? Is this a "necessary evil", or am I missing something?
In fact, it is a "grey area", depending of what you want to achieve.
ReplyDeleteIf 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.
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
ReplyDeleteTTableStorage = 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;
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.
ReplyDeleteThe 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.
Christopher Wosinski So basically, this ITableWriter is an interface for a Controller style class? As per the MVC model I mean.
ReplyDeleteThe 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.
ReplyDeleteThe Single Responsibility Principle (1 class = 1 function) is a good criteria to resolve the Inheritance Or Composition dilemma
ReplyDeleteI agree that "holding table data" and "formatting value to 2 decimal places" are different responsibilities and so they should be in differrent classes.
ReplyDeleteCreate 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.
Andris Klaipins That was an excellent video!
ReplyDeleteI'm really glad I asked this question. The answers have been really helpful!