Проектирование ПО при объектном подходе

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

Объектная декомпозиция задачи

Решение поставленной задачи можно разделить на следующие части:

· хранение данных о текущем положении мяча и его скорости;

· управление движением мяча (изменение его положения, проверка касания поверхности, расчет параметров движения);

· визуализация движения.

Таким образом, задачу можно делегировать 3 классам – классу мяча (TBall), классу, контролирующему физическое поведение мяча (TBallPhysicController), и классу, контролирующему отрисовку мяча и поверхности (TBallVisualController). Кроме того понадобится ещё один класс – класс формы приложения.

Определение отношений между объектами

На рисунке 3.1 приведена схема отношений между выявленными сущностями.

Рисунок 3.1 – Отношения между объектами

Класс TMainForm имеет свойство типа TBallVisualController для доступа к отрисовке мяча. Класс TBallVisualController должен иметь свойство типа TBallPhysicController для доступа к данным о поверхности, а также, чтобы класс TMainForm имел доступ к экземпляру TBallPhysicController (для задания параметров). Класс TBallPhysicController имеет свойство типа TBall для доступа к данным о мяче, а также, чтобы классы TBallVisualController и TMainForm имели доступ к объекту-мячу.

Проектирование классов

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

 

Рисунок 3.2 – Диаграмма классов

Класс TMainForm обеспечивает взаимодействие с пользователем, и имеет методы Restart и Pause, которые инициируются пользователем. Также класс имеет метод Timer, который вызывается обработчиком компонента типа TTimer по прошествии определенного времени, что позволяет использовать его для инициации начала новой итерации (т.е. движения мяча за малый промежуток времени).

Класс TBallVisualController инкапсулирует рисование поверхности и мяча. Т.к. поверхности со временем не меняется выгодно нарисовать её один раз, а затем копировать при перерисовывании, поэтому в классе есть поле FBackground и метод GenerateBackground. Также для метода является важным изменение границ окна вывода, поэтому у него есть методы Restart и ResizeBox, имеющие в качестве параметров ширину и высоту окна вывода. Логично, что метод TBallVisualController.Restart вызывается из одноименного метода класса TMainForm.

Класс TBallVisualController инкапсулирует изменение координат мяча по рассчитанным параметрам, проверку касания поверхности и расчет новых параметров при столкновении. Метод Iterate вызывается из метода Timer класса TMainForm и выполняет расчет координат мяча и его текущей скорости по известным параметрам и измененному моменту времени. Приватные методы CheckContact, CalculateAlpha и GenerateSurface соответственно проверяют касание поверхности, вычисляют новые параметры движения при столкновении с поверхностью, и генерируют поверхность при рестарте.

Класс TBall инкапсулирует координаты мяча, его текущую скорость, и радиус. И имеет 1 метод – метод получения квадратной области, в которую вписан мяч GetRect.

Исходный код программы


unit MainFm;

 

interface

 

uses

Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,

Dialogs, StdCtrls, ExtCtrls, ActnList, Menus, Ball, XPMan, ComCtrls;

 

type

TMainForm = class(TForm)

WorkAreaPanel: TPanel;

LabMainMenu: TMainMenu;

FileMenuItem: TMenuItem;

ExitMenuItem: TMenuItem;

ParamMenuItem: TMenuItem;

AboutMenuItem: TMenuItem;

RestartMenuItem: TMenuItem;

ActionList1: TActionList;

ExitAction: TAction;

RestartAction: TAction;

AboutAction: TAction;

ManageGroupBox: TGroupBox;

RestartButton: TButton;

AnimateTimer: TTimer;

PauseButton: TButton;

XPManifest1: TXPManifest;

PauseAction: TAction;

PauseItem: TMenuItem;

SpeedEdit: TEdit;

SpeedUpDown: TUpDown;

RadiusEdit: TEdit;

RadiusUpDown: TUpDown;

AngleLabel: TLabel;

RadiusLabel: TLabel;

HideControlPanelAction: TAction;

HideControlPanelMenuItem: TMenuItem;

ButtonGroupBox: TGroupBox;

BallGroupBox: TGroupBox;

SurfaceGroupBox: TGroupBox;

AutoGenerateCheckBox: TCheckBox;

FirstSinLabel: TLabel;

FirstSinMEdit: TEdit;

FirstSinPiShiftEdit: TEdit;

OperationEdit: TEdit;

SecondSinMEdit: TEdit;

SecondSinLabel: TLabel;

SecondSinPiShiftEdit: TEdit;

Label1: TLabel;

AnglePiLabel: TLabel;

AngleEdit: TEdit;

RandomAngleCheckBox: TCheckBox;

FirstSinDividerEdit: TEdit;

SecondSinDividerEdit: TEdit;

procedure AboutActionExecute(Sender: TObject);

procedure RestartActionExecute(Sender: TObject);

procedure ExitActionExecute(Sender: TObject);

procedure AnimateTimerTimer(Sender: TObject);

procedure PauseButtonClick(Sender: TObject);

procedure FormPaint(Sender: TObject);

procedure PauseActionExecute(Sender: TObject);

procedure SpeedUpDownClick(Sender: TObject; Button: TUDBtnType);

procedure SpeedEditKeyDown(Sender: TObject; var Key: Word;

Shift: TShiftState);

procedure FormCreate(Sender: TObject);

procedure RadiusUpDownClick(Sender: TObject; Button: TUDBtnType);

procedure RadiusEditKeyDown(Sender: TObject; var Key: Word;

Shift: TShiftState);

procedure HideControlPanelActionExecute(Sender: TObject);

procedure WorkAreaPanelResize(Sender: TObject);

procedure AutoGenerateCheckBoxClick(Sender: TObject);

procedure RandomAngleCheckBoxClick(Sender: TObject);

private

FBallVisualController: TBallVisualController;

// Начать заново.

procedure Restart;

end;

 

var

MainForm: TMainForm;

 

implementation

 

uses Math;

 

{$R *.dfm}

 

const

MAX_START_SPEED = 100;

 

type

EWrongOperation = class(Exception);

 

procedure TMainForm.AboutActionExecute(Sender: TObject);

const

ABOUT_MESSAGE = 'Программу разработал в 2011 г.' + sLineBreak +

'студент гр. М01-784-1 - Полин А.Ю.' + sLineBreak +

'(Объекто-ориентированная реализация)';

begin

ShowMessage(ABOUT_MESSAGE);

end;

 

procedure TMainForm.RestartActionExecute(Sender: TObject);

begin

Restart;

end;

 

procedure TMainForm.ExitActionExecute(Sender: TObject);

begin

Close;

end;

 

procedure TMainForm.AnimateTimerTimer(Sender: TObject);

begin

AnimateTimer.Enabled := False;

FBallVisualController.Iterate;

AnimateTimer.Enabled := True;

end;

 

procedure TMainForm.PauseButtonClick(Sender: TObject);

begin

AnimateTimer.Enabled := False;

end;

 

procedure TMainForm.FormPaint(Sender: TObject);

begin

if Assigned(FBallVisualController) then

FBallVisualController.Draw;

end;

 

procedure TMainForm.PauseActionExecute(Sender: TObject);

const

PAUSE_CAPTION = 'Пауза';

PLAY_CAPTION = 'Возобновить';

begin

AnimateTimer.Enabled := not AnimateTimer.Enabled;

if AnimateTimer.Enabled then

PauseAction.Caption := PAUSE_CAPTION

else

PauseAction.Caption := PLAY_CAPTION;

end;

 

procedure TMainForm.SpeedUpDownClick(Sender: TObject; Button: TUDBtnType);

begin

SpeedEdit.Text := FloatToStr(SpeedUpDown.Position / 10);

end;

 

procedure TMainForm.SpeedEditKeyDown(Sender: TObject; var Key: Word;

Shift: TShiftState);

const

HIGH_SPEED_MESSAGE = 'К сожалению, скорость мяча не может быть более %d м/c';

var

Value: Double;

begin

if Key = VK_RETURN then

try

Value := StrToFloat(SpeedEdit.Text);

if Value < 0 then

SpeedEdit.Text := '0'

else

if Value > MAX_START_SPEED then

begin

SpeedEdit.Text := FloatToStr(MAX_START_SPEED);

ShowMessage(Format(HIGH_SPEED_MESSAGE, [MAX_START_SPEED]));

end;

SpeedUpDown.Position := Round(StrToFloat(SpeedEdit.Text) * 10);

except

SpeedEdit.Text := FloatToStr(SpeedUpDown.Position / 10);

end;

end;

 

procedure TMainForm.FormCreate(Sender: TObject);

begin

SpeedUpDown.Max := MAX_START_SPEED * 10;

WorkAreaPanel.DoubleBuffered := True;

WorkAreaPanel.ControlStyle := WorkAreaPanel.ControlStyle + [ csOpaque ];

end;

 

procedure TMainForm.RadiusUpDownClick(Sender: TObject;

Button: TUDBtnType);

begin

RadiusEdit.Text := FloatToStr(RadiusUpDown.Position / 10);

end;

 

procedure TMainForm.Restart;

const

PAUSE_CAPTION = 'Пауза';

OPERATION_MINUS = '-';

OPERATION_PLUS = '+';

E_CONVERT_ERROR = '''%s'' не является вещественным значением';

var

Buffer: String;

WrongFloatValue: String;

begin

try

if not Assigned(FBallVisualController) then

FBallVisualController := TBallVisualController.Create(WorkAreaPanel);

PauseAction.Enabled := True;

PauseAction.Caption := PAUSE_CAPTION;

AnimateTimer.Enabled := False;

FBallVisualController.BallPhysicController.BallDefaultZeroSpeed :=

SpeedUpDown.Position / 10;

FBallVisualController.BallPhysicController.Ball.Radius :=

RadiusUpDown.Position * 10;

if not AutoGenerateCheckBox.Checked then

with FBallVisualController.BallPhysicController.SurfaceGenerationParams do

begin

FirstSinusoidMultiplier := StrToFloat(FirstSinMEdit.Text);

FirstSinusoidPIShift := StrToFloat(FirstSinPiShiftEdit.Text);

FirstSinusoidDivider := StrToFloat(FirstSinDividerEdit.Text);

SecondSinusoidMultiplier := StrToFloat(SecondSinMEdit.Text);

SecondSinusoidPIShift := StrToFloat(SecondSinPiShiftEdit.Text);

SecondSinusoidDivider := StrToFloat(SecondSinDividerEdit.Text);

if Trim(OperationEdit.Text) = OPERATION_PLUS then

SinOperation := 1

else

if Trim(OperationEdit.Text) = OPERATION_MINUS then

SinOperation := -1

else

raise EWrongOperation.Create('Операция может быть только + или -');

end;

if not RandomAngleCheckBox.Checked then

FBallVisualController.BallPhysicController.Alpha := Pi / StrToFloat(AngleEdit.Text);

FBallVisualController.Restart(WorkAreaPanel.ClientWidth, WorkAreaPanel.ClientHeight,

AutoGenerateCheckBox.Checked, RandomAngleCheckBox.Checked);

if AutoGenerateCheckBox.Checked then

with FBallVisualController.BallPhysicController.SurfaceGenerationParams do

begin

FirstSinMEdit.Text := FloatToStr(FirstSinusoidMultiplier);

FirstSinPiShiftEdit.Text := FloatToStr(FirstSinusoidPIShift);

FirstSinDividerEdit.Text := FloatToStr(FirstSinusoidDivider);

SecondSinMEdit.Text := FloatToStr(SecondSinusoidMultiplier);

SecondSinPiShiftEdit.Text := FloatToStr(SecondSinusoidPIShift);

SecondSinDividerEdit.Text := FloatToStr(SecondSinusoidDivider);

case SinOperation of

-1:

OperationEdit.Text := OPERATION_MINUS;

1:

OperationEdit.Text := OPERATION_PLUS;

end;

end;

if RandomAngleCheckBox.Checked then

AngleEdit.Text := FloatToStr(Pi / FBallVisualController.BallPhysicController.Alpha);

AnimateTimer.Enabled := True;

except

on E: EConvertError do

begin

Buffer := Copy(E.Message, 2, Length(E.Message) - 1);

WrongFloatValue := Copy(Buffer, 1, Pos('''', Buffer) - 1);

ShowMessage(Format(E_CONVERT_ERROR, [WrongFloatValue]));

end;

on E: EWrongOperation do

ShowMessage(E.Message);

end;

end;

 

procedure TMainForm.RadiusEditKeyDown(Sender: TObject; var Key: Word;

Shift: TShiftState);

var

Value: Double;

begin

if Key = VK_RETURN then

try

Value := StrToFloat(RadiusEdit.Text);

if Value < 0 then

RadiusEdit.Text := '0'

else

if Value > 0.5 then

RadiusEdit.Text := '0.5';

RadiusUpDown.Position := Round(StrToFloat(RadiusEdit.Text) * 10);

except

RadiusEdit.Text := FloatToStr(RadiusUpDown.Position / 10);

end;

end;

 

procedure TMainForm.HideControlPanelActionExecute(Sender: TObject);

const

HIDE_ACTION_CAPTION = 'Скрыть панель управления';

SHOW_ACTION_CAPTION = 'Показать панель управления';

begin

ManageGroupBox.Visible := not ManageGroupBox.Visible;

case ManageGroupBox.Visible of

True:

HideControlPanelAction.Caption := HIDE_ACTION_CAPTION;

False:

HideControlPanelAction.Caption := SHOW_ACTION_CAPTION;

end;

end;

 

procedure TMainForm.WorkAreaPanelResize(Sender: TObject);

begin

if Assigned(FBallVisualController) then

FBallVisualController.ResizeBox(WorkAreaPanel.ClientWidth, WorkAreaPanel.ClientHeight);

end;

 

procedure TMainForm.AutoGenerateCheckBoxClick(Sender: TObject);

begin

FirstSinMEdit.Enabled := not FirstSinMEdit.Enabled;

FirstSinPiShiftEdit.Enabled := not FirstSinPiShiftEdit.Enabled;

OperationEdit.Enabled := not OperationEdit.Enabled;

SecondSinMEdit.Enabled := not SecondSinMEdit.Enabled;

SecondSinPiShiftEdit.Enabled := not SecondSinPiShiftEdit.Enabled;

FirstSinLabel.Enabled := not FirstSinLabel.Enabled;

SecondSinLabel.Enabled := not SecondSinLabel.Enabled;

FirstSinDividerEdit.Enabled := not FirstSinDividerEdit.Enabled;

SecondSinDividerEdit.Enabled := not SecondSinDividerEdit.Enabled;

end;

 

procedure TMainForm.RandomAngleCheckBoxClick(Sender: TObject);

begin

AnglePiLabel.Enabled := not AngleLabel.Enabled;

AngleEdit.Enabled := not AngleEdit.Enabled;

end;

 

end.

unit Ball;

 

interface

 

uses

Windows, Graphics, Controls;

 

type

// Мяч.

TBall = class

private

FRadius: Double;

FX: Double;

FY: Double;

FSpeed: Double;

function GetRect: TRect;

public

// Координата X центра.

property X: Double read FX write FX;

// Координата Y центра.

property Y: Double read FY write FY;

// Радиус.

property Radius: Double read FRadius write FRadius;

// Скорость.

property Speed: Double read FSpeed write FSpeed;

// Область, описанная вокруг мяча.

property Rect: TRect read GetRect;

end;

 

// Поверхность.

TSurface = array of TPoint;

 

// Параметры генерации поверхности из двух синусоид.

TSurfaceGenerationParams = record

// Множитель первой синусоиды.

FirstSinusoidMultiplier: Double;

// Множитель второй синусоиды.

SecondSinusoidMultiplier: Double;

// Делитель аргумента первой синусоиды.

FirstSinusoidDivider: Double;

// Делитель аргумента второй синусоиды.

SecondSinusoidDivider: Double;

// Делитель смещения первой синусоиды.

FirstSinusoidPIShift: Double;

// Делитель смещения второй синусоиды.

SecondSinusoidPIShift: Double;

// Операция применяемая над синусоидами.

SinOperation: Integer;

// Минимальное значение координаты Y в точках поверхности.

SurfaceMinY: Integer;

end;

 

// Тип соприкосновения с поверхностью.

TContactType = (

// С левой стороной коробки.

ctBoxLeft,

// С правой стороной коробки.

ctBoxRight,

// С верхом коробки.

ctBoxTop,

// С дном коробки.

ctBoxBottom,

// С поверхностью.

ctSurface);

 

// Контролер физического поведения мяча.

TBallPhysicController = class

private

// Мяч.

FBall: TBall;

// Поверхность.

FSurface: TSurface;

// Ширина коробки.

FBoxWidth: Integer;

// Высота коробки.

FBoxHeight: Integer;

// Текущий момент времени для расчета параболы.

FTimeMoment: Double;

// Угол альфа для расчета параболы.

FAlpha: Double;

// Предыдущее значение координаты X мяча.

FBallPreviousX: Double;

// Предыдущее значение координаты Y мяча.

FBallPreviousY: Double;

// 0 значение координаты X движения по параболе.

FBallZeroPointX: Double;

// 0 значение координаты Y движения по параболе.

FBallZeroPointY: Double;

// Скорость в 0 точке.

FBallZeroSpeed: Double;

// Скорость в 0 точке при рестарте.

FBallDefaultZeroSpeed: Double;

// Параметры генерации поверхности.

FSurfaceGenerationParams: TSurfaceGenerationParams;

// Cгенерировать поверхность.

procedure GenerateSurface;

// Сгенерировать случайные параметры поверхности.

procedure GenerateRandomSurfaceParams;

// Вычислить минимальное значение Y синусоиды поверхности.

procedure CalculateSurfaceMinY;

// Проверить соприкосновение с поверхностью и коробкой.

procedure CheckContact;

// Вычислить угол альфа при отскоке.

procedure CalculateAlpha(

// Тип соприкосновения.

const AContactType: TContactType;

// Индекс точки поверхности.

const ASurfacePointIndex: Integer = 0);

public

property Ball: TBall read FBall;

property Surface: TSurface read FSurface;

property BallDefaultZeroSpeed: Double read FBallDefaultZeroSpeed

write FBallDefaultZeroSpeed;

property SurfaceGenerationParams: TSurfaceGenerationParams

read FSurfaceGenerationParams;

property Alpha: Double read FAlpha write FAlpha;

// Изменить размер коробки.

procedure ResizeBox(

// Ширина коробки.

const ABoxWidth: Integer;

// Высота коробки.

const ABoxHeight: Integer);

// Начать заново.

procedure Restart(

// Ширина коробки.

const ABoxWidth: Integer;

// Высота коробки.

const ABoxHeight: Integer;

// Автоматически генерировать поверхность.

const AIsAutoGeneratedSurface: Boolean;

// Случайный угол падения мяча.

const AIsRandomAngle: Boolean);

// Выполнить итерацию.

procedure Iterate;

// Конструктор.

constructor Create;

// Деструктор.

destructor Destroy; override;

end;

 

// Контроллер отрисовки мяча.

TBallVisualController = class

private

// Контролер физического поведения мяча.

FBallPhysicController: TBallPhysicController;

// Окно на котором идет отрисовка.

FWindow: TWinControl;

// Высота коробки.

FBoxHeight: Integer;

// Ширина коробки.

FBoxWidth: Integer;

// Задний план.

FBackground: TBitmap;

// Инвертировать координату Y точки.

function Invert(APoint: TPoint): TPoint;

// Инвертировать координаты Y области.

function InvertRect(ARect: TRect): TRect;

// Сгенерировать задний план.

procedure GenerateBackground;

public

// Отрисовать.

procedure Draw;

// Изменить размер коробки.

procedure ResizeBox(

// Ширина коробки.

const ABoxWidth: Integer;

// Высота коробки.

const ABoxHeight: Integer);

// Начать заново.

procedure Restart(

// Ширина коробки.

const ABoxWidth: Integer;

// Высота коробки.

const ABoxHeight: Integer;

// Автоматически генерировать поверхность.

const AIsAutoGeneratedSurface: Boolean;

// Случайный угол падения мяча.

const AIsRandomAngle: Boolean);

// Конструктор.

constructor Create(

// Окно на котором идет отрисовка.

const AWindow: TWinControl);

// Деструктор.

destructor Destroy; override;

// Выполнить итерацию.

procedure Iterate;

// Контролер физического поведения мяча.

property BallPhysicController: TBallPhysicController read

FBallPhysicController;

end;

 

implementation

 

uses

Math, VectoryAlgebra, Types, SysUtils, Classes;

 

{ TBallPhysicController }

 

procedure TBallPhysicController.CalculateAlpha(

const AContactType: TContactType; const ASurfacePointIndex: Integer);

var

ContactPoint, PreviousPointMove, Perpendicular: TVector2R;

BufferAlpha, Teta, AxisDifferenceAngle: Double;

begin

// Определить точку контакта.

case AContactType of

ctBoxLeft:

ContactPoint := AddVect2R(Vector2R(FBall.X, FBall.Y), Vector2R(- FBall.Radius, 0));

ctBoxRight:

ContactPoint := AddVect2R(Vector2R(FBall.X, FBall.Y), Vector2R(FBall.Radius, 0));

ctBoxTop:

ContactPoint := AddVect2R(Vector2R(FBall.X, FBall.Y), Vector2R(0, FBall.Radius));

ctBoxBottom:

ContactPoint := AddVect2R(Vector2R(FBall.X, FBall.Y), Vector2R(0, - FBall.Radius));

ctSurface:

ContactPoint := Vector2R(FSurface[ASurfacePointIndex].X, FSurface[ASurfacePointIndex].Y);

end;

PreviousPointMove := SubVect2R(Vector2R(FBallPreviousX, FBallPreviousY),

Vector2R(FBall.X, FBall.Y));

Perpendicular := RightPerpendicularVector2R(SubVect2R(Vector2R(FBall.X, FBall.Y), ContactPoint));

BufferAlpha := AngelFromVectorToVector(Perpendicular, PreviousPointMove);

Teta := Pi - BufferAlpha;

AxisDifferenceAngle := AngelFromVectorToVector2Pi(Vector2R(1, 0), Perpendicular);

FAlpha := Teta + AxisDifferenceAngle;

FBallZeroSpeed := FBall.Speed;

FTimeMoment := 0;

FBallZeroPointX := FBall.X;

FBallZeroPointY := FBall.Y;

end;

procedure TBallPhysicController.CalculateSurfaceMinY;

var

I, CalculatedValue: Integer;

begin

with FSurfaceGenerationParams do

begin

SurfaceMinY := High(Integer);

// Минимальное значение функции встречается на [0; 2Pi].

for I := 0 to 628 do

begin

CalculatedValue := Round((FirstSinusoidMultiplier * Sin(I / FirstSinusoidDivider) -

pi / FirstSinusoidPIShift) + SinOperation * (SecondSinusoidMultiplier *

Sin(I / SecondSinusoidDivider) + pi / SecondSinusoidPIShift));

if CalculatedValue < SurfaceMinY then

SurfaceMinY := CalculatedValue;

end;

SurfaceMinY := Abs(SurfaceMinY) + 10;

end;

end;

 

procedure TBallPhysicController.CheckContact;

var

I, MinSurfaceDistanceIndex: Integer;

MinSurfaceDistance, Distance: Double;

begin

// Проверить касание коробки.

if (FBall.Rect.Left <= 0) and (FBall.X <= FBallPreviousX) then

CalculateAlpha(ctBoxLeft)

else

if (FBall.Rect.Right >= FBoxWidth) and (FBall.X >= FBallPreviousX) then

CalculateAlpha(ctBoxRight)

else

if (FBall.Rect.Top >= FBoxHeight) and (FBall.Y >= FBallPreviousY) then

CalculateAlpha(ctBoxTop)

else

if (FBall.Rect.Bottom <= 0) and (FBall.Y <= FBallPreviousY) then

CalculateAlpha(ctBoxBottom)

else

begin

// Проверить касание поверхности.

MinSurfaceDistance := 1.7e308;

MinSurfaceDistanceIndex := -1;

for I := 0 to Length(FSurface) - 1 do

begin

Distance := DistBetweenPoints2R(Vector2R(FSurface[I].X, FSurface[I].Y),

Vector2R(FBall.X, FBall.Y));

if Distance < MinSurfaceDistance then

begin

MinSurfaceDistance := Distance;

MinSurfaceDistanceIndex := I;

end;

end;

if (MinSurfaceDistanceIndex <> -1) and (MinSurfaceDistance <= FBall.Radius) and

(MinSurfaceDistance < DistBetweenPoints2R(Vector2R(FSurface[MinSurfaceDistanceIndex].X,

FSurface[MinSurfaceDistanceIndex].Y), Vector2R(FBallPreviousX, FBallPreviousY))) then

CalculateAlpha(ctSurface, MinSurfaceDistanceIndex);

end;

end;

 

constructor TBallPhysicController.Create;

begin

inherited Create;

FBall := TBall.Create;

FBall.FRadius := 20.0;

FBallDefaultZeroSpeed := 0;

end;

 

destructor TBallPhysicController.Destroy;

begin

SetLength(FSurface, 0);

FBall.Free;

inherited;

end;

 

procedure TBallPhysicController.GenerateRandomSurfaceParams;

begin

with FSurfaceGenerationParams do

begin

Randomize;

FirstSinusoidMultiplier := 25 * (Random(5) + 1);

SecondSinusoidMultiplier := 15 * (Random(3) + 1);

FirstSinusoidDivider := 100;

SecondSinusoidDivider := 50;

FirstSinusoidPIShift := Random(3) + 1;

SecondSinusoidPIShift := Random(6) + 1;

SinOperation := Random(2) - 1;

if SinOperation = 0 then

Inc(SinOperation);

end;

end;

 

procedure TBallPhysicController.GenerateSurface;

var

I: Integer;

begin

SetLength(FSurface, 0);

SetLength(FSurface, FBoxWidth);

with FSurfaceGenerationParams do

begin

for I := 0 to FBoxWidth - 1 do

begin

FSurface[I].X := I;

// Сложить 2 синуосоиды.

FSurface[I].Y := Round(

(FirstSinusoidMultiplier * Sin(I / FirstSinusoidDivider) - pi / FirstSinusoidPIShift) +

SinOperation * (SecondSinusoidMultiplier * Sin(I/ SecondSinusoidDivider) +

pi / SecondSinusoidPIShift));

end;

// Необходимо поднять поверхность, чтобы она отображалась целиком.

for I := 0 to FBoxWidth - 1 do

FSurface[I].Y := FSurface[I].Y + Abs(SurfaceMinY);

end;

end;

 

procedure TBallPhysicController.Iterate;

var

I: Integer;

IterateCount: Integer;

begin

if FBall.Speed > 1 then

IterateCount := Round(FBall.Speed) + 10

else

IterateCount := 1;

for I := 1 to IterateCount do

begin

FTimeMoment := FTimeMoment + 0.01 / IterateCount;

FBallPreviousX := FBall.X;

FBallPreviousY := FBall.Y;

FBall.X := FBallZeroPointX + Cos(FAlpha) * FBallZeroSpeed * FTimeMoment * 100;

FBall.Y := FBallZeroPointY + (Sin(FAlpha) * FBallZeroSpeed * FTimeMoment -

4.9 * Sqr(FTimeMoment)) * 100;

FBall.Speed := Sqrt(Sqr(FBallZeroSpeed * Cos(FAlpha)) +

Sqr(FBallZeroSpeed * Sin(FAlpha) - 9.8 * FTimeMoment));

CheckContact;

end;

end;

 

procedure TBallPhysicController.ResizeBox(const ABoxWidth,

ABoxHeight: Integer);

begin

FBoxWidth := ABoxWidth;

FBoxHeight := ABoxHeight;

GenerateSurface;

end;

 

procedure TBallPhysicController.Restart(const ABoxWidth,

ABoxHeight: Integer; const AIsAutoGeneratedSurface,

AIsRandomAngle: Boolean);

var

RandomAnglePart: Integer;

begin

FBoxWidth := ABoxWidth;

FBoxHeight := ABoxHeight;

if AIsAutoGeneratedSurface then

GenerateRandomSurfaceParams;

CalculateSurfaceMinY;

GenerateSurface;

FBall.X := FBoxWidth / 2;

FBall.Y := FBoxHeight / 4 * 3;

FBall.Speed := 0;

FBallPreviousX := FBall.X;

FBallPreviousY := FBall.Y;

FBallZeroPointX := FBall.X;

FBallZeroPointY := FBall.Y;

FBallZeroSpeed := FBallDefaultZeroSpeed;

FTimeMoment := 0;

if AIsRandomAngle then

begin

Randomize;

RandomAnglePart := Random(11) - 5;

if InRange (RandomAnglePart, -1, 1) then

RandomAnglePart := - 3;

FAlpha := pi / RandomAnglePart;

end;

end;

 

{ TBallVisualController }

 

constructor TBallVisualController.Create(

const AWindow: TWinControl);

begin

inherited Create;

FWindow := AWindow;

FBallPhysicController := TBallPhysicController.Create;

end;

 

procedure TBallVisualController.Draw;

var

BufferBitmap: TBitmap;

DC: HDC;

begin

BufferBitmap := TBitmap.Create;

try

BufferBitmap.Width := FBoxWidth;

BufferBitmap.Height := FBoxHeight;

if Assigned(FBackground) then

BitBlt(BufferBitmap.Canvas.Handle, 0, 0, FBoxWidth, FBoxHeight,

FBackground.Canvas.Handle, 0, 0, SRCCOPY);

if Assigned(FBallPhysicController) then

with BufferBitmap.Canvas do

begin

Pen.Color := clBlue;

Brush.Color := clBlue;

Ellipse(InvertRect(FBallPhysicController.Ball.Rect));

end;

DC := GetWindowDC(FWindow.Handle);

try

BitBlt(DC, 0, 0, FBoxWidth, FBoxHeight,

BufferBitmap.Canvas.Handle, 0, 0, SRCCOPY);

finally

ReleaseDC(FWindow.Handle, DC);

end;

finally

BufferBitmap.Free;

end;

end;

 

function TBallVisualController.InvertRect(ARect: TRect): TRect;

begin

Result := ARect;

Result.TopLeft.Y := FBoxHeight - Result.TopLeft.Y;

Result.BottomRight.Y := FBoxHeight - Result.BottomRight.Y;

end;

 

function TBallVisualController.Invert(APoint: TPoint): TPoint;

begin

Result.X := APoint.X;

Result.Y := FBoxHeight - APoint.Y;

end;

 

procedure TBallVisualController.Iterate;

begin

FBallPhysicController.Iterate;

Draw;

end;

 

procedure TBallVisualController.Restart(const ABoxWidth,

ABoxHeight: Integer; const AIsAutoGeneratedSurface,

AIsRandomAngle: Boolean);

begin

FBallPhysicController.Restart(ABoxWidth, ABoxHeight,

AIsAutoGeneratedSurface, AIsRandomAngle);

FBoxHeight := ABoxHeight;

FBoxWidth := ABoxWidth;

GenerateBackground;

Draw;

end;

 

procedure TBallVisualController.GenerateBackground;

var

DrawableSurface: array of TPoint;

I: Integer;

begin

if not Assigned(FBackground) then

FBackGround := TBitmap.Create;

FBackground.Width := FBoxWidth;

FBackground.Height := FBoxHeight;

SetLength(DrawableSurface, Length(FBallPhysicController.Surface) + 2);

try

for I := 0 to Length(FBallPhysicController.Surface) - 1 do

DrawableSurface[I] := Invert(FBallPhysicController.Surface[I]);

DrawableSurface[Length(FBallPhysicController.Surface)].X := FBoxWidth;

DrawableSurface[Length(FBallPhysicController.Surface)].Y := FBoxHeight;

DrawableSurface[Length(FBallPhysicController.Surface) + 1].X := 0;

DrawableSurface[Length(FBallPhysicController.Surface) + 1].Y := FBoxHeight;

 

with FBackground.Canvas do

begin

Pen.Color := clRed;

Brush.Color := clWhite;

Rectangle(FWindow.ClientRect);

Brush.Color := clRed;

Polygon(DrawableSurface);

end;

finally

SetLength(DrawableSurface, 0);

end;

end;

 

destructor TBallVisualController.Destroy;

begin

if Assigned(FBackGround) then

FBackground.Free;

FBallPhysicController.Free;

inherited;

end;

 

procedure TBallVisualController.ResizeBox(const ABoxWidth,

ABoxHeight: Integer);

begin

FBallPhysicController.ResizeBox(ABoxWidth, ABoxHeight);

FBoxHeight := ABoxHeight;

FBoxWidth := ABoxWidth;

GenerateBackground;

end;

 

{ TBall }

 

function TBall.GetRect: TRect;

begin

Result.TopLeft.X := Round(FX - FRadius);

Result.TopLeft.Y := Round(FY + FRadius);

Result.BottomRight.X := Round(FX + FRadius);

Result.BottomRight.Y := Round(FY - FRadius);

end;

end.


Вывод

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

 


Лабораторная работа № 4 «Технология тестирования ПО при объектном подходе»

Постановка задачи

Разработать и провести тесты для приложения, реализованного в лабораторной работе № 3, по методу «черного ящика».

Цель работы

Изучить метод «черного ящика» тестирования ПО и применить полученные знания на практике.

Проектирование тестов

Спроектируем тесты, применяя принцип предположения об ошибке. Возможны следующие ситуации, которые можно легко проверить визуально:

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

· при большой частоте амплитуды синусоид поверхности мяч может отскакивать на неадекватные углы;

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

Тест Ожидаемый результат
Задать для множителя любой из синусоид значение более 300 Такое значение задать не получится, либо мяч появится в зоне над поверхностью.
Задать знаменатель аргумента функции sin обеих синусоид значение менее 3 Мяч будет отскакивать на адекватные углы, т.к. нет привязки к форме поверхности – мяч может отскакивать от любых точек на поверхности.
Задать для мяча начальную скорость более 100 м/c Такое значение задать не получится, либо мяч не будет проваливаться под поверхность или исчезать из окна.

Результаты тестирования

Тест 1.

На рисунке 4.1 изображен результат выполнения теста. Тест можно считать проваленным, т.к. мяч всегда появляется в одном и том же месте – половина ширины окна и ¾ высоты окна, поэтому он может оказаться под поверхностью при больших значениях амплитуды. При этом ввод больших значений амплитуды не ограничен.

Рисунок 4.1 – Тест № 1

Тест 2.

На рисунке 4.2 изображен результат выполнения второго теста. Тест проходит успешно – мяч отскакивает на адекватные углы.

 

 

Рисунок 4.2 – Тест № 2

Тест 3.

На рисунке 4.3 изображен результат выполнения третьего теста. Тест проходит успешно – выводится сообщение «К сожалению, скорость мяча не может быть более 100 м/с», введенное больше значение заменяется на 100. Таким образом логика работы программы не нарушается данными на которые она не рассчитана.

 

Рисунок 4.3 – Тест № 3

Вывод

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