【问题标题】:Delphi - Proxy Design Pattern - interface problemDelphi - 代理设计模式 - 接口问题
【发布时间】:2011-06-29 10:30:33
【问题描述】:

嗨 我正在尝试在 Delphi 中进行设计模式,由于在 Delphi 中找不到我喜欢的参考资料,我正在转换 O'Reilly C# 3.0 Design Patterns 书中的模式。但这不是问题。我已经从这本书中创建了代理模式,但是我显然不理解 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) 没问题,但我读到不推荐但(主题为 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/ 上有完整的模式实现

【问题讨论】:

  • 一些快速提示:除非需要,否则不要使用Self.。在调用 Free(或 FreeAndNil)之前不要测试 Assigned(),免费使用 nil 参考。始终在构造函数中调用 inherited;。是的,您将对象构造结果分配给subject 是正确的方法。但不要尝试将接口强制转换为对象。如果你想调用Authenticate,通过接口暴露出来。
  • 不测试的原因 nil 在 Free id 之前 Free 有效地表示 if Self &lt;&gt; nil then Destroy;
  • 我使用 Self 的原因是为一个类引用正确的 FSubject 字段。我不应该这样做吗?我认为应该这样使用Assigned。如果变量没有赋值有没有必要FreeAndNil或Free呢? @Garry:对不起,我不太明白你在说什么?你能提供更多细节吗?我想这与我不了解析构函数和 Free 和 FreeAndNil 有关
  • @David Heffernan 和@Garry:这只是为了通知您我的评论。谢谢
  • @elector 最佳实践:Self 是可选的,除非有歧义,否则请忽略它,甚至更好:尽量避免歧义!在免费拨打电话之前,切勿测试Assigned(obj)。请改写FreeAndNil(obj)

标签: delphi oop design-patterns interface


【解决方案1】:

您并没有说您使用的是什么版本的 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 的对象的指针。然后 TProtectionProxy.Authenticate 被这个(正确的)Self 指针调用。

如果您尝试使用旧版本的 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 允许您正确地从接口转换回对象,但您应该非常小心地使用它。

【讨论】:

【解决方案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 已按应有的方式分配。

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

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

    主题:= 无; 阅读;

TProtectionProxy(subject) 没问题,但我读到不推荐但(主题为 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 - 在您的代码中不需要它。

【讨论】:

  • 谢谢军士。事实上,我可以使用 const 作为密码字段。我知道在.net 中将接口变量转换为对象是合法的,这就是为什么我认为它在Delphi 中也是合法的?我真的不明白这些不使用 Self 的建议?我认为这是一种合法的方式,可以确保在我的示例中,变量 FSubject 属于正确的类,即当前使用的类?再次感谢
  • @Serg:这只是为了通知您我之前的评论。我刚刚阅读了说明:)
  • 在你的代码中有很多“自我”。您可以进行搜索并替换“自我”。到“”而不改变代码的功能。
  • @Thorsten Engler:我肯定会删除自我,因为我看到没有人喜欢它:)。我认为这是 OOP 中的一个好习惯。
  • @elector 您通常使用什么语言编写代码?我猜像 Python 之类的东西,使用 self 是必不可少的。
猜你喜欢
  • 1970-01-01
  • 2021-04-20
  • 2011-08-17
  • 2014-03-21
  • 2011-04-03
  • 1970-01-01
相关资源
最近更新 更多