tsmr
tsmr

Reputation: 197

TThread and inheritance

I am having a hard time implementing multi-tier inheritance from the basic TThread class.
Based on my knowledge of OOP it should be possible but maybe it just can't be applied to threads.
My goal is to use TMyBaseThread to implement all the code that will be common to descendent classes. This is what I have have tried:

TMyBaseThread = class(TThread)
private
    procedure BuildBaseObjects(aParam : TParam);
    procedure Execute; virtual; abstract;
protected
    constructor Create(Param : TParam); reintroduce; virtual;
end;

TMyFileThread = class(TMyBaseThread)
private
    procedure Execute; reintroduce;
public
    constructor Create(OtherParam : TOtherParam); reintroduce; overload;
end;

TMyDBThread = class(TMyBaseThread)
private
    procedure Execute; reintroduce;
public
    constructor Create(aDB : TDatabase); reintroduce; overload;
end;


implementation


constructor TMyBaseThread.Create(Param : TParam);
begin
    inherited Create(False);
    Self.BuildBaseObjects(Param);
    [do some stuff]
end;

constructor TMyFileThread.Create(OtherParam : TOtherParam);
var
    param : TParam;
begin
    inherited Create(param);
    [do some other stuff]
end;

procedure TMyFileThread.Execute;
begin
    while not Terminated do
        doWork(); <-- this is never called
end;


constructor TMyDBThread.Create(aDB : TDatabase);
var
    param : TParam;
begin
    inherited Create(param);
end;

procedure TMyDBThread.Execute;
begin
    while not Terminated do
        doDatabaseWork(); <-- this is never called
end;

I see in TThread's implementation that the Executed method is called automatically in the AfterConstruction but how can I have it point to the one declared in the derived classes?

Thanks!

Upvotes: 2

Views: 1859

Answers (1)

quasoft
quasoft

Reputation: 5448

First, I can't support more Craig's comment about using composition instead of inheritance for implementing the common functionality.

And although the architecture choice is under question, there is a lot to learn from your example.

Before inheriting a class, you should investigate the interface of the parent class that you want to inherit. To do that you can either look for the class definition in the interface section of the source code or look up the relevant documentation - System.Classes.TThread.

It seems you have already read the documentation, so let's take a look at an except from the class definition of TThread:

TThread = class
  private
    ...
  protected
    procedure CheckThreadError(ErrCode: Integer); overload;
    procedure CheckThreadError(Success: Boolean); overload;
    procedure DoTerminate; virtual;
    procedure Execute; virtual; abstract;
    procedure Queue(AMethod: TThreadMethod); overload;
    procedure Synchronize(AMethod: TThreadMethod); overload;
    property ReturnValue: Integer read FReturnValue write FReturnValue;
    property Terminated: Boolean read FTerminated;
  public
    constructor Create(CreateSuspended: Boolean);
    destructor Destroy; override;
    procedure AfterConstruction; override;
    procedure Resume;
    procedure Suspend;
    procedure Terminate;
    function WaitFor: LongWord;
    class procedure Queue(AThread: TThread; AMethod: TThreadMethod); overload;
    class procedure RemoveQueuedEvents(AThread: TThread; AMethod: TThreadMethod);
    class procedure StaticQueue(AThread: TThread; AMethod: TThreadMethod);
    class procedure Synchronize(AThread: TThread; AMethod: TThreadMethod); overload;
    class procedure StaticSynchronize(AThread: TThread; AMethod: TThreadMethod);
    property FatalException: TObject read FFatalException;
    property FreeOnTerminate: Boolean read FFreeOnTerminate write FFreeOnTerminate;
    property Handle: THandle read FHandle;
    property Priority: TThreadPriority read GetPriority write SetPriority;
    property Suspended: Boolean read FSuspended write SetSuspended;
    property ThreadID: THandle read FThreadID;
    property OnTerminate: TNotifyEvent read FOnTerminate write FOnTerminate;
  end;

First, ignore anything that is in the private section of the class. If those fields and methods were marked as private, we are not supposed to be able to use them at all in descendant classes.

Then, look for any abstract methods. The implementation of abstract methods is left for descendant classes. So those are the methods you are expected to implement in your code. Abstract methods are usually called indirectly by using one of the methods from the parent class.

In your case the TThread class has only one abstract method:

procedure Execute; virtual; abstract;

The documentation says that you need to

Define the thread object's Execute method by inserting the code that should execute when the thread is executed.

It's true that the documentation sounds a bit vague, but the right way to do that is to "override" the method in the interface, and not to "reintroduce" it:

  TMyFileThread = class(TMyBaseThread)
  ...
  protected
      procedure Execute; override;
  ...

and then implement it in the implementation:

procedure TMyFileThread.Execute;
begin
    while not Terminated do
      Sleep(1); // do some other stuff
end;

You probably notice how we declared the overrided definition of Execute method in the protected section. This is required as the definition of the method in the parent class is also in the protected section, so we can only override it in a section with higher visibility (protected or public).

You rarely need to increase the visibility when overriding a method, so we just keep the same visibility.

We used the override keyword to tell the base class to use this variant of the method instead of its own. If you miss the override keyword the Execute method will not be called at all, and the base class will try calling it's own Execute method, if there is any.

Another thing to note is that you don't need to redeclare the Execute method in your base class as you are not implementing it there. That's why you should remove the following definition:

TMyBaseThread = class(TThread)
...
    //procedure Execute; virtual; abstract;  <- remove this
...

The execute method is already defined in the TThread class.

Now, let's look at the constructors. The base class has a regular constructor that is neither virtual, nor dynamic:

  public
    constructor Create(CreateSuspended: Boolean);

This means you cannot override those constructors and if you want to add additional logic on object creation, you should create your own constructors that wrap those.

The proper way to do that is to just declare a constructor with a different set of parameters, without reintroducing, overloading or overriding the base one:

public
      //constructor Create(Param : TParam); reintroduce; virtual;
      constructor Create(Param : TParam);

Also, remember that constructors should almost always be in the public section.

You also don't need to make the constructor virtual. You could do that if your TMyFileThread and TMyDBThread classes needed to add some logic inside the constructor, without changing the constructor parameters.

When you change the set of parameters all that is required is that you call the inherited constructor as first thing inside the new one:

constructor TMyFileThread.Create(OtherParam : TOtherParam);
var
    param : TParam;
begin
    inherited Create(param); // It is enough to call the base constructor at the top
    // do some other stuff
end;

The definition requires no keywords:

  TMyFileThread = class(TMyBaseThread)
  ...
  public
      constructor Create(OtherParam : TOtherParam);

Did you notice how we used inherited Create(param) to call the base constructor, but we did not use inherited Execute;? That's because the Execute method was marked as abstract and have no default implementation in the base class. Using inherited on an abstract method would cause an exception as there is no default method to call.

As a general rule calling inherited Create is a must if the base constructor you are calling is marked as virtual, but is almost always required even if it is not marked so.

Finally, I want to formulate a summary of the relevant keywords and their most common use:

  • virtual - declare a method that can be reimplemented in descendant classes. That is descendant classes can change the behaviour of the method. They can replace all the code inside it with their own, or can both call the base method and then add additional behaviour before/after that. This keyword is used only in the base class that defines the method for the first time. Descendant classes use the other keywords.
  • dynamic - consider it the same as virtual. Can save some memory resources, but only if the method is not reimplemented in all descendant classes and there are many objects created from such classes. Rarely used.
  • override - declare a method that reimplements the base class by providing code that replaces the default one in the base class. You use override in descendant classes. You can use the inherited keyword inside your implementation to call the base method.
  • overload - You declare an alternative variant of the method with another set of parameters. Note that overloading a method has nothing to do with inheritance. An overloaded version of the method is not executed when the base class is calling the original method. Usually overloaded methods normalize the parameters (converting them to other types, adding default values for some parameters) and then call one of the other overloaded methods. Again, this has nothing to do with inheritance and the virtual, override keywords. Although sometimes you can combine both effects.
  • reintroduce - This again is not inheritance. You use this keyword in descendant classes. The purpose is to make calls to the method from inside your class (and only calls from your class, not calls from the base class) to execute that reintroduced version of the method instead of the base one. Calls from the base class still execute the original version. Rarely used.

Having said that, here is my interpretation of your code:

interface

uses
  Classes, SysUtils;

type
  TParam = class
  end;

  TOtherParam = class
  end;

  TDatabase = class
  end;

  TMyBaseThread = class(TThread)
  private
      procedure BuildBaseObjects(aParam : TParam);
  protected
  public
      constructor Create(Param : TParam);
  end;

  TMyFileThread = class(TMyBaseThread)
  private
  protected
      procedure Execute; override;
  public
      constructor Create(OtherParam : TOtherParam);
  end;

  TMyDBThread = class(TMyBaseThread)
  private
  protected
      procedure Execute; override;
  public
      constructor Create(aDB : TDatabase);
  end;

implementation

{ TMyBaseThread }

constructor TMyBaseThread.Create(Param : TParam);
begin
    inherited Create(False);
    Self.BuildBaseObjects(Param);
    // Do some stuff
end;

procedure TMyBaseThread.BuildBaseObjects(aParam : TParam);
begin
    // Do some stuff
end;

{ TMyFileThread }

constructor TMyFileThread.Create(OtherParam : TOtherParam);
var
    param : TParam;
begin
    inherited Create(param); // Remember to initialize param somehow
    // Do some other stuff
end;

procedure TMyFileThread.Execute;
begin
    while not Terminated do
      Sleep(1);
end;

{ TMyDBThread }

constructor TMyDBThread.Create(aDB : TDatabase);
var
    param : TParam;
begin
    inherited Create(param); // Remember to initialize param somehow
end;

procedure TMyDBThread.Execute;
begin
    while not Terminated do
        Sleep(1);
end;

PS. Actually using inheritance on TThread is mostly useful for plugin architectures or task workers. You can look up for examples on that.

Upvotes: 15

Related Questions