Как хорошо использовать помощников в классе?

В Delphi (и, возможно, во многих других языках) есть помощники класса. Они предоставляют способ добавить дополнительные методы в существующий класс. Без создания подкласса.

Итак, что хорошего в использовании помощников класса?

10 ответов

Решение

Я использую их:

Сначала я скептически относился к помощникам в классе. Но потом я прочитал интересную запись в блоге, и теперь я убежден, что они действительно полезны.

Например, если вам нужна дополнительная функциональность для существующего класса экземпляра и по какой-то причине вы не можете изменить существующий источник. Вы можете создать помощника класса, чтобы добавить эту функциональность.

Пример:

type
  TStringsHelper = class helper for TStrings
  public
    function IsEmpty: Boolean;
  end;

function TStringsHelper.IsEmpty: Boolean;
begin
  Result := Count = 0;
end;

Каждый раз мы теперь используем экземпляр (подкласс) TStrings, и TStringsHelper находится в области видимости. У нас есть доступ к методу IsEmpty.

Пример:

procedure TForm1.Button1Click(Sender: TObject);
begin
  if Memo1.Lines.IsEmpty then
    Button1.Caption := 'Empty'
  else
    Button1.Caption := 'Filled';
end;

Заметки:

  • Помощники классов могут храниться в отдельном модуле, поэтому вы можете добавить своих собственных классных помощников. Обязательно дайте этим модулям легко запоминающееся имя, например ClassesHelpers, для помощников для модуля Classes.
  • Также есть помощники по записи.
  • Если в области видимости есть несколько помощников классов, ожидайте некоторых проблем, можно использовать только один помощник.

Это очень похоже на методы расширения в C#3 (и VB9). Лучшее использование, которое я видел для них - это расширения IEnumerable<T> (а также IQueryable<T>) который позволяет LINQ работать против произвольных последовательностей:

var query = someOriginalSequence.Where(person => person.Age > 18)
                                .OrderBy(person => person.Name)
                                .Select(person => person.Job);

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

Я не рекомендовал бы использовать их, так как я прочитал этот комментарий:

"Самая большая проблема с помощниками классов, из-за необходимости их использования в ваших собственных приложениях, заключается в том, что только ОДИН помощник класса для данного класса может находиться в области видимости в любое время".... "То есть, если у вас есть два помощника в области видимости, компилятор распознает только ОДИН. Вы не получите никаких предупреждений или даже намеков о любых других помощниках, которые могут быть скрыты".

http://davidglassborow.blogspot.com/2006/05/class-helpers-good-or-bad.html

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

type
   TCompetitionToMyClass = class helper for TMyClass
   public
      constructor Convert(base: TCompetition);
   end;

А затем определите конвертер. Одно предостережение: помощник класса не является другом класса. Этот метод будет работать, только если возможно полностью настроить новый объект TMyClass с помощью его открытых методов и свойств. Но если вы можете, это работает очень хорошо.

Как показывает GameCat, TStrings - хороший кандидат, чтобы избежать некоторой типизации:

type
  TMyObject = class
  public
    procedure DoSomething;
  end;

  TMyObjectStringsHelper = class helper for TStrings
  private
    function GetMyObject(const Name: string): TMyObject;
    procedure SetMyObject(const Name: string; const Value: TMyObject);
  public
    property MyObject[const Name: string]: TMyObject read GetMyObject write SetMyObject; default;
  end;

function TMyObjectStringsHelper.GetMyObject(const Name: string): TMyObject;
var
  idx: Integer;
begin
  idx := IndexOf(Name);
  if idx < 0 then
    result := nil
  else
    result := Objects[idx] as TMyObject;
end;

procedure TMyObjectStringsHelper.SetMyObject(const Name: string; const Value:
    TMyObject);
var
  idx: Integer;
begin
  idx := IndexOf(Name);
  if idx < 0 then
    AddObject(Name, Value)
  else
    Objects[idx] := Value;
end;

var
  lst: TStrings;
begin
  ...
  lst['MyName'] := TMyObject.Create; 
  ...
  lst['MyName'].DoSomething;
  ...
end;

Вам когда-нибудь нужно было получать доступ к многострочным строкам в реестре?

type
  TRegistryHelper = class helper for TRegistry
  public
    function ReadStrings(const ValueName: string): TStringDynArray;
  end;

function TRegistryHelper.ReadStrings(const ValueName: string): TStringDynArray;
var
  DataType: DWord;
  DataSize: DWord;
  Buf: PChar;
  P: PChar;
  Len: Integer;
  I: Integer;
begin
  result := nil;
  if RegQueryValueEx(CurrentKey, PChar(ValueName), nil, @DataType, nil, @DataSize) = ERROR_SUCCESS then begin
    if DataType = REG_MULTI_SZ then begin
      GetMem(Buf, DataSize + 2);
      try
        if RegQueryValueEx(CurrentKey, PChar(ValueName), nil, @DataType, PByte(Buf), @DataSize) = ERROR_SUCCESS then begin
          for I := 0 to 1 do begin
            if Buf[DataSize - 2] <> #0 then begin
              Buf[DataSize] := #0;
              Inc(DataSize);
            end;
          end;

          Len := 0;
          for I := 0 to DataSize - 1 do
            if Buf[I] = #0 then
              Inc(Len);
          Dec(Len);
          if Len > 0 then begin
            SetLength(result, Len);
            P := Buf;
            for I := 0 to Len - 1 do begin
              result[I] := StrPas(P);
              Inc(P, Length(P) + 1);
            end;
          end;
        end;
      finally
        FreeMem(Buf, DataSize);
      end;
    end;
  end;
end;

Я помню, как впервые увидел то, что вы называете "помощниками класса", во время изучения Objective C. Какао (платформа Apple Objective C) использует так называемые "Категории".

Категория позволяет расширять существующий класс, добавляя ваши собственные методы без создания подклассов. На самом деле, какао призывает вас избегать подклассов, когда это возможно. Часто имеет смысл создавать подклассы, но часто этого можно избежать, используя категории.

Хорошим примером использования категории в Какао является то, что называется "Код значения ключа (KVC)" и "Наблюдение значения ключа (KVO)".

Эта система реализована с использованием двух категорий (NSKeyValueCoding и NSKeyValueObserving). Эти категории определяют и реализуют методы, которые могут быть добавлены в любой класс, который вы хотите. Например, Cocoa добавляет "соответствие" в KVC/KVO, используя эти категории для добавления методов в NSArray, таких как:

- (id)valueForKey:(NSString *)key

Класс NSArray не имеет ни объявления, ни реализации этого метода. Тем не менее, за счет использования категории. Вы можете вызвать этот метод в любом классе NSArray. Вы не обязаны создавать подкласс NSArray, чтобы получить соответствие KVC/KVO.

NSArray *myArray = [NSArray array]; // Make a new empty array
id myValue = [myArray valueForKey:@"name"]; // Call a method defined in the category

Использование этой техники позволяет легко добавлять поддержку KVC/KVO в ваши собственные классы. Интерфейсы Java позволяют добавлять объявления методов, но категории позволяют также добавлять фактические реализации к существующим классам.

Другие языки имеют правильно спроектированные помощники классов.

В Delphi есть вспомогательные классы, которые были введены исключительно для того, чтобы помочь инженерам Borland решить проблему совместимости между Delphi и Delphi.net.

Они никогда не предназначались для использования в «пользовательском» коде и с тех пор не улучшались. Они могут быть полезны при разработке фреймворков (для частного использования внутри фреймворка, как и в исходном решении для обеспечения совместимости с .NET); опасно ошибочно приравнивать помощники классов Delphi к помощникам на других языках или использовать примеры из этих других языков, пытаясь определить варианты использования для тех, что в Delphi.

По сей день текущая документация Delphi говорит о хелперах классов и записей следующее:

их не следует рассматривать как инструмент проектирования, который следует использовать при разработке нового кода.

ссылка: https://docwiki.embarcadero.com/RADStudio/Alexandria/en/Class_and_Record_Helpers_(Delphi)

Таким образом, ответ на вопрос «какое полезное использование помощников классов» в Delphi довольно прост:

Существует только одно безопасное использование: для контекстно-зависимых расширений полезности и интересов только в одной кодовой базе, которая реализует и использует помощника (подробный пример здесь: https://www.deltics.co.nz/blog/posts/683).

Пример представляет собой структуру для restful API, где расширения интересующего класса только для кода на стороне клиента предоставляются расширениями «Client Helper», явно импортированными из модулей, специфичных для клиента, а не (чрезмерной) загрузкой проблем клиента в исходный класс. как с серверным, так и с клиентским контекстом.

Кроме этого: не используйте их вообще (либо реализуйте свои собственные, либо потребляйте предоставленные другими) , если вы не готовы иметь дело с последствиями :

В первую очередь: в любой момент времени в области видимости может находиться только один помощник.

Во-вторых: нет способа квалифицировать помощника, на который ссылается

Из-за основной проблемы:

  1. Добавление юнита к (или даже просто изменение порядка) юнитов вusesпредложение может непреднамеренно «спрятать» нужный в вашем коде хелпер (вы можете даже не знать откуда)

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

И благодаря вторичной проблеме, если вы не можете переупорядочить список использования, чтобы сделать желаемого помощника «видимым», или вам нужны 2 несвязанных помощника (не знающих друг друга и поэтому неспособных «расширить» друг друга), тогда есть это не способ использовать его!

Здесь стоит подчеркнуть, что способность хелперов класса Delphi взламывать чужой код является практически однозначно плохой характеристикой. Многие языковые функции во многих языках могут быть использованы для взлома вашего собственного кода; не многие позволяют сломать чужое!

Подробнее в различных сообщениях здесь: https://www.deltics.co.nz/blog/?s=class+helpers

В частности, этот: https://www.deltics.co.nz/blog/posts/273/

Если Dephi поддерживает методы расширения, я хочу использовать следующие варианты:

      TGuidHelper = class
public
   class function IsEmpty(this Value: TGUID): Boolean;
end;

class function TGuidHelper(this Value: TGUID): Boolean;
begin
   Result := (Value = TGuid.Empty);
end;

Так что я могу позвонить if customerGuid.IsEmpty then ....

Еще один хороший пример - возможность читать значения из XML-документа (или JSON, если вам нравятся подобные вещи) с помощью IDataRecord парадигма (которую я люблю):

      orderGuid := xmlDocument.GetGuid('/Order/OrderID');

Что намного лучше, чем:

      var
   node: IXMLDOMNode;

   node := xmlDocument.selectSingleNode('/Order/OrderID');
   if Assigned(node) then
      orderID := StrToGuid(node.Text) //throw convert error on empty or invalid
   else
      orderID := TGuid.Empty; // "DBNull" becomes the null guid

Я видел, как они используются для обеспечения доступности методов классов в разных классах: добавление Open/Close и Show/Hide ко всем классам данного типа, а не только к активным и видимым свойствам.

Другие вопросы по тегам