Как хорошо использовать помощников в классе?
В Delphi (и, возможно, во многих других языках) есть помощники класса. Они предоставляют способ добавить дополнительные методы в существующий класс. Без создания подкласса.
Итак, что хорошего в использовании помощников класса?
10 ответов
Я использую их:
- Вставить перечислители в классы VCL, которые их не реализуют.
- Для улучшения классов VCL.
Чтобы добавить методы в класс TStrings, чтобы я мог использовать те же методы в своих производных списках и в TStringList.
TGpStringListHelper = class helper for TStringList public function Last: string; function Contains(const s: string): boolean; function FetchObject(const s: string): TObject; procedure Sort; procedure Remove(const s: string); end; { TGpStringListHelper }
Для упрощения доступа к полям записи и удаления приведения.
Сначала я скептически относился к помощникам в классе. Но потом я прочитал интересную запись в блоге, и теперь я убежден, что они действительно полезны.
Например, если вам нужна дополнительная функциональность для существующего класса экземпляра и по какой-то причине вы не можете изменить существующий источник. Вы можете создать помощника класса, чтобы добавить эту функциональность.
Пример:
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», явно импортированными из модулей, специфичных для клиента, а не (чрезмерной) загрузкой проблем клиента в исходный класс. как с серверным, так и с клиентским контекстом.
Кроме этого: не используйте их вообще (либо реализуйте свои собственные, либо потребляйте предоставленные другими) , если вы не готовы иметь дело с последствиями :
В первую очередь: в любой момент времени в области видимости может находиться только один помощник.
Во-вторых: нет способа квалифицировать помощника, на который ссылается
Из-за основной проблемы:
Добавление юнита к (или даже просто изменение порядка) юнитов в
uses
предложение может непреднамеренно «спрятать» нужный в вашем коде хелпер (вы можете даже не знать откуда)Помощник, добавленный к юниту, который уже находится в вашем списке использований, может скрыть какой-либо другой помощник, ранее «импортированный» и использовавшийся из другого
И благодаря вторичной проблеме, если вы не можете переупорядочить список использования, чтобы сделать желаемого помощника «видимым», или вам нужны 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 ко всем классам данного типа, а не только к активным и видимым свойствам.