Безболезненная прививка объектного мышления

Или как можно проще об основных принципах ООП в Lazarus и FreePascal


Часть I


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


Во всех книгах, посвященных паскалю, delphi и lazarus (я нашел аж целых две о последнем), очень схожая часть, посвященная ООП. По этим книгам можно много узнать о том, насколько круче ООП устаревшего структурного подхода, но так и не получить достаточных навыков применения этого на практике. Конечно, любой программист, использующий визуальные IDE, уже по умолчанию использует ООП, так как все компоненты и структурные элементы визуального приложения представляют собой объекты, однако свои собственные структуры и абстракции перенести в парадигму ООП бывает очень сложно. Чтобы понять всю прелесть и оценить открывающиеся перспективы, я решил сделать небольшое приложение, которое в конечном итоге превратилось в простенький screensaver. Заодно вспомнил о существовании тригонометрии.

Приложение будет рисовать на экране в случайных местах пятьдесят полярных роз с разными характеристиками: размер, цвет, количество лепестков. Потом их же затирать и рисовать новые, и т.д. Используя принципы структурного программирования, можно, конечно, сделать обычный многомерный массив объемом на 50 и в нем сохранять все уникальные характеристики. Однако стоит вспомнить, что паскаль подразумевает строгую типизацию данных, а, следовательно, массив не может состоять их элементов с разными типами. Можно сделать массив из записей (record), но чего уж мелочиться, от записи до класса — один шаг. Вот его мы и сделаем.


Важный принцип ООП — инкапсуляция. Это значит, что класс должен скрывать внутри себя всю логику своей работы. Наш класс, назовем его TPetal, имеет поля с разным типом данных, которые определяют уникальные характеристики (координаты центра, размер, коэффициенты для уравнения полярной розы, цвет), и методы работы. Все другие элементы программы должны лишь вызывать эти методы, не вникая в подробности их реализации. Пока класс должен уметь нарисовать себя и стереть. Для начала достаточно:


  { TPetal }

  TPetal = class
    private
      R, phi: double;
      X, Y, CX, CY: integer;
      Scale, RColor, PetalI: integer;
    public
      constructor Create (Xmax, Ymax: integer);
      procedure Draw (Canvas: TCanvas; Erase: boolean = FALSE);
  end;

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


constructor TPetal.Create(Xmax, Ymax: integer);
begin
     inherited Create;
     CX:=Random(Xmax);
     CY:=Random(Ymax);
     RColor:=1+Random($FFFFFF);
     Scale:=2+Random(12);
     PetalI:=2+Random(6);
end;

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


Дальше метод рисования. Как вы видете, чтобы нарисовать или стереть объект я написал единственный метод, в котором имеется второй параметр и по умолчанию он имеет значение FALSE. Это связано с тем, что нарисовать и стереть — одна и та же операция, только рисуется объект случайным цветом, а стирается — черным. При вызове метода из программы без использования второго параметра объект рисуется, а при использовании параметра Erase — объект стирается:


procedure TPetal.Draw(Canvas: TCanvas; Erase: boolean);
begin
     phi:=0;
     if Erase then RColor:=clBlack;
     with Canvas do
       while phi < 2*pi do
         begin
           R:=10*sin(PetalI*phi);
           X:=CX+Trunc(Scale*R*cos(phi));
           Y:=CY-Trunc(Scale*R*sin(phi));
           Pixels[X,Y]:=RColor;
           phi+=pi/1800;
         end;
end;

Для рисования лепестков используется функция полярной розы в полярной системе координат:

image

где ρ определяет радиальную координату, а φ — угловую. α — это коэффициент, определяющий длину лепестков. В нашей формуле коэффициент сразу равен 10, чтобы розы не получались слишком мелкими. Угловая координата у нас пробегает от 0 до 2π, чтобы захватить все 360 градусов (цикл while). А после получения радиальной координаты мы вычисляем декартовы: x и y, чтобы отрисовать эту точку на канве (поразитесь в очередной раз, насколько быстро современные компьютеры производят вычисления: внутри метода длиннющий цикл, в котором тригонометрические вычисления; вспомните, как «быстро» рисовал подобное Zx-spectrum). Коэффициент k в формуле (в программе — PetalI) определяет количество лепестков. У нас пока для простоты используются только целые числа, поэтому все розы получаются гипотрохоидные с неперекрывающимися лепестками.


Итак, наш класс реализован и все нужные навыки он имеет. Пришло время использовать. В главном модуле приложения в первую очередь нам нужно объявить массив из 50 объектов класса TPetal, потом на форму кидаем Image и Timer, изображение растягиваем на всю форму (Client), а у таймера устанавливаем период срабатывания в 100 миллисекунд. Метод таймера будет такой:

var
  Form1: TForm1;
  Marg: boolean;
  Petals: array [0..49] of TPetal;
  CurPetal: smallint;
//-----------------------------------------------------------------------------------
procedure TForm1.Timer1Timer(Sender: TObject);
begin
     if not Marg then
       begin
            Petals[CurPetal]:=TPetal.Create(img.Width,img.Height);
            Petals[CurPetal].Draw(img.Canvas);
            CurPetal+=1;
            if CurPetal=50 then Marg:=TRUE;
       end else begin
            Petals[50-CurPetal].Draw(img.Canvas,TRUE);
            Petals[50-CurPetal].Free;
            CurPetal-=1;
            if CurPetal=0 then Marg:=FALSE;
       end;
     img.Canvas.TextOut(10,10,IntToStr(CurPetal)+'  ');
end;

Как видно я использовал еще пару глобальных переменных: CurPetal — счетчик объектов, принимает значения от 0 до 50 и обратно; Marg — сигнализатор границ счетчика, но из логики работы метода все должно быть предельно понятно.


Если бы мы использовали парадигму структурного программирования, то внутри обработчика таймера нам нужно было бы самостоятельно инициализировать все характеристики уникальной розы, отрисовать её, а затем стирать. Метод бы разросся и стал бы не наглядным. Но теперь у нас есть класс, который делает всё сам — конструктор класса сразу же инициализирует все характеристики, а метод класса DrawPetal инкапсулирует всю логику вычислений и отрисовки, для чего ему всего лишь передается указатель на необходимый объект, имеющий свойство Canvas (а это любая форма, и почти любой компонент). В итоге получается такой симпатичный screensaver:



Исследуя следующий принцип ООП — наследование, в дальнейшем можно от класса TPetal породить потомка, к примеру TOverlappingPetal, в котором полярная роза будет с перекрывающимися лепестками. Для этого (в целях универсализации) в классе-предке нужно изменить тип поля PetalI на действительное число, а конструктор потомка перегрузить так, чтобы это поле могло инициализироваться случайным дробным числом по соответствующим правилам.


Файлы проекта я сохранил в своем хранилище bitbucket, и для каждого этапа создал отдельную ветку. Пример выше вы найдете в ветке lesson1.


Часть II


Теперь предлагаю сделать то, на чём мы остановились. Итак, у нас есть класс TPetal, который умеет рисовать полярную розу с количеством лепестков от 3 до 16. Однако все объекты у нас получаются с неперекрывающимися лепестками. Меж тем, если посмотреть на табличку ниже, мы увидим, что разновидностей их больше. Форма определяется коэффициентом, равным n/d:


image

Породим потомка от класса TPetal:


  { TPetal }

  TPetal = class
    protected
      R, phi, PetalI: double;
      X, Y, CX, CY: integer;
      Scale, RColor: integer;
    public
      constructor Create (Xmax, Ymax: integer);
      procedure Draw (Canvas: TCanvas; Erase: boolean = FALSE); overload;
  end;

  { TOverlappedPetal }

  TOverlappedPetal = class (TPetal)
    public
      constructor Create (Xmax, Ymax: integer);
      procedure Draw (Canvas: TCanvas; Erase: boolean = FALSE); overload;
    end;

В классе TOverlappedPetal мы добавляем свой конструктор, который будет действовать вместе с конструктором предка, а также перегруженный метод DrawPetal (на самом деле в конце мы обойдемся вообще без него, однако в настоящий момент — это хороший способ продемонстрировать перегрузку методов в наследниках класса). Вот реализация:


{ TOverlappedPetal }

constructor TOverlappedPetal.Create(Xmax, Ymax: integer);
begin
  inherited Create (Xmax,Ymax);
  while PetalI=Round(PetalI) do
    PetalI:=(1+Random(6))/(1+Random(6));
end;

procedure TOverlappedPetal.Draw(Canvas: TCanvas; Erase: boolean);
begin
  phi:=0;
  if Erase then RColor:=clBlack;
  with Canvas do
    while phi < 12*pi do
      begin
        R:=10*sin(PetalI*phi);
        X:=CX+Trunc(Scale*R*cos(phi));
        Y:=CY-Trunc(Scale*R*sin(phi));
        Pixels[X,Y]:=RColor;
        phi+=pi/1800;
      end;
end;

Видно, что конструктор класса TOverlappedPetal использует метод предка (inherited), но потом меняет значение поля PetalI, которым и задается коэффициент, влияющий на форму розы. При вычислении поля мы исключаем целые числа, чтобы не дублировать формы, уже имеющиеся у предка TPetal.


Файлы этого примера можно найти в ветке lesson2 в хранилище.


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

image

Разница реализаций — лишь в коэффициенте, умноженном на число π (2 или 12). Выносим этот коэффициент в отдельное поле предка TPetal (поле K), убираем излишнюю теперь перегрузку метода DrawPetal и получаем следующую структуру наших классов:


  { TPetal }

  TPetal = class
    protected
      R, phi, PetalI: double;
      X, Y, K, CX, CY: integer;
      Scale, RColor: integer;
    public
      constructor Create (Xmax, Ymax: integer);
      procedure Draw (Canvas: TCanvas; Erase: boolean = FALSE);
  end;

  { TOverlappedPetal }

  TOverlappedPetal = class (TPetal)
    public
      constructor Create (Xmax, Ymax: integer);
    end;

Хоть потомок TOverlappedPetal и отличается теперь от предка TPetal только своим конструктором, мы наглядно и полноценно продемонстрировали все принципы объектно-ориентированного программирования:

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


Вот какая реализация классов получилась в итоге:


constructor TOverlappedPetal.Create(Xmax, Ymax: integer);
begin
  inherited Create (Xmax,Ymax);
  K:=12;
  while PetalI=Round(PetalI) do
    PetalI:=(1+Random(6))/(1+Random(6));
end;

{ TPetal }

constructor TPetal.Create(Xmax, Ymax: integer);
begin
     inherited Create;
     CX:=Random(Xmax);
     CY:=Random(Ymax);
     K:=2;
     RColor:=1+Random($FFFFF0);
     Scale:=2+Random(12);
     PetalI:=2+Random(6);
end;

procedure TPetal.Draw(Canvas: TCanvas; Erase: boolean);
begin
     phi:=0;
     if Erase then RColor:=clBlack;
     with Canvas do
       while phi < K*pi do
         begin
           R:=10*sin(PetalI*phi);
           X:=CX+Trunc(Scale*R*cos(phi));
           Y:=CY-Trunc(Scale*R*sin(phi));
           Pixels[X,Y]:=RColor;
           phi+=pi/1800;
         end;
end;

Использовал я новые конструкции так: дополнил программу вторым массивом на 50 объектов класса TOverlappedPatel, кинкул второй таймер с периодом срабатывания 166 миллисекунд, прописал в его обработчик примерно такой же код, как у первого таймера. Из-за возникаемой между таймерами задержки визуально screensaver даже стал работать немного приятнее:



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


Часть III


Обычно в книгах по паскалю, delphi и lazarus описанию полиморфизма отводится пара страниц (в лучшем случае), не считая листинги кода (не считая, потому что от пары страниц текста понимание этих листингов не приходит). И поскольку книги по паскалю традиционно пишутся для студентов, все примеры кочуют из одного издания в другое и связаны с описанием абстрактного класса Человек и двух его наследников Студент и Преподаватель. Так как я не являюсь ни тем, ни другим, мне так и не удалось из этих книг почерпнуть знания о полиморфизме. Где бы можно было применить на практике классы Студентов и Преподавателей, чтобы понять сущность полиморфизма, я так и не придумал, поэтому пришлось постигать всё снова методом тыка.

Очень существенным недостатком всех этих книг я бы назвал то, что в главах о полиморфизме главным считается вопрос «как», хотя первостепенным является вопрос «зачем», ибо ответ на него поглощает 99% всей сути полиморфизма. Такой ответ я нашёл в чудесной статье Всеволода Леонова в блогах Embarcadero (текущее название владельца delphi). А в общих чертах полиморфизм представлен, например, так: есть базовый абстрактный класс Кошачьи, от которого порождены многочисленные наследники — Котик, Леопардик, Тигра, Лёва и т.д. Все они имеют схожие свойства, но методы их существования разные. Метод «мяу», к примеру, будет разным у котика и тигры. Метод базовго класса «поиграться» у котика перекрывается методом с реализацией «потереться об ноги», а вот у лёвы он перекрыт реализацией «сожрать руку». Однако все конкретные кошки будут являться объектами класса Кошачьи, и неопытный ребёнок будет настойчиво вызывать метод «поиграться» у всех кошачьих, не осознавая разницы.

Вернёмся к нашей практике рисования странных кругов полярных роз. Мы остановились на том, что усложнили работу программы, параллельно создав два массива по 50 объектов разных классов и два таймера с разным периодом срабатывания, у которых практически идентичные обработчики:

procedure TForm1.Timer1Timer(Sender: TObject);
begin
     if not Marg then
       begin
            Petals[CurPetal]:=TPetal.Create(img.Width,img.Height);
            Petals[CurPetal].Draw(img.Canvas);
            CurPetal+=1;
            if CurPetal=50 then Marg:=TRUE;
       end else begin
            Petals[50-CurPetal].Draw(img.Canvas,TRUE);
            Petals[50-CurPetal].Free;
            CurPetal-=1;
            if CurPetal=0 then Marg:=FALSE;
       end;
     img.Canvas.TextOut(10,10,IntToStr(CurPetal)+'  ');
end;

procedure TForm1.Timer2Timer(Sender: TObject);
begin
     if not MargO then
       begin
            OPetals[CurOPetal]:=TOverlappedPetal.Create(img.Width,img.Height);
            OPetals[CurOPetal].Draw(img.Canvas);
            CurOPetal+=1;
            if CurOPetal=50 then MargO:=TRUE;
       end else begin
            OPetals[50-CurOPetal].Draw(img.Canvas,TRUE);
            OPetals[50-CurOPetal].Free;
            CurOPetal-=1;
            if CurOPetal=0 then MargO:=FALSE;
       end;
     img.Canvas.TextOut(50,10,IntToStr(CurOPetal)+'  ');
end;

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


Сейчас у нас есть два класса: TPetal и его потомок TOverlappedPetal. Однако это немного неправильно и сейчас мы исправим ситуацию. Полярная роза с перекрывающимися лепестками и полярная роза с неперекрывающимися лепестками должны быть классами одного уровня, поскольку с точки зрения классовой теории они равнозначны. Поднимаясь на более высокую ступень абстракции, мы понимаем, что следует ввести базовый класс полярной розы, а вот от него уже порождать два вышеназванных. Всё схожее, что было в двух прежних классах, мы переносим в базовый, таким образом, два потомка будут различаться только самым необходимым:


  { TCustomPetal }

  TCustomPetal = class
    protected
      R, phi, PetalI: double;
      X, Y, K, CX, CY: integer;
      Scale, RColor: integer;
    public
      constructor Create (Xmax, Ymax: integer); virtual;
      procedure Draw (Canvas: TCanvas; Erase: boolean = FALSE);
  end;

  { TPetal }

  TPetal = class (TCustomPetal)
    public
      constructor Create (Xmax, Ymax: integer); override;
    end;

  { TOverlappedPetal }

  TOverlappedPetal = class (TCustomPetal)
    public
      constructor Create (Xmax, Ymax: integer); override;
    end;

Полиморфизм выражен в том, что в реализации потомков перекрывается конструктор базового класса (по миру ходит легенда, что в древних версиях объектного паскаля нельзя было перекрывать конструкторы классов, но я в это не верю). Данный прием реализуется с помощью использования зарезервированных директив virtual и override. Объявляя метод create базового класса виртуальным, мы тем самым даём понять, что реализация конструктора может быть (но не обязательно) перекрыта в потомках. Если бы мы добавили (что в нашем случае вполне возможно, но это усложнит код) к директиве virtual директиву abstract, то это бы значило, что реализации конструктора в базовом классе не будет, но потомки обязаны иметь такую реализацию. Мы не станем делать конструктор базового класса абстрактным, поскольку его реализация имеет и общие черты для потомков:


constructor TCustomPetal.Create(Xmax, Ymax: integer);
begin
  inherited Create;
  CX:=Random(Xmax);
  CY:=Random(Ymax);
  RColor:=1+Random($FFFFF0);
  Scale:=2+Random(12);
end;

constructor TOverlappedPetal.Create(Xmax, Ymax: integer);
begin
  inherited Create (Xmax,Ymax);
  K:=12;
  while PetalI=Round(PetalI) do
    PetalI:=(1+Random(6))/(1+Random(6));
end;

constructor TPetal.Create(Xmax, Ymax: integer);
begin
  inherited Create (Xmax,Ymax);
  K:=2;
  PetalI:=1+Random(8);
end;

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


Теперь посмотрите, как упрощается основной код программы. Вместо двух массивов с пятидесятью объектов разных классов мы объявляем один массив с объектами класса TCustomPetal, а обработчик события в таймере переписываем следующим образом:


procedure TForm1.Timer1Timer(Sender: TObject);
begin
     if not Marg then
       begin
            if Random(2)=1 then Petals[CurPetal]:=TPetal.Create(img.Width,img.Height)
              else Petals[CurPetal]:=TOverlappedPetal.Create(img.Width,img.Height);
            Petals[CurPetal].Draw(img.Canvas);
            CurPetal+=1;
            if CurPetal=50 then Marg:=TRUE;
       end else begin
            Petals[50-CurPetal].Draw(img.Canvas,TRUE);
            Petals[50-CurPetal].Free;
            CurPetal-=1;
            if CurPetal=0 then Marg:=FALSE;
       end;
     img.Canvas.TextOut(10,10,IntToStr(CurPetal)+'  ');
end;

Логика работы: берется случайное число от 0 до 3 и если оно равно 1, то для очередного объекта из массива CustomPetal вызывается конструктор класса-потомка TPetal, в противном случае вызывается конструктор класса-потомка TOverlappedPetal. В этом и проявляется полиморфизм: не смотря на то, что массив объектов у нас одного и того же типа TCustomPetal, по факту объекты создаются с типом потомка. Поскольку у них одни и те же поля, одни и те же методы — работа с ними ничем не отличается для программы. Мы вызываем одинаковый метод DrawPetal, но ведет он себя по-разному в зависимости от типа объекта. Согласитесь, код программы заметно упростился и стал более наглядным (для тех, кто все-таки вкурил парадигму ООП).


Свежий пример с изменениями — в ветке lersson3.


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


procedure TForm1.Timer1Timer(Sender: TObject);
begin
     if Assigned (Petals[CurPetal]) then
       begin
         Petals[CurPetal].Draw(img.Canvas,TRUE);
         Petals[CurPetal].Free;
       end;
     if Random(2)=1 then Petals[CurPetal]:=TPetal.Create(img.Width,img.Height)
        else Petals[CurPetal]:=TOverlappedPetal.Create(img.Width,img.Height);
     Petals[CurPetal].Draw(img.Canvas);
     CurPetal+=1;
     if CurPetal=PetalC then CurPetal:=0;
     img.Canvas.TextOut(10,10,IntToStr(CurPetal)+'  ');
end;

В целях еще большего совершенствования программы, я сделал массив роз динамическим (Petals: array of TCustomPetal), а в обработчике события при создании формы ему устанавливается размер — увеличил до 70, поскольку 50 роз на экране выглядят слишком жиденько. Логика работы обработчика таймера изменилась и вместе с тем упростилась: CurPetal — это наш указатель на текущий номер розы в массиве, и он пробегает бесконечно от 0 до 70 (т.к. динамические массивы всегда начинают нумерацию с нуля). Сначала проверяется, создан ли ранее элемент массива роз с номером CurPetal, и если да, то он затирается и уничтожается. Затем случайным способом создается тот же элемент с тем же номером. Указатель на номер массива инкременируется, и если он становится больше границы массива, то обнуляется (в нашем случае, последним существующим элементом массива будет элемент с номером 69, т.к. нумерация, напомню, идет с нуля). Окончательный вид скринсейвера (вверху для наглядности — счетчик):



Итоговый проект — в ветке lesson3-1.


В самом последнем варианте объем используемой во время работы памяти компьютера сократился до 7 с небольшим Мб, тогда как в начале приложение во всю веселилось с 30 Мб. Использование ООП, рефакторинг кода и знание математики позволяют делать красивый и эффективный код, помните это всегда.



P.S. Апдейт
В связи с дельными комментариями я исправил неточности и глюки, а также перенес работу рисовальщика в класс TPetals (TQPEtals в другом варианте), который «рулит» всем процессом с помощью списка объектов (TObjectList) или очереди объектов (TObjectQueue). Теперь метод таймера выглядит лаконично:
procedure TForm1.Timer1Timer(Sender: TObject);
begin
  P.DrawNext;
end;

А также в методе Draw включена проверка при стирании розы — не будут ли «испорчены» черными точками другие розы. Последний код проекта — в ветках lesson4-1 (список) и lesson 4-2 (очередь).

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 28

    +2
    покритикуем реализацию?
    не только ООП, но раз уж Вы упомянули «дублирование», «рефакторинг», «код с душком», etc, то явно академичность статьи выходит чуть дальше за рамки ООП
    а я как раз прочёл Р. Мартина, Фаулера, GoF (а ща читаю Физерса)


    0. Ctrl +D в Lazarus'е форматирует код (неформатированный код — отстой; правильные привычки нужно формировать с самого начала)

    1.
    procedure Draw (Canvas: TCanvas; Erase: boolean = FALSE);
    

    При вызове метода из программы без использования второго параметра объект рисуется, а при использовании параметра Erase — объект стирается:

    Метод стирания следует назвать Erase, и в нём уже вызывать «рисование с параметром»

    2. При этом стирание происходит непосредственно перед удалением объекта, поэтому место ему в деструкторе
    2.1 Для стирания нужен TCanvas, поэтому передаём TImage в конструкторе (тем более, что нам нужны его ширина и высота, которые имеют мало смысла, если рисовать мы будем на каком-то другом канвасе), и запоминаем его (использование интерфейса «дружеского класса» не нарушает инкапсуляцию)

    3. Управление списком созданных объектов нужно отдать классу TPetals/TPetalList, а не рулить глобальным индексом.
    3.1 При этом у Вас утечка памяти в
    procedure TForm1.FormClose(Sender: TObject; var CloseAction: TCloseAction);
    begin
      Petals := nil;
    end;
    

    тут не удаляются уже созданные объекты (да, знаю, при завершении программы, в общем-то пофиг, но «привычки...»)

    3.2. Он же (TPetals) рулит созданием нового экземпляра
    при этом
      if Random(2) = 1 then
        Petals[CurPetal] := TPetal.Create(img.Width, img.Height)
      else
        Petals[CurPetal] := TOverlappedPetal.Create(img.Width, img.Height);
    

    может выглядеть как (удаляем дублирование)
    type
      TCustomPetalClass = class of TCustomPetal;
    ...
    var
      LPetalClassToCreate: TCustomPetalClass;
    ...
      if Random(2) = 1 then
        LPetalClassToCreate := TPetal
      else
        LPetalClassToCreate := TOverlappedPetal
    
      Petals[CurPetal] := LPetalClassToCreate.Create(img.Width, img.Height);
    
    


    4. Кроме того, в строке
    if Assigned(Petals[CurPetal]) then
    


    происходит SEGV, если Timer сработает быстрее, чем выполнится FormCreate (у меня на Linux)
    поэтому надо выключить таймер в Design time
    и включать его во время исполнения
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      PetalC := 70;
      SetLength(Petals, PetalC);
      CurPetal := 0;
      timer1.Enabled := True;
    end;
    


    с учётом вышесказанного обработчик таймера будет выглядеть примерно так

    type
      TForm1 =
      private
        FPetals: TPetals;
        ...
      end;
    ...
    procedure TForm1.Timer1Timer(Sender: TObject);
    begin
      FPetals.DrawNext;
      img.Canvas.TextOut(10, 10, IntToStr(FPetals.Count) + '  ');
    end;
    
      0
      Здравствуйте.
      Большое спасибо за комментарии и то, что потратили время на такой дельный разбор. Я на самом деле этот учебный (для себя) проект делал пару лет назад, и тогда, если бы я дорабатывал его до идеала, я бы тоже решил запрятать полностью всю работу в классы. Потом забросил.
      Теперь вот решил допилить, учел практически полностью Ваши замечания, новый файл в ветке lesson4.
      Я только не понял в чем разница — передавать свойство Canvas или весь объект Image, я пока оставил Canvas. И отдельный метод для стирания писать не стал, т.к. считаю это избыточным, ведь нужна всего одна строчка, а ее я запрятал в аналог деструктора (метод kill), то есть в конечном итоге, как и предлагалось, обработчик таймера делает лишь DrawNext ))
        0
        сорре за придирчивость )) но чисто для полноты картины:

        Теперь вот решил допилить, учел практически полностью Ваши замечания

        и всё сломали )) код не компилируется (Unit1 not found) (заменили main на какой-то Unit1)
        если пофиксить это, то при закрытии формы возникает AV/SEGV

        И отдельный метод для стирания писать не стал, т.к. считаю это избыточным,

        ИМХО, тут речь не о Вас, пишущем код, а о том, что считает тот, кто читает ваш код ;) кстати, это можете быть и Вы (через полгода-год-два...)
        Один умный человек сказал: «вы всегда работаете в команде: „вы“ и „вы через две недели“».

        ведь нужна всего одна строчка, а ее я запрятал

        вот именно — «запрятал» )))
        а ведь код должен быть понятным

        в аналог деструктора (метод kill),

        зачем? если есть «стандарный способ»
        destructor Destroy; override;
        

        который вызывается при уничтожении объекта

        и опять глобальная переменная (P) ))) а не поле класса TForm1


        прошу прощения, если утомил, но дьявол, как известно, в мелочах, а настоящим программистам важны мелочи
          0
          Всё поправил)
          Интересно, а у меня одного лазарус в убунте ведет себя странно? То окна не найдешь, то новый проект сохраняет в старом (вот поэтому и вылез unit1).
          Ошибка при закрытии выпала, видимо, от того, что пока удаляются объекты, успевал сработать еще раз таймер. Выключил его при закрытии.
          Теперь затираемые розы не «портят» перекрытые.
          А насчет деструктора — в него нельзя передать параметр, поэтому сделал метод kill.
            0
            :)
            Теперь без косяков

            А насчет деструктора — в него нельзя передать параметр, поэтому сделал метод kill.

            потому я и говорил
            2.1 Для стирания нужен TCanvas, поэтому передаём TImage в конструкторе (тем более, что нам нужны его ширина и высота, которые имеют мало смысла, если рисовать мы будем на каком-то другом канвасе), и запоминаем его (использование интерфейса «дружеского класса» не нарушает инкапсуляцию)


            но говоря про TPetals как объект, управляющий списком объектов TPetals, я имел в виду
            что-то вроде
             TPetals = class(TObjectList)
            

            добавление нового «лепестка»
            P.Add(...)


            это удобнее, чем манипуляция массивом с проверками, типа
              if Assigned(fPetals[Count]) then
            

            и к тому же TPetals.Free, fSize, fCount становятся не нужны вовсе
            а меньше кода = чище код = понятней код = проще сопровождение

              0
              Ну теперь, кажется, всё) lesson4-1
              Таки-пришлось TImage передавать.
                0
                Нет, не всё)
                Я вот только погружаюсь в структуры, кажется, вместо списка здесь даже лучше использовать очередь, и в fcl есть для такого случая класс.
                  0
                  возможно, и очередь
                  но когда я посмотрел на
                      TCustomPetal(First).Erase;
                      Remove(First);
                  

                  то подумал про TCollection ))

                  а кстати, если бы Вы согласились на вызов Erase в деструкторе (а сейчас уже в нём есть доступ к TCanvas), то достаточно было бы
                      Remove(First);
                  
      +1
      На видео видно, как розы постоянно портятся, цвета близкие к чёрному надо бы из рандома исключить.
        –1
        Это не из-за цветов, просто рисуются они все на одной канве пикселями, друг на дружке, соответственно и затираются черным цветом тоже друг на дружке. У меня на работе целый год скринсейвером стоял, не придавал значения этому «дефекту».
          0
          А вообще я подумал, когда роза затирается, то можно организовать проверку пикселя перед сменой цветы на черный — если цвет не принадлежит этой розе, то и не затирать этот пиксель. Позже надо попробовать. Будет красивее.
            0

            … или исключить из рандома цвета, близкие к фону (чёрному в данном случае).

        –1
        Здорово! Наверно такого мне не хватало когда изучали в институте ООП с delphi.
        А существует что-нибудь подобное для других языков?
          0
          Есть видеокурс по C#, там на основе ООП делается простенькая игра «змейка»; я после него и вспомнил про этот свой проект.
            0
            Интересные примеры на C# и HAML здесь, а также понятное толкование сути наследования в классах
            книжка
              0
              еще книжка
              Microsoft Windows Presentation Foundation: базовый курс (много примеров графики в проекте клона блокнота, в дальнейшем парсера HAML)
              0
              TL;DR. Осторожно, красная капсула далее.

              Это не ООП. И вся статья про то, как мимикрировать под ООП, не задействовав сути идеи и ее преимуществ.

              > мы наглядно и полноценно продемонстрировали все принципы объектно-ориентированного программирования… инкапсуляция… наследование… полиморфизм.

              Нет, не все. Нет самого главного — абстракции.

              > (в комментах) Я только не понял в чем разница — передавать свойство Canvas или весь объект Image
              В этом ВСЯ разница. В данном случае — НАДО передавать Canvas, потому что Canvas в Delphi — это более универсальная сущность, чем Image. Передадите Image — сможете рисовать только на Image, передадите Canvas — на Image и на других типах, имеющих Canvas.

              И это все равно будет не ООП. Чтобы сделать это ООП (именно в этом месте) — создайте интерфейс с методами рисования и передавайте его в то, что хочет отрисоваться (фигуру) и реализуйте в том, на чем хотите отрисоваться — в данном случае в Image на базе методов того же Canvas'а. Читайте про Dependency Inversion Principle.

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

              Image.Canvas — вот вы и разрушили всю инкапсуляцию. Читайте про что-нибудь вроде «avoid getters», в т.ч. у упомянутых здесь Мартина и компании. Something.Anything — это намного хуже, чем геттеры.

              > полиморфизм представлен, например, так: есть базовый абстрактный класс Кошачьи, от которого порождены многочисленные наследники
              Про наследование лучше просто забудьте. Ищите по кивордам «composition over inheritance».

              P.S. Настоятельно рекомендую ознакомиться хотя бы со страницами с результатами поиска по ключевым словам выше (например avoid getters). Вы будете удивлены тем, насколько далеко то, что предлагает стандартный дельфийский подход, от ООП.
                0
                В данном случае — НАДО передавать Canvas, потому что Canvas в Delphi — это более универсальная сущность, чем Image. Передадите Image — сможете рисовать только на Image, передадите Canvas — на Image и на других типах, имеющих Canvas.

                а Вы правы, да. Это я посоветовал не сильно подумав (сам ещё только меняю привычки проектирования классов)

                ну, а интерфейсы я не стал «трогать»…
                  0
                  Почему, если есть время, давайте потрогаем. Только наведите на мысли, потому что для меня все вышеназванное пока голая теория.
                    0
                    ну, вы знакомы вообще с интерфейсами? ;) и вышеупомянутым DIP? *хотя бы в теории?
                      0
                      Только в теории, на уровне википедии и ряда статей на Хабре, которые вот нашел утром) Но я и со списками, стеками, очередями и ассоциированными массивами познакомился только на той неделе, но уже использую в отлаженном проекте. Так что быстро учусь.
                  0
                  Спасибо. Я как раз продолжаю сам изучать теорию ооп.
                  Но в данном конкретном примере, для начинающих, так сказать, не будет ли это избыточным усложнением? То есть, когда в школе учат формулам скорости, веса, ведь никто сходу не говорит, что нужно учитывать теорию относительности взамен простых вычислений s=vt.
                    0
                    Это не избыточное усложнение, это самая суть. Вышеприведенный пример —
                    создайте интерфейс с методами рисования и передавайте его в то, что хочет отрисоваться (фигуру) и реализуйте в том, на чем хотите отрисоваться

                    — это и есть s=vt. Абстракции и интерфейсы к ним — основа практического ООП.
                0
                Zapped,velvetcat, набросал заготовку с «интерфейсом» рисовальщика. На мой взгляд, чрезмерно усложнено по сравнению с предыдущим вариантом.
                Ну или я не так понял.
                  0
                  немножко облегчил код, добавил метод Erase в интерфейс рисовальщика, чтоб и затереть и сразу удалить объект
                    0
                    эээ… боюсь, вы не поняли интерфейсы
                    почитайте, например

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

                    N.B.:
                    модуль main ничего не знает про (= не зависит от) petalclass
                    также petalclass и ничего не знает про main
                    они связаны опосредовано через petalintf
                    таким образом мы в любой момент можем заменить классы TPetal и TPetals другими, и нам не надо будет менять main
                    так же мы можем протестировать в unit-тестах классы TPetals/TPetal, не используя TForm, а просто «подсунув» stub/mock интерфейса IPetalDrawer.
                      0
                      Большое спасибо за труд)
                      Как раз сижу и вникаю, как в книги по проектированию, так и в ваш код. Применил его на своем другом учебном проекте. Всё же, возвращаясь к методу обучения основам ООП… Ну вот, чтобы вникнуть в ваш код, мне пришлось его повторить, чтоб понять кто кого куда и зачем). И это имея уже представление о том, что делают те или иные классы, судя по названиям. А в тот момент, когда я только начинал изучать ООП, мне трудно было даже понять реализацию полиморфизма и смысл этого. Понял, только на ходу, делая вот этот проект с рисульками. Вряд ли на первых порах обучения стоит сразу же в простейшие проекты вставлять сложные шаблоны проектирования. Это и усложняет их, и делает трудными для чтения и понимания.

                Only users with full accounts can post comments. Log in, please.