Refactoring And Testing A Delphi App
Bruno Sonnino - 21/Jan/2019
Bruno Sonnino - 21/Jan/2019
[SHOWTOGROUPS=4,20]
While writing my last article, something occurred to me: what if the app uses an external dll for some functions, how can I package them to send them to the store. Once you are sending an app to the Windows Store, everything it needs to start and run must be packaged, or it won’t be certified.
When using an installer to package an app, all you must do is to include the main executable and all the needed files in the install script and the installer will take care of packaging everything. But when your are packaging an app for the Windows Store, there is no such thing as an install script. If you are packaging a Delphi app, just compile it to the Store and voilà!, an appx file is created. With Visual Studio, you can create a packaging project, add all the projects in the solution that you want and it will create an appx file for you.
But sometimes, you need to use an external dll, which you may not have the source code, and it must be packaged with the main executable. In this article, I will show how to package an external dll with the main executable with Delphi and with Visual Studio.
For the article, we will take on the Financial Calculator that we created for the last article and refactor it: the financial functions will be in an external Win32 dll and the UI will be in the main executable. That way, we will do two things:
As you can see from this code, the business rules are mixed with the UI, thus making it difficult to understand it and change the code, if it’s needed.
For example, all the values are dependent on the text box values and the result is also posted in the result text box. This is bad design and not testable. A better thing would be something like this:
This is cleaner, does not depend on the UI and easier to understand. But it is not testable, yet, because the method is in the code behind for the UI, so to test it you should need to instantiate a new Form1, which is not feasible under automated tests (unless you are doing UI tests, which is not the case). You could move this code to another unit, to allow it to be testable, but it won’t be reusable. If you want to use the same code in another program, you should have to copy the unit, with all the problems you may have with that:
The first step is to create a new project in the project group, a dynamic library. Save it and call it FinCalcDll. Then add a new unit to it and save it as PVCalculator.
You should be asking why am I saving the unit with this name and not as FinancialCalculators. I am doing this because I want to treat each unit as a single class and respect the Для просмотра ссылки Войдиили Зарегистрируйся. Following that principle, the class should have only one reason to change.
If I put all calculators in a single unit (class), there will be more than one reason to change it: any change in any of the calculators will be a reason to change. Then, we can add the first function:
We must use the Math unit, to have the Power function available and declare the function in the Interface section, so it can be visible externally. It must be declared as stdcall to be called by other languages. Create new units and save them as FVCalculator, IRRCalculator and PmtCalculator and add these functions:
In the dpr file, you must export the functions. In the Projects window, select the dll and right-click on it, selecting the View Source option. In the source for the dpr file, add the Exports clause:
Then, in the Unit1 for the executable, make the changes needed to use the new dll functions:
We declare the functions and use them in the OnChange handlers of the textboxes. When you build and run the program, you will see something like this:
Для просмотра ссылки Войдиили Зарегистрируйся
That’s because the dll is not where it should be, in the same folder of the executable. For that, you must take some steps:
Для просмотра ссылки Войдиили Зарегистрируйся
[/SHOWTOGROUPS]
While writing my last article, something occurred to me: what if the app uses an external dll for some functions, how can I package them to send them to the store. Once you are sending an app to the Windows Store, everything it needs to start and run must be packaged, or it won’t be certified.
When using an installer to package an app, all you must do is to include the main executable and all the needed files in the install script and the installer will take care of packaging everything. But when your are packaging an app for the Windows Store, there is no such thing as an install script. If you are packaging a Delphi app, just compile it to the Store and voilà!, an appx file is created. With Visual Studio, you can create a packaging project, add all the projects in the solution that you want and it will create an appx file for you.
But sometimes, you need to use an external dll, which you may not have the source code, and it must be packaged with the main executable. In this article, I will show how to package an external dll with the main executable with Delphi and with Visual Studio.
For the article, we will take on the Financial Calculator that we created for the last article and refactor it: the financial functions will be in an external Win32 dll and the UI will be in the main executable. That way, we will do two things:
- Separate the UI and the business rules
- Create an external component that may be used in many situations: we will use it for our two apps – the Delphi and the WPF one. That is a great way to refactor your code when you have stable business rules that you don’t want to touch and you need to evolve the UI of your app
As you can see from this code, the business rules are mixed with the UI, thus making it difficult to understand it and change the code, if it’s needed.
Код:
procedure TForm1.CalculatePV;
begin
try
var FutureValue := StrToFloat(FvPresentValue.Text);
var InterestRate := StrToFloat(IrPresentValue.Text) / 100.0;
var NumPeriods := StrToInt(NpPresentValue.Text);
var PresentValue := FutureValue / Power((1 + InterestRate), NumPeriods);
PvPresentValue.Text := FormatFloat('0.00', PresentValue);
except
On EConvertError do
PvPresentValue.Text := '';
end;
end;
For example, all the values are dependent on the text box values and the result is also posted in the result text box. This is bad design and not testable. A better thing would be something like this:
Код:
function TForm1.CalculatePV(FutureValue, InterestRate : Double; NumPeriods : Integer); double;
begin
try
Result := FutureValue / Power((1 + InterestRate), NumPeriods);
except
Result := NAN;
end;
end;
This is cleaner, does not depend on the UI and easier to understand. But it is not testable, yet, because the method is in the code behind for the UI, so to test it you should need to instantiate a new Form1, which is not feasible under automated tests (unless you are doing UI tests, which is not the case). You could move this code to another unit, to allow it to be testable, but it won’t be reusable. If you want to use the same code in another program, you should have to copy the unit, with all the problems you may have with that:
- Difficulty to change the code: if you find an error or want to refactor the code, you should fix the same thing in many places
- Impossible to use in programs written in other languages, unless you rewrite the code
The first step is to create a new project in the project group, a dynamic library. Save it and call it FinCalcDll. Then add a new unit to it and save it as PVCalculator.
You should be asking why am I saving the unit with this name and not as FinancialCalculators. I am doing this because I want to treat each unit as a single class and respect the Для просмотра ссылки Войди
If I put all calculators in a single unit (class), there will be more than one reason to change it: any change in any of the calculators will be a reason to change. Then, we can add the first function:
Код:
unit PVCalculator;
interface
Uses Math;
function CalculatePV(FutureValue, InterestRate: Double; NumPeriods : Integer): double; stdcall;
implementation
function CalculatePV(FutureValue, InterestRate: Double; NumPeriods : Integer):
double;
begin
try
Result := FutureValue / Power((1 + InterestRate), NumPeriods);
except
Result := NAN;
end;
end;
end.
We must use the Math unit, to have the Power function available and declare the function in the Interface section, so it can be visible externally. It must be declared as stdcall to be called by other languages. Create new units and save them as FVCalculator, IRRCalculator and PmtCalculator and add these functions:
Код:
unit PVCalculator;
interface
Uses Math;
function CalculatePV(FutureValue, InterestRate: Double; NumPeriods : Integer):
double; stdcall;
implementation
function CalculatePV(FutureValue, InterestRate: Double; NumPeriods : Integer):
double;
begin
try
Result := FutureValue / Power((1 + InterestRate), NumPeriods);
except
Result := NAN;
end;
end;
end.
Код:
unit FVCalculator;
interface
Uses Math;
function CalculateFV(PresentValue, InterestRate: Double;NumPeriods : Integer):
Double; stdcall;
implementation
function CalculateFV(PresentValue, InterestRate: Double;NumPeriods : Integer):
Double;
begin
try
Result := PresentValue * Power((1 + InterestRate), NumPeriods);
except
Result := NAN;
end;
end;
end.
Код:
unit IRRCalculator;
interface
Uses Math;
function CalculateIRR(PresentValue, Payment: Double;NumPeriods : Integer):
Double; stdcall;
implementation
function CalculateIRR(PresentValue, Payment: Double;NumPeriods : Integer):
Double;
begin
Result := Nan;
try
var FoundRate := False;
var MinRate := 0.0;
var MaxRate := 1.0;
if Payment * NumPeriods < PresentValue then begin
Result := -1;
exit;
end;
if Payment * NumPeriods = PresentValue then begin
Result := 0;
exit;
end;
while not FoundRate do begin
var Rate := (MaxRate + MinRate) / 2.0;
var SumPayments := 0.0;
for var I := 1 to NumPeriods do
SumPayments := SumPayments + Payment / Power((1 + Rate), I);
if Abs(SumPayments - PresentValue) > 0.01 then begin
if PresentValue < SumPayments then begin
MinRate := Rate;
end
else begin
MaxRate := Rate;
end;
end
else begin
FoundRate := True;
Result := Rate;
end;
end;
except
end;
end;
end.
[code]
[/spoiler]
[spoiler="Code"]
[code]
unit PmtCalculator;
interface
Uses Math;
function CalculatePmt(PresentValue, InterestRate: Double;NumPeriods : Integer):
Double; stdcall;
implementation
function CalculatePmt(PresentValue, InterestRate: Double;NumPeriods : Integer):
Double;
begin
try
Result := (PresentValue * InterestRate) * Power((1 + InterestRate),
NumPeriods) / (Power((1 + InterestRate), NumPeriods) - 1);
except
Result := Nan;
end;
end;
end.
In the dpr file, you must export the functions. In the Projects window, select the dll and right-click on it, selecting the View Source option. In the source for the dpr file, add the Exports clause:
Код:
{$R *.res}
Exports
CalculatePV, CalculateFV, CalculateIRR, CalculatePmt;
Then, in the Unit1 for the executable, make the changes needed to use the new dll functions:
Код:
...
implementation
{$R *.dfm}
function CalculatePV(FutureValue, InterestRate : Double;NumPeriods : Integer) :
Double; stdcall; external 'FinCalcDll.dll';
function CalculateFV(PresentValue, InterestRate : Double;NumPeriods : Integer) :
Double; stdcall; external 'FinCalcDll.dll';
function CalculateIRR(PresentValue, Payment: Double;NumPeriods : Integer) :
Double; stdcall; external 'FinCalcDll.dll';
function CalculatePmt(PresentValue, InterestRate : Double;NumPeriods : Integer) :
Double; stdcall; external 'FinCalcDll.dll';
procedure TForm1.PaymentChange(Sender: TObject);
begin
try
var PresentValue := StrToFloat(PvPayment.Text);
var InterestRate := StrToFloat(IRPayment.Text) / 100.0;
var NumPayments := StrToInt(NpPayment.Text);
var Payment := CalculatePmt(PresentValue,InterestRate, NumPayments);
PmtPayment.Text := FormatFloat('0.00', Payment);
except
On EConvertError do
PmtPayment.Text := '';
end;
end;
procedure TForm1.PresentValueChange(Sender: TObject);
begin
try
var FutureValue := StrToFloat(FvPresentValue.Text);
var InterestRate := StrToFloat(IrPresentValue.Text) / 100.0;
var NumPeriods := StrToInt(NpPresentValue.Text);
var PresentValue := CalculatePV(FutureValue, InterestRate, NumPeriods);
if IsNan(PresentValue) then
PvPresentValue.Text := ''
else
PvPresentValue.Text := FormatFloat('0.00', PresentValue);
except
On EConvertError do
PvPresentValue.Text := '';
end;
end;
procedure TForm1.IRRChange(Sender: TObject);
begin
try
var NumPayments := StrToInt(NpIRR.Text);
var PresentValue := StrToFloat(PvIRR.Text);
var Payment := StrToFloat(PmtIRR.Text);
var Rate := CalculateIRR(PresentValue, Payment, NumPayments);
if Rate < 0 then begin
IRIRR.Text := 'Rate Less than 0';
exit;
end;
if IsNan(Rate) then begin
IRIRR.Text := 'Error calculating rate';
exit;
end;
IRIRR.Text := FormatFloat('0.00', Rate * 100.0);
except
On EConvertError do
IRIRR.Text := '';
end;
end;
procedure TForm1.FutureValueChange(Sender: TObject);
begin
try
var PresentValue := StrToFloat(PvFutureValue.Text);
var InterestRate := StrToFloat(IrFutureValue.Text) / 100.0;
var NumPeriods := StrToInt(NpFutureValue.Text);
var FutureValue := CalculateFV(PresentValue,InterestRate, NumPeriods);
if IsNan(FutureValue) then
FvFutureValue.Text := ''
else
FvFutureValue.Text := FormatFloat('0.00', FutureValue);
except
On EConvertError do
FvFutureValue.Text := '';
end;
end;
We declare the functions and use them in the OnChange handlers of the textboxes. When you build and run the program, you will see something like this:
Для просмотра ссылки Войди
That’s because the dll is not where it should be, in the same folder of the executable. For that, you must take some steps:
- Build the dll before the executable. If you don’t do that, the executable will be built and will use an outdated dll
- Copy the dll after building the executable
Для просмотра ссылки Войди
[/SHOWTOGROUPS]