Constructor Dependency Injection alternatives
I am working solo on an employee timekeeping project which uses a database to store time sheet entries. I am using Delphi Pro 10.2.3 Tokyo for the project and have created a library of wrapper classes to facilitate working with datasets like normal classes. For example, to access the FirstName field in the Employee table, I can write LFirstName := FEmployee.FirstName; rather than LFirstName := Dataset.FieldByName('FirstName').AsString;
Some of my classes have a significant number of dependencies (as many as eight) which I am injecting through the class' constructor. I use a domain object to create the required interfaces and inject them into the class being created.
Some of the interfaces being injected themselves are also very complex and it is starting to get difficult to keep track of everything in the domain object.
The dependencies being injected include wrapper interfaces for other tables which provide lookup values for calculated fields, pointers to functions which create create objects used by the class or call back functions which resolve master/detail relationships. These relationships are static and need to be set in the constructor so that any calculated fields will function when the class is created.
Are there any alternatives to constructor injection which might may decrease complexity of the constructor while maintaining decoupled classes. Here is a sample of code from one of my modules for time sheet entries.
unit LevelPay.DbModel.TimesheetEntry;
interface
uses
Data.Db
, FireDAC.Comp.DataSet
, MyLib.Model.Interfaces
, LevelPay.Model.Types
, LevelPay.Model.Constants
, LevelPay.Model.Interfaces
, LevelPay.DbModel.AppModel
;
type
TDbCustomTimesheetEntry = class(
TDbAppModel<ITimesheetEntry>,
ITimesheetEntry
)
strict private
FCopyFunc: TCopyFunc<ITimesheetEntry>;
procedure ClearFilter;
procedure FilterEntries(const ADate: TDate);
strict protected
FTimesheet: ITimesheet;
FID: TField;
FEmployeeID: TField;
FPayPeriodEndDate: TField;
FFiscalYearEndDate: TField;
FFiscalYearStartDate: TField;
FRowNbr: TField;
FEntryTypeID: TField;
FDateIn: TField;
FTimein: TField;
FDateOut: TField;
FTimeOut: TField;
FCreatedBy: TField;
FCreatedTimestamp: TField;
FLastModifiedBy: TField;
FLastModifiedTimestamp: TField;
FNote: TField;
FClockable: TField;
FClockableHours: TField;
FDayOfWeek: TField;
FDifference: TField;
FEmployeeName: TField;
FEntryTypeCaption: TField;
FTimeElapsed: TField;
FDateIndex: TFDIndex;
FTimeScheduled: TField;
FScheduledTimeIn: TField;
FScheduledTimeOut: TField;
FWeekOf: TField;
function GetID: TIdentifier;
function GetModel: ITimesheetEntry; override;
function GetClockable: Boolean;
function GetClockableHours: THours;
function GetDateIn: TDate;
function GetDateOut: TDate;
function GetDifference: THours;
function GetEmployeeID: TIdentifier;
function GetEmployeeName: string;
function GetPayPeriodEndDate: TDate;
function GetFiscalYearStartDate: TDate;
function GetFiscalYearEndDate: TDate;
function GetEntryTypeID: TIdentifier;
function GetEntryTypeCaption: TCaption;
function GetPlaceholder: Boolean;
function GetRowNbr: TRowNbr;
function GetScheduledTimeIn: TTime;
function GetScheduledTimeOut: TTime;
function GetTimeElapsed: THours;
function GetTimein: TTime;
function GetTimeOut: TTime;
function GetTimeScheduled: THours;
function GetWeekOf: TDate;
function GetWeekDay: string;
function GetCreatedBy: TUserName;
function GetCreatedTimestamp: TDateTime;
function GetLastModifiedBy: TUserName;
function GetLastModifiedTimestamp: TDateTime;
function GetNote: AnsiString;
function GetEntry: ITimesheetEntry;
function GetTimesheet: ITimesheet;
function GetHasEntries: Boolean;
function Find(AModel: ITimesheetEntry): Boolean; override;
procedure DoUpdate(AModel: ITimesheetEntry); override;
procedure Load; virtual;
procedure CreateFields; override;
procedure CreateCalcFields; override;
procedure CreateIndexes; override;
procedure FormatFields; override;
procedure OnCalcFields(Dataset: TDataset); override;
procedure OnNewRecord(Dataset: TDataset); override;
public
constructor Create(
ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>;
ACopyFunc: TCopyFunc<ITimesheetEntry>;
ATimesheet: ITimesheet;
ACreateFields: Boolean
); reintroduce;
property ID: TIdentifier read GetID;
property EmployeeID: TIdentifier read GetEmployeeID;
property PayPeriodEndDate: TDate read GetPayPeriodEndDate;
property FiscalYearEndDate: TDate read GetFiscalYearEndDate;
property FiscalYearStartDate: TDate read GetFiscalYearStartDate;
property EntryTypeID: TIdentifier read GetEntryTypeID;
property EntryTypeCaption: TCaption read GetEntryTypeCaption;
property RowNbr: TRowNbr read GetRowNbr;
property Clockable: Boolean read GetClockable;
property ClockableHours: THours read GetClockableHours;
property DateIn: TDate read GetDateIn;
property EmployeeName: string read GetEmployeeName;
property ScheduledTimeIn: TTime read GetScheduledTimeIn;
property ScheduledTimeOut: TTime read GetScheduledTimeOut;
property TimeIn: TTime read GetTimein;
property DateOut: TDate read GetDateOut;
property TimeOut: TTime read GetTimeOut;
property TimeElapsed: THours read GetTimeElapsed;
property Placeholder: Boolean read GetPlaceholder;
property TimeScheduled: THours read GetTimeScheduled;
property Difference: THours read GetDifference;
property WeekDay: string read GetWeekDay;
property WeekOf: TDate read GetWeekOf;
property CreatedBy: TUserName read GetCreatedBy;
property CreatedTimestamp: TDateTime read GetCreatedTimestamp;
property LastModifiedBy: TUserName read GetLastModifiedBy;
property LastModifiedTimestamp: TDateTime read GetLastModifiedTimestamp;
property Note: AnsiString read GetNote;
property Timesheet: ITimesheet read GetTimesheet;
end;
TDbSourceEntry = class(TDbCustomTimesheetEntry, ISourceEntryList)
strict private
FLoadTimesheetEntries: TLoadTimesheetProc;
FElectionList: ILevelPayElectionList;
FPositionList: IHourlyPositionList;
strict protected
procedure BeforePost(Dataset: TDataset); override;
procedure Load; override;
public
constructor Create(
ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>;
ATimesheet: ITimesheet;
ACopyFunc: TCopyFunc<ITimesheetEntry>;
AProc: TLoadTimesheetProc;
AElectionList: ILevelPayElectionList;
APositionList: IHourlyPositionList;
ACreateFields: Boolean = True
); reintroduce;
end;
TDbDummyEntry = class(TDbCustomTimesheetEntry, IDummyEntryList)
strict private
FPlaceholderID: TIdentifier;
FClosureList: ISchoolClosureList;
procedure EntryTypeIDOnChange(Sender: TField);
strict protected
procedure AddPlacedholder(ADate: TDate; ARowNbr: TRowNbr);
procedure CreateFields; override;
procedure DoAdd(AModel: ITimesheetEntry); override;
property PlaceholderID: TIdentifier read FPlaceholderID write FPlaceholderID;
public
constructor Create(
ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>;
ACopyFunc: TCopyFunc<ITimesheetEntry>;
ATimesheet: ITimesheet;
AClosureList: ISchoolClosureList;
ACreateFields: Boolean
); reintroduce;
end;
TDbTimesheetEntry = class(TDbDummyEntry, ITimesheetEntryList)
strict private
FClone: TFDDataset;
FSource: ISourceEntryList;
function GetNextRowNbr: TRowNbr;
strict protected
function WorkweekList: IWorkweekList;
procedure Clear;
procedure Load; override;
public
procedure Add(AModel: ITimesheetEntry); //replaces inherited add
procedure Delete(AModel: ITimesheetEntry); //replace inherited delete
procedure Update(OldModel, NewModel: ITimesheetEntry);
constructor Create(
ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>;
ACopyFunc: TCopyFunc<ITimesheetEntry>;
ATimesheet: ITimesheet;
ASourceFunc: TSourceListFunc;
AClosureList: ISchoolClosureList;
ACreateFields: Boolean
); reintroduce;
end;
implementation
uses
System.SysUtils
, System.Classes
, System.Variants
, System.DateUtils
, FireDAC.Comp.Client
, DateTimeHelper
, LevelPay.Model.Helpers
;
TCustomShift
procedure TDbCustomTimesheetEntry.ClearFilter;
begin
CancelRange;
end;
constructor TDbCustomTimesheetEntry.Create(ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>; ACopyFunc: TCopyFunc<ITimesheetEntry>;
ATimesheet: ITimesheet; ACreateFields: Boolean);
begin
inherited Create(ADataset, AModelFunc, ACreateFields);
FCopyFunc := ACopyFunc;
FTimesheet := ATimesheet;
end;
procedure TDbCustomTimesheetEntry.CreateCalcFields;
begin
inherited;
FClockable := CreateCalcBooleanField(k_Clockable);
FClockableHours := CreateCalcFloatField(k_ClockableHours);
FDayOfWeek := CreateCalcStringField(k_WeekDay, 13);
FDifference := CreateCalcFloatField(k_Difference);
FEmployeeName := CreateCalcStringField(k_EmployeeName, 40);
FEntryTypeCaption := CreateCalcStringField(k_EntryTypeCaption, 20);
FFiscalYearEndDate := CreateCalcDateTimeField(k_FiscalYearEndDate);
FFiscalYearStartDate := CreateCalcDateTimeField(k_FiscalYearStartDate);
FScheduledTimeIn := CreateCalcDateTimeField(k_ScheduledTimeIn);
FScheduledTimeOut := CreateCalcDateTimeField(k_ScheduledTimeOut);
FTimeElapsed := CreateCalcFloatField(k_TimeElapsed);
FTimeScheduled := CreateCalcFloatField(k_TimeScheduled);
FWeekOf := CreateCalcDateTimeField(k_WeekOf);
end;
procedure TDbCustomTimesheetEntry.CreateFields;
begin
FID := CreateField(k_Id);
FEmployeeID := CreateField(k_EmployeeID);
FEntryTypeID := CreateField(k_EntryTypeID);
FRowNbr := CreateField(k_RowNbr);
FTimeIn := CreateField(k_TimeIn);
FTimeOut := CreateField(k_TimeOut);
FID := CreateField(k_ID);
FDateIn := CreateField(k_DateIn);
FDateOut := CreateField(k_DateOut);
FCreatedBy := CreateField(k_CreatedBy);
FCreatedTimestamp := CreateField(k_CreatedTimeStamp);
FLastModifiedBy := CreateField(k_LastModifiedBy);
FLastModifiedTimestamp := CreateField(k_LastModifiedTimestamp);
FNote := CreateField(k_Note);
FPayPeriodEndDate := CreateField(k_PayPeriodEndDate);
end;
procedure TDbCustomTimesheetEntry.CreateIndexes;
const
FIELD_LIST = k_DateIn + ';' + k_RowNbr;
begin
inherited;
FDateIndex := CreateIndex('ByDate', FIELD_LIST);
FDateIndex.Selected := True;
Dataset.IndexesActive := True;
end;
function TDbCustomTimesheetEntry.GetClockable: Boolean;
begin
Result := Rules.Clockable;
end;
function TDbCustomTimesheetEntry.GetClockableHours: THours;
begin
Result := Rules.ClockableHours;
end;
function TDbCustomTimesheetEntry.GetCreatedBy: TUserName;
begin
Result := FCreatedBy.AsUserName;
end;
function TDbCustomTimesheetEntry.GetCreatedTimestamp: TDateTime;
begin
Result := FCreatedTimestamp.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetDateIn: TDate;
begin
Result := FDateIn.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetDateOut: TDate;
begin
Result := FDateOut.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetDifference: THours;
begin
Result := Rules.Difference;
end;
function TDbCustomTimesheetEntry.GetEmployeeID: TIdentifier;
begin
Result := FEmployeeID.AsIdentifier;
end;
function TDbCustomTimesheetEntry.GetEmployeeName: string;
begin
Result := Rules.EmployeeName;
end;
function TDbCustomTimesheetEntry.GetEntryTypeID: TIdentifier;
begin
Result := FEntryTypeID.AsIdentifier;
end;
function TDbCustomTimesheetEntry.GetFiscalYearEndDate: TDate;
begin
Result := FFiscalYearEndDate.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetFiscalYearStartDate: TDate;
begin
Result := FFiscalYearStartDate.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetHasEntries: Boolean;
begin
Result := RecordCount > 0;
end;
function TDbCustomTimesheetEntry.GetID: TIdentifier;
begin
Result := FID.AsInteger;
end;
function TDbCustomTimesheetEntry.GetLastModifiedBy: TUserName;
begin
Result := FLastModifiedBy.AsUserName;
end;
function TDbCustomTimesheetEntry.GetLastModifiedTimestamp: TDateTime;
begin
Result := FLastModifiedTimestamp.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetModel: ITimesheetEntry;
var
LResult: ITimesheetEntry;
begin
LResult := FCopyFunc(Self);
Result := LResult;
end;
function TDbCustomTimesheetEntry.GetNote: AnsiString;
begin
Result := FNote.AsAnsiString;
end;
function TDbCustomTimesheetEntry.GetPayPeriodEndDate: TDate;
begin
Result := FPayPeriodEndDate.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetPlaceholder: Boolean;
begin
Result := Rules.Placeholder;
end;
function TDbCustomTimesheetEntry.GetEntry: ITimesheetEntry;
begin
Result := Model;
end;
function TDbCustomTimesheetEntry.GetEntryTypeCaption: TCaption;
begin
Result := Rules.EntryTypeCaption
end;
function TDbCustomTimesheetEntry.GetRowNbr: TRowNbr;
begin
Result := FRowNbr.AsRowNbr;
end;
function TDbCustomTimesheetEntry.GetScheduledTimeIn: TTime;
begin
Result := Rules.ScheduledTimeIn;
end;
function TDbCustomTimesheetEntry.GetScheduledTimeOut: TTime;
begin
Result := Rules.ScheduledTimeOut;
end;
function TDbCustomTimesheetEntry.GetTimeElapsed: THours;
begin
Result := Rules.TimeElapsed;
end;
function TDbCustomTimesheetEntry.GetTimein: TTime;
begin
Result := FTimeIn.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetTimeOut: TTime;
begin
Result := FTimeOut.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetTimeScheduled: THours;
begin
Result := Rules.TimeScheduled;
end;
function TDbCustomTimesheetEntry.GetTimesheet: ITimesheet;
begin
Result := FTimesheet;
end;
function TDbCustomTimesheetEntry.GetWeekDay: string;
begin
Result := Rules.WeekDay;
end;
function TDbCustomTimesheetEntry.GetWeekOf: TDate;
begin
Result := Rules.WeekOf;
end;
procedure TDbCustomTimesheetEntry.Load;
begin
//Stub procedure
end;
procedure TDbCustomTimesheetEntry.OnCalcFields(Dataset: TDataset);
begin
inherited;
if not Assigned(Rules) then Exit;
FClockable.AsBoolean := GetClockable;
FClockableHours.AsHours := GetClockableHours;
FDayOfWeek.AsString := GetWeekDay;
FDifference.AsHours := GetDifference;
FEmployeeName.AsString := GetEmployeeName;
FEntryTypeCaption.AsCaption := GetEntryTypeCaption;
FTimeElapsed.AsHours := GetTimeElapsed;
FTimeScheduled.AsHours := GetTimeScheduled;
FScheduledTimeIn.AsDateTime := GetScheduledTimeIn;
FScheduledTimeOut.AsDateTime := GetScheduledTimeOut;
FWeekOf.AsDateTime := GetWeekOf;
end;
procedure TDbCustomTimesheetEntry.OnNewRecord(Dataset: TDataset);
begin
inherited;
FEmployeeID.AsIdentifier := FTimesheet.EmployeeID;
FFiscalYearEndDate.AsDateTime := FTimesheet.FiscalYearEndDate;
FFiscalYearStartDate.AsDateTime := FTimesheet.FiscalYearStartDate;
FPayPeriodEndDate.AsDateTime := FTimesheet.PayPeriodEndDate;
end;
procedure TDbCustomTimesheetEntry.DoUpdate(AModel: ITimesheetEntry);
begin
inherited;
FEmployeeID.AsIdentifier := AModel.EmployeeID;
FRowNbr.AsRowNbr := AModel.RowNbr;
FEntryTypeID.AsIdentifier := AModel.EntryTypeID;
FDateIn.AsDateTime := AModel.DateIn;
FTimeIn.AsDateTime := AModel.TimeIn;
FDateOut.AsDateTime := AModel.DateOut;
FTimeOut.AsDateTime := AModel.TimeOut;
FNote.AsAnsiString := AModel.Note;
end;
procedure TDbCustomTimesheetEntry.FilterEntries(const ADate: TDate);
begin
FDateIndex.Selected := True;
SetRange([ADate], [ADate]);
end;
function TDbCustomTimesheetEntry.Find(AModel: ITimesheetEntry): Boolean;
begin
Result := Locate(k_ID, AModel.ID);
end;
procedure TDbCustomTimesheetEntry.FormatFields;
begin
inherited;
FTimeElapsed.OnGetText := HoursFieldGetText;
FClockableHours.OnGetText := HoursFieldGetText;
FDifference.OnGetText := HoursFieldGetText;
FTimeScheduled.OnGetText := HoursFieldGetText;
SetTimeFieldDisplayFormat(FTimeIn);
SetTimeFieldDisplayFormat(FTimeOut);
SetDateFieldDisplayFormat(FDateIn);
SetDateFieldDisplayFormat(FDateOut);
SetDateFieldDisplayFormat(FPayPeriodEndDate);
SetSQLTimestampFieldDisplayFormat(FCreatedTimestamp);
SetSQLTimestampFieldDisplayFormat(FLastModifiedTimestamp);
end;
TDbDummyEntry
procedure TDbSourceEntry.BeforePost(Dataset: TDataset);
var
LTimestamp: TDateTime;
begin
inherited;
LTimestamp := Now;
FLastModifiedBy.AsUserName := FTimesheet.User.UserName;
FLastModifiedTimestamp.AsDateTime := LTimestamp;
if State in [dsInsert] then
begin
FCreatedBy.AsUserName := FTimesheet.User.UserName;
FCreatedTimestamp.AsDateTime := LTimestamp;
end;
end;
constructor TDbSourceEntry.Create(ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>; ATimesheet: ITimesheet;
ACopyFunc: TCopyFunc<ITimesheetEntry>; AProc: TLoadTimesheetProc;
AElectionList: ILevelPayElectionList; APositionList: IHourlyPositionList;
ACreateFields: Boolean);
begin
inherited Create(ADataset, AModelFunc, ACopyFunc, ATimesheet, ACreateFields);
FLoadTimesheetEntries := AProc;
FElectionList := AElectionList;
FPositionList := APositionList;
end;
procedure TDbSourceEntry.Load;
var
LEmployeeID: TIdentifier;
LFirstEntryDate: TDate;
LLastEntryDate: TDate;
begin
LEmployeeID := FTimesheet.EmployeeID;
LFirstEntryDate := FTimesheet.FirstEntryDate;
LLastEntryDate := FTimesheet.LastEntryDate;
FLoadTimesheetEntries(LEmployeeID, LFirstEntryDate, LLastEntryDate);
end;
TDbDummyEntry
procedure TDbDummyEntry.AddPlacedholder(ADate: TDate; ARowNbr: TRowNbr);
var
LEntryTypeID: TIdentifier;
LClosure: ISchoolClosure;
LNote: AnsiString;
LRowNbr: TRowNbr;
begin
Dec(FPlaceholderID);
LNote := '';
LEntryTypeID := 0;
LRowNbr := ARowNbr;
if LRowNbr < 2 then //This is a first entry for the date
begin
if FClosureList.Find(ADate) then
begin
LClosure := FClosureList.Closure;
LEntryTypeID := LClosure.EntryTypeID;
LNote := AnsiString(LClosure.Caption);
end
else
begin
if TDateTime(ADate).DayOfWeek in [MONDAY..FRIDAY] then
LEntryTypeID := k_Regular
else
LEntryTypeID := 0;
end;
end;
Append;
FId.AsIdentifier := FPlaceholderID;
FRowNbr.AsRowNbr := LRowNbr;
FDateIn.AsDateTime := ADate;
FDateOut.AsDateTime := ADate;
FEntryTypeID.AsIdentifier := LEntryTypeID;
FNote.AsAnsiString := LNote;
Post;
end;
constructor TDbDummyEntry.Create(ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>; ACopyFunc: TCopyFunc<ITimesheetEntry>;
ATimesheet: ITimesheet; AClosureList: ISchoolClosureList; ACreateFields: Boolean);
begin
inherited Create(ADataset, AModelFunc, ACopyFunc, ATimesheet, ACreateFields);
FClosureList := AClosureList;
end;
procedure TDbDummyEntry.CreateFields;
begin
inherited;
FId.ReadOnly := False;
FId.AutoGenerateValue := arNone;
FEntryTypeID.OnChange := EntryTypeIDOnChange;
end;
procedure TDbDummyEntry.DoAdd(AModel: ITimesheetEntry);
begin
inherited;
FId.AsIdentifier := AModel.ID;
end;
procedure TDbDummyEntry.EntryTypeIDOnChange(Sender: TField);
var
LDateIn: TDate;
LDateOut: TDate;
begin
if Sender.AsIdentifier = k_LandSchool then
begin
LDateIn := FDateIn.AsDateTime;
LDateOut := LDateIn + 1;
FDateOut.AsDateTime := LDateOut;
FTimeIn.AsDateTime := GetScheduledTimeOut;
FTimeOut.AsDateTime := GetScheduledTimeIn;
end;
end;
TDbTimesheetEntry
procedure TDbTimesheetEntry.Add(AModel: ITimesheetEntry);
var
LEntry: ITimesheetEntry;
begin
FSource.Add(AModel);
LEntry := FSource.Entry; //Get model with updated ID
FTimesheet.WorkweekList.AddEntry(LEntry);
inherited Add(LEntry);
end;
procedure TDbTimesheetEntry.Clear;
begin
PlaceholderID := 0;
Dataset.CancelUpdates;
DisableControls;
try
First;
while RecordCount > 0 do
Dataset.Delete;
finally
EnableControls;
end;
end;
constructor TDbTimesheetEntry.Create(ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>;
ACopyFunc: TCopyFunc<ITimesheetEntry>; ATimesheet: ITimesheet;
ASourceFunc: TSourceListFunc; AClosureList: ISchoolClosureList;
ACreateFields: Boolean);
begin
inherited Create(ADataset, AModelFunc, ACopyFunc, ATimesheet, AClosureList, ACreateFields);
FSource := ASourceFunc(FTimesheet);
FClone := TFDMemTable.Create(ADataset);
FClone.CloneCursor(ADataset);
end;
procedure TDbTimesheetEntry.Delete(AModel: ITimesheetEntry);
var
LDate: TDate;
LRowNbr: SmallInt;
LEntry: ITimesheetEntry;
begin
FSource.Delete(AModel);
WorkweekList.DeleteEntry(AModel);
DisableControls;
try
LDate := AModel.DateIn;
LRowNbr := AModel.RowNbr;
inherited Delete(AModel);
if (LRowNbr = 1) then
begin
AddPlacedholder(LDate, 0);
LEntry := Model; //Get placeholder
WorkweekList.AddEntry(LEntry);
end;
finally
EnableControls;
end;
end;
function TDbTimesheetEntry.GetNextRowNbr: TRowNbr;
begin
Result := 0;
FClone.SetRange([GetDateIn], [GetDateIn]);
while not FClone.Eof do
begin
Result := FClone.FieldByName(k_RowNbr).AsRowNbr + 1;
FClone.Next;
end;
FClone.CancelRange;
end;
procedure TDbTimesheetEntry.Load;
var
LDate: TDate;
LFirstEntryDate: TDate;
LCutoffDate: TDate;
LEntry: ITimesheetEntry;
LWeekOf: TDate;
LWorkweekList: IWorkweekList;
begin
LWorkweekList := FTimesheet.WorkweekList;
LFirstEntryDate := FTimesheet.FirstEntryDate;
LCutOffDate := FTimesheet.LastEntryDate;
LWeekOf := TDateTime(LFirstEntryDate).StartOfWeek;
LWorkweekList.Init(LFirstEntryDate, LCutOffDate);
FSource.Load;
DisableControls;
try
Clear;
LDate := LWeekOf;
repeat
FSource.FilterEntries(LDate);
if FSource.HasEntries then
begin
for LEntry in FSource do
begin
if LDate >= LFirstEntryDate then
begin
inherited Add(LEntry); //this must call inherited add to adding to FSource
end;
LWorkweekList.AddEntry(LEntry);
end;
end
else
begin
if LDate >= LFirstEntryDate then
begin
AddPlacedholder(LDate, 0);
LEntry := GetModel; //This retrieves the placeholder
LWorkweekList.AddEntry(LEntry);
end;
end;
LDate := LDate + 1;
until LDate > LCutOffDate;
finally
FSource.ClearFilter;
EnableControls;
First;
end;
end;
procedure TDbTimesheetEntry.Update(OldModel, NewModel: ITimesheetEntry);
var
LEntry: ITimesheetEntry;
begin
DisableControls;
try
if Assigned(OldModel) then
begin
if OldModel.Placeholder then
begin
FSource.Add(NewModel);
end
else
begin
FSource.Update(NewModel);
end;
inherited Delete(OldModel);
WorkweekList.DeleteEntry(OldModel);
end
else
begin
FSource.Add(NewModel);
end;
LEntry := FSource.Entry; //Get entry with new ID
inherited Add(LEntry);
WorkweekList.AddEntry(LEntry);
finally
EnableControls;
end;
end;
function TDbTimesheetEntry.WorkweekList: IWorkweekList;
begin
Result := FTimesheet.WorkweekList;
end;
end.
delphi dependency-injection
add a comment |
I am working solo on an employee timekeeping project which uses a database to store time sheet entries. I am using Delphi Pro 10.2.3 Tokyo for the project and have created a library of wrapper classes to facilitate working with datasets like normal classes. For example, to access the FirstName field in the Employee table, I can write LFirstName := FEmployee.FirstName; rather than LFirstName := Dataset.FieldByName('FirstName').AsString;
Some of my classes have a significant number of dependencies (as many as eight) which I am injecting through the class' constructor. I use a domain object to create the required interfaces and inject them into the class being created.
Some of the interfaces being injected themselves are also very complex and it is starting to get difficult to keep track of everything in the domain object.
The dependencies being injected include wrapper interfaces for other tables which provide lookup values for calculated fields, pointers to functions which create create objects used by the class or call back functions which resolve master/detail relationships. These relationships are static and need to be set in the constructor so that any calculated fields will function when the class is created.
Are there any alternatives to constructor injection which might may decrease complexity of the constructor while maintaining decoupled classes. Here is a sample of code from one of my modules for time sheet entries.
unit LevelPay.DbModel.TimesheetEntry;
interface
uses
Data.Db
, FireDAC.Comp.DataSet
, MyLib.Model.Interfaces
, LevelPay.Model.Types
, LevelPay.Model.Constants
, LevelPay.Model.Interfaces
, LevelPay.DbModel.AppModel
;
type
TDbCustomTimesheetEntry = class(
TDbAppModel<ITimesheetEntry>,
ITimesheetEntry
)
strict private
FCopyFunc: TCopyFunc<ITimesheetEntry>;
procedure ClearFilter;
procedure FilterEntries(const ADate: TDate);
strict protected
FTimesheet: ITimesheet;
FID: TField;
FEmployeeID: TField;
FPayPeriodEndDate: TField;
FFiscalYearEndDate: TField;
FFiscalYearStartDate: TField;
FRowNbr: TField;
FEntryTypeID: TField;
FDateIn: TField;
FTimein: TField;
FDateOut: TField;
FTimeOut: TField;
FCreatedBy: TField;
FCreatedTimestamp: TField;
FLastModifiedBy: TField;
FLastModifiedTimestamp: TField;
FNote: TField;
FClockable: TField;
FClockableHours: TField;
FDayOfWeek: TField;
FDifference: TField;
FEmployeeName: TField;
FEntryTypeCaption: TField;
FTimeElapsed: TField;
FDateIndex: TFDIndex;
FTimeScheduled: TField;
FScheduledTimeIn: TField;
FScheduledTimeOut: TField;
FWeekOf: TField;
function GetID: TIdentifier;
function GetModel: ITimesheetEntry; override;
function GetClockable: Boolean;
function GetClockableHours: THours;
function GetDateIn: TDate;
function GetDateOut: TDate;
function GetDifference: THours;
function GetEmployeeID: TIdentifier;
function GetEmployeeName: string;
function GetPayPeriodEndDate: TDate;
function GetFiscalYearStartDate: TDate;
function GetFiscalYearEndDate: TDate;
function GetEntryTypeID: TIdentifier;
function GetEntryTypeCaption: TCaption;
function GetPlaceholder: Boolean;
function GetRowNbr: TRowNbr;
function GetScheduledTimeIn: TTime;
function GetScheduledTimeOut: TTime;
function GetTimeElapsed: THours;
function GetTimein: TTime;
function GetTimeOut: TTime;
function GetTimeScheduled: THours;
function GetWeekOf: TDate;
function GetWeekDay: string;
function GetCreatedBy: TUserName;
function GetCreatedTimestamp: TDateTime;
function GetLastModifiedBy: TUserName;
function GetLastModifiedTimestamp: TDateTime;
function GetNote: AnsiString;
function GetEntry: ITimesheetEntry;
function GetTimesheet: ITimesheet;
function GetHasEntries: Boolean;
function Find(AModel: ITimesheetEntry): Boolean; override;
procedure DoUpdate(AModel: ITimesheetEntry); override;
procedure Load; virtual;
procedure CreateFields; override;
procedure CreateCalcFields; override;
procedure CreateIndexes; override;
procedure FormatFields; override;
procedure OnCalcFields(Dataset: TDataset); override;
procedure OnNewRecord(Dataset: TDataset); override;
public
constructor Create(
ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>;
ACopyFunc: TCopyFunc<ITimesheetEntry>;
ATimesheet: ITimesheet;
ACreateFields: Boolean
); reintroduce;
property ID: TIdentifier read GetID;
property EmployeeID: TIdentifier read GetEmployeeID;
property PayPeriodEndDate: TDate read GetPayPeriodEndDate;
property FiscalYearEndDate: TDate read GetFiscalYearEndDate;
property FiscalYearStartDate: TDate read GetFiscalYearStartDate;
property EntryTypeID: TIdentifier read GetEntryTypeID;
property EntryTypeCaption: TCaption read GetEntryTypeCaption;
property RowNbr: TRowNbr read GetRowNbr;
property Clockable: Boolean read GetClockable;
property ClockableHours: THours read GetClockableHours;
property DateIn: TDate read GetDateIn;
property EmployeeName: string read GetEmployeeName;
property ScheduledTimeIn: TTime read GetScheduledTimeIn;
property ScheduledTimeOut: TTime read GetScheduledTimeOut;
property TimeIn: TTime read GetTimein;
property DateOut: TDate read GetDateOut;
property TimeOut: TTime read GetTimeOut;
property TimeElapsed: THours read GetTimeElapsed;
property Placeholder: Boolean read GetPlaceholder;
property TimeScheduled: THours read GetTimeScheduled;
property Difference: THours read GetDifference;
property WeekDay: string read GetWeekDay;
property WeekOf: TDate read GetWeekOf;
property CreatedBy: TUserName read GetCreatedBy;
property CreatedTimestamp: TDateTime read GetCreatedTimestamp;
property LastModifiedBy: TUserName read GetLastModifiedBy;
property LastModifiedTimestamp: TDateTime read GetLastModifiedTimestamp;
property Note: AnsiString read GetNote;
property Timesheet: ITimesheet read GetTimesheet;
end;
TDbSourceEntry = class(TDbCustomTimesheetEntry, ISourceEntryList)
strict private
FLoadTimesheetEntries: TLoadTimesheetProc;
FElectionList: ILevelPayElectionList;
FPositionList: IHourlyPositionList;
strict protected
procedure BeforePost(Dataset: TDataset); override;
procedure Load; override;
public
constructor Create(
ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>;
ATimesheet: ITimesheet;
ACopyFunc: TCopyFunc<ITimesheetEntry>;
AProc: TLoadTimesheetProc;
AElectionList: ILevelPayElectionList;
APositionList: IHourlyPositionList;
ACreateFields: Boolean = True
); reintroduce;
end;
TDbDummyEntry = class(TDbCustomTimesheetEntry, IDummyEntryList)
strict private
FPlaceholderID: TIdentifier;
FClosureList: ISchoolClosureList;
procedure EntryTypeIDOnChange(Sender: TField);
strict protected
procedure AddPlacedholder(ADate: TDate; ARowNbr: TRowNbr);
procedure CreateFields; override;
procedure DoAdd(AModel: ITimesheetEntry); override;
property PlaceholderID: TIdentifier read FPlaceholderID write FPlaceholderID;
public
constructor Create(
ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>;
ACopyFunc: TCopyFunc<ITimesheetEntry>;
ATimesheet: ITimesheet;
AClosureList: ISchoolClosureList;
ACreateFields: Boolean
); reintroduce;
end;
TDbTimesheetEntry = class(TDbDummyEntry, ITimesheetEntryList)
strict private
FClone: TFDDataset;
FSource: ISourceEntryList;
function GetNextRowNbr: TRowNbr;
strict protected
function WorkweekList: IWorkweekList;
procedure Clear;
procedure Load; override;
public
procedure Add(AModel: ITimesheetEntry); //replaces inherited add
procedure Delete(AModel: ITimesheetEntry); //replace inherited delete
procedure Update(OldModel, NewModel: ITimesheetEntry);
constructor Create(
ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>;
ACopyFunc: TCopyFunc<ITimesheetEntry>;
ATimesheet: ITimesheet;
ASourceFunc: TSourceListFunc;
AClosureList: ISchoolClosureList;
ACreateFields: Boolean
); reintroduce;
end;
implementation
uses
System.SysUtils
, System.Classes
, System.Variants
, System.DateUtils
, FireDAC.Comp.Client
, DateTimeHelper
, LevelPay.Model.Helpers
;
TCustomShift
procedure TDbCustomTimesheetEntry.ClearFilter;
begin
CancelRange;
end;
constructor TDbCustomTimesheetEntry.Create(ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>; ACopyFunc: TCopyFunc<ITimesheetEntry>;
ATimesheet: ITimesheet; ACreateFields: Boolean);
begin
inherited Create(ADataset, AModelFunc, ACreateFields);
FCopyFunc := ACopyFunc;
FTimesheet := ATimesheet;
end;
procedure TDbCustomTimesheetEntry.CreateCalcFields;
begin
inherited;
FClockable := CreateCalcBooleanField(k_Clockable);
FClockableHours := CreateCalcFloatField(k_ClockableHours);
FDayOfWeek := CreateCalcStringField(k_WeekDay, 13);
FDifference := CreateCalcFloatField(k_Difference);
FEmployeeName := CreateCalcStringField(k_EmployeeName, 40);
FEntryTypeCaption := CreateCalcStringField(k_EntryTypeCaption, 20);
FFiscalYearEndDate := CreateCalcDateTimeField(k_FiscalYearEndDate);
FFiscalYearStartDate := CreateCalcDateTimeField(k_FiscalYearStartDate);
FScheduledTimeIn := CreateCalcDateTimeField(k_ScheduledTimeIn);
FScheduledTimeOut := CreateCalcDateTimeField(k_ScheduledTimeOut);
FTimeElapsed := CreateCalcFloatField(k_TimeElapsed);
FTimeScheduled := CreateCalcFloatField(k_TimeScheduled);
FWeekOf := CreateCalcDateTimeField(k_WeekOf);
end;
procedure TDbCustomTimesheetEntry.CreateFields;
begin
FID := CreateField(k_Id);
FEmployeeID := CreateField(k_EmployeeID);
FEntryTypeID := CreateField(k_EntryTypeID);
FRowNbr := CreateField(k_RowNbr);
FTimeIn := CreateField(k_TimeIn);
FTimeOut := CreateField(k_TimeOut);
FID := CreateField(k_ID);
FDateIn := CreateField(k_DateIn);
FDateOut := CreateField(k_DateOut);
FCreatedBy := CreateField(k_CreatedBy);
FCreatedTimestamp := CreateField(k_CreatedTimeStamp);
FLastModifiedBy := CreateField(k_LastModifiedBy);
FLastModifiedTimestamp := CreateField(k_LastModifiedTimestamp);
FNote := CreateField(k_Note);
FPayPeriodEndDate := CreateField(k_PayPeriodEndDate);
end;
procedure TDbCustomTimesheetEntry.CreateIndexes;
const
FIELD_LIST = k_DateIn + ';' + k_RowNbr;
begin
inherited;
FDateIndex := CreateIndex('ByDate', FIELD_LIST);
FDateIndex.Selected := True;
Dataset.IndexesActive := True;
end;
function TDbCustomTimesheetEntry.GetClockable: Boolean;
begin
Result := Rules.Clockable;
end;
function TDbCustomTimesheetEntry.GetClockableHours: THours;
begin
Result := Rules.ClockableHours;
end;
function TDbCustomTimesheetEntry.GetCreatedBy: TUserName;
begin
Result := FCreatedBy.AsUserName;
end;
function TDbCustomTimesheetEntry.GetCreatedTimestamp: TDateTime;
begin
Result := FCreatedTimestamp.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetDateIn: TDate;
begin
Result := FDateIn.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetDateOut: TDate;
begin
Result := FDateOut.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetDifference: THours;
begin
Result := Rules.Difference;
end;
function TDbCustomTimesheetEntry.GetEmployeeID: TIdentifier;
begin
Result := FEmployeeID.AsIdentifier;
end;
function TDbCustomTimesheetEntry.GetEmployeeName: string;
begin
Result := Rules.EmployeeName;
end;
function TDbCustomTimesheetEntry.GetEntryTypeID: TIdentifier;
begin
Result := FEntryTypeID.AsIdentifier;
end;
function TDbCustomTimesheetEntry.GetFiscalYearEndDate: TDate;
begin
Result := FFiscalYearEndDate.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetFiscalYearStartDate: TDate;
begin
Result := FFiscalYearStartDate.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetHasEntries: Boolean;
begin
Result := RecordCount > 0;
end;
function TDbCustomTimesheetEntry.GetID: TIdentifier;
begin
Result := FID.AsInteger;
end;
function TDbCustomTimesheetEntry.GetLastModifiedBy: TUserName;
begin
Result := FLastModifiedBy.AsUserName;
end;
function TDbCustomTimesheetEntry.GetLastModifiedTimestamp: TDateTime;
begin
Result := FLastModifiedTimestamp.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetModel: ITimesheetEntry;
var
LResult: ITimesheetEntry;
begin
LResult := FCopyFunc(Self);
Result := LResult;
end;
function TDbCustomTimesheetEntry.GetNote: AnsiString;
begin
Result := FNote.AsAnsiString;
end;
function TDbCustomTimesheetEntry.GetPayPeriodEndDate: TDate;
begin
Result := FPayPeriodEndDate.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetPlaceholder: Boolean;
begin
Result := Rules.Placeholder;
end;
function TDbCustomTimesheetEntry.GetEntry: ITimesheetEntry;
begin
Result := Model;
end;
function TDbCustomTimesheetEntry.GetEntryTypeCaption: TCaption;
begin
Result := Rules.EntryTypeCaption
end;
function TDbCustomTimesheetEntry.GetRowNbr: TRowNbr;
begin
Result := FRowNbr.AsRowNbr;
end;
function TDbCustomTimesheetEntry.GetScheduledTimeIn: TTime;
begin
Result := Rules.ScheduledTimeIn;
end;
function TDbCustomTimesheetEntry.GetScheduledTimeOut: TTime;
begin
Result := Rules.ScheduledTimeOut;
end;
function TDbCustomTimesheetEntry.GetTimeElapsed: THours;
begin
Result := Rules.TimeElapsed;
end;
function TDbCustomTimesheetEntry.GetTimein: TTime;
begin
Result := FTimeIn.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetTimeOut: TTime;
begin
Result := FTimeOut.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetTimeScheduled: THours;
begin
Result := Rules.TimeScheduled;
end;
function TDbCustomTimesheetEntry.GetTimesheet: ITimesheet;
begin
Result := FTimesheet;
end;
function TDbCustomTimesheetEntry.GetWeekDay: string;
begin
Result := Rules.WeekDay;
end;
function TDbCustomTimesheetEntry.GetWeekOf: TDate;
begin
Result := Rules.WeekOf;
end;
procedure TDbCustomTimesheetEntry.Load;
begin
//Stub procedure
end;
procedure TDbCustomTimesheetEntry.OnCalcFields(Dataset: TDataset);
begin
inherited;
if not Assigned(Rules) then Exit;
FClockable.AsBoolean := GetClockable;
FClockableHours.AsHours := GetClockableHours;
FDayOfWeek.AsString := GetWeekDay;
FDifference.AsHours := GetDifference;
FEmployeeName.AsString := GetEmployeeName;
FEntryTypeCaption.AsCaption := GetEntryTypeCaption;
FTimeElapsed.AsHours := GetTimeElapsed;
FTimeScheduled.AsHours := GetTimeScheduled;
FScheduledTimeIn.AsDateTime := GetScheduledTimeIn;
FScheduledTimeOut.AsDateTime := GetScheduledTimeOut;
FWeekOf.AsDateTime := GetWeekOf;
end;
procedure TDbCustomTimesheetEntry.OnNewRecord(Dataset: TDataset);
begin
inherited;
FEmployeeID.AsIdentifier := FTimesheet.EmployeeID;
FFiscalYearEndDate.AsDateTime := FTimesheet.FiscalYearEndDate;
FFiscalYearStartDate.AsDateTime := FTimesheet.FiscalYearStartDate;
FPayPeriodEndDate.AsDateTime := FTimesheet.PayPeriodEndDate;
end;
procedure TDbCustomTimesheetEntry.DoUpdate(AModel: ITimesheetEntry);
begin
inherited;
FEmployeeID.AsIdentifier := AModel.EmployeeID;
FRowNbr.AsRowNbr := AModel.RowNbr;
FEntryTypeID.AsIdentifier := AModel.EntryTypeID;
FDateIn.AsDateTime := AModel.DateIn;
FTimeIn.AsDateTime := AModel.TimeIn;
FDateOut.AsDateTime := AModel.DateOut;
FTimeOut.AsDateTime := AModel.TimeOut;
FNote.AsAnsiString := AModel.Note;
end;
procedure TDbCustomTimesheetEntry.FilterEntries(const ADate: TDate);
begin
FDateIndex.Selected := True;
SetRange([ADate], [ADate]);
end;
function TDbCustomTimesheetEntry.Find(AModel: ITimesheetEntry): Boolean;
begin
Result := Locate(k_ID, AModel.ID);
end;
procedure TDbCustomTimesheetEntry.FormatFields;
begin
inherited;
FTimeElapsed.OnGetText := HoursFieldGetText;
FClockableHours.OnGetText := HoursFieldGetText;
FDifference.OnGetText := HoursFieldGetText;
FTimeScheduled.OnGetText := HoursFieldGetText;
SetTimeFieldDisplayFormat(FTimeIn);
SetTimeFieldDisplayFormat(FTimeOut);
SetDateFieldDisplayFormat(FDateIn);
SetDateFieldDisplayFormat(FDateOut);
SetDateFieldDisplayFormat(FPayPeriodEndDate);
SetSQLTimestampFieldDisplayFormat(FCreatedTimestamp);
SetSQLTimestampFieldDisplayFormat(FLastModifiedTimestamp);
end;
TDbDummyEntry
procedure TDbSourceEntry.BeforePost(Dataset: TDataset);
var
LTimestamp: TDateTime;
begin
inherited;
LTimestamp := Now;
FLastModifiedBy.AsUserName := FTimesheet.User.UserName;
FLastModifiedTimestamp.AsDateTime := LTimestamp;
if State in [dsInsert] then
begin
FCreatedBy.AsUserName := FTimesheet.User.UserName;
FCreatedTimestamp.AsDateTime := LTimestamp;
end;
end;
constructor TDbSourceEntry.Create(ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>; ATimesheet: ITimesheet;
ACopyFunc: TCopyFunc<ITimesheetEntry>; AProc: TLoadTimesheetProc;
AElectionList: ILevelPayElectionList; APositionList: IHourlyPositionList;
ACreateFields: Boolean);
begin
inherited Create(ADataset, AModelFunc, ACopyFunc, ATimesheet, ACreateFields);
FLoadTimesheetEntries := AProc;
FElectionList := AElectionList;
FPositionList := APositionList;
end;
procedure TDbSourceEntry.Load;
var
LEmployeeID: TIdentifier;
LFirstEntryDate: TDate;
LLastEntryDate: TDate;
begin
LEmployeeID := FTimesheet.EmployeeID;
LFirstEntryDate := FTimesheet.FirstEntryDate;
LLastEntryDate := FTimesheet.LastEntryDate;
FLoadTimesheetEntries(LEmployeeID, LFirstEntryDate, LLastEntryDate);
end;
TDbDummyEntry
procedure TDbDummyEntry.AddPlacedholder(ADate: TDate; ARowNbr: TRowNbr);
var
LEntryTypeID: TIdentifier;
LClosure: ISchoolClosure;
LNote: AnsiString;
LRowNbr: TRowNbr;
begin
Dec(FPlaceholderID);
LNote := '';
LEntryTypeID := 0;
LRowNbr := ARowNbr;
if LRowNbr < 2 then //This is a first entry for the date
begin
if FClosureList.Find(ADate) then
begin
LClosure := FClosureList.Closure;
LEntryTypeID := LClosure.EntryTypeID;
LNote := AnsiString(LClosure.Caption);
end
else
begin
if TDateTime(ADate).DayOfWeek in [MONDAY..FRIDAY] then
LEntryTypeID := k_Regular
else
LEntryTypeID := 0;
end;
end;
Append;
FId.AsIdentifier := FPlaceholderID;
FRowNbr.AsRowNbr := LRowNbr;
FDateIn.AsDateTime := ADate;
FDateOut.AsDateTime := ADate;
FEntryTypeID.AsIdentifier := LEntryTypeID;
FNote.AsAnsiString := LNote;
Post;
end;
constructor TDbDummyEntry.Create(ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>; ACopyFunc: TCopyFunc<ITimesheetEntry>;
ATimesheet: ITimesheet; AClosureList: ISchoolClosureList; ACreateFields: Boolean);
begin
inherited Create(ADataset, AModelFunc, ACopyFunc, ATimesheet, ACreateFields);
FClosureList := AClosureList;
end;
procedure TDbDummyEntry.CreateFields;
begin
inherited;
FId.ReadOnly := False;
FId.AutoGenerateValue := arNone;
FEntryTypeID.OnChange := EntryTypeIDOnChange;
end;
procedure TDbDummyEntry.DoAdd(AModel: ITimesheetEntry);
begin
inherited;
FId.AsIdentifier := AModel.ID;
end;
procedure TDbDummyEntry.EntryTypeIDOnChange(Sender: TField);
var
LDateIn: TDate;
LDateOut: TDate;
begin
if Sender.AsIdentifier = k_LandSchool then
begin
LDateIn := FDateIn.AsDateTime;
LDateOut := LDateIn + 1;
FDateOut.AsDateTime := LDateOut;
FTimeIn.AsDateTime := GetScheduledTimeOut;
FTimeOut.AsDateTime := GetScheduledTimeIn;
end;
end;
TDbTimesheetEntry
procedure TDbTimesheetEntry.Add(AModel: ITimesheetEntry);
var
LEntry: ITimesheetEntry;
begin
FSource.Add(AModel);
LEntry := FSource.Entry; //Get model with updated ID
FTimesheet.WorkweekList.AddEntry(LEntry);
inherited Add(LEntry);
end;
procedure TDbTimesheetEntry.Clear;
begin
PlaceholderID := 0;
Dataset.CancelUpdates;
DisableControls;
try
First;
while RecordCount > 0 do
Dataset.Delete;
finally
EnableControls;
end;
end;
constructor TDbTimesheetEntry.Create(ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>;
ACopyFunc: TCopyFunc<ITimesheetEntry>; ATimesheet: ITimesheet;
ASourceFunc: TSourceListFunc; AClosureList: ISchoolClosureList;
ACreateFields: Boolean);
begin
inherited Create(ADataset, AModelFunc, ACopyFunc, ATimesheet, AClosureList, ACreateFields);
FSource := ASourceFunc(FTimesheet);
FClone := TFDMemTable.Create(ADataset);
FClone.CloneCursor(ADataset);
end;
procedure TDbTimesheetEntry.Delete(AModel: ITimesheetEntry);
var
LDate: TDate;
LRowNbr: SmallInt;
LEntry: ITimesheetEntry;
begin
FSource.Delete(AModel);
WorkweekList.DeleteEntry(AModel);
DisableControls;
try
LDate := AModel.DateIn;
LRowNbr := AModel.RowNbr;
inherited Delete(AModel);
if (LRowNbr = 1) then
begin
AddPlacedholder(LDate, 0);
LEntry := Model; //Get placeholder
WorkweekList.AddEntry(LEntry);
end;
finally
EnableControls;
end;
end;
function TDbTimesheetEntry.GetNextRowNbr: TRowNbr;
begin
Result := 0;
FClone.SetRange([GetDateIn], [GetDateIn]);
while not FClone.Eof do
begin
Result := FClone.FieldByName(k_RowNbr).AsRowNbr + 1;
FClone.Next;
end;
FClone.CancelRange;
end;
procedure TDbTimesheetEntry.Load;
var
LDate: TDate;
LFirstEntryDate: TDate;
LCutoffDate: TDate;
LEntry: ITimesheetEntry;
LWeekOf: TDate;
LWorkweekList: IWorkweekList;
begin
LWorkweekList := FTimesheet.WorkweekList;
LFirstEntryDate := FTimesheet.FirstEntryDate;
LCutOffDate := FTimesheet.LastEntryDate;
LWeekOf := TDateTime(LFirstEntryDate).StartOfWeek;
LWorkweekList.Init(LFirstEntryDate, LCutOffDate);
FSource.Load;
DisableControls;
try
Clear;
LDate := LWeekOf;
repeat
FSource.FilterEntries(LDate);
if FSource.HasEntries then
begin
for LEntry in FSource do
begin
if LDate >= LFirstEntryDate then
begin
inherited Add(LEntry); //this must call inherited add to adding to FSource
end;
LWorkweekList.AddEntry(LEntry);
end;
end
else
begin
if LDate >= LFirstEntryDate then
begin
AddPlacedholder(LDate, 0);
LEntry := GetModel; //This retrieves the placeholder
LWorkweekList.AddEntry(LEntry);
end;
end;
LDate := LDate + 1;
until LDate > LCutOffDate;
finally
FSource.ClearFilter;
EnableControls;
First;
end;
end;
procedure TDbTimesheetEntry.Update(OldModel, NewModel: ITimesheetEntry);
var
LEntry: ITimesheetEntry;
begin
DisableControls;
try
if Assigned(OldModel) then
begin
if OldModel.Placeholder then
begin
FSource.Add(NewModel);
end
else
begin
FSource.Update(NewModel);
end;
inherited Delete(OldModel);
WorkweekList.DeleteEntry(OldModel);
end
else
begin
FSource.Add(NewModel);
end;
LEntry := FSource.Entry; //Get entry with new ID
inherited Add(LEntry);
WorkweekList.AddEntry(LEntry);
finally
EnableControls;
end;
end;
function TDbTimesheetEntry.WorkweekList: IWorkweekList;
begin
Result := FTimesheet.WorkweekList;
end;
end.
delphi dependency-injection
add a comment |
I am working solo on an employee timekeeping project which uses a database to store time sheet entries. I am using Delphi Pro 10.2.3 Tokyo for the project and have created a library of wrapper classes to facilitate working with datasets like normal classes. For example, to access the FirstName field in the Employee table, I can write LFirstName := FEmployee.FirstName; rather than LFirstName := Dataset.FieldByName('FirstName').AsString;
Some of my classes have a significant number of dependencies (as many as eight) which I am injecting through the class' constructor. I use a domain object to create the required interfaces and inject them into the class being created.
Some of the interfaces being injected themselves are also very complex and it is starting to get difficult to keep track of everything in the domain object.
The dependencies being injected include wrapper interfaces for other tables which provide lookup values for calculated fields, pointers to functions which create create objects used by the class or call back functions which resolve master/detail relationships. These relationships are static and need to be set in the constructor so that any calculated fields will function when the class is created.
Are there any alternatives to constructor injection which might may decrease complexity of the constructor while maintaining decoupled classes. Here is a sample of code from one of my modules for time sheet entries.
unit LevelPay.DbModel.TimesheetEntry;
interface
uses
Data.Db
, FireDAC.Comp.DataSet
, MyLib.Model.Interfaces
, LevelPay.Model.Types
, LevelPay.Model.Constants
, LevelPay.Model.Interfaces
, LevelPay.DbModel.AppModel
;
type
TDbCustomTimesheetEntry = class(
TDbAppModel<ITimesheetEntry>,
ITimesheetEntry
)
strict private
FCopyFunc: TCopyFunc<ITimesheetEntry>;
procedure ClearFilter;
procedure FilterEntries(const ADate: TDate);
strict protected
FTimesheet: ITimesheet;
FID: TField;
FEmployeeID: TField;
FPayPeriodEndDate: TField;
FFiscalYearEndDate: TField;
FFiscalYearStartDate: TField;
FRowNbr: TField;
FEntryTypeID: TField;
FDateIn: TField;
FTimein: TField;
FDateOut: TField;
FTimeOut: TField;
FCreatedBy: TField;
FCreatedTimestamp: TField;
FLastModifiedBy: TField;
FLastModifiedTimestamp: TField;
FNote: TField;
FClockable: TField;
FClockableHours: TField;
FDayOfWeek: TField;
FDifference: TField;
FEmployeeName: TField;
FEntryTypeCaption: TField;
FTimeElapsed: TField;
FDateIndex: TFDIndex;
FTimeScheduled: TField;
FScheduledTimeIn: TField;
FScheduledTimeOut: TField;
FWeekOf: TField;
function GetID: TIdentifier;
function GetModel: ITimesheetEntry; override;
function GetClockable: Boolean;
function GetClockableHours: THours;
function GetDateIn: TDate;
function GetDateOut: TDate;
function GetDifference: THours;
function GetEmployeeID: TIdentifier;
function GetEmployeeName: string;
function GetPayPeriodEndDate: TDate;
function GetFiscalYearStartDate: TDate;
function GetFiscalYearEndDate: TDate;
function GetEntryTypeID: TIdentifier;
function GetEntryTypeCaption: TCaption;
function GetPlaceholder: Boolean;
function GetRowNbr: TRowNbr;
function GetScheduledTimeIn: TTime;
function GetScheduledTimeOut: TTime;
function GetTimeElapsed: THours;
function GetTimein: TTime;
function GetTimeOut: TTime;
function GetTimeScheduled: THours;
function GetWeekOf: TDate;
function GetWeekDay: string;
function GetCreatedBy: TUserName;
function GetCreatedTimestamp: TDateTime;
function GetLastModifiedBy: TUserName;
function GetLastModifiedTimestamp: TDateTime;
function GetNote: AnsiString;
function GetEntry: ITimesheetEntry;
function GetTimesheet: ITimesheet;
function GetHasEntries: Boolean;
function Find(AModel: ITimesheetEntry): Boolean; override;
procedure DoUpdate(AModel: ITimesheetEntry); override;
procedure Load; virtual;
procedure CreateFields; override;
procedure CreateCalcFields; override;
procedure CreateIndexes; override;
procedure FormatFields; override;
procedure OnCalcFields(Dataset: TDataset); override;
procedure OnNewRecord(Dataset: TDataset); override;
public
constructor Create(
ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>;
ACopyFunc: TCopyFunc<ITimesheetEntry>;
ATimesheet: ITimesheet;
ACreateFields: Boolean
); reintroduce;
property ID: TIdentifier read GetID;
property EmployeeID: TIdentifier read GetEmployeeID;
property PayPeriodEndDate: TDate read GetPayPeriodEndDate;
property FiscalYearEndDate: TDate read GetFiscalYearEndDate;
property FiscalYearStartDate: TDate read GetFiscalYearStartDate;
property EntryTypeID: TIdentifier read GetEntryTypeID;
property EntryTypeCaption: TCaption read GetEntryTypeCaption;
property RowNbr: TRowNbr read GetRowNbr;
property Clockable: Boolean read GetClockable;
property ClockableHours: THours read GetClockableHours;
property DateIn: TDate read GetDateIn;
property EmployeeName: string read GetEmployeeName;
property ScheduledTimeIn: TTime read GetScheduledTimeIn;
property ScheduledTimeOut: TTime read GetScheduledTimeOut;
property TimeIn: TTime read GetTimein;
property DateOut: TDate read GetDateOut;
property TimeOut: TTime read GetTimeOut;
property TimeElapsed: THours read GetTimeElapsed;
property Placeholder: Boolean read GetPlaceholder;
property TimeScheduled: THours read GetTimeScheduled;
property Difference: THours read GetDifference;
property WeekDay: string read GetWeekDay;
property WeekOf: TDate read GetWeekOf;
property CreatedBy: TUserName read GetCreatedBy;
property CreatedTimestamp: TDateTime read GetCreatedTimestamp;
property LastModifiedBy: TUserName read GetLastModifiedBy;
property LastModifiedTimestamp: TDateTime read GetLastModifiedTimestamp;
property Note: AnsiString read GetNote;
property Timesheet: ITimesheet read GetTimesheet;
end;
TDbSourceEntry = class(TDbCustomTimesheetEntry, ISourceEntryList)
strict private
FLoadTimesheetEntries: TLoadTimesheetProc;
FElectionList: ILevelPayElectionList;
FPositionList: IHourlyPositionList;
strict protected
procedure BeforePost(Dataset: TDataset); override;
procedure Load; override;
public
constructor Create(
ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>;
ATimesheet: ITimesheet;
ACopyFunc: TCopyFunc<ITimesheetEntry>;
AProc: TLoadTimesheetProc;
AElectionList: ILevelPayElectionList;
APositionList: IHourlyPositionList;
ACreateFields: Boolean = True
); reintroduce;
end;
TDbDummyEntry = class(TDbCustomTimesheetEntry, IDummyEntryList)
strict private
FPlaceholderID: TIdentifier;
FClosureList: ISchoolClosureList;
procedure EntryTypeIDOnChange(Sender: TField);
strict protected
procedure AddPlacedholder(ADate: TDate; ARowNbr: TRowNbr);
procedure CreateFields; override;
procedure DoAdd(AModel: ITimesheetEntry); override;
property PlaceholderID: TIdentifier read FPlaceholderID write FPlaceholderID;
public
constructor Create(
ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>;
ACopyFunc: TCopyFunc<ITimesheetEntry>;
ATimesheet: ITimesheet;
AClosureList: ISchoolClosureList;
ACreateFields: Boolean
); reintroduce;
end;
TDbTimesheetEntry = class(TDbDummyEntry, ITimesheetEntryList)
strict private
FClone: TFDDataset;
FSource: ISourceEntryList;
function GetNextRowNbr: TRowNbr;
strict protected
function WorkweekList: IWorkweekList;
procedure Clear;
procedure Load; override;
public
procedure Add(AModel: ITimesheetEntry); //replaces inherited add
procedure Delete(AModel: ITimesheetEntry); //replace inherited delete
procedure Update(OldModel, NewModel: ITimesheetEntry);
constructor Create(
ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>;
ACopyFunc: TCopyFunc<ITimesheetEntry>;
ATimesheet: ITimesheet;
ASourceFunc: TSourceListFunc;
AClosureList: ISchoolClosureList;
ACreateFields: Boolean
); reintroduce;
end;
implementation
uses
System.SysUtils
, System.Classes
, System.Variants
, System.DateUtils
, FireDAC.Comp.Client
, DateTimeHelper
, LevelPay.Model.Helpers
;
TCustomShift
procedure TDbCustomTimesheetEntry.ClearFilter;
begin
CancelRange;
end;
constructor TDbCustomTimesheetEntry.Create(ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>; ACopyFunc: TCopyFunc<ITimesheetEntry>;
ATimesheet: ITimesheet; ACreateFields: Boolean);
begin
inherited Create(ADataset, AModelFunc, ACreateFields);
FCopyFunc := ACopyFunc;
FTimesheet := ATimesheet;
end;
procedure TDbCustomTimesheetEntry.CreateCalcFields;
begin
inherited;
FClockable := CreateCalcBooleanField(k_Clockable);
FClockableHours := CreateCalcFloatField(k_ClockableHours);
FDayOfWeek := CreateCalcStringField(k_WeekDay, 13);
FDifference := CreateCalcFloatField(k_Difference);
FEmployeeName := CreateCalcStringField(k_EmployeeName, 40);
FEntryTypeCaption := CreateCalcStringField(k_EntryTypeCaption, 20);
FFiscalYearEndDate := CreateCalcDateTimeField(k_FiscalYearEndDate);
FFiscalYearStartDate := CreateCalcDateTimeField(k_FiscalYearStartDate);
FScheduledTimeIn := CreateCalcDateTimeField(k_ScheduledTimeIn);
FScheduledTimeOut := CreateCalcDateTimeField(k_ScheduledTimeOut);
FTimeElapsed := CreateCalcFloatField(k_TimeElapsed);
FTimeScheduled := CreateCalcFloatField(k_TimeScheduled);
FWeekOf := CreateCalcDateTimeField(k_WeekOf);
end;
procedure TDbCustomTimesheetEntry.CreateFields;
begin
FID := CreateField(k_Id);
FEmployeeID := CreateField(k_EmployeeID);
FEntryTypeID := CreateField(k_EntryTypeID);
FRowNbr := CreateField(k_RowNbr);
FTimeIn := CreateField(k_TimeIn);
FTimeOut := CreateField(k_TimeOut);
FID := CreateField(k_ID);
FDateIn := CreateField(k_DateIn);
FDateOut := CreateField(k_DateOut);
FCreatedBy := CreateField(k_CreatedBy);
FCreatedTimestamp := CreateField(k_CreatedTimeStamp);
FLastModifiedBy := CreateField(k_LastModifiedBy);
FLastModifiedTimestamp := CreateField(k_LastModifiedTimestamp);
FNote := CreateField(k_Note);
FPayPeriodEndDate := CreateField(k_PayPeriodEndDate);
end;
procedure TDbCustomTimesheetEntry.CreateIndexes;
const
FIELD_LIST = k_DateIn + ';' + k_RowNbr;
begin
inherited;
FDateIndex := CreateIndex('ByDate', FIELD_LIST);
FDateIndex.Selected := True;
Dataset.IndexesActive := True;
end;
function TDbCustomTimesheetEntry.GetClockable: Boolean;
begin
Result := Rules.Clockable;
end;
function TDbCustomTimesheetEntry.GetClockableHours: THours;
begin
Result := Rules.ClockableHours;
end;
function TDbCustomTimesheetEntry.GetCreatedBy: TUserName;
begin
Result := FCreatedBy.AsUserName;
end;
function TDbCustomTimesheetEntry.GetCreatedTimestamp: TDateTime;
begin
Result := FCreatedTimestamp.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetDateIn: TDate;
begin
Result := FDateIn.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetDateOut: TDate;
begin
Result := FDateOut.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetDifference: THours;
begin
Result := Rules.Difference;
end;
function TDbCustomTimesheetEntry.GetEmployeeID: TIdentifier;
begin
Result := FEmployeeID.AsIdentifier;
end;
function TDbCustomTimesheetEntry.GetEmployeeName: string;
begin
Result := Rules.EmployeeName;
end;
function TDbCustomTimesheetEntry.GetEntryTypeID: TIdentifier;
begin
Result := FEntryTypeID.AsIdentifier;
end;
function TDbCustomTimesheetEntry.GetFiscalYearEndDate: TDate;
begin
Result := FFiscalYearEndDate.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetFiscalYearStartDate: TDate;
begin
Result := FFiscalYearStartDate.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetHasEntries: Boolean;
begin
Result := RecordCount > 0;
end;
function TDbCustomTimesheetEntry.GetID: TIdentifier;
begin
Result := FID.AsInteger;
end;
function TDbCustomTimesheetEntry.GetLastModifiedBy: TUserName;
begin
Result := FLastModifiedBy.AsUserName;
end;
function TDbCustomTimesheetEntry.GetLastModifiedTimestamp: TDateTime;
begin
Result := FLastModifiedTimestamp.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetModel: ITimesheetEntry;
var
LResult: ITimesheetEntry;
begin
LResult := FCopyFunc(Self);
Result := LResult;
end;
function TDbCustomTimesheetEntry.GetNote: AnsiString;
begin
Result := FNote.AsAnsiString;
end;
function TDbCustomTimesheetEntry.GetPayPeriodEndDate: TDate;
begin
Result := FPayPeriodEndDate.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetPlaceholder: Boolean;
begin
Result := Rules.Placeholder;
end;
function TDbCustomTimesheetEntry.GetEntry: ITimesheetEntry;
begin
Result := Model;
end;
function TDbCustomTimesheetEntry.GetEntryTypeCaption: TCaption;
begin
Result := Rules.EntryTypeCaption
end;
function TDbCustomTimesheetEntry.GetRowNbr: TRowNbr;
begin
Result := FRowNbr.AsRowNbr;
end;
function TDbCustomTimesheetEntry.GetScheduledTimeIn: TTime;
begin
Result := Rules.ScheduledTimeIn;
end;
function TDbCustomTimesheetEntry.GetScheduledTimeOut: TTime;
begin
Result := Rules.ScheduledTimeOut;
end;
function TDbCustomTimesheetEntry.GetTimeElapsed: THours;
begin
Result := Rules.TimeElapsed;
end;
function TDbCustomTimesheetEntry.GetTimein: TTime;
begin
Result := FTimeIn.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetTimeOut: TTime;
begin
Result := FTimeOut.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetTimeScheduled: THours;
begin
Result := Rules.TimeScheduled;
end;
function TDbCustomTimesheetEntry.GetTimesheet: ITimesheet;
begin
Result := FTimesheet;
end;
function TDbCustomTimesheetEntry.GetWeekDay: string;
begin
Result := Rules.WeekDay;
end;
function TDbCustomTimesheetEntry.GetWeekOf: TDate;
begin
Result := Rules.WeekOf;
end;
procedure TDbCustomTimesheetEntry.Load;
begin
//Stub procedure
end;
procedure TDbCustomTimesheetEntry.OnCalcFields(Dataset: TDataset);
begin
inherited;
if not Assigned(Rules) then Exit;
FClockable.AsBoolean := GetClockable;
FClockableHours.AsHours := GetClockableHours;
FDayOfWeek.AsString := GetWeekDay;
FDifference.AsHours := GetDifference;
FEmployeeName.AsString := GetEmployeeName;
FEntryTypeCaption.AsCaption := GetEntryTypeCaption;
FTimeElapsed.AsHours := GetTimeElapsed;
FTimeScheduled.AsHours := GetTimeScheduled;
FScheduledTimeIn.AsDateTime := GetScheduledTimeIn;
FScheduledTimeOut.AsDateTime := GetScheduledTimeOut;
FWeekOf.AsDateTime := GetWeekOf;
end;
procedure TDbCustomTimesheetEntry.OnNewRecord(Dataset: TDataset);
begin
inherited;
FEmployeeID.AsIdentifier := FTimesheet.EmployeeID;
FFiscalYearEndDate.AsDateTime := FTimesheet.FiscalYearEndDate;
FFiscalYearStartDate.AsDateTime := FTimesheet.FiscalYearStartDate;
FPayPeriodEndDate.AsDateTime := FTimesheet.PayPeriodEndDate;
end;
procedure TDbCustomTimesheetEntry.DoUpdate(AModel: ITimesheetEntry);
begin
inherited;
FEmployeeID.AsIdentifier := AModel.EmployeeID;
FRowNbr.AsRowNbr := AModel.RowNbr;
FEntryTypeID.AsIdentifier := AModel.EntryTypeID;
FDateIn.AsDateTime := AModel.DateIn;
FTimeIn.AsDateTime := AModel.TimeIn;
FDateOut.AsDateTime := AModel.DateOut;
FTimeOut.AsDateTime := AModel.TimeOut;
FNote.AsAnsiString := AModel.Note;
end;
procedure TDbCustomTimesheetEntry.FilterEntries(const ADate: TDate);
begin
FDateIndex.Selected := True;
SetRange([ADate], [ADate]);
end;
function TDbCustomTimesheetEntry.Find(AModel: ITimesheetEntry): Boolean;
begin
Result := Locate(k_ID, AModel.ID);
end;
procedure TDbCustomTimesheetEntry.FormatFields;
begin
inherited;
FTimeElapsed.OnGetText := HoursFieldGetText;
FClockableHours.OnGetText := HoursFieldGetText;
FDifference.OnGetText := HoursFieldGetText;
FTimeScheduled.OnGetText := HoursFieldGetText;
SetTimeFieldDisplayFormat(FTimeIn);
SetTimeFieldDisplayFormat(FTimeOut);
SetDateFieldDisplayFormat(FDateIn);
SetDateFieldDisplayFormat(FDateOut);
SetDateFieldDisplayFormat(FPayPeriodEndDate);
SetSQLTimestampFieldDisplayFormat(FCreatedTimestamp);
SetSQLTimestampFieldDisplayFormat(FLastModifiedTimestamp);
end;
TDbDummyEntry
procedure TDbSourceEntry.BeforePost(Dataset: TDataset);
var
LTimestamp: TDateTime;
begin
inherited;
LTimestamp := Now;
FLastModifiedBy.AsUserName := FTimesheet.User.UserName;
FLastModifiedTimestamp.AsDateTime := LTimestamp;
if State in [dsInsert] then
begin
FCreatedBy.AsUserName := FTimesheet.User.UserName;
FCreatedTimestamp.AsDateTime := LTimestamp;
end;
end;
constructor TDbSourceEntry.Create(ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>; ATimesheet: ITimesheet;
ACopyFunc: TCopyFunc<ITimesheetEntry>; AProc: TLoadTimesheetProc;
AElectionList: ILevelPayElectionList; APositionList: IHourlyPositionList;
ACreateFields: Boolean);
begin
inherited Create(ADataset, AModelFunc, ACopyFunc, ATimesheet, ACreateFields);
FLoadTimesheetEntries := AProc;
FElectionList := AElectionList;
FPositionList := APositionList;
end;
procedure TDbSourceEntry.Load;
var
LEmployeeID: TIdentifier;
LFirstEntryDate: TDate;
LLastEntryDate: TDate;
begin
LEmployeeID := FTimesheet.EmployeeID;
LFirstEntryDate := FTimesheet.FirstEntryDate;
LLastEntryDate := FTimesheet.LastEntryDate;
FLoadTimesheetEntries(LEmployeeID, LFirstEntryDate, LLastEntryDate);
end;
TDbDummyEntry
procedure TDbDummyEntry.AddPlacedholder(ADate: TDate; ARowNbr: TRowNbr);
var
LEntryTypeID: TIdentifier;
LClosure: ISchoolClosure;
LNote: AnsiString;
LRowNbr: TRowNbr;
begin
Dec(FPlaceholderID);
LNote := '';
LEntryTypeID := 0;
LRowNbr := ARowNbr;
if LRowNbr < 2 then //This is a first entry for the date
begin
if FClosureList.Find(ADate) then
begin
LClosure := FClosureList.Closure;
LEntryTypeID := LClosure.EntryTypeID;
LNote := AnsiString(LClosure.Caption);
end
else
begin
if TDateTime(ADate).DayOfWeek in [MONDAY..FRIDAY] then
LEntryTypeID := k_Regular
else
LEntryTypeID := 0;
end;
end;
Append;
FId.AsIdentifier := FPlaceholderID;
FRowNbr.AsRowNbr := LRowNbr;
FDateIn.AsDateTime := ADate;
FDateOut.AsDateTime := ADate;
FEntryTypeID.AsIdentifier := LEntryTypeID;
FNote.AsAnsiString := LNote;
Post;
end;
constructor TDbDummyEntry.Create(ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>; ACopyFunc: TCopyFunc<ITimesheetEntry>;
ATimesheet: ITimesheet; AClosureList: ISchoolClosureList; ACreateFields: Boolean);
begin
inherited Create(ADataset, AModelFunc, ACopyFunc, ATimesheet, ACreateFields);
FClosureList := AClosureList;
end;
procedure TDbDummyEntry.CreateFields;
begin
inherited;
FId.ReadOnly := False;
FId.AutoGenerateValue := arNone;
FEntryTypeID.OnChange := EntryTypeIDOnChange;
end;
procedure TDbDummyEntry.DoAdd(AModel: ITimesheetEntry);
begin
inherited;
FId.AsIdentifier := AModel.ID;
end;
procedure TDbDummyEntry.EntryTypeIDOnChange(Sender: TField);
var
LDateIn: TDate;
LDateOut: TDate;
begin
if Sender.AsIdentifier = k_LandSchool then
begin
LDateIn := FDateIn.AsDateTime;
LDateOut := LDateIn + 1;
FDateOut.AsDateTime := LDateOut;
FTimeIn.AsDateTime := GetScheduledTimeOut;
FTimeOut.AsDateTime := GetScheduledTimeIn;
end;
end;
TDbTimesheetEntry
procedure TDbTimesheetEntry.Add(AModel: ITimesheetEntry);
var
LEntry: ITimesheetEntry;
begin
FSource.Add(AModel);
LEntry := FSource.Entry; //Get model with updated ID
FTimesheet.WorkweekList.AddEntry(LEntry);
inherited Add(LEntry);
end;
procedure TDbTimesheetEntry.Clear;
begin
PlaceholderID := 0;
Dataset.CancelUpdates;
DisableControls;
try
First;
while RecordCount > 0 do
Dataset.Delete;
finally
EnableControls;
end;
end;
constructor TDbTimesheetEntry.Create(ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>;
ACopyFunc: TCopyFunc<ITimesheetEntry>; ATimesheet: ITimesheet;
ASourceFunc: TSourceListFunc; AClosureList: ISchoolClosureList;
ACreateFields: Boolean);
begin
inherited Create(ADataset, AModelFunc, ACopyFunc, ATimesheet, AClosureList, ACreateFields);
FSource := ASourceFunc(FTimesheet);
FClone := TFDMemTable.Create(ADataset);
FClone.CloneCursor(ADataset);
end;
procedure TDbTimesheetEntry.Delete(AModel: ITimesheetEntry);
var
LDate: TDate;
LRowNbr: SmallInt;
LEntry: ITimesheetEntry;
begin
FSource.Delete(AModel);
WorkweekList.DeleteEntry(AModel);
DisableControls;
try
LDate := AModel.DateIn;
LRowNbr := AModel.RowNbr;
inherited Delete(AModel);
if (LRowNbr = 1) then
begin
AddPlacedholder(LDate, 0);
LEntry := Model; //Get placeholder
WorkweekList.AddEntry(LEntry);
end;
finally
EnableControls;
end;
end;
function TDbTimesheetEntry.GetNextRowNbr: TRowNbr;
begin
Result := 0;
FClone.SetRange([GetDateIn], [GetDateIn]);
while not FClone.Eof do
begin
Result := FClone.FieldByName(k_RowNbr).AsRowNbr + 1;
FClone.Next;
end;
FClone.CancelRange;
end;
procedure TDbTimesheetEntry.Load;
var
LDate: TDate;
LFirstEntryDate: TDate;
LCutoffDate: TDate;
LEntry: ITimesheetEntry;
LWeekOf: TDate;
LWorkweekList: IWorkweekList;
begin
LWorkweekList := FTimesheet.WorkweekList;
LFirstEntryDate := FTimesheet.FirstEntryDate;
LCutOffDate := FTimesheet.LastEntryDate;
LWeekOf := TDateTime(LFirstEntryDate).StartOfWeek;
LWorkweekList.Init(LFirstEntryDate, LCutOffDate);
FSource.Load;
DisableControls;
try
Clear;
LDate := LWeekOf;
repeat
FSource.FilterEntries(LDate);
if FSource.HasEntries then
begin
for LEntry in FSource do
begin
if LDate >= LFirstEntryDate then
begin
inherited Add(LEntry); //this must call inherited add to adding to FSource
end;
LWorkweekList.AddEntry(LEntry);
end;
end
else
begin
if LDate >= LFirstEntryDate then
begin
AddPlacedholder(LDate, 0);
LEntry := GetModel; //This retrieves the placeholder
LWorkweekList.AddEntry(LEntry);
end;
end;
LDate := LDate + 1;
until LDate > LCutOffDate;
finally
FSource.ClearFilter;
EnableControls;
First;
end;
end;
procedure TDbTimesheetEntry.Update(OldModel, NewModel: ITimesheetEntry);
var
LEntry: ITimesheetEntry;
begin
DisableControls;
try
if Assigned(OldModel) then
begin
if OldModel.Placeholder then
begin
FSource.Add(NewModel);
end
else
begin
FSource.Update(NewModel);
end;
inherited Delete(OldModel);
WorkweekList.DeleteEntry(OldModel);
end
else
begin
FSource.Add(NewModel);
end;
LEntry := FSource.Entry; //Get entry with new ID
inherited Add(LEntry);
WorkweekList.AddEntry(LEntry);
finally
EnableControls;
end;
end;
function TDbTimesheetEntry.WorkweekList: IWorkweekList;
begin
Result := FTimesheet.WorkweekList;
end;
end.
delphi dependency-injection
I am working solo on an employee timekeeping project which uses a database to store time sheet entries. I am using Delphi Pro 10.2.3 Tokyo for the project and have created a library of wrapper classes to facilitate working with datasets like normal classes. For example, to access the FirstName field in the Employee table, I can write LFirstName := FEmployee.FirstName; rather than LFirstName := Dataset.FieldByName('FirstName').AsString;
Some of my classes have a significant number of dependencies (as many as eight) which I am injecting through the class' constructor. I use a domain object to create the required interfaces and inject them into the class being created.
Some of the interfaces being injected themselves are also very complex and it is starting to get difficult to keep track of everything in the domain object.
The dependencies being injected include wrapper interfaces for other tables which provide lookup values for calculated fields, pointers to functions which create create objects used by the class or call back functions which resolve master/detail relationships. These relationships are static and need to be set in the constructor so that any calculated fields will function when the class is created.
Are there any alternatives to constructor injection which might may decrease complexity of the constructor while maintaining decoupled classes. Here is a sample of code from one of my modules for time sheet entries.
unit LevelPay.DbModel.TimesheetEntry;
interface
uses
Data.Db
, FireDAC.Comp.DataSet
, MyLib.Model.Interfaces
, LevelPay.Model.Types
, LevelPay.Model.Constants
, LevelPay.Model.Interfaces
, LevelPay.DbModel.AppModel
;
type
TDbCustomTimesheetEntry = class(
TDbAppModel<ITimesheetEntry>,
ITimesheetEntry
)
strict private
FCopyFunc: TCopyFunc<ITimesheetEntry>;
procedure ClearFilter;
procedure FilterEntries(const ADate: TDate);
strict protected
FTimesheet: ITimesheet;
FID: TField;
FEmployeeID: TField;
FPayPeriodEndDate: TField;
FFiscalYearEndDate: TField;
FFiscalYearStartDate: TField;
FRowNbr: TField;
FEntryTypeID: TField;
FDateIn: TField;
FTimein: TField;
FDateOut: TField;
FTimeOut: TField;
FCreatedBy: TField;
FCreatedTimestamp: TField;
FLastModifiedBy: TField;
FLastModifiedTimestamp: TField;
FNote: TField;
FClockable: TField;
FClockableHours: TField;
FDayOfWeek: TField;
FDifference: TField;
FEmployeeName: TField;
FEntryTypeCaption: TField;
FTimeElapsed: TField;
FDateIndex: TFDIndex;
FTimeScheduled: TField;
FScheduledTimeIn: TField;
FScheduledTimeOut: TField;
FWeekOf: TField;
function GetID: TIdentifier;
function GetModel: ITimesheetEntry; override;
function GetClockable: Boolean;
function GetClockableHours: THours;
function GetDateIn: TDate;
function GetDateOut: TDate;
function GetDifference: THours;
function GetEmployeeID: TIdentifier;
function GetEmployeeName: string;
function GetPayPeriodEndDate: TDate;
function GetFiscalYearStartDate: TDate;
function GetFiscalYearEndDate: TDate;
function GetEntryTypeID: TIdentifier;
function GetEntryTypeCaption: TCaption;
function GetPlaceholder: Boolean;
function GetRowNbr: TRowNbr;
function GetScheduledTimeIn: TTime;
function GetScheduledTimeOut: TTime;
function GetTimeElapsed: THours;
function GetTimein: TTime;
function GetTimeOut: TTime;
function GetTimeScheduled: THours;
function GetWeekOf: TDate;
function GetWeekDay: string;
function GetCreatedBy: TUserName;
function GetCreatedTimestamp: TDateTime;
function GetLastModifiedBy: TUserName;
function GetLastModifiedTimestamp: TDateTime;
function GetNote: AnsiString;
function GetEntry: ITimesheetEntry;
function GetTimesheet: ITimesheet;
function GetHasEntries: Boolean;
function Find(AModel: ITimesheetEntry): Boolean; override;
procedure DoUpdate(AModel: ITimesheetEntry); override;
procedure Load; virtual;
procedure CreateFields; override;
procedure CreateCalcFields; override;
procedure CreateIndexes; override;
procedure FormatFields; override;
procedure OnCalcFields(Dataset: TDataset); override;
procedure OnNewRecord(Dataset: TDataset); override;
public
constructor Create(
ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>;
ACopyFunc: TCopyFunc<ITimesheetEntry>;
ATimesheet: ITimesheet;
ACreateFields: Boolean
); reintroduce;
property ID: TIdentifier read GetID;
property EmployeeID: TIdentifier read GetEmployeeID;
property PayPeriodEndDate: TDate read GetPayPeriodEndDate;
property FiscalYearEndDate: TDate read GetFiscalYearEndDate;
property FiscalYearStartDate: TDate read GetFiscalYearStartDate;
property EntryTypeID: TIdentifier read GetEntryTypeID;
property EntryTypeCaption: TCaption read GetEntryTypeCaption;
property RowNbr: TRowNbr read GetRowNbr;
property Clockable: Boolean read GetClockable;
property ClockableHours: THours read GetClockableHours;
property DateIn: TDate read GetDateIn;
property EmployeeName: string read GetEmployeeName;
property ScheduledTimeIn: TTime read GetScheduledTimeIn;
property ScheduledTimeOut: TTime read GetScheduledTimeOut;
property TimeIn: TTime read GetTimein;
property DateOut: TDate read GetDateOut;
property TimeOut: TTime read GetTimeOut;
property TimeElapsed: THours read GetTimeElapsed;
property Placeholder: Boolean read GetPlaceholder;
property TimeScheduled: THours read GetTimeScheduled;
property Difference: THours read GetDifference;
property WeekDay: string read GetWeekDay;
property WeekOf: TDate read GetWeekOf;
property CreatedBy: TUserName read GetCreatedBy;
property CreatedTimestamp: TDateTime read GetCreatedTimestamp;
property LastModifiedBy: TUserName read GetLastModifiedBy;
property LastModifiedTimestamp: TDateTime read GetLastModifiedTimestamp;
property Note: AnsiString read GetNote;
property Timesheet: ITimesheet read GetTimesheet;
end;
TDbSourceEntry = class(TDbCustomTimesheetEntry, ISourceEntryList)
strict private
FLoadTimesheetEntries: TLoadTimesheetProc;
FElectionList: ILevelPayElectionList;
FPositionList: IHourlyPositionList;
strict protected
procedure BeforePost(Dataset: TDataset); override;
procedure Load; override;
public
constructor Create(
ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>;
ATimesheet: ITimesheet;
ACopyFunc: TCopyFunc<ITimesheetEntry>;
AProc: TLoadTimesheetProc;
AElectionList: ILevelPayElectionList;
APositionList: IHourlyPositionList;
ACreateFields: Boolean = True
); reintroduce;
end;
TDbDummyEntry = class(TDbCustomTimesheetEntry, IDummyEntryList)
strict private
FPlaceholderID: TIdentifier;
FClosureList: ISchoolClosureList;
procedure EntryTypeIDOnChange(Sender: TField);
strict protected
procedure AddPlacedholder(ADate: TDate; ARowNbr: TRowNbr);
procedure CreateFields; override;
procedure DoAdd(AModel: ITimesheetEntry); override;
property PlaceholderID: TIdentifier read FPlaceholderID write FPlaceholderID;
public
constructor Create(
ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>;
ACopyFunc: TCopyFunc<ITimesheetEntry>;
ATimesheet: ITimesheet;
AClosureList: ISchoolClosureList;
ACreateFields: Boolean
); reintroduce;
end;
TDbTimesheetEntry = class(TDbDummyEntry, ITimesheetEntryList)
strict private
FClone: TFDDataset;
FSource: ISourceEntryList;
function GetNextRowNbr: TRowNbr;
strict protected
function WorkweekList: IWorkweekList;
procedure Clear;
procedure Load; override;
public
procedure Add(AModel: ITimesheetEntry); //replaces inherited add
procedure Delete(AModel: ITimesheetEntry); //replace inherited delete
procedure Update(OldModel, NewModel: ITimesheetEntry);
constructor Create(
ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>;
ACopyFunc: TCopyFunc<ITimesheetEntry>;
ATimesheet: ITimesheet;
ASourceFunc: TSourceListFunc;
AClosureList: ISchoolClosureList;
ACreateFields: Boolean
); reintroduce;
end;
implementation
uses
System.SysUtils
, System.Classes
, System.Variants
, System.DateUtils
, FireDAC.Comp.Client
, DateTimeHelper
, LevelPay.Model.Helpers
;
TCustomShift
procedure TDbCustomTimesheetEntry.ClearFilter;
begin
CancelRange;
end;
constructor TDbCustomTimesheetEntry.Create(ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>; ACopyFunc: TCopyFunc<ITimesheetEntry>;
ATimesheet: ITimesheet; ACreateFields: Boolean);
begin
inherited Create(ADataset, AModelFunc, ACreateFields);
FCopyFunc := ACopyFunc;
FTimesheet := ATimesheet;
end;
procedure TDbCustomTimesheetEntry.CreateCalcFields;
begin
inherited;
FClockable := CreateCalcBooleanField(k_Clockable);
FClockableHours := CreateCalcFloatField(k_ClockableHours);
FDayOfWeek := CreateCalcStringField(k_WeekDay, 13);
FDifference := CreateCalcFloatField(k_Difference);
FEmployeeName := CreateCalcStringField(k_EmployeeName, 40);
FEntryTypeCaption := CreateCalcStringField(k_EntryTypeCaption, 20);
FFiscalYearEndDate := CreateCalcDateTimeField(k_FiscalYearEndDate);
FFiscalYearStartDate := CreateCalcDateTimeField(k_FiscalYearStartDate);
FScheduledTimeIn := CreateCalcDateTimeField(k_ScheduledTimeIn);
FScheduledTimeOut := CreateCalcDateTimeField(k_ScheduledTimeOut);
FTimeElapsed := CreateCalcFloatField(k_TimeElapsed);
FTimeScheduled := CreateCalcFloatField(k_TimeScheduled);
FWeekOf := CreateCalcDateTimeField(k_WeekOf);
end;
procedure TDbCustomTimesheetEntry.CreateFields;
begin
FID := CreateField(k_Id);
FEmployeeID := CreateField(k_EmployeeID);
FEntryTypeID := CreateField(k_EntryTypeID);
FRowNbr := CreateField(k_RowNbr);
FTimeIn := CreateField(k_TimeIn);
FTimeOut := CreateField(k_TimeOut);
FID := CreateField(k_ID);
FDateIn := CreateField(k_DateIn);
FDateOut := CreateField(k_DateOut);
FCreatedBy := CreateField(k_CreatedBy);
FCreatedTimestamp := CreateField(k_CreatedTimeStamp);
FLastModifiedBy := CreateField(k_LastModifiedBy);
FLastModifiedTimestamp := CreateField(k_LastModifiedTimestamp);
FNote := CreateField(k_Note);
FPayPeriodEndDate := CreateField(k_PayPeriodEndDate);
end;
procedure TDbCustomTimesheetEntry.CreateIndexes;
const
FIELD_LIST = k_DateIn + ';' + k_RowNbr;
begin
inherited;
FDateIndex := CreateIndex('ByDate', FIELD_LIST);
FDateIndex.Selected := True;
Dataset.IndexesActive := True;
end;
function TDbCustomTimesheetEntry.GetClockable: Boolean;
begin
Result := Rules.Clockable;
end;
function TDbCustomTimesheetEntry.GetClockableHours: THours;
begin
Result := Rules.ClockableHours;
end;
function TDbCustomTimesheetEntry.GetCreatedBy: TUserName;
begin
Result := FCreatedBy.AsUserName;
end;
function TDbCustomTimesheetEntry.GetCreatedTimestamp: TDateTime;
begin
Result := FCreatedTimestamp.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetDateIn: TDate;
begin
Result := FDateIn.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetDateOut: TDate;
begin
Result := FDateOut.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetDifference: THours;
begin
Result := Rules.Difference;
end;
function TDbCustomTimesheetEntry.GetEmployeeID: TIdentifier;
begin
Result := FEmployeeID.AsIdentifier;
end;
function TDbCustomTimesheetEntry.GetEmployeeName: string;
begin
Result := Rules.EmployeeName;
end;
function TDbCustomTimesheetEntry.GetEntryTypeID: TIdentifier;
begin
Result := FEntryTypeID.AsIdentifier;
end;
function TDbCustomTimesheetEntry.GetFiscalYearEndDate: TDate;
begin
Result := FFiscalYearEndDate.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetFiscalYearStartDate: TDate;
begin
Result := FFiscalYearStartDate.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetHasEntries: Boolean;
begin
Result := RecordCount > 0;
end;
function TDbCustomTimesheetEntry.GetID: TIdentifier;
begin
Result := FID.AsInteger;
end;
function TDbCustomTimesheetEntry.GetLastModifiedBy: TUserName;
begin
Result := FLastModifiedBy.AsUserName;
end;
function TDbCustomTimesheetEntry.GetLastModifiedTimestamp: TDateTime;
begin
Result := FLastModifiedTimestamp.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetModel: ITimesheetEntry;
var
LResult: ITimesheetEntry;
begin
LResult := FCopyFunc(Self);
Result := LResult;
end;
function TDbCustomTimesheetEntry.GetNote: AnsiString;
begin
Result := FNote.AsAnsiString;
end;
function TDbCustomTimesheetEntry.GetPayPeriodEndDate: TDate;
begin
Result := FPayPeriodEndDate.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetPlaceholder: Boolean;
begin
Result := Rules.Placeholder;
end;
function TDbCustomTimesheetEntry.GetEntry: ITimesheetEntry;
begin
Result := Model;
end;
function TDbCustomTimesheetEntry.GetEntryTypeCaption: TCaption;
begin
Result := Rules.EntryTypeCaption
end;
function TDbCustomTimesheetEntry.GetRowNbr: TRowNbr;
begin
Result := FRowNbr.AsRowNbr;
end;
function TDbCustomTimesheetEntry.GetScheduledTimeIn: TTime;
begin
Result := Rules.ScheduledTimeIn;
end;
function TDbCustomTimesheetEntry.GetScheduledTimeOut: TTime;
begin
Result := Rules.ScheduledTimeOut;
end;
function TDbCustomTimesheetEntry.GetTimeElapsed: THours;
begin
Result := Rules.TimeElapsed;
end;
function TDbCustomTimesheetEntry.GetTimein: TTime;
begin
Result := FTimeIn.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetTimeOut: TTime;
begin
Result := FTimeOut.AsDateTime;
end;
function TDbCustomTimesheetEntry.GetTimeScheduled: THours;
begin
Result := Rules.TimeScheduled;
end;
function TDbCustomTimesheetEntry.GetTimesheet: ITimesheet;
begin
Result := FTimesheet;
end;
function TDbCustomTimesheetEntry.GetWeekDay: string;
begin
Result := Rules.WeekDay;
end;
function TDbCustomTimesheetEntry.GetWeekOf: TDate;
begin
Result := Rules.WeekOf;
end;
procedure TDbCustomTimesheetEntry.Load;
begin
//Stub procedure
end;
procedure TDbCustomTimesheetEntry.OnCalcFields(Dataset: TDataset);
begin
inherited;
if not Assigned(Rules) then Exit;
FClockable.AsBoolean := GetClockable;
FClockableHours.AsHours := GetClockableHours;
FDayOfWeek.AsString := GetWeekDay;
FDifference.AsHours := GetDifference;
FEmployeeName.AsString := GetEmployeeName;
FEntryTypeCaption.AsCaption := GetEntryTypeCaption;
FTimeElapsed.AsHours := GetTimeElapsed;
FTimeScheduled.AsHours := GetTimeScheduled;
FScheduledTimeIn.AsDateTime := GetScheduledTimeIn;
FScheduledTimeOut.AsDateTime := GetScheduledTimeOut;
FWeekOf.AsDateTime := GetWeekOf;
end;
procedure TDbCustomTimesheetEntry.OnNewRecord(Dataset: TDataset);
begin
inherited;
FEmployeeID.AsIdentifier := FTimesheet.EmployeeID;
FFiscalYearEndDate.AsDateTime := FTimesheet.FiscalYearEndDate;
FFiscalYearStartDate.AsDateTime := FTimesheet.FiscalYearStartDate;
FPayPeriodEndDate.AsDateTime := FTimesheet.PayPeriodEndDate;
end;
procedure TDbCustomTimesheetEntry.DoUpdate(AModel: ITimesheetEntry);
begin
inherited;
FEmployeeID.AsIdentifier := AModel.EmployeeID;
FRowNbr.AsRowNbr := AModel.RowNbr;
FEntryTypeID.AsIdentifier := AModel.EntryTypeID;
FDateIn.AsDateTime := AModel.DateIn;
FTimeIn.AsDateTime := AModel.TimeIn;
FDateOut.AsDateTime := AModel.DateOut;
FTimeOut.AsDateTime := AModel.TimeOut;
FNote.AsAnsiString := AModel.Note;
end;
procedure TDbCustomTimesheetEntry.FilterEntries(const ADate: TDate);
begin
FDateIndex.Selected := True;
SetRange([ADate], [ADate]);
end;
function TDbCustomTimesheetEntry.Find(AModel: ITimesheetEntry): Boolean;
begin
Result := Locate(k_ID, AModel.ID);
end;
procedure TDbCustomTimesheetEntry.FormatFields;
begin
inherited;
FTimeElapsed.OnGetText := HoursFieldGetText;
FClockableHours.OnGetText := HoursFieldGetText;
FDifference.OnGetText := HoursFieldGetText;
FTimeScheduled.OnGetText := HoursFieldGetText;
SetTimeFieldDisplayFormat(FTimeIn);
SetTimeFieldDisplayFormat(FTimeOut);
SetDateFieldDisplayFormat(FDateIn);
SetDateFieldDisplayFormat(FDateOut);
SetDateFieldDisplayFormat(FPayPeriodEndDate);
SetSQLTimestampFieldDisplayFormat(FCreatedTimestamp);
SetSQLTimestampFieldDisplayFormat(FLastModifiedTimestamp);
end;
TDbDummyEntry
procedure TDbSourceEntry.BeforePost(Dataset: TDataset);
var
LTimestamp: TDateTime;
begin
inherited;
LTimestamp := Now;
FLastModifiedBy.AsUserName := FTimesheet.User.UserName;
FLastModifiedTimestamp.AsDateTime := LTimestamp;
if State in [dsInsert] then
begin
FCreatedBy.AsUserName := FTimesheet.User.UserName;
FCreatedTimestamp.AsDateTime := LTimestamp;
end;
end;
constructor TDbSourceEntry.Create(ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>; ATimesheet: ITimesheet;
ACopyFunc: TCopyFunc<ITimesheetEntry>; AProc: TLoadTimesheetProc;
AElectionList: ILevelPayElectionList; APositionList: IHourlyPositionList;
ACreateFields: Boolean);
begin
inherited Create(ADataset, AModelFunc, ACopyFunc, ATimesheet, ACreateFields);
FLoadTimesheetEntries := AProc;
FElectionList := AElectionList;
FPositionList := APositionList;
end;
procedure TDbSourceEntry.Load;
var
LEmployeeID: TIdentifier;
LFirstEntryDate: TDate;
LLastEntryDate: TDate;
begin
LEmployeeID := FTimesheet.EmployeeID;
LFirstEntryDate := FTimesheet.FirstEntryDate;
LLastEntryDate := FTimesheet.LastEntryDate;
FLoadTimesheetEntries(LEmployeeID, LFirstEntryDate, LLastEntryDate);
end;
TDbDummyEntry
procedure TDbDummyEntry.AddPlacedholder(ADate: TDate; ARowNbr: TRowNbr);
var
LEntryTypeID: TIdentifier;
LClosure: ISchoolClosure;
LNote: AnsiString;
LRowNbr: TRowNbr;
begin
Dec(FPlaceholderID);
LNote := '';
LEntryTypeID := 0;
LRowNbr := ARowNbr;
if LRowNbr < 2 then //This is a first entry for the date
begin
if FClosureList.Find(ADate) then
begin
LClosure := FClosureList.Closure;
LEntryTypeID := LClosure.EntryTypeID;
LNote := AnsiString(LClosure.Caption);
end
else
begin
if TDateTime(ADate).DayOfWeek in [MONDAY..FRIDAY] then
LEntryTypeID := k_Regular
else
LEntryTypeID := 0;
end;
end;
Append;
FId.AsIdentifier := FPlaceholderID;
FRowNbr.AsRowNbr := LRowNbr;
FDateIn.AsDateTime := ADate;
FDateOut.AsDateTime := ADate;
FEntryTypeID.AsIdentifier := LEntryTypeID;
FNote.AsAnsiString := LNote;
Post;
end;
constructor TDbDummyEntry.Create(ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>; ACopyFunc: TCopyFunc<ITimesheetEntry>;
ATimesheet: ITimesheet; AClosureList: ISchoolClosureList; ACreateFields: Boolean);
begin
inherited Create(ADataset, AModelFunc, ACopyFunc, ATimesheet, ACreateFields);
FClosureList := AClosureList;
end;
procedure TDbDummyEntry.CreateFields;
begin
inherited;
FId.ReadOnly := False;
FId.AutoGenerateValue := arNone;
FEntryTypeID.OnChange := EntryTypeIDOnChange;
end;
procedure TDbDummyEntry.DoAdd(AModel: ITimesheetEntry);
begin
inherited;
FId.AsIdentifier := AModel.ID;
end;
procedure TDbDummyEntry.EntryTypeIDOnChange(Sender: TField);
var
LDateIn: TDate;
LDateOut: TDate;
begin
if Sender.AsIdentifier = k_LandSchool then
begin
LDateIn := FDateIn.AsDateTime;
LDateOut := LDateIn + 1;
FDateOut.AsDateTime := LDateOut;
FTimeIn.AsDateTime := GetScheduledTimeOut;
FTimeOut.AsDateTime := GetScheduledTimeIn;
end;
end;
TDbTimesheetEntry
procedure TDbTimesheetEntry.Add(AModel: ITimesheetEntry);
var
LEntry: ITimesheetEntry;
begin
FSource.Add(AModel);
LEntry := FSource.Entry; //Get model with updated ID
FTimesheet.WorkweekList.AddEntry(LEntry);
inherited Add(LEntry);
end;
procedure TDbTimesheetEntry.Clear;
begin
PlaceholderID := 0;
Dataset.CancelUpdates;
DisableControls;
try
First;
while RecordCount > 0 do
Dataset.Delete;
finally
EnableControls;
end;
end;
constructor TDbTimesheetEntry.Create(ADataset: TFDDataset;
AModelFunc: TModelFunc<ITimesheetEntry>;
ACopyFunc: TCopyFunc<ITimesheetEntry>; ATimesheet: ITimesheet;
ASourceFunc: TSourceListFunc; AClosureList: ISchoolClosureList;
ACreateFields: Boolean);
begin
inherited Create(ADataset, AModelFunc, ACopyFunc, ATimesheet, AClosureList, ACreateFields);
FSource := ASourceFunc(FTimesheet);
FClone := TFDMemTable.Create(ADataset);
FClone.CloneCursor(ADataset);
end;
procedure TDbTimesheetEntry.Delete(AModel: ITimesheetEntry);
var
LDate: TDate;
LRowNbr: SmallInt;
LEntry: ITimesheetEntry;
begin
FSource.Delete(AModel);
WorkweekList.DeleteEntry(AModel);
DisableControls;
try
LDate := AModel.DateIn;
LRowNbr := AModel.RowNbr;
inherited Delete(AModel);
if (LRowNbr = 1) then
begin
AddPlacedholder(LDate, 0);
LEntry := Model; //Get placeholder
WorkweekList.AddEntry(LEntry);
end;
finally
EnableControls;
end;
end;
function TDbTimesheetEntry.GetNextRowNbr: TRowNbr;
begin
Result := 0;
FClone.SetRange([GetDateIn], [GetDateIn]);
while not FClone.Eof do
begin
Result := FClone.FieldByName(k_RowNbr).AsRowNbr + 1;
FClone.Next;
end;
FClone.CancelRange;
end;
procedure TDbTimesheetEntry.Load;
var
LDate: TDate;
LFirstEntryDate: TDate;
LCutoffDate: TDate;
LEntry: ITimesheetEntry;
LWeekOf: TDate;
LWorkweekList: IWorkweekList;
begin
LWorkweekList := FTimesheet.WorkweekList;
LFirstEntryDate := FTimesheet.FirstEntryDate;
LCutOffDate := FTimesheet.LastEntryDate;
LWeekOf := TDateTime(LFirstEntryDate).StartOfWeek;
LWorkweekList.Init(LFirstEntryDate, LCutOffDate);
FSource.Load;
DisableControls;
try
Clear;
LDate := LWeekOf;
repeat
FSource.FilterEntries(LDate);
if FSource.HasEntries then
begin
for LEntry in FSource do
begin
if LDate >= LFirstEntryDate then
begin
inherited Add(LEntry); //this must call inherited add to adding to FSource
end;
LWorkweekList.AddEntry(LEntry);
end;
end
else
begin
if LDate >= LFirstEntryDate then
begin
AddPlacedholder(LDate, 0);
LEntry := GetModel; //This retrieves the placeholder
LWorkweekList.AddEntry(LEntry);
end;
end;
LDate := LDate + 1;
until LDate > LCutOffDate;
finally
FSource.ClearFilter;
EnableControls;
First;
end;
end;
procedure TDbTimesheetEntry.Update(OldModel, NewModel: ITimesheetEntry);
var
LEntry: ITimesheetEntry;
begin
DisableControls;
try
if Assigned(OldModel) then
begin
if OldModel.Placeholder then
begin
FSource.Add(NewModel);
end
else
begin
FSource.Update(NewModel);
end;
inherited Delete(OldModel);
WorkweekList.DeleteEntry(OldModel);
end
else
begin
FSource.Add(NewModel);
end;
LEntry := FSource.Entry; //Get entry with new ID
inherited Add(LEntry);
WorkweekList.AddEntry(LEntry);
finally
EnableControls;
end;
end;
function TDbTimesheetEntry.WorkweekList: IWorkweekList;
begin
Result := FTimesheet.WorkweekList;
end;
end.
delphi dependency-injection
delphi dependency-injection
asked Nov 13 '18 at 11:22
joeb545joeb545
1797
1797
add a comment |
add a comment |
2 Answers
2
active
oldest
votes
Generally the saying is that when you have many constructor parameters (that means dependencies) it is a sign that your class might do too much (see single responsible principle).
If certain dependencies most of the time only interact with each other this is a sign that these dependencies might be subject to refactor them into their own component/class which is then injected. This does not only reduce dependencies in the first place but reduces complexity of your components.
I suggest reading the blog of Mark Seemann where he explained many areas that play into properly practicing dependency injection and software design and architecture.
Just two examples that I remember:
- http://blog.ploeh.dk/2010/02/02/RefactoringtoAggregateServices/
- http://blog.ploeh.dk/2018/08/27/on-constructor-over-injection/
Thanks for the quick reply and the links. They were helpful. I was unsure how much separation of concerns was necessary and thought I had already struck a balance. For example, I had already refactored the rules for the calculated fields to a rules class. I can see that the classes I show in this post are trying to be a dataset wrapper, provide an interface to a single model object and also an interface to a list object. I will look at separating each of these responsibilities into separate classes and see what I get.
– joeb545
Nov 13 '18 at 14:36
add a comment |
You have default property in root all TDataSet
:
property FieldValues[const FieldName: string]: Variant read GetFieldValue write SetFieldValue; default;
and you can use
aTest: TDataSet; //any descant
aValue := aTest['NameField'];
Why complicate this with injections?
Syntax purity?
Like Paradox ObjectPAL from 1994.
One simple reason, unit testing! please read this QA
– whosrdaddy
Nov 13 '18 at 15:49
Poorly formatted, and fails to address the question that was asked
– David Heffernan
Nov 13 '18 at 16:34
add a comment |
Your Answer
StackExchange.ifUsing("editor", function ()
StackExchange.using("externalEditor", function ()
StackExchange.using("snippets", function ()
StackExchange.snippets.init();
);
);
, "code-snippets");
StackExchange.ready(function()
var channelOptions =
tags: "".split(" "),
id: "1"
;
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function()
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled)
StackExchange.using("snippets", function()
createEditor();
);
else
createEditor();
);
function createEditor()
StackExchange.prepareEditor(
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader:
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
,
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
);
);
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53279936%2fconstructor-dependency-injection-alternatives%23new-answer', 'question_page');
);
Post as a guest
Required, but never shown
2 Answers
2
active
oldest
votes
2 Answers
2
active
oldest
votes
active
oldest
votes
active
oldest
votes
Generally the saying is that when you have many constructor parameters (that means dependencies) it is a sign that your class might do too much (see single responsible principle).
If certain dependencies most of the time only interact with each other this is a sign that these dependencies might be subject to refactor them into their own component/class which is then injected. This does not only reduce dependencies in the first place but reduces complexity of your components.
I suggest reading the blog of Mark Seemann where he explained many areas that play into properly practicing dependency injection and software design and architecture.
Just two examples that I remember:
- http://blog.ploeh.dk/2010/02/02/RefactoringtoAggregateServices/
- http://blog.ploeh.dk/2018/08/27/on-constructor-over-injection/
Thanks for the quick reply and the links. They were helpful. I was unsure how much separation of concerns was necessary and thought I had already struck a balance. For example, I had already refactored the rules for the calculated fields to a rules class. I can see that the classes I show in this post are trying to be a dataset wrapper, provide an interface to a single model object and also an interface to a list object. I will look at separating each of these responsibilities into separate classes and see what I get.
– joeb545
Nov 13 '18 at 14:36
add a comment |
Generally the saying is that when you have many constructor parameters (that means dependencies) it is a sign that your class might do too much (see single responsible principle).
If certain dependencies most of the time only interact with each other this is a sign that these dependencies might be subject to refactor them into their own component/class which is then injected. This does not only reduce dependencies in the first place but reduces complexity of your components.
I suggest reading the blog of Mark Seemann where he explained many areas that play into properly practicing dependency injection and software design and architecture.
Just two examples that I remember:
- http://blog.ploeh.dk/2010/02/02/RefactoringtoAggregateServices/
- http://blog.ploeh.dk/2018/08/27/on-constructor-over-injection/
Thanks for the quick reply and the links. They were helpful. I was unsure how much separation of concerns was necessary and thought I had already struck a balance. For example, I had already refactored the rules for the calculated fields to a rules class. I can see that the classes I show in this post are trying to be a dataset wrapper, provide an interface to a single model object and also an interface to a list object. I will look at separating each of these responsibilities into separate classes and see what I get.
– joeb545
Nov 13 '18 at 14:36
add a comment |
Generally the saying is that when you have many constructor parameters (that means dependencies) it is a sign that your class might do too much (see single responsible principle).
If certain dependencies most of the time only interact with each other this is a sign that these dependencies might be subject to refactor them into their own component/class which is then injected. This does not only reduce dependencies in the first place but reduces complexity of your components.
I suggest reading the blog of Mark Seemann where he explained many areas that play into properly practicing dependency injection and software design and architecture.
Just two examples that I remember:
- http://blog.ploeh.dk/2010/02/02/RefactoringtoAggregateServices/
- http://blog.ploeh.dk/2018/08/27/on-constructor-over-injection/
Generally the saying is that when you have many constructor parameters (that means dependencies) it is a sign that your class might do too much (see single responsible principle).
If certain dependencies most of the time only interact with each other this is a sign that these dependencies might be subject to refactor them into their own component/class which is then injected. This does not only reduce dependencies in the first place but reduces complexity of your components.
I suggest reading the blog of Mark Seemann where he explained many areas that play into properly practicing dependency injection and software design and architecture.
Just two examples that I remember:
- http://blog.ploeh.dk/2010/02/02/RefactoringtoAggregateServices/
- http://blog.ploeh.dk/2018/08/27/on-constructor-over-injection/
answered Nov 13 '18 at 11:45
Stefan GlienkeStefan Glienke
16.2k13478
16.2k13478
Thanks for the quick reply and the links. They were helpful. I was unsure how much separation of concerns was necessary and thought I had already struck a balance. For example, I had already refactored the rules for the calculated fields to a rules class. I can see that the classes I show in this post are trying to be a dataset wrapper, provide an interface to a single model object and also an interface to a list object. I will look at separating each of these responsibilities into separate classes and see what I get.
– joeb545
Nov 13 '18 at 14:36
add a comment |
Thanks for the quick reply and the links. They were helpful. I was unsure how much separation of concerns was necessary and thought I had already struck a balance. For example, I had already refactored the rules for the calculated fields to a rules class. I can see that the classes I show in this post are trying to be a dataset wrapper, provide an interface to a single model object and also an interface to a list object. I will look at separating each of these responsibilities into separate classes and see what I get.
– joeb545
Nov 13 '18 at 14:36
Thanks for the quick reply and the links. They were helpful. I was unsure how much separation of concerns was necessary and thought I had already struck a balance. For example, I had already refactored the rules for the calculated fields to a rules class. I can see that the classes I show in this post are trying to be a dataset wrapper, provide an interface to a single model object and also an interface to a list object. I will look at separating each of these responsibilities into separate classes and see what I get.
– joeb545
Nov 13 '18 at 14:36
Thanks for the quick reply and the links. They were helpful. I was unsure how much separation of concerns was necessary and thought I had already struck a balance. For example, I had already refactored the rules for the calculated fields to a rules class. I can see that the classes I show in this post are trying to be a dataset wrapper, provide an interface to a single model object and also an interface to a list object. I will look at separating each of these responsibilities into separate classes and see what I get.
– joeb545
Nov 13 '18 at 14:36
add a comment |
You have default property in root all TDataSet
:
property FieldValues[const FieldName: string]: Variant read GetFieldValue write SetFieldValue; default;
and you can use
aTest: TDataSet; //any descant
aValue := aTest['NameField'];
Why complicate this with injections?
Syntax purity?
Like Paradox ObjectPAL from 1994.
One simple reason, unit testing! please read this QA
– whosrdaddy
Nov 13 '18 at 15:49
Poorly formatted, and fails to address the question that was asked
– David Heffernan
Nov 13 '18 at 16:34
add a comment |
You have default property in root all TDataSet
:
property FieldValues[const FieldName: string]: Variant read GetFieldValue write SetFieldValue; default;
and you can use
aTest: TDataSet; //any descant
aValue := aTest['NameField'];
Why complicate this with injections?
Syntax purity?
Like Paradox ObjectPAL from 1994.
One simple reason, unit testing! please read this QA
– whosrdaddy
Nov 13 '18 at 15:49
Poorly formatted, and fails to address the question that was asked
– David Heffernan
Nov 13 '18 at 16:34
add a comment |
You have default property in root all TDataSet
:
property FieldValues[const FieldName: string]: Variant read GetFieldValue write SetFieldValue; default;
and you can use
aTest: TDataSet; //any descant
aValue := aTest['NameField'];
Why complicate this with injections?
Syntax purity?
Like Paradox ObjectPAL from 1994.
You have default property in root all TDataSet
:
property FieldValues[const FieldName: string]: Variant read GetFieldValue write SetFieldValue; default;
and you can use
aTest: TDataSet; //any descant
aValue := aTest['NameField'];
Why complicate this with injections?
Syntax purity?
Like Paradox ObjectPAL from 1994.
edited Nov 13 '18 at 21:35
zx485
14.3k123047
14.3k123047
answered Nov 13 '18 at 14:16
cehaceha
353
353
One simple reason, unit testing! please read this QA
– whosrdaddy
Nov 13 '18 at 15:49
Poorly formatted, and fails to address the question that was asked
– David Heffernan
Nov 13 '18 at 16:34
add a comment |
One simple reason, unit testing! please read this QA
– whosrdaddy
Nov 13 '18 at 15:49
Poorly formatted, and fails to address the question that was asked
– David Heffernan
Nov 13 '18 at 16:34
One simple reason, unit testing! please read this QA
– whosrdaddy
Nov 13 '18 at 15:49
One simple reason, unit testing! please read this QA
– whosrdaddy
Nov 13 '18 at 15:49
Poorly formatted, and fails to address the question that was asked
– David Heffernan
Nov 13 '18 at 16:34
Poorly formatted, and fails to address the question that was asked
– David Heffernan
Nov 13 '18 at 16:34
add a comment |
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53279936%2fconstructor-dependency-injection-alternatives%23new-answer', 'question_page');
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown