Delphi - 代理设计模式 - 接口问题

5

你好,我正在尝试在 Delphi 中使用设计模式,但是由于找不到喜欢的 Delphi 参考资料,所以我正在将 O'Reilly C# 3.0 设计模式书中的模式转换为 Delphi。但这不是问题所在。我已经按照此书创建了代理模式,但是似乎有一些 Delphi 接口、构造函数和析构函数以及对象生命周期和行为的概念我不太理解。接下来是我的代码:

unit Unit2;  

interface  

uses
  SysUtils;

type
  ISubject = interface
  ['{78E26A3C-A657-4327-93CB-F3EB175AF85A}']
  function Request(): string;
end;

  TSubject = class
  public
    function Request(): string;
    constructor Create();
  end;

  TProxy = class (TInterfacedObject, ISubject)
  private
    FSubject: TSubject;
  public
    function Request(): String;
    destructor Destroy(); override;
  end;

  TProtectionProxy = class (TInterfacedObject, ISubject)
  private
    FSubject: TSubject;
    FPassword: String;
  public
    constructor Create();
    destructor Destroy(); override;
    function Authenticate(supplied: String): String;
    function Request(): String;
  end;

implementation

{ TSubjectAccessor.TProxy }

destructor TProxy.Destroy;
begin
  if Assigned(Self.FSubject) then
    FreeAndNil(Self.FSubject);
  inherited;
end;

function TProxy.Request: String;
begin
  if not Assigned(Self.FSubject) then begin
    WriteLn('Subject Inactive');
    Self.FSubject := TSubject.Create();
  end;
  WriteLn('Subject active');
  Result := 'Proxy: Call to ' + Self.FSubject.Request();
end;

{ TSubject }

constructor TSubject.Create;
begin
  inherited;
end;

function TSubject.Request: string;
begin
  Result := 'Subject Request Choose left door' + #10;
end;

{ TProtectionProxy }

function TProtectionProxy.Authenticate(supplied: String): String;
begin
  if (supplied <> Self.FPassword) then begin
    Result := 'Protection proxy: No Access!';
  end else begin
    Self.FSubject := TSubject.Create();
    Result := 'Protection Proxy: Authenticated';
  end;
end;

constructor TProtectionProxy.Create;
begin
  Self.FPassword := 'Abracadabra';
end;

destructor TProtectionProxy.Destroy;
begin
  if Assigned(Self.FSubject) then
    FreeAndNil(Self.FSubject);
  inherited;
end;

function TProtectionProxy.Request: String;
begin
  if not Assigned(Self.FSubject) then begin
    Result := 'Protection Proxy: Authenticate first!';
  end else begin
    Result := 'Protection Proxy: Call to ' + Self.FSubject.Request();
  end;
end;

end.

以下是模式中使用的接口和类。下面是使用这些类型的代码:
program Structural.Proxy.Pattern;

{$APPTYPE CONSOLE}

uses
  SysUtils,
  Unit2 in 'Unit2.pas';

var
  subject: ISubject;

begin
  ReportMemoryLeaksOnShutdown := DebugHook <> 0;

  try
    WriteLn('Proxy Pattern' +  #10);

    try
      subject := TProxy.Create();
      WriteLn(subject.Request());
      WriteLn(subject.Request());

      subject := TProtectionProxy.Create();
      WriteLn(subject.Request());
      WriteLn(TProtectionProxy(subject).Authenticate('Secret'));
      WriteLn(TProtectionProxy(subject).Authenticate('Abracadabra'));
      WriteLn(subject.Request());

      ReadLn;      
    finally

    end;

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

只是将新的对象实例分配给接口变量是否合法?在调试中,我看到TProtectionProxy的构造函数首先被执行,然后是TProxy的析构函数。在创建TProtectionProxy之后,Authenticate('Abracadabra')应该在逻辑上得到验证,但是在调试器中,FPassword为空,而它在构造函数中被赋值了?这个问题非常令人困惑。但是当我关闭应用程序时,在析构函数中,密码存在吗? TProtectionProxy(subject)没问题,但我读到不推荐使用,但是(subject as TProtectionProxy)由于某些原因无法编译(运算符不适用...)? 我添加了析构函数,因为有FSubject字段。那可以吗? 一个字段变量可以在声明它的同一行上初始化吗,还是需要像TProtectionProxy中一样在构造函数中初始化?
我知道我在这里问了很多问题,但我没有认识到可以询问Delphi OOP方面非常熟练的人。
谢谢你。
这是对我有效的新版本。感谢您的所有帮助。
unit Unit2;

interface

uses
  SysUtils;

type
  ISubject = interface
  ['{78E26A3C-A657-4327-93CB-F3EB175AF85A}']
    function Request(): string;
  end;

  IProtected = interface
  ['{928BA576-0D8D-47FE-9301-DA3D8F9639AF}']
    function Authenticate(supplied: string): String;
  end;

  TSubject = class
  public
    function Request(): string;
  end;

  TProxy = class (TInterfacedObject, ISubject)
  private
    FSubject: TSubject;
  public
    function Request(): String;
    destructor Destroy(); override;
  end;

  TProtectionProxy = class (TInterfacedObject, ISubject, IProtected)
  private
    FSubject: TSubject;
    const FPassword: String =  'Abracadabra';
  public
    destructor Destroy(); override;
    function Authenticate(supplied: String): String;
    function Request(): String;
  end;

implementation

{ TSubjectAccessor.TProxy }

destructor TProxy.Destroy;
begin
  if Assigned(FSubject) then
    FreeAndNil(FSubject);
  inherited;
end;

function TProxy.Request: String;
begin
  if not Assigned(FSubject) then begin
    WriteLn('Subject Inactive');
    FSubject := TSubject.Create();
  end;
  WriteLn('Subject active');
  Result := 'Proxy: Call to ' + FSubject.Request();
end;

{ TSubject }

function TSubject.Request: string;
begin
  Result := 'Subject Request Choose left door' + #10;
end;

{ TProtectionProxy }

function TProtectionProxy.Authenticate(supplied: String): String;
begin
  if (supplied <> FPassword) then begin
    Result := 'Protection proxy: No Access!';
  end else begin
    FSubject := TSubject.Create();
    Result := 'Protection Proxy: Authenticated';
  end;
end;

destructor TProtectionProxy.Destroy;
begin
  if Assigned(FSubject) then
    FreeAndNil(FSubject);
  inherited;
end;

function TProtectionProxy.Request: String;
begin
  if not Assigned(FSubject) then begin
    Result := 'Protection Proxy: Authenticate first!';
  end else begin
    Result := 'Protection Proxy: Call to ' + FSubject.Request();
  end;
end;

end.

以及程序代码:

program Structural.Proxy.Pattern;

{$APPTYPE CONSOLE}

uses
  SysUtils,
  Unit2 in 'Unit2.pas';

var
  subject: ISubject;
  protect: IProtected;

begin
  ReportMemoryLeaksOnShutdown := DebugHook <> 0;

  try
    WriteLn('Proxy Pattern' +  #10);

    try
      subject := TProxy.Create();
      WriteLn(subject.Request());
      WriteLn(subject.Request());

      subject := nil;
      subject := TProtectionProxy.Create();
      WriteLn(subject.Request());
      if Supports(subject, IProtected, protect) then begin
        WriteLn(protect.Authenticate('Secret'));
        WriteLn(protect.Authenticate('Abracadabra'));
      end;
      WriteLn(subject.Request());
      ReadLn;      
    finally

    end;

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

我已经移除了所有构造函数,因为它们现在确实没有任何作用。默认的无参数构造函数是从TInrefacedObject继承而来的,对吗? 我留下了Self,我想知道为什么不应该使用它?
谢谢。
我在http://delphipatterns.blog.com/2011/02/22/proxy-2/上有完整的模式实现。

1
一些快速提示:除非必要,否则不要使用 Self.。在调用 Free(或 FreeAndNil)之前不要测试 Assigned(),Free 可以处理 nil 引用。始终在构造函数中调用 inherited;。是的,将结果的 subject 分配给对象构造的方式是正确的。但不要尝试将接口转换为对象。如果想要调用 Authenticate,请通过接口公开它。 - David Heffernan
@elector 如果 obj=nil,那么你可以安全地调用 obj.FreeFreeAndNil(obj),因为 TObject.Free 会执行 if Assigned(obj) 测试。你的代码是正确的,只是有点啰嗦。同样的情况也适用于 Self。这不是 Python。你可以省略 Self,它会被隐式推断出来。只有当你有一个同名的局部变量将你的字段超出作用域时才需要使用它。 - David Heffernan
@David Heffernan:所以使用Self并不是错误的。但它可以省略吗? 释放变量的测试应该使用if obj=nil then FreeAndNil(obj)吗?还是应该使用obj.Free?或者Free(obj)?我从未学过区别和最佳实践。 因为我想学习这里的最佳实践。 谢谢David。 - elector
1
最佳实践:Self是可选的,除非存在歧义,更好的方式是尽量避免歧义!在调用free之前不要测试Assigned(obj)。应使用FreeAndNil(obj)代替。 - David Heffernan
@David Heffernan:谢谢。我同意有歧义。如果 obj = nil,我能调用 FreeAndNil(obj) 吗? 并且您能否简要解释何时使用 Free(obj)、FreeAndNil(obj) 或 obj.Free? - elector
显示剩余10条评论
2个回答

5

您没有说明使用的Delphi版本。您提供的代码仅在Delphi XE中有效,并在那里产生以下(正确)输出:

Proxy Pattern

Subject Inactive
Subject active
Proxy: Call to Subject Request Choose left door

Subject active
Proxy: Call to Subject Request Choose left door

Protection Proxy: Authenticate first!
Protection proxy: No Access!
Protection Proxy: Authenticated
Protection Proxy: Call to Subject Request Choose left door

如果您查看生成的机器代码:

Project2.dpr.25: WriteLn(TProtectionProxy(subject).Authenticate('Secret'));
004122C2 A1788E4100       mov eax,[$00418e78]
004122C7 8B154CF84000     mov edx,[$0040f84c]
004122CD E8E22BFFFF       call @SafeIntfAsClass
004122D2 8D4DE0           lea ecx,[ebp-$20]
004122D5 BA38244100       mov edx,$00412438
004122DA E875D9FFFF       call TProtectionProxy.Authenticate
004122DF 8B55E0           mov edx,[ebp-$20]
004122E2 A1EC3C4100       mov eax,[$00413cec]
004122E7 E8BC24FFFF       call @Write0UString
004122EC E82F25FFFF       call @WriteLn
004122F1 E82A1CFFFF       call @_IOTest

你可以看到编译器首先生成调用SafeIntfAsClass的代码,用于从ISubject指针获取指向实现ISubject的对象的指针。然后使用这个(正确的)Self指针调用TProtectionProxy.Authenticate。
如果您尝试在旧版本的Delphi中运行相同的代码,则会失败。
var
  subject: ISubject;
begin
...
      subject := TProtectionProxy.Create();
      WriteLn(subject.Request());
      WriteLn(TProtectionProxy(subject).Authenticate('Secret'));

旧版的Delphi不支持从接口安全地转换回对象。此时编译器只是简单地取主体变量的值,并调用TProtectionProxy.Authenticate。
调用本身成功是因为TProtectionProxy.Authenticate是一个简单的静态方法,而不是虚方法,所以编译器只是为它生成了一个绝对地址的调用。但在TProtectionProxy.Authenticate内部,Self是错误的。因为实现ISubject的TProtectionProxy的主体指针与对象指针不同。
旧版delphi的正确解决方案是引入一个额外的接口:
type
  IProtection = interface
    ['{ACA182BF-7675-4346-BDE4-9D47CA4ADBCA}']
    function Authenticate(supplied: String): String;
  end;
...
  TProtectionProxy = class (TInterfacedObject, ISubject, IProtection)
...

var
  subject: ISubject;
  protection: IProtection;
...
      subject := TProtectionProxy.Create();
      WriteLn(subject.Request());
      if Supports(subject, IProtection, protection) then begin
        WriteLn(protection.Authenticate('Secret'));
        WriteLn(protection.Authenticate('Abracadabra'));
      end else
        WriteLn('IProtection not supported!');
      WriteLn(subject.Request());

一般来说,你不应该混合使用基于对象和基于接口的访问方式。一旦你得到一个对象的接口引用,就不应该再保留任何对象的引用(因为当最后一个接口引用超出作用域时,对象会自动释放)。尽管Delphi XE允许你正确地从接口向对象转换,但这是你应该非常小心使用的。


谢谢Thorsten。这非常有用。我正在使用Delphi 2007。正如我所说,我正在尝试让C#设计模式在Delphi中起作用。我喜欢接口的使用方式。而且Delphi可以进行引用计数。我认为针对接口进行编程非常有用。但是我不知道Delphi是以这种方式处理它的。现在它更加清晰了。 - elector
关于接口的一般主题,您可能会发现以下链接非常有用:http://www.nexusdb.com/support/index.php?q=intf-fundamentals http://www.nexusdb.com/support/index.php?q=intf-advanced http://www.nexusdb.com/support/index.php?q=intf-aggregation - Thorsten Engler

2

将新的对象实例分配给接口变量是否合法?

  • 是的。不仅如此,在Delphi中这是使用接口的正确方式。

我在调试中看到TProtectionProxy的构造函数首先被执行,然后是TProxy的析构函数。

  • 这对你有任何影响吗?这是实现细节。

如果你想先销毁TProxy对象,请将subject分配为nil:

  subject := TProxy.Create();
  WriteLn(subject.Request());
  WriteLn(subject.Request());

  subject := nil;
  subject := TProtectionProxy.Create();
  ..

创建TProtectionProxy后,逻辑上应该验证Authenticate('Abracadabra'),但是在调试器中,FPassword为空,尽管它在构造函数中被分配了?这个问题非常令人困惑。
  • 我看不出来。FPassword按照应有的方式被分配了。

但是当我关闭应用程序时,在析构函数中,密码是存在的?

  • 那是因为subject是全局变量。在调用readln之前强制手动销毁对象,可以将其分配给nil:

    Subject:= nil; Readln;

TProtectionProxy(subject)没问题,但我读到说不推荐使用,但(subject as TProtectionProxy)由于某些原因无法编译(运算符不可用...)?

  • 我不明白你想做什么。TProtectionProxy(subject)和(subject as TProtectionProxy)都不太合理。

我添加了析构函数,因为有FSubject字段。这样可以吗?

  • 是的,你应该在析构函数中销毁FSubject对象实例。

一个字段变量是否可以在声明它的同一行初始化,或者我需要像TProtectionProxy中一样在构造函数中初始化?

  • 不,你应该像你所做的那样在构造函数中初始化FPassword。

如果你不打算更改FPassword,可以将其声明为常量:

  TProtectionProxy = class (TInterfacedObject, ISubject)
  private
    FSubject: TSubject;
    const FPassword: String = 'Abracadabra';
  public
    constructor Create();
    destructor Destroy(); override;
    function Authenticate(supplied: String): String;
    function Request(): String;
  end;

不要使用Self - 在您的代码中没有必要。


谢谢Serg。实际上,我可以在密码字段中使用const。 我知道在.net中将接口变量转换为对象是合法的,这就是为什么我认为在Delphi中也是合法的原因? 我真的不明白这些建议不要使用Self?我认为这是一种合法的方法,以确保在我的示例中,变量FSubject属于正确的类,即当前使用的类? 再次感谢 - elector
@Serg:这只是为了让您收到我之前的评论通知。我刚刚读了说明 :) - elector
在你的代码中有很多 "Self.",你可以进行搜索并将 "Self." 替换为 "",而不会改变代码的功能。 - Thorsten Engler
@Thorsten Engler:我一定会删除Self,因为我看到没有人喜欢它 : )。我以为这是面向对象编程的好习惯。 - elector
@elector 你通常使用哪种编程语言?我猜想你使用的是类似Python这样的语言,其中使用self是必要的。 - David Heffernan
@Thorsten Engler:我用C#做.net。这不是强制性的,只是因为我认为这是一个好的实践,因为我想让这些模式示例变得更好,并且可能会将它们公开给想要在Delphi中学习它们的人。也许将它们放在codeplex上,让人们可以贡献和改进它们。这就是为什么我想包含我所知道的所有最佳实践。 感谢您的意见。 - elector

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接