6

几天前我发布了一个问题,答案告诉我创建自己的课程。

我是 OOP 之前的老派程序员,我的编程结构良好、高效且有条理,但除了使用 Delphi 和 3rd 方对象之外,缺乏任何自定义 OOP。

当我开始使用 Delphi 2 时,我曾研究过 Delphi 的面向对象类是如何工作的,但它们对我的编程背景来说似乎很陌生。我了解它们对于设计组件和用户界面上的可视化控件的开发人员来说是如何并且非常出色。但是我从来没有发现需要在我的程序本身的编码中使用它们。

因此,15 年后,现在我再次回顾 Delphi 的课程和 OOPing。例如,如果我采用一个结构,例如:

type
  TPeopleIncluded = record
    IndiPtr: pointer;
    Relationship: string;
  end;
var
  PeopleIncluded: TList<TPeopleIncluded>;

然后一个 OOP 倡导者可能会告诉我把它变成一个类。从逻辑上讲,我认为这将是一个从通用 TList 继承的类。我猜这将是这样完成的:

TPeopleIncluded<T: class> = class(TList<T>)

但这就是我陷入困境的地方,并且没有关于如何做其余部分的良好说明。

当我查看 Delphi 在 Generics.Collections 单元中作为示例的某个类时,我看到:

TObjectList<T: class> = class(TList<T>)
private
  FOwnsObjects: Boolean;
protected
  procedure Notify(const Value: T; Action: TCollectionNotification); override;
public
  constructor Create(AOwnsObjects: Boolean = True); overload;
  constructor Create(const AComparer: IComparer<T>; AOwnsObjects: Boolean = True); overload;
  constructor Create(Collection: TEnumerable<T>; AOwnsObjects: Boolean = True); overload;
  property OwnsObjects: Boolean read FOwnsObjects write FOwnsObjects;
end;

然后他们对构造函数和过程的定义是:

{ TObjectList<T> }

constructor TObjectList<T>.Create(AOwnsObjects: Boolean);
begin
  inherited;
  FOwnsObjects := AOwnsObjects;
end;

constructor TObjectList<T>.Create(const AComparer: IComparer<T>; AOwnsObjects: Boolean);
begin
  inherited Create(AComparer);
  FOwnsObjects := AOwnsObjects;
end;

constructor TObjectList<T>.Create(Collection: TEnumerable<T>; AOwnsObjects: Boolean);
begin
  inherited Create(Collection);
  FOwnsObjects := AOwnsObjects;
end;

procedure TObjectList<T>.Notify(const Value: T; Action: TCollectionNotification);
begin
  inherited;
  if OwnsObjects and (Action = cnRemoved) then
    Value.Free;
end;

让我告诉你,这个“简单”的类定义对于那些在 Delphi 中使用 OOP 多年的人来说可能是显而易见的,但对我来说,它只为我提供了数百个关于我使用什么以及如何使用它的未回答的问题.

对我来说,这似乎不是一门科学。它似乎是一门如何将信息最好地构造成对象的艺术。

所以这个问题,我希望它不会被关闭,因为我真的需要帮助,我在哪里或如何获得关于使用 Delphi 创建类的最佳指导 - 以及如何以正确的 Delphi 方式进行。

4

1 回答 1

10

对我来说,这似乎不是一门科学。它似乎是一门如何将信息最好地构造成对象的艺术。

嗯...是的。正式的要求真的不多。它实际上只是一组工具,可以帮助您组织您的想法,并在此过程中消除大量重复。

然后一个 OOP 倡导者可能会告诉我把它变成一个类。从逻辑上讲,我认为这将是一个从通用 TList 继承的类。

实际上,通用容器的全部意义在于您不必为每种类型的对象创建一个新的容器类。相反,您将创建一个新的内容类,然后创建一个TList<TWhatever>.

将类实例视为指向记录的指针。

现在:既然可以使用指向记录的指针,为什么还要使用类?几个原因:

  • encapsulation:您可以使用关键字隐藏实现的某些方面,private以便其他开发人员(包括您未来的自己)知道不要依赖可能更改或对于理解概念并不重要的实现细节。
  • 多态性:您可以通过为每个记录提供一组指向函数的指针来避免许多特殊的调度逻辑。然后,不是有一个大的case语句来为每种类型的对象做不同的事情,而是循环遍历列表并向每个对象发送相同的消息,然后它遵循函数指针来决定要做什么。
  • 继承:当您开始使用指向函数和过程的指针创建记录时,您会发现您经常需要一个新的函数调度记录,这与您已经拥有的非常相似,除了您需要更改一两个过程. 子类化只是实现这一目标的一种便捷方式。

因此,在您的另一篇文章中,您表示您的整体程序如下所示:

procedure PrintIndiEntry(JumpID: string);
  var PeopleIncluded : TList<...>;
begin      
   PeopleIncluded := result_of_some_loop;
   DoSomeProcess(PeopleIncluded);
end;

我不清楚是什么Indi意思JumpID,所以我要假装你们公司做跳伞婚礼,Indi意思是“个人”,JumpID是数据库中的主键,表示所有这些人都参加婚礼的航班并计划从同一架飞机上跳下来......对于这对幸福的夫妇来说,了解他们至关重要,Relationship这样您就可以给他们正确的颜色降落伞。

显然,这与您的域不完全匹配,但是由于您在这里提出一般性问题,因此细节并不重要。

另一篇文章中的人试图告诉您的(无论如何我的猜测)不是用一个类替换您的列表,而是用一个替换 JumpID。

换句话说,不是传递JumpID给一个过程并使用它从数据库中获取人员列表,而是创建一个Jump类。

如果您的 JumpID 实际上表示如 in 中的跳转goto,那么您实际上可能会包含一堆类,它们都是同一事物的子类,并以不同的方式覆盖同一方法。

事实上,让我们假设您举办了一些不是婚礼的派对,在这种情况下,您不需要关系,而只需要一个简单的人员列表:

type TPassenger = record
   FirstName, LastName: string;
end;

type TJump = class
  private
    JumpID   : string;
    manifest : TList< TPassenger >;
  public
    constructor Init( JumpID: string );
    function GetManifest( ) : TList< TPassenger >;
    procedure PrintManifest( ); virtual;
end;

所以现在PrintManifest()完成了你的工作PrintIndyEntry(),但它不是计算内联列表,而是调用Self.GetManifest().

现在也许您的数据库没有太大变化,并且您的TJump实例总是短暂的,所以您决定只填充Self.manifest构造函数。在这种情况下,GetManifest()只需返回该列表。

或者您的数据库可能经常更改,或者TJump停留时间足够长以至于数据库可能会在其下更改。在这种情况下,GetManifest()每次调用时都会重建列表...或者您可能添加另一个private值来指示您上次查询的时间,并且仅在信息过期后更新。

关键是PrintManifest不必关心它是如何GetManifest工作的,因为你已经隐藏了这些信息。

当然,在 Delphi 中,您可以使用 a 来做同样的事情,在您的部分unit中隐藏缓存的乘客列表列表。implementation

但是,当需要实现婚礼派对特定的功能时,类会带来更多的好处:

type TWeddingGuest = record
  public
    passenger    : TPassenger;
    Relationship : string;
end;

type TWeddingJump = class ( TJump )
  private
    procedure GetWeddingManifest( ) : TList< TWeddingGuest >;
    procedure PrintManifest( ); override;
end;

所以在这里, theTWeddingJump继承了Initand GetManifestTJump但它也添加了 a GetWeddingManifest( );,它会PrintManifest()用一些自定义实现来覆盖 of 的行为。(你知道它这样做是因为override这里的标记,它对应virtualTJump.

但是现在,假设这PrintManifest实际上是一个相当复杂的过程,并且当您只想在标题中添加一列,并在正文中列出关系字段的另一列时,您不想复制所有代码。你可以这样做:

type TJump = class
   // ... same as earlier, but add:
   procedure PrintManfestHeader(); virtual;
   procedure PrintManfiestRow(passenger:TPassenger); virtual;
end;
type TWeddingJump = class (TJump)
   // ... same as earlier, but:
   // * remove the PrintManifest override
   // * add:
   procedure PrintManfestHeader(); override;
   procedure PrintManfiestRow(passenger:TPassenger); override;

end;

现在,你想这样做:

procedure TJump.PrintManifest( )
   var passenger: TPassenger;
begin;
   // ...
   Self.PrintManifestHeader();
   for guest in Self.GetManifest() do begin
      Self.PrintManifestRow();
   end;
   // ...
end;

但是你还不能,因为GetManifest()returnTList< TPassenger >;和 for TWeddingJump,你需要它来 return TList< TWeddingGuest >

好吧,你怎么能处理呢?

在您的原始代码中,您有以下内容:

IndiPtr: pointer

指向什么?我的猜测是,就像这个例子一样,你有不同类型的个体,你需要他们做不同的事情,所以你只使用一个通用指针,让它指向不同类型的记录,并希望你将它转换为以后做对的事。但是类为您提供了几种更好的方法来解决这个问题:

  • 您可以创建TPassenger一个类并添加一个GetRelationship()方法。这将消除对 的需要TWeddingGuest,但这意味着该GetRelationship方法始终存在,即使您不是在谈论婚礼。
  • 你可以GetRelationship(guest:TPassenger)TWeddingGuest类中添加一个,然后在里面调用它TWeddingGuest.PrintManifestRow()

但是假设您必须查询数据库来填充该信息。使用上述两种方法,您将为每位乘客发出一个新查询,这可能会使您的数据库陷入困境。您真的想一次性获取所有内容,在GetManifest().

因此,相反,您再次应用继承:

type TPassenger = class
  public
    firstname, lastname: string;
end;
type TWeddingGuest = class (TPassenger)
  public
    relationship: string;
end;

因为GetManifest()返回一个乘客列表,并且所有婚礼宾客都是乘客,所以您现在可以这样做:

type TWeddingJump = class (TJump)
  // ... same as before, but:
  // replace: procedure GetWeddingManfiest...
  // with:
  procedure GetManifest( ) : TList<TPassenger>; override;
  // (remember to add the corresponding 'virtual' in TJump)
end;

现在,您为和填写TWeddingJump.PrintManifestRow相同版本的PrintManifest作品的详细信息。TJumpTWeddingJump

还有一个问题:我们声明PrintManifestRow(passenger:TPassenger)了,但实际上我们传入了一个TWeddingGuest. 这是合法的,因为TWeddingGuest它是TPassenger...的子类,但我们需要获取该.relationship字段,并且TPassenger没有该字段。

编译器怎么能相信在 aTWeddingJump中,你总是会传入 aTWeddingGuest而不仅仅是一个普通的TPassenger?您必须确保该relationship字段确实存在。

您不能仅仅将其声明为,TWeddingJupmp.(passenger:TWeddingGuest)因为通过子类化,您基本上承诺做父类可以做的所有事情,而父类可以处理任何 TPassenger.

所以你可以回去手动检查类型并强制转换它,就像一个无类型的指针一样,但同样,有更好的方法来处理这个:

  • 多态方法:PrintManifestRow()方法移动到TPassenger类(删除passenger:TPassenger参数,因为这现在是隐式参数Self),覆盖该方法TWeddingGuest,然后TJump.PrintManifest调用passenger.PrintManifestRow()
  • 泛型类方法:使TJump自己成为泛型类(类型TJump<T:TPassenger> = class),而不是GetManifest()return a TList<TPassenger>,而是让它 return TList<T>。同样,PrintManifestRow(passenger:TPassenger)变成PrintManifestRow(passenger:T);。现在您可以说:TWeddingJump = class(TJump<TWeddingGuest>)现在您可以自由地将覆盖的版本声明为PrintManifestRow(passenger:TWeddingGuest).

无论如何,这比我想写的要多得多。我希望它有所帮助。:)

于 2012-07-01T21:39:18.970 回答