Recent

Author Topic: Managing circular dependencies  (Read 7774 times)

Nitorami

  • Sr. Member
  • ****
  • Posts: 481
Managing circular dependencies
« on: February 21, 2017, 07:57:13 pm »
My program has a a central Class "MainSystem" which creates and keeps a list of worker Classes all inherited from "TNode". The Nodes and their descendants do all the work, and can interact with each other, but they also need to call central services from the MainSystem.

That requires that the TNodes know the methods of the MainSystem. Therefore on TNode.Create, I pass a pointer "Owner" which points to the MainSystem Class.
Of course, MainSystem vice versa needs to know the methods of the TNodes. To avoid circular references, I put TNodes and MainSystem into a single unit via include files.

That's fine, but I stopped working on it a year ago. When I took it up again, I wasted hours with messing up everything, mainly because Nodes and MainSystem are not encapsulated against each other. I really would like to put them into separate units but don't know how when they need to call each other. Should I pass pointers to the individual MainSystem services on TNode.Create, rather than to the Class as such? Is that good practice ? Any alternatives ?

jacmoe

  • Full Member
  • ***
  • Posts: 249
    • Jacmoe's Cyber SoapBox
Re: Managing circular dependencies
« Reply #1 on: February 21, 2017, 08:10:40 pm »
They don't need to know about MainSystem if MainSystem implements an interface. They only need to know the mehods of that interface.
more signal - less noise

howardpc

  • Hero Member
  • *****
  • Posts: 4144
Re: Managing circular dependencies
« Reply #2 on: February 21, 2017, 08:13:45 pm »
  Any alternatives ?

One alternative is to have a global instance of MainSystem. Something like this:
Code: Pascal  [Select][+][-]
  1. unit uMainSystem;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. type
  8.   TMainSystem = class
  9.     // all your stuff here
  10.   end;
  11.  
  12. var
  13.   MainSystem: TMainSystem = nil;
  14.  
  15. implementation
  16.  
  17. initialization
  18.   MainSystem:=TMainSystem.Create;
  19.  
  20. finalization
  21.   MainSystem.Free;
  22.   MainSystem:=nil;
  23.  
  24. end.

Nitorami

  • Sr. Member
  • ****
  • Posts: 481
Re: Managing circular dependencies
« Reply #3 on: February 21, 2017, 08:24:57 pm »
Thank you -

@howardpc: I need to avoid globals because I want to run several independent instances of MainSystem in separate threads, all with their individual TNodes assigned to them. Just to increase performance. It is a Monte Carlo simulation and can be parallelized quite well.

@JacMode: I have not ventured into interfaces so far. Maybe that is an option. Will have a closer look.

jacmoe

  • Full Member
  • ***
  • Posts: 249
    • Jacmoe's Cyber SoapBox
Re: Managing circular dependencies
« Reply #4 on: February 21, 2017, 08:26:16 pm »
Or at least an abstract base class.  ;)
more signal - less noise

Nitorami

  • Sr. Member
  • ****
  • Posts: 481
Re: Managing circular dependencies
« Reply #5 on: February 21, 2017, 08:47:30 pm »
Hm, abstract base Class... would that mean I define an abstract predecessor of TMainSystem somewhere in a commonly shared unit, with all methods required by the TNodes already defined as abstract methods ?

jacmoe

  • Full Member
  • ***
  • Posts: 249
    • Jacmoe's Cyber SoapBox
Re: Managing circular dependencies
« Reply #6 on: February 21, 2017, 09:02:21 pm »
Er, yes.

I am not sure sure if it has to be an interface, but in my world (mainly C++), that would definitely work, since TMainSystem is a TAbstractMainSystem (or IMainSystem ).
I nicked that idea from LazPaint.

It would be nice if some Lazarus wizard could confirm that this would be a good idea or not.  ;)
more signal - less noise

lainz

  • Hero Member
  • *****
  • Posts: 4460
    • https://lainz.github.io/
Re: Managing circular dependencies
« Reply #7 on: February 21, 2017, 09:04:12 pm »
I also suggest interfaces or abstract class.

I've solved a problem like yours with that.

Interface:

https://github.com/bgrabitmap/bgracontrolsfx/blob/master/fxcontainer.pas

Code: Pascal  [Select][+][-]
  1. const
  2.   SFXDrawable = '{46c3b16f-a846-4b27-a3d3-2313cc9be63e}';
  3.  
  4. type
  5.  
  6.   IFXDrawable = interface
  7.     [SFXDrawable]
  8.     procedure FXDraw;
  9.     procedure FXPreview(aCanvas: TCanvas);
  10.   end;

Code: Pascal  [Select][+][-]
  1. if Controls[i].GetInterface(SFXDrawable, IFX) then
  2.         IFX.FXPreview(FCanvas);

Abstract class:

https://github.com/bgrabitmap/bgracontrols/blob/master/bcthememanager.pas
https://github.com/bgrabitmap/bgracontrols/blob/master/bcdefaultthememanager.pas

Code: Pascal  [Select][+][-]
  1. TBCThemeManager = class(TComponent)
  2.   private
  3.  
  4.   protected
  5.  
  6.   public
  7.     procedure Apply(AControl: TWinControl); virtual abstract;
  8.     procedure Apply(); virtual abstract;
  9.   published
  10.  
  11.   end;

Code: Pascal  [Select][+][-]
  1.  TBCDefaultThemeManager = class(TBCThemeManager)
  2.   ...
  3. procedure Apply(AControl: TWinControl); override;
  4.     procedure Apply(); override;
  5. ...

Then you can use the unit anywhere:

Code: Pascal  [Select][+][-]
  1. unit BCButton;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. uses
  8.   ...
  9.   BCThemeManager;
« Last Edit: February 21, 2017, 09:06:07 pm by lainz »

jacmoe

  • Full Member
  • ***
  • Posts: 249
    • Jacmoe's Cyber SoapBox
Re: Managing circular dependencies
« Reply #8 on: February 21, 2017, 09:37:21 pm »
Thanks a lot, lainz - then he can choose what works best for his project.  ;D
more signal - less noise

Nitorami

  • Sr. Member
  • ****
  • Posts: 481
Re: Managing circular dependencies
« Reply #9 on: February 24, 2017, 07:22:26 pm »
Thank you for the examples. I found that interfaces may be a bit over the top for my low level simulation core, and tried the abstract base class first. I got it working indeed, but it does not look too elegant. I'll set out to explain below in case somebody is willing to follow me through this and possibly tell me how to do it better.

Problem:
MainSystem creates and owns TNodes and descendants, and manages them in lists. Vice versa, the TNodes and descendants require services from TMainSystem. Due to these dependancies, I put all into a single unit, which now becomes messy. I would like to encapsulate the Nodes from the MainSystem.

Approach: abstract base Class for MainSystem

Defined a globally available abstract base Class of TMainSystem, called TAbstractSystem.
Obviously this Class needs to declare abstract virtual methods for all services the TNodes require.
A descendant of TNode is TEvent - nothing to do with system events. The MainSystem keeps a sorted list of TEvent instances. The sorting criterion is a variable in TEvent. It can change, in which case the TEvent instance will request to have itself re-sorted in the MainSystem list by calling Owner.ResortEventList (self). So the bottom would look like:

unit FTA_Abstract;
type TAbstractSystem = Class abstract
           procedure ResortEventList (Event: pointer); virtual; abstract;

--- > Problem: The pointer should be a TEvent, but TEvent is unknown at this place. I can only use a generic pointer.
Then we have

unit FTA_Nodes uses FTA_Abstract
  type TNode = Class
     var Owner: TAbstractSystem;

  type TEvent = Class (TNode);
     var SortingCriterion: int64;

and finally

unit FTA_Systems uses FTA_Abstract, FTA_Nodes
  type TMainSystem = Class (TAbstractSystem)
       procedure ResortEventList (Event: pointer); override;
     
Obviously I must keep using "pointer" because inherited methods must declare the same parameters as their ancestor. Due to this, the implementation of ResortEventList does not know TEvent's sorting criterion, and I must explicitly type cast the pointer to a TEvent. That's not very nice. Also, it defeats Pascal's type checking and I could by negligence pass any pointer and cause all sorts of crash. I could tolerate this if it was a single occurrence but I run into the same problem in dozens of other methods.

SymbolicFrank

  • Hero Member
  • *****
  • Posts: 1313
Re: Managing circular dependencies
« Reply #10 on: February 28, 2017, 04:14:21 pm »
How do you know if the nodes still exist? Or MainSystem? Otherwise you get a null-pointer exception.

The only way to reliably do what you want is pass messages (records or strings), instead of calling functions in other threads. Through sockets is the most convenient. That does have overhead, but it won't crash all the time.

howardpc

  • Hero Member
  • *****
  • Posts: 4144
Re: Managing circular dependencies
« Reply #11 on: February 28, 2017, 07:14:22 pm »
You can use a forward declared class to implement TEvent.
However, I don't see why you need separate TNode and TEvent classes.
If TEvent exists only in order to sort a collection of TNode, then the design in the attached project might work for you.

Mr.Madguy

  • Hero Member
  • *****
  • Posts: 844
Re: Managing circular dependencies
« Reply #12 on: March 01, 2017, 09:13:51 am »
Short answer - both MainSystem and TEvent must be either abstract classes or interfaces.

This is standard problem, all programmers encounter sooner or later. Strange thing about Delphi/FPC - you can cross reference two classes, if they're declared in one unit, but can't, if they're declared in two different units. C/C++ is better in this case, cuz "#include MyUnit.h" - works differently. It doesn't try to also recursively include all units, MyUnit depends on - it just adds "flat" declarations to c/cpp file - all other work is being done by linker. It simply works as if two units would be merged into single one. In Delphi/FPC you can't do it - therefore large amount of extra work is needed to achieve the same result.

Then this
Code: Pascal  [Select][+][-]
  1. unit MyUnit;
  2.  
  3. interface
  4.  
  5. type
  6.     TSystem = class;
  7.  
  8.     TEvent = class
  9.         protected
  10.             FSystem:TSystem;
  11.     end;
  12.  
  13.     TSystem = class
  14.         public
  15.             procedure DoSomething(AEvent:TEvent);
  16.     end;
  17.  
  18. implementation
  19. end.
  20.  

must be converted into this
Code: Pascal  [Select][+][-]
  1. unit AbstractClasses
  2.  
  3. interface
  4.  
  5. type
  6.     TAbstractSystem = class;
  7.  
  8.     TAbstractEvent = class
  9.         protected
  10.             FSystem:TAbstractSystem;
  11.     end;
  12.  
  13.     TAbstractSystem = class
  14.         public
  15.             procedure DoSomething(AEvent:TAbstractEvent);virtual;abstract;
  16.     end;
  17.  
  18. implementation
  19. end.
  20.  
  21. unit EventImplementation
  22.  
  23. interface
  24.  
  25. uses AbstractClasses;
  26.  
  27. type
  28.     TEvent = class(TAbstractEvent)
  29.     ...
  30.     end;
  31.  
  32. implementation
  33. end.
  34.  
  35. unit SystemImplementation;
  36.  
  37. interface
  38.  
  39. uses AbstractClasses;
  40.  
  41. type
  42.     TSystem = class(TAbstractSystem)
  43.         public
  44.             procedure DoSomething(AEvent:TAbstractEvent);override;
  45.     end;
  46.  
  47. implementation
  48. end.
  49.  

I know, this is hard and means 2x work, that seems to be unnecessary, but this is the only way, it can be implemented. Key things here are - right structure of classes and right choice of abstraction. In my projects I've encountered even harder cases. Such as when there is one abstract class and other abstract class is inherited from it and you need to reuse implementation of first class in second class. In this case only interfaces can help. What I want to say - there are no impossible situations. You can construct any arbitrary class structure via using abstract classes and interfaces. For example if you need to sort TNodes somehow, you can get rid of accessing TNode directly - you can implement ISortable interface like this:
Code: Pascal  [Select][+][-]
  1. type
  2.     ISortable = interface
  3.         function GetSortCriterion:Int64;
  4.         property SortCriterion:Int64 read GetSortCriterion;
  5.     end;
  6.  
and then implement it in TNode. There are some other approaches, like inheriting all classes from base one, that supports Compare method, or implementing comparer interfaces.
 
If you can't do something - then your class structure is wrong.
« Last Edit: March 01, 2017, 11:51:55 am by Mr.Madguy »
Is it healthy for project not to have regular stable releases?
Just for fun: Code::Blocks, GCC 13 and DOS - is it possible?

JuhaManninen

  • Global Moderator
  • Hero Member
  • *****
  • Posts: 4459
  • I like bugs.
Re: Managing circular dependencies
« Reply #13 on: March 01, 2017, 12:19:33 pm »
Short answer - both MainSystem and TEvent must be either abstract classes or interfaces.
You people forget one important way to isolate classes: Events.
They are basically callback functions but the Pascal syntax makes them easy to use. They can use any function signature.
Every big well made Pascal program uses them and they are not restricted to GUI programming.

Nitorami, don't use name TEvent. FPC libs already has it and the name confuses with the whole event system naming conventions. Make it TEventOfNitorami if you must.

Anyway, here is a real event to be triggered when TNode changes:
Code: Pascal  [Select][+][-]
  1. type
  2.   TNodeChangedEvent = procedure(ANode: TNode) of object;
Then TNode defines:
Code: Pascal  [Select][+][-]
  1.   OnChanged: TNodeChangedEvent;
.. and in the code somewhere:
Code: Pascal  [Select][+][-]
  1.   if Assigned(OnChanged) then
  2.     OnChanged(Self);
TNode must be known by the MainSystem, an abstract base class if need be.
The MainSystem now can have a private:
Code: Pascal  [Select][+][-]
  1.   procedure ResortList(ANode: TNode);
which is then given to a newly created Node instance, maybe like:
Code: Pascal  [Select][+][-]
  1.   MyNode.OnChanged := @ResortList;
TNode does not need to know about MainSystem. It only knows that somebody wants to be notified when it changes.

See Lazarus IDE code for plenty more examples of using events.

Quote
This is standard problem, all programmers encounter sooner or later. Strange thing about Delphi/FPC - you can cross reference two classes, if they're declared in one unit, but can't, if they're declared in two different units. C/C++ is better in this case, cuz "#include MyUnit.h" - works differently. It doesn't try to also recursively include all units, MyUnit depends on - it just adds "flat" declarations to c/cpp file - all other work is being done by linker. It simply works as if two units would be merged into single one. In Delphi/FPC you can't do it - therefore large amount of extra work is needed to achieve the same result.
Amazingly the extra work improves code, I would say always.
Some 8 years ago I used to think like you. I remember writing that the restriction of circular dependencies is a PITA and hinders serious development with Pascal.
At some point I realized that after an initial PITA the code becomes better and easier to maintain.

Pascal allows circular dependencies through implementation section, but your code becomes even better if you avoid them!
Think of design patterns. They are proven maintainable solutions for common design problems. There the lines between boxes represent dependencies. The lines always point to one direction. Design patterns never have circular dependencies! However code implementations in C or Java or other languages often have them which means they don't follow the diagrams they are based on. Allowing free circular dependencies encourages to write spaghetti code which is a real PITA in lots of legacy code made by many people over the years.
So, good code design boils down to one thing: dependency management. Less dependencies means better code.
« Last Edit: March 01, 2017, 12:28:43 pm by JuhaManninen »
Mostly Lazarus trunk and FPC 3.2 on Manjaro Linux 64-bit.

Mr.Madguy

  • Hero Member
  • *****
  • Posts: 844
Re: Managing circular dependencies
« Reply #14 on: March 01, 2017, 01:44:53 pm »
You people forget one important way to isolate classes: Events.
They are basically callback functions but the Pascal syntax makes them easy to use. They can use any function signature.
Every big well made Pascal program uses them and they are not restricted to GUI programming.
Well, he has said, that he needs to call MainSystem methods from TNode object. Events would be waste of memory in this case, cuz every procedure/function of object variable implicitly stores reference to object. If you need to call many different methods of Owner - it's better to store it explicitly.

Amazingly the extra work improves code, I would say always.
Some 8 years ago I used to think like you. I remember writing that the restriction of circular dependencies is a PITA and hinders serious development with Pascal.
At some point I realized that after an initial PITA the code becomes better and easier to maintain.

Pascal allows circular dependencies through implementation section, but your code becomes even better if you avoid them!
Think of design patterns. They are proven maintainable solutions for common design problems. There the lines between boxes represent dependencies. The lines always point to one direction. Design patterns never have circular dependencies! However code implementations in C or Java or other languages often have them which means they don't follow the diagrams they are based on. Allowing free circular dependencies encourages to write spaghetti code which is a real PITA in lots of legacy code made by many people over the years.
So, good code design boils down to one thing: dependency management. Less dependencies means better code.
Yeah, when your project grows and becomes more complex - some abstraction is needed to improve it's quality. But in some simple cases, when you don't need any extensibility, cross references - are all you need. And the only problem - is size of module. I.e. all you need - to split implementation to make code more readable. Nothing else. Therefore duplicating all class declarations - is just waste of time.

First time, when I encountered this problem - was object oriented wrapper around OpenGL/Direct3D. All objects needed reference to T3DDevice, but device also needed to know about all other object types - just because all this objects were created via methods of T3DDevice in a first place. At the end - 2 levels of abstraction are needed to solve this task. First level of abstraction - implementation-independent classes, that are used by main application. Second level of abstraction - implementation-dependent classes, that are used internally by implementation.
Is it healthy for project not to have regular stable releases?
Just for fun: Code::Blocks, GCC 13 and DOS - is it possible?

 

TinyPortal © 2005-2018