Set properties of child objects without synchronisation errors

3 次查看(过去 30 天)
I have a parent object with properties which hold some child objects. The child objects are all of the same class. When I change certain properties of the parent object I would like the equivalent child object properties to change too. For example
classdef PressureVessel
properties
massFlowRate % mass flow rate through the pressure vessel
inletConnection ConnectingPipe % pipe which is the inlet to the pressure vessel
outletConnection ConnectingPipe % pipe which is the outlet to the pressure vessel
end
methods
function vesselObj = set.massFlowRate(vesselObj,flowRate)
% Set the mass flow rate through the vessel, and through each of the nozzles, to be equal
% Causes warning about "The set method should not access another property"
vesselObj.massFlowRate = flowRate;
vesselObj.inletConnection.massFlowRate = flowRate;
vesselObj.outletConnection.massFlowRate = flowRate;
end
end
end
The connecting pipe class is as follows
classdef ConnectingPipe
properties
massFlowRate % mass flow rate through the connecting pipe
velocity % velocity of fluid, calculated from the mass flow rate
end
end
I can see from the warning and reading the documentation that this is not good practice and can cause property initialisation order dependency problems. Is there a better way of achieving the desired behaviour? Should the ConnectingPipe objects be told what their parent object is when they are created, and then pull the mass flow rate from the parent? Ideally I'd like to be able to use the ConnectingPipe object without a parent so that would be non-preferred. I'm not clear how the use of a private property (see example here: https://uk.mathworks.com/help/matlab/matlab_oop/avoiding-property-initialization-order-dependency.html) can help in this situation, and I'd rather avoid it if possible because there are loads of properties which this applies to, and I don't think it will be obvious to the next user of the code what is going on.
Any suggestions gratefully received.

回答(4 个)

Steven Lord
Steven Lord 2019-12-5
Let's take a step back from the code for a second. Can you describe the design of your system for which this set of classes will be the implementation?
You have a pressure vessel. That pressure vessel has two pipes, named inlet and outlet, and a mass flow rate.
Each pipe has its own mass flow rate.
  • When are pressure vessels allowed to change pipes? Are they fixed when the pressure vessels are created or can they be changed at all?
  • Can you have a pipe without a pressure vessel?
  • Can you have a pressure vessel without a pipe?
  • Who is allowed to query and/or change a pressure vessel's mass flow rate? Can the pressure vessel? Can either of its associated pipes?
  • Who is allowed to query and/or change a pipe's mass flow rate? Can the pipe itself? Can the pressure vessel with which it's associated?
  • What is the interaction between a pressure vessel's mass flow rate and its inlet and outlet pipe's mass flow rate?
  • If a pipe must be associated with a pressure vessel, and if its mass flow rate can change to a value that's incompatible with the pressure vessel's, whose responsibility is it to detect that incompatibility?
  • What actions should you be able to take on a pressure vessel?
  • What actions should you be able to take on a pipe?
Knowing the answers to some of these questions will help determine how tightly coupled the software modeling those objects should be. [I hope that the physical pressure vessel and its pipes are tighly coupled, say with strong welds, especially if high pressures are involved!]
  1 个评论
Guillaume
Guillaume 2019-12-5
This is a very good point by Steven and my answer and comments are only valid if you're trying to simulate some air flow through a system of pipes/components with no loss of mass (so mass flow is constant throughout) as you would with GT-power or similar software. That's what my students are doing now, so I didn't think of anything else. But indeed, depending on the answers to the above questions a completely different design would be much better.

请先登录,再进行评论。


J. Alex Lee
J. Alex Lee 2019-12-5
Is your "ConnectingPipe" class really just the 2 properties, or is that just part of the class? If it's only 2 properties, would you consider not having them be their own classes? I suppose on name/context you'd want the "ConnectingPipe" to be endowed with other things like diameter, and perhaps a method to compute velocity from the diameter and massflowrate...
Can you just give your ConnectingPipe class a property that refers to its parent vessel object, and then make its massFlowRate a dependent property, which will read its parent vessel's massFlowRate on-demand? Is this also bad practice?
classdef ConnectingPipe
properties
ParentVessel PressureVessel
Diameter double
end
properties (Dependent)
massFlowRate
velocity
end
methods
function this = ConnectingPipe(ParentVessel,Diameter)
this.ParentVessel = ParentVessel;
this.Diameter = Diameter;
end
function out = get.massFlowRate(this)
out = this.ParentVessel.massFlowRate;
end
function out = get.velocity(this)
out = this.massFlowRate / (pi*D^2/4);
end
end
end
and
classdef PressureVessel
properties
massFlowRate
inletPipe ConnectingPipe
outletPipe ConnectingPipe
end
methods
function this = PressureVessel(massFlowRate)
this.massFlowRate = massFlowRate;
this.inletPipe.ParentVessel = this;
this.outletPipe.ParentVessel = this;
end
% no more need for set function for massFlowRate
end
end
Also just found this, which makes it seem the above strategy is not so bad, https://www.mathworks.com/matlabcentral/answers/158010-get-property-of-the-parent-object
  3 个评论
J. Alex Lee
J. Alex Lee 2019-12-5
Oops, sorry I didn't see the stipulation.
However, my understanding about the warning is about the unpredictable order of property-setting, not about inadvertent access to properties that are "downstream" of some chain of operations...so while in this simple example the privatizing will practically solve the danger, it wouldn't necessarily eliminate the warning, right?
Also, would it not be harder to maintain this strategy as you kept adding shared properties among these 2 classes, so that you would need to keep worry about adding/updating the set methods for "shared" properties?
I think Guillaume's answer with the handle class for massflow makes more sense!
Guillaume
Guillaume 2019-12-5
The reason for the warning is that if you're not careful it's easy to have the three mass flows disconnected from each other. For example, with the initial design, you can set the mass flow in the pressure vessel which sets the mass flow in the pipes and then inadvertently override that by accessing the property of the pipe. Thus, forcing the pipe property to be handled by the pressure vessel avoids that issue.
You also have to consider reloading the classes from a mat file which again could result in different values in properties that are essentially the same. This shouldn't be an issue with the original design.
The warning is a warning and not an error because you can use this design if you're careful.
If there are more shared properties between the classes, then my handle class solution is even more worhwile since all the shared properties can be stored in that single handle class and you're guaranteed that all the classes are always using the same properties.

请先登录,再进行评论。


Guillaume
Guillaume 2019-12-5
As I wrote in my comment to J. Alex, I don't see a problem with your initial design if the pipes can't be modified outside of the PressureVessel class (i.e. the pipes are private properties of the vessel).
Otherwise, I'd use a handle class to store the mass flow and share an instance between all three class:
classdef MassFlow < handle
properties
value;
end
end
classdef PressureVessel
properties
inletConnection ConnectingPipe % pipe which is the inlet to the pressure vessel
outletConnection ConnectingPipe % pipe which is the outlet to the pressure vessel
end
properties (SetAccess = private)
massFlowRate MassFlow = MassFlow();
end
methods
function this = PressureVessel
this.inletConnection.massFlowRate = this.massFlowRate; %share the same MassFlow variable between all three
this.outletCOnnection.massFlowRate = this.massFlowRate;
end
end
%...
end
classdef ConnectingPipe
properties
velocity % velocity of fluid, calculated from the mass flow rate
end
properties (SetAccess = ?PressureVessel) %can be set by PressureVessel (when it's constructed
massFlowRate MassFLow = MassFlow();
end
end
PV = PressureVessel;
PV.massFlowRate.value = 4; %automatically set the connecting pipes mass flow rate
PV.inletConnection.massflowRate %displays 4
PV.inletConnection.massFlowRate = 6;
PV.massFlowRate.value %displays 6
pipe = ConnectingPipe; %connecting pipes without a parent
pipe.massFlowRate.value = 8;
  4 个评论
men8th
men8th 2019-12-5
Thanks for your solutions. After some messing about I came up with this:
classdef HeatExchangerNozzleClass
%HEATEXCHANGERNOZZLECLASS Summary of this class goes here
% Detailed explanation goes here
properties
mass
massFlowRate
density
designPressure
designTemperature
insideDiameter
parentObj
end
properties(Dependent=true)
insideXSectionArea
velocity
end
methods
function obj = HeatExchangerNozzleClass(parentObj)
%HEATEXCHANGERNOZZLECLASS Construct an instance of this class
end
function obj = set.massFlowRate(obj,flowRate)
if isempty(obj.parentObj)
obj.massFlowRate = flowRate;
else
warning('Ignoring set value for mass flow and taking parent value')
obj.massFlowRate = obj.parentObj.massFlowRate;
end
end
function flowRate = get.massFlowRate(obj)
if ~isempty(obj.parentObj) && isempty(obj.massFlowRate)
obj.massFlowRate = obj.parent.massFlowRate;
flowRate = obj.massFlowRate;
else
flowRate = obj.massFlowRate;
end
end
function flowXSA = get.insideXSectionArea(obj)
flowXSA = pi*obj.insideXSectionArea^2 / 4;
end
function vel = get.velocity(obj)
qVolFlowRate = obj.massFlowRate / obj.density;
vel = qVolFlowRate / obj.insideXSectionArea;
end
end
end
Although this seems to work (I didn't spend very long testing it) there is still the warning about accessing other properties (this time the property .parentObj) from inside a set/get method, so I don't think it is much of an improvement on the initial design. In fact it's worse because there is more code but it achieves essentially the same thing.
I think I'm going to stick with my original design as Guillaume (who clearly has far more experience than me) doesn't think it is objectionable. I don't really like the idea of brewing up yet another class (Guillaume's handle class) to work as a kind of globa variable although it is a neat solution and I can see how it could be useful.
who reinforces the point that that the code analyser warning is just that: a warning. If handled carefully I think there should be no problems.
Guillaume
Guillaume 2019-12-5
编辑:Guillaume 2019-12-5
Your new design is not a good idea. It mixes dependent property pattern with a non-dependent property and there would be a real risk that one of the mass flow is not in sync with the other. I particularly dislike the design of your set.massFlowRate function which may result in unexpected behaviour.
As I said to J. Alex, the handle class becomes extremely useful if you have other properties shared between the components. But I can't really think of any other property of the pipework that wouldn't be local.
In my opinion, your initial design is indeed fine. However, pay heeds to my comment on access properties. If the mass flow is controlled by the plenum, you cannot allow it to be set directly by the pipe. Otherwise, you allow one to be disconnected from the other.

请先登录,再进行评论。


men8th
men8th 2019-12-5
Hi Steven,
Some answers below. Let me know what you think.
◾When are pressure vessels allowed to change pipes? Are they fixed when the pressure vessels are created or can they be changed at all?
I anticipage that the pipe diameter could be changed after the pressure vessel is constructed.
◾Can you have a pipe without a pressure vessel?
With the current implimentation, I only have one use for the pipe object, as a child of the pressure vessel object. It would be possible to have a pipe without a pressure vessel, and it would be nice if the pipe object could be reused for other projects later on.
◾Can you have a pressure vessel without a pipe?
In this application all vessels will have at least one pipe.
◾Who is allowed to query and/or change a pressure vessel's mass flow rate? Can the pressure vessel? Can either of its associated pipes?
The pressure vessel's mass flow rate is set at the parent level, and it would only make sense to change it at the parent level. It would not make sense for the pressure vessel mass flow rate to be set by changing one nof the nozzle mass flow rates in the current application. The mass flow rate can be queried from any level (parent or child)
◾Who is allowed to query and/or change a pipe's mass flow rate? Can the pipe itself? Can the pressure vessel with which it's associated?
As above.
◾What is the interaction between a pressure vessel's mass flow rate and its inlet and outlet pipe's mass flow rate?
The mass flow rate is used to calculate the pressure drop through the inlet/outlet pipes. There is then an overall pressure drop for the entire vessel.
◾If a pipe must be associated with a pressure vessel, and if its mass flow rate can change to a value that's incompatible with the pressure vessel's, whose responsibility is it to detect that incompatibility?
I'm hoping that won't occur, but if it did happen I anticipate that the vessel should throw an error. It is not the responsibility of the pipes to detect that the parent vessel has messed something up.
◾What actions should you be able to take on a pressure vessel?
Make a new one, set the mass flow rate, get the pressure drop
◾What actions should you be able to take on a pipe?
As for the vessel
After even more messing about I've now got an implementation as below, which I think mostly does the job, but needs some polish. HeatExchangerHeadAssemblyClass is the pressure vessel we were discussing above, nozzle is the connecting pipes - I changed the names to something a bit less specialised. The properties which are set at the parent level but which must cascade down onto the children are defined as dependent properties.
When you set them the value is written to the child objects. When you get them the retrieve their values from the child objects, and check for consistency. I believe this is robust, and if the object is saved to disc it will recreate itself properly by pulling all the properties of the parent from the children. Feedback welcome though, if there's a better way to do it I'd love to know.
classdef HeatExchangerHeadAssemblyClass
%HEATEXCHANGERHEADASSEMBLYCLASS Summary of this class goes here
% Detailed explanation goes here
% TODO: This is implemented up to a point where pressure drop can be
% calculated properly. Needs improving upto a point where all of the child
% objects (head, tubesheet etc) inheret the design pressure and
% temperature, and an estimate of the mass is generated.
properties
end
properties (GetAccess = public, SetAccess = private)
tubesheet HeatExchangerTubesheetClass % Tubesheet object
nozzle HeatExchangerNozzleClass % Nozzle object
passPartitionPlate HeatExchangerPassPartitionPlateClass % Pass partition plate object
head HeatExchangerHeadClass % Head object
isInlet (1,1) {mustBeBoolean} % True if the head has an inlet nozzle
isOutlet (1,1) {mustBeBoolean} % True if the head has an outlet nozzle
end
properties(Dependent = true)
massFlowRate (1,1) {mustBeReal, mustBePositive} % Mass flow rate through the nozzle
densityEnd1 (1,1) {mustBeReal, mustBePositive} % Density at nozzle 1
densityEnd2 (1,1) {mustBeReal, mustBePositive} % Density at nozzle 2
end
properties(Dependent = true, SetAccess = private)
mass (1,1) {mustBeReal, mustBePositive} % Total mass of the head assembly
numberOfPasses (1,1) {mustBeInteger, mustBePositive} % Number of tubeside passes
pressureDrop (1,1) {mustBeReal, mustBePositive} % Pressure drop across the head
end
methods
function obj = HeatExchangerHeadAssemblyClass(nPasses,isIn,isOut,nozzleInsideDiameters)
%HEATEXCHANGERHEADASSEMBLYCLASS Construct an instance of this class
if nargin == 0
obj.tubesheet = HeatExchangerTubesheetClass.empty;
obj.nozzle = HeatExchangerNozzleClass.empty;
obj.passPartitionPlate = HeatExchangerPassPartitionPlateClass.empty;
obj.head = HeatExchangerHeadClass.empty;
else
if nPasses == 0
% This is a return head with no tubesheet and no
% nozzles
obj.tubesheet = HeatExchangerTubesheetClass.empty;
obj.nozzle = HeatExchangerNozzleClass.empty;
if isIn || isOut
warning('Head is a return head with no inlet or outlet nozzles, so isInlet and isOutlet should both be false')
end
obj.isInlet = false;
obj.isOutlet = false;
elseif mod(nPasses,2)
% Number of passes is odd, this head must be either an
% inlet or an outlet so has one nozzle and a tubesheet
obj.tubesheet = HeatExchangerTubesheetClass;
obj.nozzle = HeatExchangerNozzleClass; % Whether nozzle is and inlet or outlet is determined by obj.isInlet and obj.isOutlet
obj.nozzle.insideDiameter = nozzleInsideDiameters;
if isIn && isOut
error('Nozzle cannot be both inlet and outlet')
end
obj.isInlet = isIn;
obj.isOutlet = isOut;
else
% Number of passes is even, this head has both inlet
% and outlet nozzles and a tubesheet
obj.tubesheet = HeatExchangerTubesheetClass;
obj.nozzle(1:2) = HeatExchangerNozzleClass; % Nozzle 1 is inlet, nozzle 2 is outlet
nozzleDiamsCell = num2cell(nozzleInsideDiameters);
[obj.nozzle.insideDiameter] = nozzleDiamsCell{:};
if not(isIn && isOut)
warning('With an even number of passes the head must have an inlet and outlet, so isInlet and isOutlet should both be true')
end
obj.isInlet = true;
obj.isOutlet = true;
end
obj.numberOfPasses = nPasses;
end
end
function obj = set.massFlowRate(obj,wFlow)
nPasses = obj.numberOfPasses;
if mod(nPasses,2)
% Number of passes is odd, this head must be either an
% inlet or an outlet. Set sign of mass flow accordingly.
if obj.isInlet
obj.nozzle.massFlowRate = abs(wFlow);
elseif obj.isOutlet
obj.nozzle.massFlowRate = -abs(wFlow);
else
error('isInlet or isOutlet must be set before setting mass flow rate')
end
else
% Number of passes is even, assume that inlet and outlet
% nozzles are both on the same head
obj.nozzle(1).massFlowRate = abs(wFlow); % make nozzle(1) the inlet
obj.nozzle(2).massFlowRate = -abs(wFlow); % and nozzle(2) the outlet
end
end
function obj = set.numberOfPasses(obj,nPasses)
nPasses = repmat(nPasses,size(obj.nozzle));
nPassesCell = num2cell(nPasses);
[obj.nozzle.numberOfPasses] = nPassesCell{:};
end
function obj = set.densityEnd1(obj,rho)
obj.nozzle(1).density = rho;
end
function obj = set.densityEnd2(obj,rho)
obj.nozzle(2).density = rho;
end
function mass = get.mass(obj)
mass = 0;
mass = mass + obj.tubesheet.mass;
mass = mass + sum([obj.nozzle.mass]); % There may be more than one nozzle
mass = mass + obj.passPartitionPlate.mass;
mass = mass + obj.head.mass;
end
function wFlow = get.massFlowRate(obj)
nozzleMassFlows = abs([obj.nozzle.massFlowRate]);
TOL = 100*eps;
if range(nozzleMassFlows) > TOL
error('Nozzle mass flow rates are inconsistent') % check for conservation of mass
elseif any(isempty(nozzleMassFlows))
wFlow = [];
else
wFlow = nozzleMassFlows(1);
end
end
function nPasses = get.numberOfPasses(obj)
nozzNumbPasses = [obj.nozzle.numberOfPasses];
if range(nozzNumbPasses) ~= 0
error('Nozzle number of passes are inconsistent')
elseif any(isempty(nozzNumbPasses))
nPasses = [];
else
nPasses = nozzNumbPasses(1);
end
end
function rho = get.densityEnd1(obj)
rho = obj.nozzle(1).density;
end
function rho = get.densityEnd2(obj)
rho = obj.nozzle(2).density;
end
function dPress = get.pressureDrop(obj)
%TODO
dPress = sum([obj.nozzle.pressureDrop]); % There may be more than one nozzle. TODO: Will this always give the correct sign for flow into/out of nozzles?
end
end % methods
end

产品

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!

Translated by