Jump to content
Dmitry Onoshko

“Transitive” type redefinitions in interface section

Recommended Posts

Consider the following structure of a project.

  1. A unit called MyTypes. Types used throughout the project are declared here (in its interface section).
  2. A unit called UnitA. It implement useful classes, and those classes have events and methods with parameters of types from MyTypes unit. Obviously there’s a “uses MyTypes” clause in the interface section of the unit.
  3. A unit called UnitB. It uses UnitA in its interface section and implements some higher-level logic on top of what that unit contains.

Now UnitB might need to use types from MyTypes unit to interact with UnitA. Say, to store some values temporary before passing them to UnitA’s classes. And since definitions are not transitive between units, I can see two possibilities here.

 

Using all the units

UnitB uses both MyTypes and UnitA. This way it knows the types declared in both units. The disadvantage is that whoever writes UnitB should now be aware of which other units to add to “uses” clause.

 

Redefining the types in UnitA

UnitB now uses only UnitA, but UnitA has the following definitions in its interface section:

type
  TSomeType = MyTypes.TSomeType;
  TOtherType = MyTypes.TOtherType;
  ...

UnitB now only has to have UnitA in its “uses” clause. But then UnitA might have like A LOT of such “transitive” definitions, and it’s quite easy to skip some of them.

 

What are the recommendations in general and for modern Delphi particularly? Is such “transitive” definition considered OK? Does it probably have some special name then?

Share this post


Link to post
Posted (edited)

I think this wouldn't scale well, because as soon as you have more types and classes, keeping track of all the aliased (or as you say, redefined) types will become a burden, let alone a refactoring. Another consideration is that some types will have helpers (eg. enumerations or records), which cannot make an alias, so you'd end up including the unit anyway, I find it clean enough to have a MyTypes unit that contains the scope-related types and then include that unit wherever I need a type from it. It's easier to track it, and the LSP will be thankful.

Edited by havrlisan
grammar

Share this post


Link to post

Hi,

 

Few thoughts on this:

1) If MyTypes is used through out the project then no point of renaming or redefining its types in other units, it will be there and everywhere.

2) From what i understand, UnitB is to somehow can be named as UnitAEx or UnitAHighLevel, this will draw clear path of using and remind of adding UnitA where it extended version is used, the one (not/ex) called UnitB.

3) About this

26 minutes ago, Dmitry Onoshko said:

Redefining the types in UnitA

UnitB now uses only UnitA, but UnitA has the following definitions in its interface section:


type
  TSomeType = MyTypes.TSomeType;
  TOtherType = MyTypes.TOtherType;

I don't think i ever used such renaming easily as it is easily can tangle things and cause more and different problem in the future, the one you missed now, see TSomeType on the left is a rename or redefine of MyTpes.TSomeType, yes they are the same, but in you view not for the compiler, example RTTI is different for them and they are not equal ! also in some places you will see the compiler might complain about the definition, usage and passing them as parameters .... also it might broke things badly in case such units used in an Design Time package.

I see if that is needed then use it where is the impact is smallest, like in UnitB to rename types from UnitA instead of redefining types from MyType unit into UnitA, try to narrow the scope in it is needed.

 

Now to the questions

31 minutes ago, Dmitry Onoshko said:

What are the recommendations in general and for modern Delphi particularly?

I am no aware of such best practice if it is exist, i follow best practice and simple and short logic, keep it stupid simple, and also untangled as much as possible.

33 minutes ago, Dmitry Onoshko said:

Is such “transitive” definition considered OK?

OK it is, yet it depends on the case and what could be changed in the future, rarely one can see that today, i made so many mistakes, not real mistakes as this is not a mistake per se, it is just short sighting and not leaving doors open but .... not tangle many things together.

34 minutes ago, Dmitry Onoshko said:

Does it probably have some special name then?

No sure i do understand the question, but i hope my points above could give you some insights.

 

The above is my personal opinion, and i am sure many in the forum can have helpful insights on this.

Share this post


Link to post
1 hour ago, Dmitry Onoshko said:

 


type
  TSomeType = MyTypes.TSomeType;
  TOtherType = MyTypes.TOtherType;
  ...

 

This kind of approach is usually seen when types previously declared in that unit have been moved to another one. Otherwise existing code would no longer compile.

 

My personal preference would be to add all needed units to the uses clause. As always there might be exceptions with valid reasons.
 

Share this post


Link to post

I have used something similar, but not for normal types but for nested types.

unit UnitA;

interface

uses
  MyTypes;

type
  TSomeType = class(TSomeOtherClass)
  public
    type
      TSomeNestedType = MyTypes.SomeType;
end;

The idea is to reference the nested type instead of the type declared in MyTypes, so if the nested type was changed later to something else (but similar enough as to not require code changes), the other code doesn't need to be updated.

 

Hm, thinking about it now, with a more modern Delphi than 2007 (which I used when I wrote this), I would probably use Generics instead.

Share this post


Link to post

If MyTypes is used throughout the whole project, why would you bother it UnitB is using it too.

Otherwise take a walk and restructure the whole thing.

Share this post


Link to post

Thanks, guys. I felt something was a bit wrong with such definitions in spite of them being quite an obvious alternative.

7 hours ago, Kas Ob. said:

TSomeType on the left is a rename or redefine of MyTpes.TSomeType, yes they are the same, but in you view not for the compiler, example RTTI is different for them and they are not equal !

Frankly speaking, this part is quite unexpected for me, since I never had a chance to dive deep into RTTI, and the “classical” Pascal treated types T1 and T2 equal whenever there ould be a T1 = T2 declaration. I think I understand the reasons, but I’ll have to read more on topic to be sure.

Share this post


Link to post
12 hours ago, Dmitry Onoshko said:
19 hours ago, Kas Ob. said:

TSomeType on the left is a rename or redefine of MyTpes.TSomeType, yes they are the same, but in you view not for the compiler, example RTTI is different for them and they are not equal !

Frankly speaking, this part is quite unexpected for me, since I never had a chance to dive deep into RTTI, and the “classical” Pascal treated types T1 and T2 equal whenever there ould be a T1 = T2 declaration. I think I understand the reasons, but I’ll have to read more on topic to be sure.

Well, hold your horse here for minute.

 

I might made a mistake here, or did i ?

Things seems a little different between Delphi 2010 and XE8, so it might be different for you with your version.

My mistake is between these 

type
  TSomeType = MyTypes.TSomeType;

type
  TSomeType = type MyTypes.TSomeType;

the latter does have different RTTI for sure, but for the first is does not, and that is my mistake, yet helpers is confused as hell between the two type and my two already old Delphi versions compiler, one allow it and the other is buggy and confuse them, and from there my mistake did come.

 

i rarely used similar approach for such renaming or like this

var
  LDefObject: TObject;
  LObject: TMyObject;
  LMyObjectEx: TMyObjectEx absolute LObject;
begin
  Memo1.Lines.Add(LDefObject.HelperName);
  Memo1.Lines.Add(LObject.HelperName);
  Memo1.Lines.Add(LMyObjectEx.HelperName);

To have my own string helper with casting and without losing the default string helper, and i don't know if this is working on the newer versions, yet now i trying to add a helper for string on my Delphi 2010 and it does allow it 

Share this post


Link to post
On 7/19/2024 at 1:41 PM, Uwe Raabe said:
  On 7/19/2024 at 12:37 PM, Dmitry Onoshko said:

 


type
  TSomeType = MyTypes.TSomeType;
  TOtherType = MyTypes.TOtherType;
  ...


This kind of approach is usually seen when types previously declared in that unit have been moved to another one. Otherwise existing code would no longer compile.

 

My personal preference would be to add all needed units to the uses clause. As always there might be exceptions with valid reasons.
 

I like to use such an approach especially if a topic is divided into a set of several related units, which tend to rely on or build a common type system.
In my opinion this helps to point out the internal relations, to better organize the units and to avoid circular references.
In that case I have defined one main "access" unit, which works as a fascade to cover a few closely related internal units,
while in the other sub-units different implementation details are encapsuled.

The idea is, that the main "access" unit provides all necessary classes and types in one single unit, with no need to include many other sub-units.

This helps to separate various sub-concerns nicely, and bring the complete functionality in one place.
That way, of course I still can use the sub-units separately, if there is any need.
Perhaps this is another philosophical question, or is there another rule of programming that speaks hardly against this?

 

Share this post


Link to post
27 minutes ago, Rollo62 said:

The idea is, that the main "access" unit provides all necessary classes and types in one single unit, with no need to include many other sub-units.

This helps to separate various sub-concerns nicely, and bring the complete functionality in one place.
That way, of course I still can use the sub-units separately, if there is any need.
Perhaps this is another philosophical question, or is there another rule of programming that speaks hardly against this?

 

There is no "general rule" against. However I have had a case where the developer have had a similair idea where an access unit was introduced that used the "smaller" units where various forms, features were implemented. Fast forward 20 years and this design resulted a hell of circular references. The best way to solve it is to not use the access unit but directly add everywhere the required units and use the classess and functions directly as needed having zero circular reference. This made the compiling more stable (less errors in the IDE), parallel to this I have had to kill unit scope names. 

Share this post


Link to post
27 minutes ago, Lajos Juhász said:

parallel to this I have had to kill unit scope names

Amen.

Share this post


Link to post
Posted (edited)
41 minutes ago, Lajos Juhász said:

units where various forms, features were implemented. Fast forward 20 years and this design resulted a hell of circular references. The best way to solve it is to not use the access unit but directly add everywhere the required units and use the classess and functions directly as needed having zero circular reference.

In general you are right, but maybe I have not explained the setup good enough.
I want to define a sub-system, which might be a little more complex, so that I can divide it into several sub-units,
to modularize the whole task and to avoid monolithic classes.

The main "access" unit shall only provide access to this sub-system, but of coarse the main purpose is to avoid any circular references in the first place.

The modularization helps to separate concerns only needed within this sub-system and improves testability.

 

You are right that code tends to get entangled over the years, but to entangle and repair only this "one" sub-system is much easier IMHO,
than to entangle these main separate sub-units, which are maybe cluttered wildly all over the project.
I use also a clear naming convention that supports the whole context, for example like:
MyTask.Types

MyTask.Main.Interfaces

MyTask.Main.Implementations

MyTask.Sub.Interfaces

MyTask.Sub.Implementations
 

MyTask   <= I use this as the main accessor unit
containing

type

    ETypes = MyTask.Types.ETypes;

    TTypes = MyTask.Types.TTypes;
    IMain = MyTask.Main.Interfaces.IMain;

    ISub   = MyTask.Main.Interfaces.ISub;


    TMyMain_Factory = class

        function New_Main : IMain;

        function New_Sub   : ISub;

    end;

....


This way, I only need MyMain unit to handle the tasks from outside.
When I just need lightweight access to the types or interfaces in other classes,
I still can use MyTask.Types, MyTask.Sub.Interfaces or MyTask.Main.Interfaces without any harm.
Not sure if this would make any big benefit.

The whole implementation details shall be well hidden behind the MyTask unit.

 

Just roughly throw the code in here, to get a better picture of what I meant.

The whole thing needs of coarse strict discipline too, not to use side-effects, which is probably the weak point.

My argument is, if you use this approach it shall be guaranteed that you never get any circular reference issues.
While using loosely couples units, you're never sure if you see any circular or other side-effects.

 

 

Edited by Rollo62

Share this post


Link to post
7 minutes ago, Rollo62 said:

MyTask   <= I use this as the main accessor unit

I'd make

MyTask -> MyTask.Common

MyTask.Main.Implementation -> MyTask.Main

but it's just my personal preferecne

 

Share this post


Link to post
Posted (edited)

This is of coarse a question of personal naming preferences.


"Common" and "Implementation" sounds to me too much like implementation details.
Thats why I personally like
MyTask

and all other related details are defined after, like
MyTask.Types
MyTask.Intf
MyTask.Impl
MyTask.Factory

My goal is, that when using MyTask, I would never consider anything of the "details" above, in the best case.
Thats also why its the shortest name and only talking about the task itself.

 

And yes, there surely can be different naming variants too, like MyTask.Utils, wherever this makes sense to use them separately.

 


 

Edited by Rollo62

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×