Создание классов и объектов

Создание классов и объектов в Delphi

Введение в объектно-ориентированное программирование

Объектно-ориентированное программирование (ООП) представляет собой парадигму программирования, основанную на концепции объектов, которые могут содержать данные в виде полей (часто называемых атрибутами или свойствами) и код в виде процедур (часто называемых методами). В Delphi ООП является фундаментальной частью языка, и понимание принципов создания классов и объектов критически важно для разработки сложных приложений. Класс в Delphi — это тип данных, определяемый программистом, который инкапсулирует данные и поведение, связанные с этими данными. Объект — это экземпляр класса, созданный во время выполнения программы. Каждый объект имеет собственное состояние (значения полей) и может выполнять операции, определенные в его классе.

Синтаксис объявления класса

В Delphi класс объявляется в разделе type модуля. Базовая структура объявления класса выглядит следующим образом:

type
  TMyClass = class
  private
    // Приватные поля и методы
  protected
    // Защищенные поля и методы
  public
    // Публичные поля и методы
    constructor Create; // Конструктор
    destructor Destroy; override; // Деструктор
  published
    // Опубликованные свойства (для работы в IDE)
  end;

Ключевое слово class указывает на объявление класса. Имена классов в Delphi традиционно начинаются с буквы T (от "Type"). Секции видимости (private, protected, public, published) определяют доступность членов класса. Приватные члены доступны только внутри самого класса, защищенные — внутри класса и его потомков, публичные — откуда угодно, а опубликованные — аналогично публичным, но дополнительно доступны для инспектирования во время разработки в IDE.

Поля класса

Поля класса (или переменные экземпляра) хранят данные, связанные с объектом. Они объявляются внутри секций видимости класса. Поля могут быть любого типа, допустимого в Delphi: целочисленные, строковые, булевы, пользовательские типы, указатели, другие объекты и т.д. Пример объявления полей:

type
  TPerson = class
  private
    FName: string;
    FAge: Integer;
    FIsActive: Boolean;
    FBirthDate: TDateTime;
  public
    // Методы будут объявлены позже
  end;

Поля обычно объявляются как приватные (с префиксом F) для обеспечения инкапсуляции, а доступ к ним предоставляется через свойства. Это позволяет контролировать чтение и запись значений, добавлять валидацию и выполнять дополнительные действия при изменении состояния объекта.

Методы класса

Методы — это процедуры и функции, принадлежащие классу. Они определяют поведение объектов. Методы могут быть объявлены как обычные (экземплярные), виртуальные, динамические, статические или абстрактные. Экземплярные методы работают с конкретным объектом и имеют неявный параметр Self, который ссылается на текущий экземпляр. Виртуальные и динамические методы поддерживают полиморфизм и могут быть переопределены в потомках. Статические методы не имеют параметра Self и связаны с классом, а не с экземпляром. Абстрактные методы не имеют реализации в базовом классе и должны быть реализованы в потомках.

type
  TCalculator = class
  public
    function Add(A, B: Integer): Integer;
    function Subtract(A, B: Integer): Integer; virtual;
    function Multiply(A, B: Integer): Integer; dynamic;
    function Divide(A, B: Integer): Double; abstract;
    class procedure DisplayInfo; static;
  end;

Конструкторы и деструкторы

Конструктор — это специальный метод, который инициализирует новый объект. В Delphi конструктор обычно называется Create, хотя это не обязательно. Конструктор может принимать параметры для начальной инициализации полей. Деструктор — это метод, который освобождает ресурсы, занятые объектом. Деструктор обычно называется Destroy и объявляется с ключевым словом override, если класс является потомком TObject (что верно для всех классов, если не указано иное).

type
  TCar = class
  private
    FModel: string;
    FYear: Integer;
  public
    constructor Create(const AModel: string; AYear: Integer);
    destructor Destroy; override;
  end;

implementation

constructor TCar.Create(const AModel: string; AYear: Integer);
begin
  inherited Create; // Вызов конструктора родительского класса
  FModel := AModel;
  FYear := AYear;
end;

destructor TCar.Destroy;
begin
  // Освобождение ресурсов
  inherited Destroy; // Вызов деструктора родительского класса
end;

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

Свойства (Properties)

Свойства — это особые члены класса, которые обеспечивают контролируемый доступ к полям. Они могут иметь методы чтения (getter) и записи (setter), что позволяет выполнять дополнительную логику при доступе к данным. Свойства делают интерфейс класса более интуитивным и безопасным.

type
  TEmployee = class
  private
    FSalary: Double;
    function GetSalary: Double;
    procedure SetSalary(Value: Double);
  public
    property Salary: Double read GetSalary write SetSalary;
  end;

implementation

function TEmployee.GetSalary: Double;
begin
  Result := FSalary;
end;

procedure TEmployee.SetSalary(Value: Double);
begin
  if Value >= 0 then
    FSalary := Value
  else
    raise Exception.Create('Зарплата не может быть отрицательной');
end;

Свойства могут быть доступны только для чтения (только read), только для записи (только write) или для чтения и записи. Они также могут быть объявлены как опубликованные (published), что делает их видимыми в инспекторе объектов во время разработки.

Создание объектов

Объекты создаются вызовом конструктора с помощью ключевого слова Create. Конструктор выделяет память для объекта и инициализирует его. После создания объект должен быть освобожден, когда он больше не нужен, чтобы избежать утечек памяти. В Delphi для этого используется вызов деструктора через Free или Destroy.

var
  MyCar: TCar;
begin
  MyCar := TCar.Create('Toyota', 2022);
  try
    // Работа с объектом
  finally
    MyCar.Free; // Освобождение памяти
  end;
end;

Использование блока try-finally гарантирует освобождение ресурсов даже в случае исключения. Для автоматического управления памятью можно использовать интерфейсы или умные указатели, но для обычных классов ручное управление памятью является стандартным подходом.

Наследование классов

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

type
  TVehicle = class
  private
    FSpeed: Integer;
  public
    procedure Move; virtual;
  end;

  TCar = class(TVehicle)
  private
    FDoors: Integer;
  public
    procedure Move; override;
    procedure Honk;
  end;

  TBicycle = class(TVehicle)
  public
    procedure Move; override;
  end;

В этом примере TCar и TBicycle являются потомками TVehicle и наследуют поле FSpeed и метод Move, который они переопределяют. Ключевое слово override указывает, что метод заменяет реализацию базового класса.

Полиморфизм

Полиморфизм позволяет объектам разных классов, связанных наследованием, обрабатываться через общий интерфейс базового класса. Это означает, что можно вызвать метод базового класса, но фактически будет выполнен метод потомка, если он переопределен. Полиморфизм достигается через виртуальные и динамические методы.

var
  Vehicle: TVehicle;
begin
  Vehicle := TCar.Create;
  try
    Vehicle.Move; // Вызовется TCar.Move
  finally
    Vehicle.Free;
  end;

  Vehicle := TBicycle.Create;
  try
    Vehicle.Move; // Вызовется TBicycle.Move
  finally
    Vehicle.Free;
  end;
end;

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

Инкапсуляция

Инкапсуляция — это принцип ООП, который заключается в сокрытии внутренней реализации класса и предоставлении только необходимого интерфейса. В Delphi инкапсуляция достигается через секции видимости (private, protected) и свойства. Поля обычно объявляются как приватные, а доступ к ним предоставляется через публичные методы или свойства. Это защищает внутреннее состояние объекта от некорректного использования и позволяет изменять реализацию класса без влияния на код, который его использует.

Статические методы и поля класса

Статические методы и поля класса принадлежат самому классу, а не его экземплярам. Они существуют в единственном экземпляре для всего класса и могут быть вызваны без создания объекта. Статические методы не имеют доступа к нестатическим полям и методам, так как у них нет параметра Self. Статические поля часто используются для хранения данных, общих для всех экземпляров класса, например, счетчиков или конфигурационных параметров.

type
  TLogger = class
  private
    class var FLogCount: Integer; // Статическое поле
  public
    class procedure Log(const Msg: string); static;
    class property LogCount: Integer read FLogCount;
  end;

implementation

class procedure TLogger.Log(const Msg: string);
begin
  Inc(FLogCount);
  // Запись сообщения в лог
end;

Для вызова статического метода используется имя класса: TLogger.Log('Сообщение').

Абстрактные классы и методы

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

type
  TShape = class abstract
  public
    function Area: Double; virtual; abstract;
    function Perimeter: Double; virtual; abstract;
  end;

  TRectangle = class(TShape)
  private
    FWidth, FHeight: Double;
  public
    constructor Create(AWidth, AHeight: Double);
    function Area: Double; override;
    function Perimeter: Double; override;
  end;

Попытка создать экземпляр абстрактного класса (TShape.Create) вызовет ошибку компиляции или исключение во время выполнения.

Интерфейсы

Интерфейс в Delphi — это тип, который определяет набор методов без реализации. Класс может реализовывать один или несколько интерфейсов, обеспечивая тем самым множественное наследование поведения. Интерфейсы объявляются с ключевым словом interface и могут содержать только методы и свойства (без полей). Все методы интерфейса являются публичными и абстрактными.

type
  IPrintable = interface
    ['{GUID}']
    procedure Print;
  end;

  TDocument = class(TInterfacedObject, IPrintable)
  public
    procedure Print;
  end;

implementation

procedure TDocument.Print;
begin
  // Реализация печати
end;

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

Лучшие практики создания классов

При создании классов в Delphi следует придерживаться нескольких лучших практик. Во-первых, всегда инициализируйте поля в конструкторе, чтобы избежать неопределенного состояния. Во-вторых, освобождайте ресурсы в деструкторе, особенно если класс владеет другими объектами или выделяет память. В-третьих, используйте свойства для доступа к полям, чтобы обеспечить инкапсуляцию и валидацию. В-четвертых, предпочитайте композицию наследованию, если нет явной иерархии "является". В-пятых, документируйте классы и методы, особенно если они являются частью публичного API. В-шестых, избегайте циклических зависимостей между классами. В-седьмых, используйте виртуальные методы осторожно, только когда необходим полиморфизм. В-восьмых, соблюдайте соглашения об именовании: классы с префиксом T, интерфейсы с префиксом I, приватные поля с префиксом F.

Пример комплексного класса

Рассмотрим пример класса TBankAccount, который демонстрирует основные принципы ООП в Delphi:

type
  TBankAccount = class
  private
    FAccountNumber: string;
    FBalance: Double;
    FIsActive: Boolean;
    function GetBalance: Double;
    procedure SetBalance(Value: Double);
  public
    constructor Create(const AAccountNumber: string; InitialBalance: Double);
    procedure Deposit(Amount: Double);
    procedure Withdraw(Amount: Double);
    procedure Deactivate;
    property AccountNumber: string read FAccountNumber;
    property Balance: Double read GetBalance write SetBalance;
    property IsActive: Boolean read FIsActive;
  end;

implementation

constructor TBankAccount.Create(const AAccountNumber: string; InitialBalance: Double);
begin
  inherited Create;
  FAccountNumber := AAccountNumber;
  FBalance := InitialBalance;
  FIsActive := True;
end;

procedure TBankAccount.Deposit(Amount: Double);
begin
  if Amount <= 0 then
    raise Exception.Create('Сумма депозита должна быть положительной');
  if not FIsActive then
    raise Exception.Create('Счет не активен');
  FBalance := FBalance + Amount;
end;

procedure TBankAccount.Withdraw(Amount: Double);
begin
  if Amount <= 0 then
    raise Exception.Create('Сумма снятия должна быть положительной');
  if not FIsActive then
    raise Exception.Create('Счет не активен');
  if Amount > FBalance then
    raise Exception.Create('Недостаточно средств');
  FBalance := FBalance - Amount;
end;

procedure TBankAccount.Deactivate;
begin
  FIsActive := False;
end;

function TBankAccount.GetBalance: Double;
begin
  Result := FBalance;
end;

procedure TBankAccount.SetBalance(Value: Double);
begin
  if Value < 0 then
    raise Exception.Create('Баланс не может быть отрицательным');
  FBalance := Value;
end;

Этот класс инкапсулирует данные банковского счета и предоставляет методы для операций с ним. Поля приватные, доступ через свойства с валидацией. Конструктор инициализирует объект, методы реализуют бизнес-логику.

Заключение

Создание классов и объектов в Delphi — это основа объектно-ориентированного программирования в этой среде. Понимание синтаксиса объявления классов, работы с полями, методами, свойствами, конструкторами и деструкторами необходимо для разработки структурированных и поддерживаемых приложений. Принципы инкапсуляции, наследования и полиморфизма позволяют создавать гибкие и расширяемые системы. Следование лучшим практикам, таким как использование свойств для доступа к полям, правильное управление памятью и документирование кода, значительно повышает качество программного обеспечения. Освоив эти концепции, разработчик сможет эффективно использовать мощь ООП в Delphi для решения сложных задач программирования.