There are two ways to study OOP (object-oriented programming): either read a hundred books that give a naked theory about class structure and principles of inheritance, polymorphism, encapsulation, but never learn anything, or stop worrying and try to learn new techniques in practice , by reworking, for example, ready-made codes, and better from scratch having made something simple, but beautiful.
In all the books devoted to Pascal, delphi and lazarus (I found as many as two about the latter), a very similar part devoted to the PLO. With these books, you can learn a lot about how much steeper the PLO of an outdated structural approach is, but you can never get enough skills to apply this in practice. Of course, any programmer using visual IDEs already uses OOP by default, since all components and structural elements of a visual application are objects, but it is very difficult to transfer your own structures and abstractions to the OOP paradigm. To understand all the charm and evaluate the opening prospects, I decided to make a small application that eventually turned into a simple screensaver. At the same time he remembered the existence of trigonometry.
The application will draw on the screen in random places fifty polar roses with different characteristics: size, color, number of petals. Then they overwrite and draw new ones, etc. Using the principles of structured programming, you can, of course, make an ordinary multidimensional array with a volume of 50 and keep all unique characteristics in it. However, it is worth remembering that Pascal implies strong data typing, and, therefore, an array cannot consist of elements with different types. It is possible to make an array of records (record), but why is it trivial, from recording to class is one step. Here we will make it.
An important principle of OOP is encapsulation. This means that the class must hide within itself the entire logic of its work. Our class, let's call it TPetal, has fields with different data types that define unique characteristics (center coordinates, size, coefficients for the polar rose equation, color), and methods of work. All other elements of the program should only call these methods, without delving into the details of their implementation. For now, the class should be able to draw and erase itself. For a start, enough:
{ 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;
The class constructor has two parameters — these are the boundaries of the canvas on which drawing will occur. The implementation of the constructor is as follows:
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;
Any man-made class in Delphi / Lazarus is a direct or indirect descendant of the TObject class, and when creating objects of its class, it is necessary to call the parent's constructor so that the object is created correctly and the computer memory is allocated for it. Therefore, at the beginning of our constructor, we call the parent constructor. Then we randomly generate the unique characteristics of our polar rose: the coordinates of the center, the color, the scale factor and the coefficient determining the number of petals.
Next is the drawing method. As you can see, to draw or erase an object, I wrote a single method in which there is a second parameter and by default it is FALSE. This is due to the fact that the draw and erase - the same operation, only the object is drawn in a random color, and erased - black. When calling a method from a program without using the second parameter, the object is drawn, and when using the Erase parameter, the object is erased:
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;
where ρ defines the radial coordinate, and φ - angular. α is the coefficient determining the length of the petals. In our formula, the coefficient is immediately equal to 10, so that the roses do not turn out too small. The angular coordinate of us runs from 0 to 2π to capture all 360 degrees (while loop). And after obtaining the radial coordinate, we calculate the Cartesian: x and y to draw this point on the canvas (once again marvel at how quickly modern computers perform calculations: inside the method there is a long cycle in which trigonometric calculations; remember how “quickly” drew a similar Zx-spectrum). The coefficient k in the formula (in the program - PetalI) determines the number of petals. For the sake of simplicity, we use only whole numbers, so all the roses are hypotrochoid with non-overlapping petals.
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;
As you can see, I used a couple of global variables: CurPetal is an object counter, takes values from 0 to 50 and back; Marg is a counter boundary indicator, but everything should be very clear from the logic of the method.
If we used the structured programming paradigm, then inside the timer handler we would have to independently initialize all the characteristics of the unique rose, draw it, and then erase it. The method would have grown and would not be visual. But now we have a class that does everything on its own - the class constructor immediately initializes all the characteristics, and the class method DrawPetal encapsulates all the computation and drawing logic, for which it only receives a pointer to the necessary object that has the Canvas property (and this is any form and almost any component). The result is such a pretty screensaver:
Exploring the next principle of OOP - inheritance, in the future it is possible to generate a descendant from the class TPetal, for example TOverlappingPetal, in which the polar rose will have overlapping petals. To do this (for universalization purposes) in the ancestor class, you need to change the type of the PetalI field to a real number, and reload the constructor of the descendant so that this field can be initialized with a random fractional number according to the appropriate rules.
I saved the files of the project in my bitbucket repository , and for each stage I created a separate branch. The example above can be found in the lesson1 branch.
Now I propose to do what we have stopped at. So, we have a class TPetal, which is able to draw a polar rose with the number of petals from 3 to 16. However, all the objects we get with non-overlapping petals. Meanwhile, if you look at the tablet below, we will see that there are more varieties of them. The form is determined by the coefficient equal to n / d:
We breed a descendant of class 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;
In the TOverlappedPetal class, we add our own constructor, which will act together with the ancestor's constructor, as well as the overloaded DrawPetal method (in fact, we will do without it at the end, but at the moment it is a good way to demonstrate method overloading in class heirs). Here is the implementation:
{ 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;
It can be seen that the constructor of the TOverlappedPetal class uses the inherited method (inherited), but then changes the value of the PetalI field, which is used to set the coefficient that influences the shape of the rose. When calculating the field, we exclude integers in order not to duplicate the forms already available to the ancestor of TPetal.
Files of this example can be found in the lesson2 branch of the repository.
The difference of realizations is only in the coefficient multiplied by the number π (2 or 12). We take out this coefficient in a separate field of the ancestor TPetal (field K), remove the overload of the DrawPetal method now unnecessary and get the following structure of our classes:
{ 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;
Although the descendant of TOverlappedPetal is now different from the ancestor of TPetal only by its constructor, we clearly and fully demonstrated all the principles of object-oriented programming:
- encapsulation (all the work of the class is inside itself, external communication with the class - only a couple of methods and the transfer of the required minimum parameters);
- inheritance (we have generated from a class of roses with non-overlapping petals a class of roses with overlapping petals);
- minimally, polymorphism (we demonstrated overloading of methods - one name, but different implementation, but I plan to demonstrate the real power of polymorphism later).
Here is the implementation of the classes as a result:
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;
I used new constructions like this: I supplemented the program with a second array of 50 objects of the TOverlappedPatel class, the second timer with a trigger period of 166 milliseconds added a second code to its handler, I wrote about the same code as the first timer. Due to the delay between the timers, the screensaver visually even began to work a little nicer:
How can I improve the program? Just with the help of the third whale OOP - polymorphism. Now our program does double work, and the processor is drenched in sweat, continuously making trigonometric calculations (well, someone like, probably). Is it possible to create a single array of objects, but of different classes? This will be the next part, and the code from the example above will be in the lesson2-1 branch.
Usually in books on pascal, delphi and lazarus, a couple of pages are given to the description of polymorphism (at best), not counting code listings (not counting, because an understanding of these listings does not come from a couple of pages of text). And since books on pascal are traditionally written for students, all the examples migrate from one publication to another and are associated with the description of the abstract class Man and his two heirs Student and Teacher. Since I am neither one nor the other, I never managed to learn from these books about polymorphism. Where it would be possible to put into practice the classes of Students and Teachers, in order to understand the essence of polymorphism, I did not invent it, so I had to comprehend everything again at random.
I would call a very significant shortcoming of all these books that in the chapters on polymorphism the main question is “how”, although the primary question is “why”, because the answer absorbs 99% of the whole essence of polymorphism. I found this answer in the wonderful article by Vsevolod Leonov in the Embarcadero blogs (the current name of the owner is delphi). And in general, polymorphism is represented, for example, as follows: there is a basic abstract class Feline, from which numerous heirs are spawned - Kotyk, Leopardik, Tigra, Lev, etc. They all have similar properties, but the methods of their existence are different. The meow method, for example, will be different for cat and tigers. The base class “play” method at the cat is overlapped with the “rub it on the legs” implementation, but at the left it is blocked by the “devour the hand” implementation. However, all specific cats will be objects of the Feline class, and an inexperienced child will persistently call the "play" method in all felines without realizing the difference.
Let us return to our practice of drawing strange circles of polar roses. We stopped at the fact that complicated the work of the program, in parallel, creating two arrays of 50 objects of different classes and two timers with different response period, which have almost identical handlers:
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;
Agree, the labor coder himself would have devoured the hand that wrote such a code "with a smell." Since we decided to evolve into real programmers, we will solve the problem of getting rid of duplicate code and facilitating the work of the program.
We now have two classes: TPetal and its descendant TOverlappedPetal. However, this is a bit wrong and now we will correct the situation. A polar rose with overlapping petals and a polar rose with non-overlapping petals should be classes of the same level, since from the point of view of the class theory they are equivalent. Rising to a higher level of abstraction, we understand that the base class of the polar rose should be introduced, but from it we should generate the above two. All that was in the two previous classes, we transfer to the base, so two descendants will differ only in the most necessary:
{ 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;
Polymorphism is expressed in the fact that the implementation of descendants overlaps the constructor of the base class (the legend goes around the world that in the ancient versions of object pascal it was impossible to overlap class constructors, but I do not believe in it). This technique is implemented using the reserved virtual and override directives . By declaring the base class's create method virtual, we thereby make it clear that the implementation of the constructor can (but not necessarily) be overlapped in the children. If we added (which is quite possible in our case, but it will complicate the code) to the virtual directive to the abstract directive, then this would mean that there will be no constructor implementation in the base class, but descendants are required to have such an implementation. We will not make the base class constructor abstract, since its implementation has common features for posterity:
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;
So, the constructor of the base class initializes the fields common to the descendants - the coordinates of the center, color and scale. But the constructors of the descendants first call the constructor of the base class, and then carry out various initialization of the remaining fields - the coefficient for the angular coordinate and the coefficient determining the shape of the polar rose.
Now look at how the main program code is simplified. Instead of two arrays with fifty objects of different classes, we declare one array with objects of the TCustomPetal class, and rewrite the event handler in the timer as follows:
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;
Work logic: a random number from 0 to 3 is taken and if it is 1, then the constructor of the descendant class TPetal is called for the next object from the CustomPetal array, otherwise the constructor of the descendant class TOverlappedPetal is called. This is where polymorphism manifests itself: in spite of the fact that we have an array of objects of the same type of TCustomPetal, in fact objects are created with the type of descendant. Since they have the same fields, the same methods - working with them is no different for a program. We call the same DrawPetal method, but it behaves differently depending on the type of object. You must admit that the program code has become much simpler and more visual (for those who, nevertheless, have tasted the OOP paradigm).
A fresh example with changes is in the lersson3 branch.
How else can you improve the work? For my taste, the more attractive is the option where 50 roses are not consistently drawn and rubbed, and when this happens continuously. To do this, change the timer handler slightly, it is no longer associated with the PLO, but makes you wiggle your brains:
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;
In order to further improve the program, I made the array of roses dynamic (Petals: array of TCustomPetal), and in the event handler when creating the form it is set to size - increased to 70, because 50 roses on the screen look too thin. The logic of the timer handler has changed and at the same time simplified: CurPetal is our pointer to the current number of roses in the array, and it runs infinitely from 0 to 70 (since dynamic arrays always start numbering from zero). First, it is checked whether the element of the Rose array with the CurPetal number was created earlier, and if so, it is overwritten and destroyed. Then the same element with the same number is created in a random way. The pointer to the array number is incremented, and if it becomes larger than the array boundary, it is reset (in our case, the last existing element of the array will be element number 69, since numbering, I recall, comes from zero). The final view of the screensaver (at the top for clarity - the counter):
The final project is in the lesson3-1 branch.
In the most recent version, the amount of computer memory used during operation was reduced to 7 with a small MB, whereas at the beginning the application was merry with 30 MB. Using OOP, code refactoring and knowledge of mathematics allow you to make a beautiful and effective code, remember this always.
procedure TForm1.Timer1Timer(Sender: TObject); begin P.DrawNext; end;
Source: https://habr.com/ru/post/325010/
All Articles