Reputation: 499
In our organisation we would like to force layered structure of source code. Each unit would be in certain "level" and units would be able to use only units in same or lower level.
Example:
Suppose we have these units: L1_A.pas
, L1_B.pas
, L2_C.pas
, L2_D.pas
, L3_E.pas
(LX_
means "level X").
L1_A
and L1_B
can use each other. L2_C
and L2_D
can use all L1_*
units and each other. L3_E
can use all other units.
If L1_*
unit tried to use either L2_*
unit or L3_*
unit, we need to abort compilation and produce some error ("Unit in lower level tried to use unit in higher level").
Have we coded in C (or other language with preprocessor), we would for example define LEVEL_1
, LEVEL_2
, LEVEL_3
constants and in all 1st (resp. 2nd) level units check if eihter LEVEL_2
or LEVEL_3
(resp. LEVEL_3
) constants were defined in which case we would emit relevant error.
Delphi defines (defined by {$DEFINE}
) don't have effect outside unit in which they were defined. Named constants and constant expressions can be used outside, but which one we see depends on the order of units in uses
(ie if L1_A
defines const Level=1
and L2_C
const Level=2
and L1_B
contains using L2_C, L1_A
than Level
in L1_B
would be 2
).
I came up only with naming convention [LX_Unit.pas
(or LX.Unit.pas), where X
is level and Unit
is "real" unit name], and checking with a script in svn
commit hook.
We would like to use only basic Delphi (no external tools).
Upvotes: 5
Views: 284
Reputation: 116110
Like Rudy said, you can use normal constants for that, and repeat them in each unit.
Note that units can be included in the interface section as well as the implementation section. Including a level 3 unit in the implementation section of a level 2 unit, doesn't bring the level 3 constant in scope of the level 2 unit's interface section. For that reason, to be completely safe, you'd need to set the constant in the interface section (to expose it in the first place), and check for the constants in the implementation section.
I would put at least the check, and maybe the constant as well, in included files. In each unit, you can include the file for that level.
So every level 1 unit would contain, in its implementation section, after the uses clause:
{$include level1check.inc}
In that file, you can do the check, and let the compiler bail using a fairly elegant FATAL message directive. level1check.inc would then look something like this:
{$IF Declared(level2)}
{$MESSAGE FATAL 'Cannot include a level 2 unit in a level 1 unit'}
{$ENDIF}
{$IF Declared(level3)}
{$MESSAGE FATAL 'Cannot include a level 3 unit in a level 1 unit'}
{$ENDIF}
I wonder though, what is keeping a developer from cheating? If they can modify the uses clause, they can also remove or circumvent the check. It's something that you may want to enforce in your build pipeline, but if you're doing that, maybe there are more elegant ways of checking this, for instance, by simply getting the level1, level2 and level3 unit names, and scanning the uses clauses of each level1 and level2 unit for illegal units.
Upvotes: 6
Reputation: 28806
But you can check for constants. It doesn't matter if several units define the same one, and what their type or value is, as long as its name matches a certain pattern. In a way, such a constant is like an "exported" $define
:
const
Level3 = 3;
uses
X, Y, Z; // Z.Level3 hides Y.Level3, but that doesn't matter.
{$IF declared(Level3)}
As the documentation states:
Declared returns True if the argument passed to it is a valid declared Delphi identifier visible within the current scope.
Upvotes: 9
Reputation: 8243
In L1*.pas include
CONST Level1 = TRUE;
Then in other units use
{$IF Declared(Level1) }
or
{$IF NOT Declared(Level1) }
etc.
You can probably figure out the rest from there :-)
Upvotes: 5