How to do it...

The sample customization is simple; however, the difficulty begins when you have to handle which customization you have to choose from among those available. Sure, you can define some sort of parameters; but your code will get a lot of if just to understand which calculation to apply. And, even worse, a change in one of your criteria could break something in another. Bad approach! We can configure our software without if statements using RTTI. In this recipe, all the calculus engines are implemented in four different classes in four different units. You can also define all the criteria in only one unit, but it is not mandatory.

In the following table, we see a summary of the customers and the customizations implemented:

  
        
Customer           Unit/class name           Calculation criteria
Default
(no customization)            
CalculationCustomerDefaultU
TCalculationCustomerDefault
           Result := (Price * Quantity) * (1 - Discount / 100);
City Mall           CalculationCustomer_CityMall
TCalculationCustomer_CityMall            
Result := (Price * Quantity) * (1 - Discount / 100);
if Result > 1000 then
  Result := Result * 0.90; Country Road Shop           
CalculationCustomer_CountryRoad
TCalculationCustomer_CountryRoad            
if Quantity > 10 then
  if Discount < 50 then
    Discount := 50;
Result := (Price * Quantity) * (1 - Disco
unt / 100); Spark Industries           
CalculationCustomer_Spark
TCalculationCustomer_Spark            
Result := (Price * Quantity) * (1 - Discount / 100);
if DayOfTheWeek(Date) in [1, 7] then
  Result :=Result * 0.50;

 

When the program starts, it looks for a configuration file. In the first line of the file, there is a fully qualified class name (UnitName.ClassName) that implements the needed calculus criteria. That string is used to create the related class, and the instance will be used to calculate the total price when needed. The interesting code is as follows:

procedure TMainForm.LoadCalculationEngine; 
var 
  TheClassName: string; 
  CalcEngineType: TRttiType; 
const 
  CONFIG_FILENAME = '..\..\calculation.config.txt'; 
begin 
  if not TFile.Exists(CONFIG_FILENAME) then 
    TheClassName := 'CalculationCustomerDefaultU.' + 
      'TCalculationCustomerDefault' 
  else 
    TheClassName := TFile.ReadAllLines(CONFIG_FILENAME)[0]; 
 
  CalcEngineType := FCTX.FindType(TheClassName.Trim); 
  if not assigned(CalcEngineType) then 
    raise Exception.CreateFmt('Class %s not found', [TheClassName]); 
  if not CalcEngineType.GetMethod('Create').IsConstructor then 
    raise Exception.CreateFmt('Cannot find Create in %s', [TheClassName]); 
 
  FCalcEngineObj := CalcEngineType.GetMethod('Create') 
    .Invoke(CalcEngineType.AsInstance.MetaclassType, []).AsObject; 
  FCalcEngineMethod := CalcEngineType.GetMethod('GetTotal'); 
  Label5.Caption := 'Current Calc Engine: ' + TheClassName; 
end; 

FCalcEngineObj is a TObject reference that holds your actual calculation engine, while FCalcEngineMethod is an RTTI object that keeps a reference to the method to call when the calculus is needed.

Now, in the dataset OnCalcFields event handler, there is this code:

procedure TMainForm.ClientDataSet1CalcFields(DataSet: TDataSet); 
begin 
ClientDataSet1TOTAL.Value := 
FCalcEngineMethod.Invoke(FCalcEngineObj, 
    [ClientDataSet1PRICE.Value, 
    ClientDataSet1QUANTITY.Value, 
    ClientDataSet1DISCOUNT.Value]).AsCurrency; 
end; 

Run the program and check which calculus engine is loaded. Then stop the program, open the configuration file, and write another QualifiedClassName (unit name plus class name), choosing from all those available. Run the program. As you can see, the correct engine is selected and the customization is applied without changing the working code.

On writing the CalculationCustomer_CityMall.TCalculationCustomer_CityMall class in the file, you will get the following behavior:

Figure 2.2: The main form using the customized calculus engine specified in the configuration file