When refactoring a large monolithic executable written in Delphi into several executables, we ran into an unanticipated issue with what I am calling the fragile abstract factory (anti) pattern, although the name is possibly not a perfect fit. To get started, have a look at the following small program that illustrates the issue.
program FragileFactory; uses MyClass in 'MyClass.pas', GreenClass in 'GreenClass.pas', //RedClass in 'RedClass.pas', SomeProgram in 'SomeProgram.pas'; var C: TMyClass; begin C := TMyClass.GetObject('TGreenClass'); writeln(C.ToString); C.Free; C := TMyClass.GetObject('TRedClass'); // oh dear… that’s going to fail writeln(C.ToString); C.Free; end.
unit MyClass; interface type TMyClass = class protected class procedure Register; public class function GetObject(ClassName: string): TMyClass; end; implementation uses System.Contnrs, System.SysUtils; var FMyClasses: TClassList = nil; { TMyObjectBase } class procedure TMyClass.Register; begin if not Assigned(FMyClasses) then FMyClasses := TClassList.Create; FMyClasses.Add(Self); end; class function TMyClass.GetObject(ClassName: string): TMyClass; var i: Integer; begin for i := 0 to FMyClasses.Count – 1 do if FMyClasses[i].ClassNameIs(ClassName) then begin Result := FMyClasses[i].Create; Exit; end; Result := nil; end; initialization finalization FreeAndNil(FMyClasses); end.
unit GreenClass; interface uses MyClass; type TGreenClass = class(TMyClass) public function ToString: string; override; end; implementation { TGreenClass } function TGreenClass.ToString: string; begin Result := 'I am Green'; end; initialization TGreenClass.Register; end.
What happens when we run this?
C:\src\fragilefactory>Win32\Debug\FragileFactory.exe I am Green Exception EAccessViolation in module FragileFactory.exe at 000495A4. Access violation at address 004495A4 in module ‘FragileFactory.exe’. Read of address 00000000.
Note the missing TRedClass in the source. We don’t discover until runtime that this class is missing. In a project of this scale, it is pretty obvious that we haven’t linked in the relevant unit, but once you get a large project (think hundreds or even thousands of units), it simply isn’t possible to manually validate that the classes you need are going to be there.
There are two problems with this fragile factory design pattern:
- Delphi use clauses are fragile (uh, hence the name of the pattern). The development environment frequently updates them, typically they are not organised, sorted, or even formatted neatly. This makes validating changes to them difficult and error-prone. Merging changes in version control systems is a frequent cause of errors.
- When starting a new Delphi project that utilises your class libraries, ensuring you use all the required units is a hard problem to solve.
Typically this pattern will be used with in a couple of ways, somewhat less naively than the example above:
- There will be an external source for the identifiers. In the project in question, these class names were retrieved from a database, or from linked resources.
- The registered classes will be iterated over and each called in turn to perform some function.
Of course, this is not a problem restricted to Delphi or even this pattern. Within Delphi, any unit that does work in its initialization section is prone to this problem. More broadly, any dynamically linking registry, such as COM, will have similar problems. The big gotcha with Delphi is that the problem can only be resolved by rebuilding the project, which necessitates rollout of an updated executable — much harder than just re-registering a COM object on a client site for example.
How then do we solve this problem? Well, I have not identified a simple, good clean fix. If you have one, please tell me! But here are a few things that can help.
- Where possible, use a reference to the class itself, such as by calling the class’s ClassName function, to enforce linking the identified class in. For example:
C := TMyClass.GetObject(TGreenClass.ClassName); C := TMyClass.GetObject(TRedClass.ClassName);
- When the identifiers are pulled from an external resource, such as a database, you have no static reference to the class. In this case, consider building a registry of units, automatically generated during your build if possible. For example, we automatically generate a registry during build that looks like this:
unit MyClassRegistry; // Do not modify; automatically generated initialization procedure AssertMyClassRegistry; implementation uses GreenClass, RedClass; procedure AssertMyClassRegistry; begin Assert(TGreenClass.InheritsFrom(TMyClass)); Assert(TRedClass.InheritsFrom(TMyClass)); end; end.
These are not assertions that are likely to fail but they do serve to ensure that the classes are linked in. The AssertMyClassRegistry function is called in the constructor of our main form, which is safer than relying on a use clause to link it in.
- Units that can cause this problem can be identified by searching for units with initialization sections in your project (don’t forget units that also use the old style begin instead of initialization — a helpful grep regular expression for finding these units is (?s)begin(?:.(?!end;))+\bend\.). This at least gives you a starting point for making sure you test every possible case. Static analysis tools are very helpful here.
- Even though it goes against every encapsulation design principle I’ve ever learned, referencing the subclass units in the base class unit is perhaps a sensible solution with a minimal cost. We’ve used this approach in a number of places.
- Format your use clauses, even sorting them if possible, with a single unit reference on each line. This is a massive boon for source control. We went as far as building a tool to clean our use clauses, and found that this helped greatly.
In summary, then, the fragile abstract factory (anti) pattern is just something to be aware of when working on large Delphi projects, mainly because it is hard to test for: the absence of a unit only comes to light when you actually call upon that unit, and due to the fragility of Delphi’s use clauses, unrelated changes are likely to trigger the issue.
One. This may help lifted from D6 code.
initialization
// after adding messages had stack this up
// windows need loaded before classes {old comments for uses clause order}
Classes.RegisterClass(Tpfxxxx);
tpfxxx normally dropped on form
//here was loaded in runtime
Also have runtime things that determine the parent and assign
Pat
In my factories I use a simple type reference list for the clients to use so that all of the types that the factories will use are defined at compile time in one place.
I add the following to FragileFactory.pas:
type
TMyClasses = (mcGreen, mcRed);
TMyClassDef = record
MyClassID: TMyClasses;
MyClassType: PTypeInfo;
end;
var
MyClassDef: TMyClassDef;
MyClassDefs: TList;
Initialization
MyClassDefs := TList.Create;
MyClassDef := TMyClassDef.Create;
MyClassID := mcGreen;
MyClassType := TGreenClass;
MyClassDefs.Add(MyClassDef);
MyClassID := mcRed;
MyClassType := TRedClass;
MyClassDefs.Add(MyClassDef);
Then, of course, instead of looping by class name in GetObject, I create an instance of each class by using RTTI (TRTTIType.Invoke) based on the pTypeInfo of the corresponding enumeration that is passed in.
C := TMyClass.GetObject(mcRed);
writeln(C.ToString);
C.Free;