In my previous post I talked about how “namespaces” in Delphi really don’t exist for any practical purposes normally associated with the concept. Having become familiar with the concept in other languages I found I was missing them, so I devised a way to obtain some of the benefits, despite the shortcomings in the language.
Before digging in to my (self-named) Scope Elevation pattern it might further help to understand what I am trying to achieve by first looking at the first-class namespace support in Oxygene. As an alternative ObjectPascal implementation, comparing the feature in that very similar language should further help identify how and where Delphi falls short in this one respect.
As mentioned in the previous post, namespaces in Oxygene are explicit but are also required. So much so that in fact they supercede the concept of units entirely. In Oxygene unit essentially has become just another name for a file but in that file you don’t declare a unit‘s identity, rather you identify the namespace that will contain the identifiers introduced in that file. Crucially, that namespace doesn’t have to have anything to do with the name of the file/unit.
Let’s use a hypothetical (at this stage) Oxygene implementation of the
TTest class in Smoketest and compare it with the Delphi implementation. We aren’t too concerned with the specifics of the code implemented in these examples so they are not complete, but stripped down to bare minimums to illustrate the concepts of interest.
So first, the Delphi reality:
// filename: Deltics.Smoketest.Test.pas unit Deltics.Smoketest.Test; interface uses Classes; type TTest = class(TInterfacedObject) // end;
This unit introduces a new class type
TTest. Adopting the best practice of one-class-per-unit, this is the only class implemented in this unit. As a result, code in a project that needs to reference this type (e.g. to implement a set of tests, since this is the base class required to do that) must include this specific unit in its
A project implementing a set of tests must also reference the
TestRun object which is an instance of a different class,
TTestRun. Again, following the one-class-per-unit practice, this means referencing a further unit containing the
// filename: Deltics.Smoketest.TestRun.pas unit Deltics.Smoketest.TestRun; interface uses Classes; type TTestRun = class(TInterfacedObject) // end; var TestRun: TTestRun;
When implementing a test project, you need to know/remember to use
Deltics.Smoketest.Test in units that implement tests and also to use
Deltics.Smoketest.TestRun where-ever you need to reference the
// filename: MyTests.dpr program MyTests; uses Deltics.Smoketest.TestRun, MyTestCases.pas in 'MyTestCases.pas'; begin TestRun.Test(TMyTestCases); end. // ---------------------------------------- // filename: MyTestCases.pas unit MyTestCases; uses Deltics.Smoketest.Test; type TMyTestCases = class(TTest) end;
Ideally you would simply use
Deltics.Smoketest consistently and have this bring into scope the public symbols from the entire Smoketest framework without you having to know which individual files in that framework those identifiers are introduced.
Anyone familiar with .net programming will be very familiar with this convenience. Some particularly large frameworks may comprise multiple namespaces but in general, each individual namespace typically introduces many different symbols which – if you examine the source for those frameworks – are contributed from many different files. Equally in some cases some of the symbols in those files are never surfaced directly to consumers at all even though they are crucial to the internal operation of the framework.
This is not directly achievable in Delphi (without Scope Elevation) but is in Oxygene. Before getting into that however, we should first address one simple alternative: The Monolith.
We could simply lump all of our classes into one, all-encompassing unit, ensuring that non-consumable internal implementation details are confined to separate units, almost guaranteeing violation of many good practices, including the one-class-per-unit principle, introducing multiple concerns and multiple responsibilities (not necessarily in the classes, but in the physical organisation of those classes). For small, simple frameworks this may be an appropriate choice, but for larger or more complex frameworks (as measured by N of identifiers as much as N LOC) it quickly becomes unwieldy.
Compounding the problem, taking such an approach paves a tempting path to tightly coupled classes in the framework since they have ready and convenient access to each others ‘internals’.
This is the path I took with the original Deltics.Smoketest framework, 1.0 which proved to be a truly horrific mistake. Multiple attempts to simplify and improve that framework over the years were thwarted by the tight coupling that the monolith created made worse by attempts to break the monolith in some areas which really only made multiple monoliths stacked on top of each other. A significant portion of the blame falls squarely on me for the lack of discipline needed to avoid falling into the traps, but I think we’ve all been down the road of believing we need only be careful to “avoid the pitfalls” and violating our own principles will be justified and worth it “this time”. On the brighter side, this pretty much dictated the “green field” do-over for 2.x which I think is much ‘cleaner’ as a result.
So let’s now look at how these types might be realised in an Oxygene implementation to see how a modern ObjectPascal language can get the benefits of genuine namespaces.
As mentioned in the introduction, units in Oxygene do not redundantly declare their own identity but instead identify the namespace container for the symbols they introduce. This means we can stick to our principles and have one-class-per-unit (where ‘unit’ now simply means ‘file’). But in each unit we can identify the in which to place the identifiers introduced by each file. Since the namespace is not tied to the identity of the unit, different units can all identify the same namespace.
// filename: Deltics.Smoketest.Test.pas namespace Deltics.Smoketest; uses Classes; type TTest = class(TInterfacedObject) // end; end.
And then the
TTestRun class and
// filename: Deltics.Smoketest.TestRun.pas namespace Deltics.Smoketest; uses Classes; type TTestRun = class(TInterfacedObject) // end; var TestRun: TTestRun; end.
Worth pointing our here is that with the Unified Syntax introduced in Oxygene 9.x there is no need for separate
implementation sections in Oxygene (though you can still have them if you prefer and even mix/match as required).
So we still have two separate files, one each for
TTestRun, but in each case they identify the same namespace as the target scope for the symbols they introduce. Meanwhile, on the other side of the tracks, when we add to the uses clauses in our Oxygene code we identify namespaces, not units or files. References to the physical files are handled separately as a set of project references maintained as project meta-data.
For different platforms Oxygene supports different forms of project references. On .net you add references to .net assemblies or nuget packages. On Java/Android you add references to .jar files or maven packages etc etc. On all platforms you can also reference Shared Code projects, which are other Elements language source files (Swift, C#, Java, Go or Oxygene).
As a result, whether we are coding our main test project and referencing the
TestRun object or implementing a set of tests in a test case, we simply reference the
// filename: MyApp.Tests.pas namespace MyApp.Tests; uses Deltics.Smoketest; begin TestRun.Test(MyTestCases); end. // ---------------------------------------- // filename: MyApp.TestCases.pas namespace MyApp.Tests; uses Deltics.Smoketest; type MyTestCases = class(TestCase) end;
This is far from being a real-world example of an Oxygene test project and is intended only to show the namespace referencing.
Notice how in this case we don’t need to use anything in our main program code in order to reference the test case class we created for the test project since this is declared to be in the same namespace as the main program itself anyway!
You might already also have recognised that this allows potentially anyone to introduce additional identifiers into the
Deltics.Smoketest namespace by identifying that namespace in their own units. As long as the code containing those units is added to the references for a project, those additional identifiers are introduced for any existing code already using the
You might find this thought discomforting at first but again, this is a pattern that will be very familiar to .net developers where this precise technique is often used to introduce additional class extenders to assist in configuration of framework extensions (unlike Delphi type helpers, type extenders on other platforms allow multiple extenders to be in scope simultaneously).
Scope Elevation provides a way in Delphi to get almost the same level of convenience for the consumers of a framework, with the additional cost of just a little more work for the implementation of the framework itself.
If you have already looked at the Smoketest 2.x framework you may already have identified what I am talking about. The
Deltics.Smoketest unit is the Scope Elevation unit for the framework (or Pseudo-Namespace if you prefer).
Deltics.Smoketest unit contains little more than declarations of symbols that are concretely defined elsewhere in the framework:
unit Deltics.Smoketest; interface uses Deltics.Smoketest.SelfTest, Deltics.Smoketest.Test, Deltics.Smoketest.TestRun, Deltics.Smoketest.Utils; // Elevate the scope of the TTest class so that test class implementation units // need only reference Deltics.Smoketest. type TTest = Deltics.Smoketest.Test.TTest; TSelfTest = Deltics.Smoketest.SelfTest.TSelfTest; EInvalidTest = Deltics.Smoketest.Utils.EInvalidTest; const METHOD_NAME = Deltics.Smoketest.Test.METHOD_NAME; TEST_NAME = Deltics.Smoketest.Test.TEST_NAME; // This function provides read-only access to the TestRun variable maintained // in the TestRun implementation unit. function TestRun: TTestRun;
For types scope elevation simply and efficiently creates type aliases which allows code that uses
Deltics.Smoketest to reference types that are actually declared in other units without having to use those other units themselves.
The Not So Good
Constants cannot be easily aliased so we have to create duplicates which happen to share the same name.
Functions either have to be wrappers, adding an additional call-hop or requiring in-lining. Or with a bit more work you can instead declare a function variable which points to the underlying concrete function, eliminating the additional call-hop or reliance on in-lining.
Enumerations (not illustrated by Smoketest) are a bit tricky. You need to both alias the type and declare constants for the enum members that you wish to be available to consumers of your framework:
type TMyEnum = MyCompany.MyFramework.TMyEnum; const meFirstValue = MyCompany.MyFramework.meFirstValue; meSecondValue = MyCompany.MyFramework.meSecondValue; meThirdValue = MyCompany.MyFramework.meThirdValue;
This can be tedious for enums with a lot of members, but also useful in the rare case that you have certain enum members which consumers of your framework perhaps should not be using. You also need to be aware that the constants will not support an enum type prefix if you habitually used scoped enums (again, this might be a “good thing” depending on your p.o.v).
Why Bother ?
As mentioned, Scope Elevation / Pseudo-Namespace Unit requires some additional work on the part of the framework developer for what initially appears to be no benefit to themselves as it primarily serves to simplify the work for the consumers of the framework.
In practice of course often a framework developer is their own best customer so they are helping themselves by helping others.
In addition, the provisioning of a specific unit to act as the “public face” of your framework can help you stick to good software practices in the implementation details instead of compromising those principles as trade-offs against consumability.