I'm migrating a legacy application from C to C# (.NET Core 5), and implementing more modern software development practices. The C application had literally all of its data in structures in RAM, all of which were globally available. This is great for simple applications but violates the heck out of the principle of encapsulation. The closest equivalent of doing this in an object-oriented fashion is to make the datastore static (which I don't want to do, for obvious reasons).
The application at hand is highly technical and involves a great deal of math. The previous application calculated a lot of derived values (sometimes repeatedly!) on demand, however this doesn't take advantage of multi-threading or caching.
To demonstrate a simpler yet analogous example, let's say we have a program to manage the costs associated with a baseball team. A team has its own fixed operating costs, as well as one manager (who has a salary), and zero to many players (who also each have salaries).
This is my redesigned data model:
public class Team {
public int Costs { get; set; }
public Manager Manager { get; set; }
public HashSet<Player> Players { get; set; }
private int _operatingCosts;
public int OperatingCosts {
get {
return this._operatingCosts;
}
private set {
// Note the private setter -- this should only be recalculated within this
// method from its own values.
this._operatingCosts = value;
}
}
}
public class Manager {
public string Name { get; set; }
public int Salary { get; set; }
}
public class Player {
public string Name { get; set; }
public int Salary { get; set; }
}
The C code would have a function that runs and puts its fingers in all of the arrays, finds the appropriate elements, and adds them up. In the rewritten C# code, MOST of these calculations are reasonably simple, so they probably are best implemented as read-only calculated properties.
public class Manager {
public int MonthlySalary {
get {
return this.Salary / 12;
}
}
}
The challenge comes when trying to calculate the total cost of running the team. This might be a complex operation that I don't want to have to run each time, so I'll want to cache a value and only recalculate it when an event fires. We know which variables cause the change, so we can fire the event from within the dependent properties' setters.
public class Team : INotifyPropertyChanged {
private int _costs;
public int Costs {
get {
return this._costs;
}
set {
this._costs = value;
NotifyPropertyChanged(); // Fire recalculation on cost change.
}
}
public int Name {
get; set; // No INotifyPropertyChanged needed; cached values aren't
// dependent on this.
}
private int _totalOperatingCost;
public int TotalOperatingCost {
get {
return this._totalOperatingCost;
}
private set {
this._totalOperatingCost = value;
}
}
protected void RecalculateCosts() {
this._totalOperatingCost = this.Team.Cost + this.Manager.Salary + this.Players.Sum(p => p.Salary);
}
}
This has been working really well with INotifyPropertyChanged -- I fire a PropertyChanged event on fields that should trigger a recalculation, and the recalculation logic is there and in one place, and all is well.
Now here's the problem: what happens if the manager's salary changes?
public class Manager : INotifyPropertyChanged {
private int _salary;
public int Salary {
get {
return this._salary;
}
set {
this._salary = value;
NotifyParentPropertyChanged();
}
}
}
The Manager class does not contain a reference to the Team to which he belongs. There's a lot of potential for error retaining a backreference from the child bean to the parent when the parent knows about its own children. So if I call NotifyPropertyChanged() here, the Manager will update his own calculations but it has no idea it needs to update on the parent. This is also mostly resolvable using events, but there's a big hitch:
public class Team {
public string Name { ... }
private Manager _manager;
public Manager Manager {
get {
return this._manager;
}
set {
if (this._manager is not null) {
this._manager.PropertyChanged -= ProcessParentPropertyChangedEvent;
}
this._manager = value;
NotifyPropertyChanged();
if (this._manager is not null) {
this._manager.PropertyChanged += ProcessParentPropertyChangedEvent;
}
}
}
public Team() {
if (this.Manager is not null) {
this.Manager.PropertyChanged += ProcessParentPropertyChangedEvent;
}
}
public static void ProcessParentPropertyChangedEvent(object sender, EventArgs e) {
// ^^^^^^ THIS IS A STATIC METHOD, so "this" is unavailable.
//
// When called from Manager.Salary, sender.GetType().Name = "Manager" and
// e can ONLY carry information from the Manager and knows nothing about
// the Team.
//
// Literally nothing in this method knows about the current team, or I believe
// can know about the team to which the manager belongs.
}
}
public class Manager {
private int _salary;
public int Salary {
get {
return this._salary;
}
set {
this._salary = value;
NotifyParentPropertyChanged();
}
}
}
The event fires properly but because the callback method is static, there's no link to the Team whose Manager's salary just updated. Player would have a similar problem, there's no way to inform the Team class that a Player's salary updated since the recalculation would take place within a static method.
Similarly, we could need to add or subtract players from the team as well, which should also force a recalculation of our team costs. I've gone down this road some with INotifyCollectionChanged, but no luck -- we still end up in a static method with no information about where the changes need to be applied to.
I believe my options are as follows:
(1) Include backreferences to parent elements. This seems somewhat straightforward, but there is a lot that can go wrong here, and garbage collection may be one of them. There are probably going to be about a million elements in memory, and I'm not sure how well this would perform by everything basically being in a doubly-connected graph.
(2) I've looked into the Mediator pattern, but I'm not convinced this still solves my problems without a great deal of error-prone accounting. It also requires the Mediator class to be injected into all of my data elements, and requires me to use extensions for base .NET generic collections types -- I don't want to be forced to always use custom types like MyPlayersList or similar, instead of the more common List<Player> -- especially when NOT using the custom list would likely end in a subtle and silent bug when a recalculation just didn't quite pull the right values.
What are my options? Am I forced to carry tons of backreferences throughout the model?
Is there a different model for event handling that I'm not aware of? I've been working with INotifyPropertyChanged because I'm used to it from a WPF context and it's really fast -- 1,000,000 calls in 60 ms on my machine. That said I don't know if there are any other event handling paradigms that exist to handle this type of case.
This seems like way too straightforward an issue to not have appeared and been solved by someone else before. Thanks so much!