In the snippet below I have two classes that have the exact same procedure names. The code within these procedures is substantially different. Is it possible to consolidate these classes down to allow for easier maintenance and future enhancements or am I going about this in an acceptable manner?
type
TMFRCore = class
private
FGridMain: TNextGrid;
FGridSummary: TNextGrid;
public
constructor Create(Grid: TNextGrid; SummaryGrid: TNextGrid);
destructor Destroy; override;
procedure SetRowColour(Grid: TNextGrid; Row: Integer; Color: TColor; Start: Integer = 0);
procedure OnSummaryDblClick(Sender: TObject);
function GetSectionId(Section: String): Integer;
property GridMain: TNextGrid read FGridMain write FGridMain;
property GridSummary: TNextGrid read FGridSummary write FGridSummary;
end;
type
TMFRDates = class(TMFRCore)
public
procedure UpdateSummary;
procedure UpdateMain(const sAircraft: string);
end;
type
TMFRHours = class(TMFRCore)
public
procedure UpdateSummary;
procedure UpdateMain(const sAircraft: string);
end;
I initialise these classes as follows;
procedure TfrmMain.FormCreate(Sender: TObject);
begin
MFRHours := TMFRHours.Create(gridHours, gridHoursSummary);
MFRDates := TMFRDates.Create(gridDates, gridDatesSummary);
end;
You can declare the methods as virtual (and probably abstract) in the base class so you can take advantage of dynamic dispatch and polymorphism, but otherwise, there isn't really anything else to consolidate. Everything common to those classes is already in the one base class. Adding those declarations will actually mean you have more code because you'd still need to keep the existing method declarations and implementations.
Related
In my specific TPersistent classes I'd like to provide a Clone function, which returns an independent copy of the object.
Is it possible to make this work correctly with descendants, without implementing the Clone function in each and every descendant?
This is not about cloning any unknown fields or deep cloning (which could be done using RTTI). In my minimal example below, you can see where I would want to put the Clone function.
Since it uses Assign() to copy the data, it would work with any descendant. The problem is the constructor, see comments. How do I call the correct constructor of that descendant? If that's very hard to do, it's okay to assume that none of the descendants override the constructor without overriding Clone, too.
program Test;
uses System.SysUtils, System.Classes;
type
TMyClassBase = class abstract(TPersistent)
public
constructor Create; virtual; abstract;
function Clone: TMyClassBase; virtual; abstract;
end;
TMyClassBase<T> = class abstract(TMyClassBase)
private
FValue: T;
public
constructor Create; overload; override;
function Clone: TMyClassBase; override;
procedure Assign(Source: TPersistent); override;
property Value: T read FValue write FValue;
end;
TMyClassInt = class(TMyClassBase<Integer>)
public
function ToString: string; override;
end;
TMyClassStr = class(TMyClassBase<string>)
public
function ToString: string; override;
end;
constructor TMyClassBase<T>.Create;
begin
Writeln('some necessary initialization');
end;
procedure TMyClassBase<T>.Assign(Source: TPersistent);
begin
if Source is TMyClassBase<T> then FValue:= (Source as TMyClassBase<T>).FValue
else inherited;
end;
function TMyClassBase<T>.Clone: TMyClassBase;
begin
{the following works, but it calls TObject.Create!}
Result:= ClassType.Create as TMyClassBase<T>;
Result.Assign(Self);
end;
function TMyClassInt.ToString: string;
begin
Result:= FValue.ToString;
end;
function TMyClassStr.ToString: string;
begin
Result:= FValue;
end;
var
ObjInt: TMyClassInt;
ObjBase: TMyClassBase;
begin
ObjInt:= TMyClassInt.Create;
ObjInt.Value:= 42;
ObjBase:= ObjInt.Clone;
Writeln(ObjBase.ToString);
Readln;
ObjInt.Free;
ObjBase.Free;
end.
The output is
some necessary initialization
42
So, the correct class came out, it works correctly in this minimal example, but unfortunately, my necessary initialization wasn't done (should appear twice).
I hope I could make it clear and you like my example code :) - I'd also appreciate any other comments or improvements. Is my Assign() implementation ok?
You don't need to make the non-generic base class constructor abstract. You can implement the clone there, because you have a virtual constructor.
Furthermore you don't need to make the Clone method virtual.
type
TMyClassBase = class abstract(TPersistent)
public
constructor Create; virtual; abstract;
function Clone: TMyClassBase;
end;
...
type
TMyClassBaseClass = class of TMyClassBase;
function TMyClassBase.Clone: TMyClassBase;
begin
Result := TMyClassBaseClass(ClassType).Create;
try
Result.Assign(Self);
except
Result.DisposeOf;
raise;
end;
end;
Note that ClassType returns TClass. We cast it to TMyClassBaseClass to make sure that we call your base class virtual constructor.
I also don't see why you made TMyClassBase<T> abstract and derived specifications from it. You should be able to implement everything you need in the generic class.
This seems to do it:
function TMyClassBase<T>.Clone: TMyClassBase;
begin
{create new instance using empty TObject constructor}
Result:= ClassType.Create as TMyClassBase<T>;
{execute correct virtual constructor of descendant on instance}
Result.Create;
{copy data to instance}
Result.Assign(Self);
end;
However, I've never seen that before - it feels very, very wrong...
I verified that it correctly initializes data of the target object and really calls the descendants constructor once. I see no problem, there is also no memory leak reported. Tested using Delphi 10.2.2. Please comment :)
Delphi has a nasty habit of duplicating code for generic classes. Even if that code is really the same, because the generic types are similar.
I want to prevent duplication for storing different classes.
In my generic container I only use Free to clean up if needed.
Suppose I have a generic container like so:
unit Unit1;
interface
uses Generics.Collections;
type
TMyContainer<T> = class(TObject)
strict private
FData: TList<T>;
public
constructor Create; virtual;
end;
I know T will often be an object. Because all objects are really TObject I don't want my container to create duplicate generic code for different types of objects.
Will the following trick work to prevent duplication?
A- Substitute the constructor with a class function:
unit Unit2;
uses Unit1;
type
TMyContainer<T> = class(Unit1.TMyContainer<T>)
public
class function Create: TMyContainer<T>; static;
end;
B: implement the class function Create like so:
class function TMyContainer<T>.Create: TMyContainer<T>;
var
X: TObject;
begin
if GetTypeKind(T) = tkClass then begin
X:= Unit1.TMyContainer<TObject>.Create;
end else begin
X:= Unit1.TMyContainer<T>.Create;
end;
TObject(Result):= X;
end;
Will this trick work to prevent the compiler from generating duplicate code for different types of objects, or will this fail because I'm using incorrect assumptions?
Note that I don't want to resort to using a non-generic store for my data.
Full sample code follows
unit Unit49;
interface
uses Generics.Collections;
type
TMyContainer<T> = class(TObject)
strict private
FData: TList<T>;
public
constructor Create; virtual;
end;
implementation
constructor TMyContainer<T>.Create;
begin
inherited Create;
FData:= TList<T>.Create;
end;
end.
Sample program
program Project85;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils,
Unit49 in 'Unit49.pas';
type
TMyContainer<T> = class(Unit49.TMyContainer<T>)
public
class function Create: TMyContainer<T>; static;
end;
{ TMyContainer<T> }
class function TMyContainer<T>.Create: TMyContainer<T>;
var
Y: T;
X: TObject;
begin
if GetTypeKind(T) = tkClass then begin
X:= Unit49.TMyContainer<TObject>.Create;
end else begin
X:= Unit49.TMyContainer<T>.Create;
end;
TObject(Result):= X;
end;
var
A: TMyContainer<TObject>;
B: TMyContainer<TLanguages>;
begin
A:= TMyContainer<TObject>.Create;
B:= TMyContainer<TLanguages>.Create;
readln;
end.
Will this trick work to prevent the compiler from generating duplicate
code for different types of objects, or will this fail because I'm
using incorrect assumptions?
No, it will not work.
Basically, compiler follows your T through whole class hierarchy and replaces it with specific type.
For start, you will have separate TList<T> code generated for both TObject and TLanguages because your container is declared as FData: TList<T>, then
your trick collection also inherits from generic T TMyContainer<T> = class(Unit49.TMyContainer<T>) and whole code in your class function is basically useless.
Compiler will generate duplicate code for Unit49.TMyContainer<TLanguages> class as well as Unit49.TMyContainer<TObject> class.
From your example it is hard to say what code duplication are you trying to avoid. If container class is as simple as you have written in your example, then all code duplication will come from TList<T> class. If you are trying to avoid that one, there is no easy way out.
Part of your problem comes from fact that you have T that can be anything. It is hard to optimize it. The most optimization you could get is using array of T for storing data and then delegating manipulation functions where you can use TObject as base for all classes and plain T for others.
How much can you gain with above also depends on which Delphi version do you use, because in most recent versions TList<T> has been optimized a bit with similar techniques.
However, if you can have separate containers for class and other types then you can achieve code folding for TObject and descendant containers using TObjectList<TObject> (or even non generic TObjectList on Windows) for storing all specific classes and implementing thin wrapper functions with typecast for any type safe functions you need. Of course, each such function will have some code generated for each specific type, but since they are just typecast wrappers that will not be as much code as it would be if you would use full TList<T> for each class type.
TMyObjectContainer<T> = class(TObject)
strict private
FData: TObjectList<TObject>;
public
constructor Create; virtual;
destructor Destroy; override;
function Data(index: integer): T;
end;
constructor TMyObjectContainer<T>.Create;
begin
inherited;
FData := TObjectList<TObject>.Create;
end;
constructor TMyObjectContainer<T>.Create;
begin
FData.Free;
inherited;
end;
function TMyObjectContainer<T>.Data(index: integer): T;
begin
Result := T(FData.Items[index]);
end;
This question is OOP-patterns related (but i'm working with delphi IDE).
I'm developing a library that needs a sort of memory management on their objects (without garbage collection nor reference counting).
I'm thinking on making initialization/finalization methods public to the users knowing that construction/destruction methods will be called automatically (allowing users to clean the object memory without destroying the object for instance). Is there a similar pattern or best practices recommended to do this?
type
TAbstractInitializable = abstract class(TAbstractBase)
constructor Create();
destructor Destroy(); override;
private
FInitialized: Boolean;
protected
procedure CheckError();
procedure DoInitialize(); virtual; abstract;
procedure DoFinalize(); virtual; abstract;
public
procedure Initialize();
procedure Finalize();
end;
//...
procedure TAbstractInitializable.CheckError();
begin
if not FInitialized then
raise Exception.Create('Error: Trying to use a non initialized object.');
end;
procedure TAbstractInitializable.Create();
begin
inherited;
DoInitialize();
end;
procedure TAbstractInitializable.Destroy();
begin
DoFinalize();
inherited;
end;
procedure TAbstractInitializable.Initialize();
begin
if not FInitialized then begin
DoInitialize();
FInitialized:=True;
end;
end;
procedure TAbstractInitializable.Finalize();
begin
if FInitialized then begin
DoFinalize();
FInitialized:=False;
end;
end;
So this could be a library non abstract class:
type
TDataModel = class(TAbstractInitializable)
private
FDataList: TList;
protected
procedure DoInitialize(); override;
procedure DoFinalize(); override;
public
procedure DoSomethingWithData();
end;
//...
procedure TDataModel.DoInitialize();
begin
FDataList:=TList.Create();
LoadData(FDataList); //for non emptyness in this example
end;
procedure TDataModel.DoFinalize();
begin
FreeData(FDataList); //for no memory leaks in this example
FDataList.Destroy();
end;
procedure TDataModel.DoSomethingWithData();
begin
CheckError();
//Do something here
end;
I don't know Delphi, but I think having public methods for initializing and deinitializing objects is the right way to go. For example in any sort of file stream object you'll usually see an open() and close() method. Although they're not always automatically called, it gives the client the ability to manage those behaviors explicitly instead of relying on the class to implicitly initialize and cleanup after itself. In the case of a file handle, you don't want to couple releasing a file handle to garbage collection because garbage collection is often difficult to predict.
I would not make your initialize() and finalize() methods public. If the language was designed for these methods to be used by clients, they would already be public, and in all the libraries I've used, I've never seen something like that. I would write two differently named methods that are descriptive of the action your performing (e.g. create() & destroy()). Your initialize() and finalize() may invoke these methods respectively.
I offloaded all ADO hood in a separate Data Module, so a single module can be referred by several applications. All my applications basically need only two worker methonds to access data:
AdoQuery delivers a result set in a form of TADODataSet.
AdoExecute performs simple update/delete queries without fetching any results.
Here is the class structure:
type
TMyDataModule = class(TDataModule)
procedure DataModuleCreate(Sender: TObject);
procedure DataModuleDestroy(Sender: TObject);
private
procedure pvtAdoConnect;
procedure pvtAdoExecute(const sql: string);
function pvtAdoQuery(const sql: string): TADODataSet;
public
AdoConnection: TADOConnection;
end;
Then I added two publicly exposed wrappers to class methods. I used that to avoid long class references in the calls:
function AdoQuery(const sql: string): TADODataSet;
procedure AdoExecute(const sql: string);
implementation
function AdoQuery(const sql: string): TADODataSet;
begin
Result := MyDataModule.pvtAdoQuery(sql);
end;
Above are the worker function which I call from within all my forms.
AdoConnect runs only once on DataModuleCreate event. TDatModule derived from TPersistent, which allows to persist the single instance of connection throughout a runtime.
The only thing that annoys me so far - is a useless .DFM which I don't need at all.Is there any option to get rid of it?
I would handle this type of thing in one of two ways, with interfaces or with inheritance. I prefer not to expose classes to the outside world in these cases. The second one could almost be called an interface without interfaces :)
Interfaces
This version returns an interface that includes the required methods. The outside world only needs to use the interface. We keep the implementation details private. Our TMyDBClass implements the interface that we have exposed to the outside world and our global function GetDBInterface returns the single instance.
interface
uses
ADODB;
type
IMyDBInterface = interface
['{2D61FC80-B89E-4265-BB3D-93356BD613FA}']
function AdoQuery(const sql: string): TADODataSet;
procedure AdoExecute(const sql: string);
end;
function GetDBInterface: IMyDBInterface;
implementation
type
TMyDBClass = class(TInterfacedObject, IMyDBInterface)
strict private
FConnection: TADOConnection;
protected
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
public
function AdoQuery(const sql: string): TADODataSet;
procedure AdoExecute(const sql: string);
end;
var
FMyDBInterface: IMyDBInterface;
procedure TMyDBClass.AdoExecute(const sql: string);
begin
// ...
end;
function TMyDBClass.AdoQuery(const sql: string): TADODataSet;
begin
// ...
end;
procedure TMyDBClass.AfterConstruction;
begin
inherited;
FConnection := TADOConnection.Create(nil);
end;
procedure TMyDBClass.BeforeDestruction;
begin
FConnection.Free;
inherited;
end;
// Our global function
function GetDBInterface: IMyDBInterface;
begin
if not Assigned(FMyDBInterface) then
FMyDBInterface := TMyDBClass.Create;
Result := FMyDBInterface;
end;
initialization
finalization
FMyDBInterface := nil;
end.
Inheritance
This version uses a base class that has the required methods. This is a bit easier for people to deal with because it excludes the interface which can be complex to people starting out. Again we hide the implementation details from the user and only expose a shell of a class that includes the two methods we want people to access. The implementation of these methods is performed by a class in the implementation that inherits from the exposed class. We also have a global function that returns the instance of this class. The big advantage that the interface approach has over this approach is that the user of this object can't free the object by accident.
interface
uses
ADODB;
type
TMyDBClass = class(TObject)
public
function AdoQuery(const sql: string): TADODataSet; virtual; abstract;
procedure AdoExecute(const sql: string); virtual; abstract;
end;
function GetDBClass: TMyDBClass;
implementation
type
TMyDBClassImplementation = class(TMyDBClass)
strict private
FConnection: TADOConnection;
protected
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
public
function AdoQuery(const sql: string): TADODataSet; override;
procedure AdoExecute(const sql: string); override;
end;
var
FMyDBClass: TMyDBClassImplementation;
procedure TMyDBClassImplementation.AdoExecute(const sql: string);
begin
inherited;
// ...
end;
function TMyDBClassImplementation.AdoQuery(const sql: string): TADODataSet;
begin
inherited;
// ...
end;
procedure TMyDBClassImplementation.AfterConstruction;
begin
inherited;
FConnection := TADOConnection.Create(nil);
end;
procedure TMyDBClassImplementation.BeforeDestruction;
begin
FConnection.Free;
inherited;
end;
// Our global function
function GetDBClass: TMyDBClass;
begin
if not Assigned(FMyDBClass) then
FMyDBClass := TMyDBClassImplementation.Create;
Result := FMyDBClass;
end;
initialization
FMyDBClass := nil;
finalization
FMyDBClass.Free;
end.
Usage
Usage of these are really easy.
implementation
uses
MyDBAccess; // The name of the unit including the code
procedure TMyMainForm.DoSomething;
var
myDataSet: TADODataSet;
begin
myDataSet := GetDBInterface.AdoQuery('SELECT * FROM MyTable');
...
// Or, for the class version
myDataSet := GetDBClass.AdoQuery('SELECT * FROM MyTable');
...
end;
If you don't have any design-time non-visual components dropped onto your data module, and don't plan to ever do so, then you shouldn't need a data module at all. The whole purpose is for design-time components, and other implementations such as a Web Module or even a Windows Service Application. But not for wrapping pure code without design-time components.
Also, as mentioned in the comments, don't be confused about the meaning of TPersistent. This class is used entirely differently, and can be integrated into the IDE Object Inspector (as sub-properties within a component).
So the ideal thing for you to do is encapsulate everything in just a single class. For your purpose, a database connection...
type
TMyData = class(TObject)
private
FConnection: TADOConnection;
public
constructor Create;
destructor Destroy; override;
procedure pvtAdoConnect;
procedure pvtAdoExecute(const sql: string);
function pvtAdoQuery(const sql: string): TADODataSet;
...
end;
implementation
{ TMyData }
constructor TMyData.Create;
begin
FConnection:= TADOConnection.Create(nil);
end;
destructor TMyData.Destroy;
begin
FConnection.Connected:= False;
FConnection.Free;
inherited;
end;
As for the interpretation of being "persistent", you can create/destroy an instance of it in many ways. For example, you could use a unit's initialization and finalization sections (requiring CoInitialize) or you can have your main form initialize a global instance upon creation.
One common way of doing so is to add...
interface
function MyData: TMyData;
implementation
var
_MyData: TMyData;
function MyData: TMyData;
begin
if not Assigned(_MyData) then
_MyData:= TMyData.Create;
Result:= _MyData;
end;
initialization
_MyData:= nil;
finalization
_MyData.Free;
end.
The first time you call MyData from anywhere, a new global instance will be instantiated. Then, every further time it re-uses the same instance. This also solves the need of ActiveX and CoInitialize etc. because at this point, COM is expected to already be instantiated (which is required for ADO).
Usage of this unit would be extremely simple - use include it in the uses anywhere, and access its instance through the MyData function.
Notes
You should get out of the habit of global variables. This is asking for trouble down the road when trying to do later work. The example above shows how to accommodate for a global object instance. All other variables should be self-contained within that object, or in general one of scope / relevance. The whole control of your TADOConnection should be within here, including connecting/disconnecting, exception handling, assigning the connection string.
In case you might be interested in an alternative without DataModules alltogether, have a look at this:
https://github.com/stijnsanders/xxm/blob/master/Delphi/demo2/03%20Data%20ADO/xxmData.pas
Queries are stored in a single .sql file, which is handy to edit it in specific SQL editors or workbenches. Queries are separated with a line with --"QueryName", and loaded in a query-store on start-up. Assuming you query for smaller recordsets most of the time, the best lock and open style is read-only and static, which offers the best possible performance and least load on the database server. Getting field values uses the Collect call which also offers a little performance gain.
I am having some trouble figuring out this case of inheritance.
In my class TBalans, I have a routine Initialiseer that takes a TBalPar object as parameter. TBalPar is the ancestor class of TNewBalPar that has additional fields. Now I would like to reach the additional fields from within my TBalans class. I still can feed a TNewBalPar object to the Initialiseer routine, but how do I get to the data of the descendant class?
What I tried is the following: I derived TBalans too into TNieuweBalans, gave it the new additional fields, and assign them in the routine:
type
TBalPar = class
//some vars
end;
TNewBalPar = class(TBalPar)
ExtraVar: TValue;
end;
TBalans = class
MyBalPar: TBalPar;
function Initialiseer(ABalPar: TBalPar): Boolean; virtual;
end;
TNieuweBalans = class(TBalans)
MyBalPar: TNewBalpar; //declared again so I don't need to cast it when using it
MyExtraVar: TValue;
function Initialiseer(ABalPar: TBalPar): Boolean; override;
end;
function TBalans.Initialiseer(ABalPar: TBalPar): Boolean;
begin
MyBalPar := ABalPar;
end;
function TNieuweBalans.Initialiseer(ABalPar: TBalPar): Boolean;
begin
inherited;
MyBalPar := TNewBalPar(ABalPar);
MyExtraVar := MyBalPar.ExtraVar; //instead of casting TNewBalPar(MyBalPar).ExtraVar
end;
This code works, but it feels wrong: I declare the MyBalPar field twice. I would like to improve on it.
Note that I am not looking for a way how to expose ExtraVar to the outside world, but how to use it conveniently within TNieuweBalans.
How can I eliminate the double MyBalPar field but still prevent frequent typecasting?
Current design
The need for a convenient designated derived field type for an ancestral field is not forbidden, nor uncommon for that matter. But your implementation, like you sense already, has some problems:
the doubled fields require unnecessary memory,
you need to synchronize changes to TBalans.MyBalPar and TNieuweBalans.MyBalPar,
you need to synchronize changes to TNieuweBalans.MyBalPar.ExtraVar and TNieuweBalans.MyExtraVar,
you do not enforce the derived class type: feeding a TBalPar object to TNieuweBalans.Initialiseer results in an access violation because MyBalPar.ExtraVar does not exist.
There are multiple ways to overcome each of these problems.
The most elementary solution to prevent extra fields is to provide properties for them with getters that extract the values from the inherited class (I renamed some of your types and variables for comprehensibility):
type
TBalPar = class(TObject)
// some variables
end;
TBalParEx = class(TBalPar)
private
FExtra: TValue;
public
property Extra: TValue read FExtra write FExtra;
end;
TBalance = class(TObject)
private
FBalPar: TBalPar;
public
function Initialize(ABalPar: TBalPar): Boolean; virtual;
property BalPar: TBalPar read FBalPar;
end;
TBalanceEx = class(TBalance)
private
function GetExtra: TValue;
procedure SetExtra(Value: TValue);
public
function BalPar: TBalParEx;
function Initialize(ABalPar: TBalPar): Boolean; override;
property Extra: TValue read GetExtra write SetExtra;
end;
function TBalanceEx.BalPar: TBalParEx;
begin
Result := TBalParEx(inherited BalPar);
end;
function TBalanceEx.GetExtra: TValue;
begin
Result := BalPar.Extra;
end;
procedure TBalanceEx.SetExtra(Value: TValue);
begin
BalPar.Extra := Value;
end;
With this approach, there is only one typecast needed and it does not require additional storage.
To enforce TBalanceEx.BalPar to be of type TBalParEx, you could raise an exception in the Initialize routine:
function TBalance.Initialize(ABalPar: TBalPar): Boolean;
begin
FBalPar := ABalPar;
Result := True;
end;
function TBalanceEx.Initialize(ABalPar: TBalPar): Boolean;
begin
if ABalPar is TBalParEx then
Result := inherited Initialize(ABalPar)
else
raise Exception.Create('Wrong BalPar type');
end;
Of course, this places the sole responsibility of a correct class functioning on the requirement to always call the Initialize routine before any other usage of the other class members. Since that is what initialization obviously is intended for, you could ignore that, but protection against misuse could be added like:
TBalance = class(TObject)
protected
function HasBalPar: Boolean; virtual;
...
TBalanceEx = class(TBalance)
protected
function HasBalPar: Boolean; override;
...
function TBalance.HasBalPar: Boolean;
begin
Result := FBalPar is TBalPar;
end;
function TBalance.Initialize(ABalPar: TBalPar): Boolean;
begin
FBalPar := ABalPar;
Result := HasPalBar;
end;
function TBalanceEx.GetExtra: TValue;
begin
if HasBalPar then
Result := BalPar.Extra
else
Result := nil;
end;
function TBalanceEx.HasBalPar: Boolean;
begin
Result := BalPar is TBalParEx;
end;
function TBalanceEx.Initialize(ABalPar: TBalPar): Boolean;
begin
Result := inherited Initialize(ABalPar);
if Result = False then
raise Exception.Create('Initialization went wrong');
end;
procedure TBalanceEx.SetExtra(Value: TValue);
begin
if HasBalPar then
BalPar.Extra := Value;
end;
In turn, this requires not to forget to implement HasBalPar for each derived class. You could 'protect' against that with:
TBalance = class(TObject)
strict private
function HasBalPar: Boolean;
private
...
TBalanceEx = class(TBalance)
strict private
function HasBalPar: Boolean;
private
...
Design considerations
All in all, making this a robust design requires some work. And your current approach raises the question why you would want to have the Extra field in the TBalanceEx class too. Even why to have a TBalanceEx class at all.
From the naming of your classes, I assume you have the following equivalent: A structure which has structural parameters like build date, owner, location, and you have a specialized structure, say a castle, with additional parameters like the number of towers and whether it has a moat:
TStructureData: Location, BuildDate
TCastleData: Location, BuildDate, TowerCount, HasMoat
TStructure: StructureData
TCastle: StructureData, CastleData
The question you need to answer is whether a structure needs to know if it is a castle, or a palace, a warehouse, a biological or chemical structure. Assume your program evolves to being able to handle all different kinds of structures, then you are always bound to add two classes to your program, resulting in a more and more complex and improvised design which in the end will get you in trouble, if not already. The challenge is to make this a more generalized and abstract design.
For example:
must TStructureData and TStructure be separate classes?
could calculations, analysations, or presentational requests on the specific data be 'outsourced' to the specific class? E.g.: if you add a GetFeatures routine to the TStructureData class, then the TStructure class can request the features of a TCastle without knowing it being a Castle.
...
Think big.