Проектирование ПО при объектном подходе
Далее приведены результаты проектирования архитектуры приложения, с использованием объектно-ориентированного подхода.
Объектная декомпозиция задачи
Решение поставленной задачи можно разделить на следующие части:
· хранение данных о текущем положении мяча и его скорости;
· управление движением мяча (изменение его положения, проверка касания поверхности, расчет параметров движения);
· визуализация движения.
Таким образом, задачу можно делегировать 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
Вывод
С помощью изученного метода черного ящика и принципа предположения об ошибке были спроектированы тесты. При выполнении одного из тестов удалось выявить дефект в программе, т.е. тестирование можно считать успешным.