В этой статье описывается прием, который я использую для борьбы с сабжем.
По сути это реализация рефакторинга Change Bidirectional Association to Unidirectional, описанного в книге Мартина Фаулера Refactoring: Improving the Design of Existing Code (купить эту книгу). Правда я пришел к нему сам, но это неважно.
Итак нам понадобится проект с двунаправленной связью. Допустим нам нужно, чтобы по клику на кнопке в форме 2 что-то происходило в форме 1. Например увеличивалось число в поле ввода.
Любой 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
можно использовать во множестве мест легко и просто.