A somewhat ugly solution would be triggering a runtime exception early on, precluding any use of their descendent classes unless they did the implementation:
TMyBaseClass = class
public function MyFunction : string; virtual; abstract;
public procedure AfterConstruction; override;
end;
....
procedure TMyBaseClass.AfterConstruction; override;
begin
MyFunction();
inherited;
end;
This is providing that the function itself is pure ( not changing the object state and not requiring much of the object state ) and cheap at execution costs.
Some optimizations might also include one-time testing only for the first instantiation and binding this to debug-builds only.
TMyBaseClass = class
public function MyFunction : string; virtual; abstract;
public procedure AfterConstruction; override;
private class var MyFunction_tested: Boolean;
end;
....
procedure TMyBaseClass.AfterConstruction; override;
begin
{$IfOpt D+}
if not MyFunction_tested then begin
MyFunction();
MyFunction_tested := true;
end;
{$EndIf}
inherited;
end;
UPDATE.
Classes are obtained by calling "GetClass" function.
GetClass('TMyDescendantClass').Create
So, you use late binding, where the program core (and compiler) do not actually know which classes they would be extended with by plugins made by third-party developers. Granted, practically you may have a fixed list of plugins and a fixed list of their developers, but for the program structure that makes no difference. You program just is not defined yet during compilation phase, only the specific set of runtime plugins defines it completely.
That means you really can not test it all when compiling your core program. Any person at any later time can implement a brand new plugin and jack it in. Your Delphi just cannot see into the future. So you do have to resort to runtime checking. That is just how plugins-based LEGO-like applications work.
The problem now becomes how to arrange that runtime check, so it would be as "greedy" as possible (failing always => failing early: during in-house testing phase, rather than few months after deployment) and at the same time it would consume as little runtime resources as possible.
Actually, checking for one and only one specific function seems questionable idea per se. Using RTTI you might check if ANY abstract function still remains.
Granted, using RTTI is relatively slow, but I think loading BPL files from HDD would be slower anyway, so it matters little here.
Personally I think you should separate two different operations - obtaining the class and instantiating it. Just like it is done in Java and DotNet runtimes, where there are dedicated class checkers/verifiers in-between these operations.
TmpClass := GetClass('TMyDescendantClass');
if not TmpClass.InheritsFrom( TMyBaseForm ) then
raise EPluginSystemMisconfiguration.Create(.....);
MyPluginFormClass := TMyBaseForm( TmpClass );
VerifyPluginClass( MyPluginFormClass ); // immediately raises exception if class is not valid
MyPluginForm := MyPluginFormClass.create(Application);
What checks should VerifyPluginClass implement is up to you. But one of the checks should be if the class has ANY abstract non-implemented functions.
See How i can determine if an abstract method is implemented?
If possible, try to establish practice of regular unit-testing or integration-testing - then that very VerifyPluginClass subroutine would be reused in the testing framework, providing your co-developers to catch this kind of their errors themselves.