Best Way to Set Private Properties in Class Constructor

21 次查看(过去 30 天)
I've just start OOP and am writing number of class constructors. Constructor input arguments often coorespond directly to class properties for which I want to override the default values. Depending on if/which defaults I want overridden, I may provide some, all, or none of these constructor input arguments. Some may be empty in order to access later arguments.
Trying to implement this has seemed either unnecessarily laborious or opaque.
What I'd like to do is something like:
classdef MyClass
properties (SetAccess = private)
prop1 = 1;
prop2 = 2;
end
methods %Constructor
function obj = MyClass(obj.prop1, obj.prop2)
...
end
end
end
This or something similar would allow immediately setting any varargins to the correct properties. It'd do this if an argument is provided (preferbly provided and not empty/missing) and would keep the default if not. This syntax also clearly lists the correct order of arguments for in-context help.
For example, a call like obj = MyClass(3,4) would set obj.prop1 = 3, obj.prop2 = 4.
Instead, it seems this needs to be belabored with something like (but often for more properties):
methods %Constructor
function obj = MyClass(prop1, prop2)
if nargin > 0
obj.prop1 = prop1;
end
if nargin > 1
obj.prop2 = prop2;
end
...
end
end
This seems to violate all kinds of Clean Code / Don't Repeat Yourself rules in addition to being frustrating. It's not even really a switch case, and as I understand it, all this needs to be written directly in-line in the classdef block. (Because the Constructor has to be defined inline and I can't pass the inputs to another function until I know they exist.)
I did write a general constructor_override function that solves this if I use a varargin reciever and provide a redundant list of property names. Unfortunately this seems like it's again is asking for clean code troubles. (Also, I'm still trying to figure out inheritence to allow it on SetAccess ~= public properties). In addition to the perils of a separate property names list, this results in the in-context help for the class call just saying "varargin", which is similarly problematic.
methods %Constructor
function obj = MyClass(varargin)
...
obj = constructor_override(obj, argin_names, varargin{:})
...
end
end
I can't imagine this is a rare problem, unless I'm fundamentally poorly designing my classes (which is possible). Either way though, there must be a better way? Thank you
  2 个评论
Adam
Adam 2019-5-8
Constructing objects that require a load of properties set from externally on construction is never an especially 'clean' process, although I'm not too familiar with where in its circle the current thinking on what is clean code is.
In the cases I feel I may want to set numerous properties I tend to go for a varargin approach similar to Matlab's functions. This is about the only time I use varargin as I hate it in general, but a long list of namd input arguments that you have to remember the name of is ghastly too. Where I use varargin I use the inputParser as Matt suggests and created a 'helper' class
This is the class I use for this:
classdef InputParserObjectBuilder
methods( Static, Access = public )
function obj = createObject( fObjectConstructor, parser, varargin )
validateattributes( fObjectConstructor, { 'function_handle' }, { 'scalar' } )
assert( nargin( fObjectConstructor ) == 0 );
validateattributes( parser, { 'inputParser' }, { 'scalar' } )
obj = fObjectConstructor();
parse( parser, varargin{:} );
parserResults = parser.Results;
fields = fieldnames( parserResults );
for n = 1:numel( fields )
obj.( fields{ n } ) = parserResults.( fields{ n } );
end
end
end
end
and this is an example of a class that I construct in this way. All the properties in the class have a default value so they are all optional. It's a fair bit of effort, but obviously a constructor taking 10 optional named arguments is ridiculous (even though I see Matlab toolbox functions taking 15 arguments, of which their examples usually pass in [] for 10 or so!)
function obj = JavaSliderSetupArgs( varargin )
if nargin > 0
p = inputParser;
p.FunctionName = 'JavaSliderSetupArgs';
addParameter( p, 'orientation', obj.orientation, @(x) validateattributes( x, { 'SliderOrientation' }, { 'scalar' } ) );
addParameter( p, 'direction', obj.direction, @(x) validateattributes( x, { 'SliderDirection' }, { 'scalar' } ) );
addParameter( p, 'spinnerWidth', obj.spinnerWidth, @(x) validateattributes( x, { 'numeric' }, { 'scalar', 'positive' } ) );
addParameter( p, 'spinnerHeight', obj.spinnerHeight, @(x) validateattributes( x, { 'numeric' }, { 'scalar', 'positive' } ) );
addParameter( p, 'marginSpacing', obj.marginSpacing, @(x) validateattributes( x, { 'numeric' }, { 'scalar', 'nonnegative' } ) );
addParameter( p, 'paintTicks', obj.paintTicks, @(x) validateattributes( x, { 'logical' }, { 'scalar' } ) );
addParameter( p, 'paintLabels', obj.paintLabels, @(x) validateattributes( x, { 'logical' }, { 'scalar' } ) );
addParameter( p, 'useJavaSpinControl', obj.useJavaSpinControl, @(x) validateattributes( x, { 'logical' }, { 'scalar' } ) );
fConstructor = @( ) JavaSliderSetupArgs( );
obj = InputParserObjectBuilder.createObject( fConstructor, p, varargin{:} );
end
end
This setup also relies on the slightly brittle idea of every string in the input matching the class property name exactly, which is no different to Matlab's own functions often.
I don't particularly like it. I wrote that class over 3 years ago and have used it about 4 times, out of the several hundred classes I've created since then. Generally I try to avoid classes needing a load of inputs, but sometimes it is hard to do.
I think the Builder pattern was aimed at solving this problem, though I think I only ever wrote one Builder object in my life and I don't really use Design Patterns in Matlab. I've no idea if it has fallen out of favour long since, but it still just shovels the ugly code somewhere else anyway, like a lot of coding solutions do!
If I have specific combinations of properties I often want to pass in together I will often create a static 'named constructor' on my class that takes those parameters and builds the class using a default constructor, but that isn't generic or extensible, they're just convenience functions for common cases.
Siri Maley
Siri Maley 2019-5-8
Thank you! I'm definitely still struggling with what it means to write a clean class, and it looks like I'm not even really at the surface yet. (Factories? Design Patterns? Oh my) I'm giving inputParser a try. It's good to know this is a rough problem!
I love the idea of enumerated names for alternative default sets; I don't know how I overlooked that.

请先登录,再进行评论。

采纳的回答

Matt J
Matt J 2019-5-8
编辑:Matt J 2019-5-8
There's nothing wrong with the first version of the code you posted (the one with the nargin tests). Also, there is no reason you can't invoke other functions in the constructor. However, you might like to try inputParser
  4 个评论
Siri Maley
Siri Maley 2019-5-8
编辑:Siri Maley 2019-5-8
Sorry, I think I misinterpreted "There's nothing wrong with the first version of the code you posted" as referring to this working
...
methods %Constructor
function obj = MyClass(obj.prop1, obj.prop2)
...
end
end
...
I'll use InputParser then if not; it looks really powerful (if though as Adam says, somewhat messy). Thank you!
Adam
Adam 2019-5-9
You can't put obj.prop1 in the argument list, this is not valid syntax. obj is being created by this function, although even if it weren't it still isn't valid syntax to have obj.prop1 and obj.prop2 as input arguments.

请先登录,再进行评论。

更多回答(0 个)

类别

Help CenterFile Exchange 中查找有关 Argument Definitions 的更多信息

产品


版本

R2018a

Community Treasure Hunt

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

Start Hunting!

Translated by