The Delphi books of my days (or why i'm not guilt of my application's poor design)As most of Lazarus developers, i started to code in Delphi (in fact i learned computer programming with turbo pascal) and to get most of the tool i read some books, i bought three or four and read part of others in bookstores. This is supposed to be a good practice when learning a new technology.
The problem, noticed by me only years later, is the lack of teaching of good application design like separation of concerns (view, business, persistence layers) and how to achieve them with Delphi. Most of the books focused in the visual aspect (how to create a good looking form, reports etc) and how to setup datasets and the db aware controls. The closer to a good practice advice was putting datasets and datasources in data modules instead of forms.
We can't even blame the book authors. The Delphi's greatest selling point was (is?) the Rapid Application Development (RAD) features.
Recipes for a bulky spaghettiIn early days, when developing my applications, i was a diligent student: i put database logic in data modules and designed the forms as specified in the books. But, as all developers that created applications with more than three forms knows, things started to get hard to evolve and maintain.
Keeping the database components in data modules did not help much. You end with shared dataset states, and all problems that comes with it, across different parts of applications.
Below is a data module's snapshot of my first big application (still in production, by the way).
|It could be even worse if i had not started to use a TDataset factory in the middle of development|
// a form to select a profile DataCenter.PrescriptionProfilesDataset.Open; with TLoadPrescriptionProfileForm.Create(AOwner) do try Result := ShowModal; if Result = mrYes then DataCenter.LoadPrescriptionProfile; finally DataCenter.PrescriptionProfilesDataset.Close; Destroy; end; //snippet of DataCenter.LoadPrescriptionProfile (copy the selected profile to PrescriptionItemsDataset) with PrescriptionItemsDataset do begin DisableControls; try FilterPrescriptionProfileItems(PrescriptionProfilesDataset.FieldByName('Id').AsInteger); while not PrescriptionProfileItemsDataset.Eof do begin NewMedication := PrescriptionProfileItemsDataset.FieldByName('Medication').AsString; if Lookup('Medication', NewMedication, 'Id') = Null then begin Append; FieldByName('PatientId').AsInteger := PatientsDatasetId.AsInteger; FieldByName('Medication').AsString := NewMedication; FieldByName('Dosage').AsString := PrescriptionProfileItemsDataset.FieldByName('Dosage').AsString; [..] Post; end; PrescriptionProfileItemsDataset.Next; end; ApplyUpdates; PrescriptionProfileItemsDataset.Close; finally EnableControls; end; end;
Its not necessary to be a software architect guru to know that this is unmanageable
Eating the pasta with business objects and inversion of controlIn the projects that succeeded the first one, most of the data related code is encapsulated in business objects. The data module does not contain TDataset instances anymore, it's responsible only to act as a TDataset factory and to implement some specific data action. To work with dataset it's necessary just reference one from a key which leads to code like the below:
FWeightHistoryDataset := DataModule.GetQuery(Self, 'weighthistory'); FWeightHistoryDataset.ParamByName('prontuaryid').AsInteger := FId; FWeightHistoryDataset.Open;
This fixes the shared state issue since each dataset has a clear, limited scope. But does not solve the business objects dependency of a global instance (DataModule), which makes testing harder.
In the project that i'm starting, i solved the dependency to the global instance by using the service locator pattern through the IoC Container i cited in a previous post. I defined a resource factory service that is resolved as soon as the business object is created, opening the doors to setup testing environments in a clear manner.