principlecsharpMinor
Multiple properties updated by event vs few properties and methods
Viewed 0 times
propertiesmethodsfewupdatedmultipleandevent
Problem
I've got a class which describes and calculates centrifugal pump impeller properties (dimensions, flow speed etc. ). At the beginning i have created it in this way
(since my class is quite large and calculations are complex i will simplify it using cylinder as example):
So i have ended up with a lot of methods which calls each other, additionally number of unnecessary method calls is growing when i'm displaying results to the screen.
My last solution to this problem was creating a class in this way:
```
enum PropertyType
{
Height,
Radius,
FlatSideArea,
Volume
}
class Cylinder
{
double height;
double radius;
double flatSideArea;
double volume;
public double Radius
{
get { return radius; }
set
{
if (radius != value)
{
radius = value;
RisePropertyChanged(PropertyType.Radius);
}
}
}
public double Height
{
get { return height; }
set
{
if (height != value)
{
height = value;
RisePropertyChanged(PropertyType.Height);
}
}
}
public double FlatSideArea
{
get { return flatSideArea; }
private set
{
if (flatSideArea != value)
{
flatSideArea = value;
RisePropertyChanged(PropertyType.FlatSideArea);
}
}
}
public double Volume
{
get { return volume; }
private set
{
if (volume != value)
{
volume = value;
(since my class is quite large and calculations are complex i will simplify it using cylinder as example):
class Cylinder
{
double height;
double radius;
//Properties rising event when value is changed
//Constructors
//Property changed event handler implementation
public double FlatSideArea()
{
return Math.PI * radius;
}
public double Volume()
{
return FlatSideArea()*height;
}
}So i have ended up with a lot of methods which calls each other, additionally number of unnecessary method calls is growing when i'm displaying results to the screen.
My last solution to this problem was creating a class in this way:
```
enum PropertyType
{
Height,
Radius,
FlatSideArea,
Volume
}
class Cylinder
{
double height;
double radius;
double flatSideArea;
double volume;
public double Radius
{
get { return radius; }
set
{
if (radius != value)
{
radius = value;
RisePropertyChanged(PropertyType.Radius);
}
}
}
public double Height
{
get { return height; }
set
{
if (height != value)
{
height = value;
RisePropertyChanged(PropertyType.Height);
}
}
}
public double FlatSideArea
{
get { return flatSideArea; }
private set
{
if (flatSideArea != value)
{
flatSideArea = value;
RisePropertyChanged(PropertyType.FlatSideArea);
}
}
}
public double Volume
{
get { return volume; }
private set
{
if (volume != value)
{
volume = value;
Solution
While commenting on your comment to Jesse's answer, I thought of a solution;
.NET 4 contains this new class, which for the purposes of most external consumers would have the following definition:
There's some additional code involved in the real implementation for thread-safety etc but this is the general idea (and it can be dropped into a .NET 3.5 solution if needed; .NET 2.0 doesn't have lambda statements or the
Here's your Cylinder class with the Lazy properties FlatArea and Volume:
Usage:
So, the net result in your full situation of 80 properties is that, while re-instantiation to incorporate a change is still needed, calculation of dependent fields is not. If those 80 properties include a large number of purely calculated fields (and the calculation is not simply for sanity within a mutable class) you save a lot of work compared to "eager" calculation when a user is only interested in one or two calculations, or compared to a mutable class when needs to make several changes before being interested in the calculated fields.
If the class absolutely has to be mutable, then I would extract the instantiation of the "lazy" private fields into a method, "ClearCalculatedValues", and call it from the setters of the user-defined values if the new value differs from the old:
You can, if you want, limit the number of calculations that must be re-performed; for instance, a change to the Height property doesn't change the current value of FlatSideArea, so you can choose to only clear the Volume value. This will make that logic more complicated to code, but could save you some steps at runtime (which is a common tradeoff).
PS: BTW, if high precision is needed and you're dealing with cylinders (or other shapes) smaller in size than that of the known universe, I'd use
Lazy..NET 4 contains this new class, which for the purposes of most external consumers would have the following definition:
public class Lazy
{
private Func func;
private T val;
private bool isCreated;
public Lazy(Func input)
{
func = input;
}
public bool IsValueCreated {get {return isCreated;}}
public T Value
{
get {
if(!isCreated)
{
val = func();
isCreated = true;
}
return val;
}
}
}There's some additional code involved in the real implementation for thread-safety etc but this is the general idea (and it can be dropped into a .NET 3.5 solution if needed; .NET 2.0 doesn't have lambda statements or the
Func class but it's still possible to implement the concept). You can use it with an immutable class to provide for lazily-evaluated properties; the calculations for calculated fields are performed if and when they are needed, and are only performed on the first access of "Value", after which the result of the calculation is used without re-running it. The class doesn't HAVE to be immutable, but if consumers can change properties, the calculated properties that depend on the change must be recalculated when next accessed.Here's your Cylinder class with the Lazy properties FlatArea and Volume:
class Cylinder
{
readonly double height;
readonly double radius;
readonly Lazy flatSideArea;
readonly Lazy volume;
public Cylinder(double height, double radius)
{
this.height = height;
this.radius = radius;
this.flatSideArea = new Lazy(()=>Math.PI * Math.Pow(radius, 2));
this.volume = new Lazy(()=>flatSideArea.Value * height);
}
public double FlatSideArea
{
get { return this.flatSideArea.Value; }
}
public double Volume
{
get { return this.volume.Value; }
}
}Usage:
//Only the user-defined properties are set here; no calculations.
Cylinder myCylinder = new Cylinder(20, 10);
//the FlatSideArea calculation is now performed:
var area = myCylinder.FlatSideArea;
//and now it's known, and doesn't have to be recalculated:
var vol = myCylinder.Volume;
//You don't have to access them in order of dependency either:
var vol2 = new Cylinder(10,20).Volume; //results in the calculation of FSA as wellSo, the net result in your full situation of 80 properties is that, while re-instantiation to incorporate a change is still needed, calculation of dependent fields is not. If those 80 properties include a large number of purely calculated fields (and the calculation is not simply for sanity within a mutable class) you save a lot of work compared to "eager" calculation when a user is only interested in one or two calculations, or compared to a mutable class when needs to make several changes before being interested in the calculated fields.
If the class absolutely has to be mutable, then I would extract the instantiation of the "lazy" private fields into a method, "ClearCalculatedValues", and call it from the setters of the user-defined values if the new value differs from the old:
class Cylinder
{
readonly double height;
readonly double radius;
readonly Lazy flatSideArea;
readonly Lazy volume;
public Cylinder(double height, double radius)
{
this.height = height;
this.radius = radius;
ClearLazyValues();
}
private void ClearLazyValues()
{
this.flatSideArea = new Lazy(()=>Math.PI * Math.Pow(radius, 2));
this.volume = new Lazy(()=>flatSideArea * height);
}
public double FlatSideArea
{
get{return this.flatSideArea.Value;}
}
public double Volume
{
get{return this.volume.Value;}
}
public double Radius
{
get{return this.radius;}
set{if(this.radius != value) {this.radius = value; ClearLazyValues();}
}
public double Height
{
get{return this.height;}
set{if(this.height != value) {this.height = value; ClearLazyValues();}
}
}You can, if you want, limit the number of calculations that must be re-performed; for instance, a change to the Height property doesn't change the current value of FlatSideArea, so you can choose to only clear the Volume value. This will make that logic more complicated to code, but could save you some steps at runtime (which is a common tradeoff).
PS: BTW, if high precision is needed and you're dealing with cylinders (or other shapes) smaller in size than that of the known universe, I'd use
decimal instead of double as it will reduce the amount of floating-point error inherent in storing base-10 numbers in base-2 scientific notation. A decimal can hold a number E28), which is suitable for most practical calculations outside astronomy and theoretical physics.Code Snippets
public class Lazy<T>
{
private Func<T> func;
private T val;
private bool isCreated;
public Lazy(Func<T> input)
{
func = input;
}
public bool IsValueCreated {get {return isCreated;}}
public T Value
{
get {
if(!isCreated)
{
val = func();
isCreated = true;
}
return val;
}
}
}class Cylinder
{
readonly double height;
readonly double radius;
readonly Lazy<double> flatSideArea;
readonly Lazy<double> volume;
public Cylinder(double height, double radius)
{
this.height = height;
this.radius = radius;
this.flatSideArea = new Lazy<double>(()=>Math.PI * Math.Pow(radius, 2));
this.volume = new Lazy<double>(()=>flatSideArea.Value * height);
}
public double FlatSideArea
{
get { return this.flatSideArea.Value; }
}
public double Volume
{
get { return this.volume.Value; }
}
}//Only the user-defined properties are set here; no calculations.
Cylinder myCylinder = new Cylinder(20, 10);
//the FlatSideArea calculation is now performed:
var area = myCylinder.FlatSideArea;
//and now it's known, and doesn't have to be recalculated:
var vol = myCylinder.Volume;
//You don't have to access them in order of dependency either:
var vol2 = new Cylinder(10,20).Volume; //results in the calculation of FSA as wellclass Cylinder
{
readonly double height;
readonly double radius;
readonly Lazy<double> flatSideArea;
readonly Lazy<double> volume;
public Cylinder(double height, double radius)
{
this.height = height;
this.radius = radius;
ClearLazyValues();
}
private void ClearLazyValues()
{
this.flatSideArea = new Lazy<double>(()=>Math.PI * Math.Pow(radius, 2));
this.volume = new Lazy<double>(()=>flatSideArea * height);
}
public double FlatSideArea
{
get{return this.flatSideArea.Value;}
}
public double Volume
{
get{return this.volume.Value;}
}
public double Radius
{
get{return this.radius;}
set{if(this.radius != value) {this.radius = value; ClearLazyValues();}
}
public double Height
{
get{return this.height;}
set{if(this.height != value) {this.height = value; ClearLazyValues();}
}
}Context
StackExchange Code Review Q#13475, answer score: 7
Revisions (0)
No revisions yet.