Как избавиться от двунаправленной связи

В этой статье описывается прием, который я использую для борьбы с сабжем.

По сути это реализация рефакторинга Change Bidirectional Association to Unidirectional, описанного в книге Мартина Фаулера Refactoring: Improving the Design of Existing Code (купить эту книгу). Правда я пришел к нему сам, но это неважно.

Итак нам понадобится проект с двунаправленной связью. Допустим нам нужно, чтобы по клику на кнопке в форме 2 что-то происходило в форме 1. Например увеличивалось число в поле ввода.

  1. Создадим новый проект и назовем его, например, BidirectionalAssociation.
  2. Сохраним проект главную форму назовем, как ни странно, MainForm.
    1. Разместим на ней поле ввода.
  3. Добавим еще одну форму IncForm.
    1. Разместим на ней кнопку.
  4. Теперь нам надо, чтобы по клику на кнопке на IncForm в поле ввода на MainForm добавлялась единица.

Любой Delphi программист средней руки, как я например :), тут же предложит вам добавить формы друг другу в uses. Но только в разные его части, чтобы небыло циркулярной ссылки.

Давайте так и сделаем. И реализуем заявленную функциональность.

unit MainFormUnit;
 
interface
 
uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, IncFormUnit, Buttons;
 
type
  TMainForm = class(TForm)
    Edit1: TEdit;
    BitBtn1: TBitBtn;
    procedure BitBtn1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;
 
var
  MainForm: TMainForm;
 
implementation
 
{$R *.dfm}
 
procedure TMainForm.BitBtn1Click(Sender: TObject);
var
  IncForm: TIncForm;
begin
  IncForm :=  TIncForm.Create(nil);
  try
    IncForm.ShowModal;
  finally
    IncForm.Free;
  end;
end;
 
end.
unit IncFormUnit;
 
interface
 
uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, Buttons;
 
type
  TIncForm = class(TForm)
    BitBtn1: TBitBtn;
    procedure BitBtn1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;
 
implementation
uses
  MainFormUnit;
{$R *.dfm}
 
procedure TIncForm.BitBtn1Click(Sender: TObject);
var
  Val: Integer;
begin
  if TryStrToInt(MainForm.Edit1.Text, Val) then
    MainForm.Edit1.text := IntToStr(Val + 1);
end;
 
end.

«Какая красота, все работает» - думает программист, но когда приходит его начальник, то он получает по шее за двунаправленную связь. За высокую связность и еще раз, для закрепления.

Как теперь от этого избавиться, ведь нам всеравно нужно обращаться к главной форме.

Элементарно - воспользуемся паттерном посетитель.

Сама двунаправленная связь получилась по тому, что программист думал, что IncForm должна изменять что-то в главной форме, но это не так все что она должна - получать откуда-то какое-то значение инкрементировать его и записывать обратно, а откуда оно взялось её не должно волновать.

Создадим новый модуль SystemInterfaces.pas и опишем выше сказанное в виде интерфейса.

unit SystemInterfaces;
 
interface
 
type
  IValueVisitor = interface
    ['{DF62F6A6-0E41-41D4-9A8B-8DE0E25B4A66}']
    function GetValue: string;
    procedure SetValue(const S: string);
  end;
 
implementation
 
end.

Теперь выходит, что IncForm вовсе не нужна главная форма, ей нужен тот, кто сможет дать и записать значение, то есть тот, кто реализует вышеописанный интерфейс.

Нужный интерфейс может передаваться в конструкторе, или через свойство. Я предпочитаю определять метод Execute по аналогии с TOpenDialog.

Так и сделаем:

  TIncForm = class(TForm)
    BitBtn1: TBitBtn;
    procedure BitBtn1Click(Sender: TObject);
  private
    { Private declarations }
    FValueVisitor: IValueVisitor;
  public
    { Public declarations }
    function Execute(AValueVisitor: IValueVisitor): Boolean;
  end;

Уберём связь с MainForm и исправим обработчик кнопки. В итоге модуль выглядит так:

 
unit IncFormUnit;
 
interface
 
uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, Buttons, SystemInterfaces;
 
type
  TIncForm = class(TForm)
    BitBtn1: TBitBtn;
    procedure BitBtn1Click(Sender: TObject);
  private
    { Private declarations }
    FValueVisitor: IValueVisitor;
  public
    { Public declarations }
    function Execute(AValueVisitor: IValueVisitor): Boolean;
  end;
 
implementation
{$R *.dfm}
 
procedure TIncForm.BitBtn1Click(Sender: TObject);
var
  Val: Integer;
begin
  if Assigned(FValueVisitor) then
  begin
    if TryStrToInt(FValueVisitor.GetValue, Val) then
     FValueVisitor.SetValue(IntToStr(Val + 1));
  end;
end;
 
function TIncForm.Execute(AValueVisitor: IValueVisitor): Boolean;
begin
  FValueVisitor := AValueVisitor;
  ShowModal;
end;
 
end.

Аллилуя связь разорвана.

Теперь надо восстановить функциональность. Кто же будет тем самым посетителем, который умеет выдавать и принимать значение? - Конечно MainForm :). Для этого она должна реализовать нужный интерфейс. Делаем, благо все потомки TComponent умеют это делать.

Осталось передать себя (это ключ ко всему) форме в методе Execute - и все, никаких повышенных связностей. В итоге модуль выглядит так:

unit MainFormUnit;
 
interface
 
uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, IncFormUnit, Buttons, SystemInterfaces;
 
type
  TMainForm = class(TForm, IValueVisitor)
    Edit1: TEdit;
    BitBtn1: TBitBtn;
    procedure BitBtn1Click(Sender: TObject);
  private
    { Private declarations }
    function GetValue: string;
    procedure SetValue(const S: string);
  public
    { Public declarations }             
  end;
 
var
  MainForm: TMainForm;
 
implementation
 
{$R *.dfm}
 
procedure TMainForm.BitBtn1Click(Sender: TObject);
var
  IncForm: TIncForm;
begin
  IncForm :=  TIncForm.Create(nil);
  try
    IncForm.Execute(Self);
  finally
    IncForm.Free;
  end;
end;
 
function TMainForm.GetValue: string;
begin
  Result := Edit1.Text;
end;
 
procedure TMainForm.SetValue(const S: string);
begin
  Edit1.Text := S;
end;
 
end.

Теперь форму IncForm можно использовать во множестве мест легко и просто.