2
votes

I am slowly converting a leagacy application into MVC style (at the moment for new code and for massively changed code). At the moment the controllers are created on demand:

FooController := TFooController.Create(GuiProvider, Model);
FooController.EditFoo();  //show Edit-Dialog for Foo

As more and more of the code is converted I want to create a main class (TMainController), that provides the controllers:

FooController := MainController.GetFooController();
FooController.EditFoo();

My problem is, that I need to access the other controllers inside one controller. (For instance I need to access BarController inside of FooController).

Any idea how to solve this cleanly? Or maybe my base idea is flawed?

Possible solutions I considered:

Solution 1 (not working):

My first idea was to provide every controller with an reference to TMainController:

//Edit-Dialog for Foo contains a button to edit Bar that belongs to Foo
procedure FooController.PerformActionEditBarOfFoo();
begin
    //do something
    BarController := FMainController.GetBarController();
    BarController.EditBar()
    //do some other things
end;

But this wont work, because of circular unit references. Because TMainController needs to know every other controller and every other controller needs a reference to TMainController.

Solution 2 (working but ugly):

A workaround to solution 1 would be to create an base class to TMainController. Then I could pass this base class to the other controllers and would need to cast to TMainController to access the other controllers:

procedure FooController.PerformActionEditBarOfFoo();
begin
    //do something
    BarController := (FMainBaseController as TMainController).GetBarController();
    BarController.EditBar()
    //do some other things
end;

This works, but this seems more like a hack.

Solution 3 (nearly impossible)

An other solution would be to provide all needed controllers at the creation of FooController. But this can be huge if BarController for instance needs other controllers and so on.

Solution 4 (impossible for me)

An other solution might be the usage of an Dependency Injection Framework like Spring. But I cannot promote such radical changes in the existing code.

2

2 Answers

2
votes

One of the solutions to avoiding circular references is to use interfaces. There are serveral ways to do this, some are simpler, some more complex depending on your existing code.

1. Solution: define IMainController interface and use it through global variable

Define MainContoller functionality you need to access in other controllers as interface in completely separate unit, and use it through global MainController variable. This is not optimal solution, but it is probably the simplest one to implement into existing codebase. It also allows implementing second solution later with minimal refactorings.

Since TMainController descends from TInterfacedObject it will be reference counted, so you have to store its reference in interface reference in order for reference counting to work properly. And you should not Free it yourself.

unit MainControllerInterfaceUnit;

interface

uses
  FooControllerUnit,
  BarControllerUnit;    

type
  IMainController = interface
    function GetFooController: TFooController;
    function GetBarController: TBarController;
  end;

var
  MainController: IMainController;

implementation

uses
  MainControllerUnit;

initialization

  MainController := TMainController.Create;

end. 

unit MainControllerUnit;

interface 

uses
  FooControllerUnit,
  BarControllerUnit,
  MainControllerInterfaceUnit;

  TMainController = class(TInterfacedObject, IMainController)
  public
    function GetFooController: TFooController;
    function GetBarController: TBarController;
  end;

Using interface and global variable will allow you to skip adding MainControllerUnit in your FooControllerUnit and only have MainControllerInterfaceUnit in your uses clause.

unit FooControllerUnit;

implementation

uses
  BarControllerUnit,
  MainControllerInterfaceUnit;

procedure TFooController.PerformActionEditBarOfFoo();
var
  BarController: TBarController;
begin
    BarController := MainController.GetBarController();
    BarController.EditBar()
end;

2. Solution: define IMainController, IFooController and IBarController interfaces and pass MainController as parameter

This is better solution since all dependencies are defined as interfaces. However, it will require more changes in existing code.

unit ControllerInterfaceUnit;

interface

type
  IFooController = interface
  ... 
  end;

  IBarController = interface
     procedure EditBar; 
  end;

  IMainController = interface
    function GetFooController: IFooController;
    function GetBarController: IBarController;
  end;

implementation

end. 

unit MainControllerUnit;

uses
  ControllerInterfaceUnit;

  TMainController = class(TInterfacedObject, IMainController)
  public
    function GetFooController: IFooController;
    function GetBarController: IBarController;
  end;

unit FooControllerUnit;

uses
  ControllerInterfaceUnit;

  TFooController = class(TInterfacedObject, IFooController)
  protected
    MainController: IMainController;
  public
    constructor Create(const AMainController: IMainController);
  end;

procedure FooController.PerformActionEditBarOfFoo();
var
  BarController: IBarController;
begin
    BarController := MainController.GetBarController();
    BarController.EditBar()
end;
2
votes

Here is a solution using generics

unit MainController;

interface

uses
  System.Generics.Collections,
  System.SysUtils;

type
  TMainController = class
  private
    FDict: TDictionary<TClass, TObject>;
  public
    constructor Create;
    destructor Destroy; override;

    function GetController<T: class>( ): T;
    procedure RegisterController<T: class>( AController: T );
  end;

implementation

{ TMainController }

constructor TMainController.Create;
begin
  inherited;
  FDict := TObjectDictionary<TClass, TObject>.Create( [ doOwnsValues ] );
end;

destructor TMainController.Destroy;
begin
  FDict.Free;
  inherited;
end;

function TMainController.GetController<T>: T;
begin
  Result := FDict[ T ] as T;
end;

procedure TMainController.RegisterController<T>( AController: T );
begin
  FDict.AddOrSetValue( T, AController );
end;

end.

unit FooController;

interface

uses
  MainController;

type
  TFooController = class
  private
    FMainController: TMainController;
  public
    constructor Create( AMainController: TMainController );
    procedure EditFoo;
  end;

implementation

uses
  BarController;

{ TFooController }

constructor TFooController.Create( AMainController: TMainController );
begin
  inherited Create;
  FMainController := AMainController;
end;

procedure TFooController.EditFoo;
begin
  FMainController.GetController<TBarController>.EditBar;
end;

end.

unit BarController;

interface

type
  TBarController = class
  public
    procedure EditBar;
  end;

implementation

{ TBarController }

procedure TBarController.EditBar;
begin
  WriteLn( Self.ClassName + '.EditBar called' );
end;

end.

program so_29916281;

{$APPTYPE CONSOLE}
{$R *.res}

uses
  System.SysUtils,
  MainController in 'MainController.pas',
  FooController in 'FooController.pas',
  BarController in 'BarController.pas';

var
  MainController: TMainController;

begin
  try

    MainController := TMainController.Create;
    try

      // Register the controller instances
      MainController.RegisterController( TFooController.Create( MainController ) );
      MainController.RegisterController( TBarController.Create );

      // test the TFooController
      MainController.GetController<TFooController>.EditFoo;

    finally
      MainController.Free;
    end;

  except
    on E: Exception do
      Writeln( E.ClassName, ': ', E.Message );
  end;
  ReadLn;

end.