The Gift of Property Cheer
Well, it's that time of year, Christmas, Hanukkah, Kwanzaa, Ramadan, Winter Solstice, or you know.......Friday. You deserve the gift of a blog post. Here's a nugget of object oriented goodness in MATLAB, what better gift could there be?
Well perhaps a quick follow up discussion on some more benefits of MATLAB object properties as we have discussed a couple times before. This time I'd like to focus on an awesome (and relatively new) feature that allows for much more streamlined validation of properties.
First, a quick reminder, take a look at this special string class called FancyString that enables decorating individual elements it the string array with additional information such as hyperlinking or bolding. This starts with the FancyString itself:
classdef FancyString properties PlainValue % Each element uses the "do nothing" decorator by default Decorator = DefaultDecorator; end properties(Dependent) DecoratedValue end methods function fancyString = FancyString(varargin) if nargin < 1 return end plainValue = string(varargin{:}); fancyString(numel(plainValue)) = FancyString; plainValueCell = num2cell(plainValue); [fancyString.PlainValue] = plainValueCell{:}; end function decorated = get.DecoratedValue(fancyString) % The decorated value returns whatever the decorator adds to % the plain string. decorated = fancyString.Decorator.decorate(fancyString.PlainValue); end function s = string(fancyString) s = reshape([fancyString.PlainValue], size(fancyString)); end end end
As well as a couple decorators to add some fanciness:
classdef DefaultDecorator methods function decoratedString = decorate(~, undecoratedString) decoratedString = undecoratedString; end end end classdef HyperlinkDecorator properties LinkTarget end methods function decorator = HyperlinkDecorator(linkTarget) decorator.LinkTarget = linkTarget; end function decoratedString = decorate(decorator, undecoratedString) decoratedString = "<a href=""" + decorator.LinkTarget + """>" + undecoratedString + "</a>"; end end end classdef BoldDecorator methods function decoratedString = decorate(~, undecoratedString) decoratedString = "<strong>" + undecoratedString + "</strong>"; end end end
Now we can have a nice message with some bolding and hyperlinking holiday cheer:
holidayMessage = ["Happy" "Holidays" "!"]; greeting = FancyString(holidayMessage); greeting(1).Decorator = BoldDecorator; % Add a link to US Holidays holidayWikipediaSite = 'https://en.wikipedia.org/wiki/List_of_multinational_festivals_and_holidays#December'; greeting(2).Decorator = HyperlinkDecorator(holidayWikipediaSite); join([greeting.DecoratedValue])
ans = "Happy Holidays !"
Alright this is great, but there is a problem. Let's say we have an enumeration containing the different holidays and we'd like to put together a few different sets of greetings for our friends and neighbors with different preferences. Maybe this looks something like this:
classdef Holidays % Simple (and incomplete) enumeration of some winter holidays enumeration Christmas Hanukkah Kwanzaa Ramadan WinterSolstice NewYears end end
That looks so nice I might just want to replace my string with one of these enumeration values. Seems reasonable.
greeting(2).PlainValue = Holidays.Kwanzaa
greeting = 1×3 FancyString array with properties: PlainValue Decorator DecoratedValue
So far so good. Quick spot check?
greeting.PlainValue
ans = "Happy" ans = Kwanzaa ans = "!"
Still looks good at first glance. Alright how about the DecoratedValue? Do the hyperlinking and bold operations still work? (hint: the try-catch I've added in the script I am using to publish this blog is a hint that something fishy might be going on):
try greeting.DecoratedValue catch e disp('Errored!'); disp(e.getReport('basic')); end
Errored! Undefined function 'plus' for input arguments of type 'Holidays'.
Alright this doesn't work because I've assigned to the PlainValue a value from our enumeration, but the FancyString (and the decorators) are expecting the value to be a string.
In fact, we could have supplied any arbitrary value to the property and we likely wouldn't have noticed until much farther down the line. For example, let's add a structure, which is meaningless in this context:
meaninglessStruct.SomeField = 5; meaninglessStruct.AnotherField = true; greeting(2).PlainValue = meaninglessStruct; % Assignment works!! greeting.PlainValue % Still doesn't error!
ans = "Happy" ans = struct with fields: SomeField: 5 AnotherField: 1 ans = "!"
It only errors when we use it farther down the line, such as in concatenation:
try [greeting.PlainValue] catch e disp('Errored!'); disp(e.getReport('basic')); end
Errored! Error using string Conversion to string from struct is not possible.
This is clearly not optimal. Property validation to the rescue! We can make the simplest of tweaks to the FancyString so that we can always know for sure that the PlainValue property will always be a string. It's as easy as this slightly more fancy string implementation:
classdef MoreFancyString properties % Property validation for the win! Let's guarantee the PlainValue % is always a string PlainValue (1,1) string Decorator = DefaultDecorator; end properties(Dependent) DecoratedValue end methods function fancyString = MoreFancyString(varargin) if nargin < 1 return end plainValue = string(varargin{:}); fancyString(numel(plainValue)) = MoreFancyString; plainValueCell = num2cell(plainValue); [fancyString.PlainValue] = plainValueCell{:}; end function decorated = get.DecoratedValue(fancyString) % The decorated value returns whatever the decorator adds to % the plain string. decorated = fancyString.Decorator.decorate(fancyString.PlainValue); end function s = string(fancyString) s = reshape([fancyString.PlainValue], size(fancyString)); end end end
The only difference here is in the property block where plain value was defined:
PlainValue (1,1) string
This states that the PlainValue must always be a scalar string, which gives us much more peace of mind as we process that code. How does it work now?
greeting = MoreFancyString(holidayMessage); greeting(1).Decorator = BoldDecorator; greeting(2).Decorator = HyperlinkDecorator(holidayWikipediaSite); greeting(2).PlainValue = Holidays.Kwanzaa
greeting = 1×3 MoreFancyString array with properties: PlainValue Decorator DecoratedValue
This time if you look closely you will see the enum was converted to a string
greeting(2).PlainValue
ans = "Kwanzaa"
...and because of that everything else like concatenation and the string decoration works as expected:
join([greeting.PlainValue]) join([greeting.DecoratedValue])
ans = "Happy Kwanzaa !" ans = "Happy Kwanzaa !"
It also correctly errors right away with bunk values that don't know how to be represented as strings:
try greeting(2).PlainValue = meaninglessStruct; catch e disp('Errored!'); disp(e.getReport('basic')); end
Errored! Error using y2017PropertyTypes (line 138) Error setting property 'PlainValue' of class 'MoreFancyString': Invalid data type. Value must be string or be convertible to string.
Awesome! A couple concluding thoughts:
- We could have also given this guarantee by implementing a set.PlainValue method that validates the input using the validateattributes or otherwise. This would be certainly possible, and a benefit of encapsulated properties. However, if this validation is all that is needed property validators are much more convenient and accessible.
- Classes that are convertible to the datatype specified (in this case string) are automatically converted. In this case the enumeration really knows how to be interpreted correctly as a string, and so it just intuitively worked out of the box!
- This conversion has a flipside. What this really provides is a guarantee that the actual stored value will be a string. However,you can set non-string values to this property as long as they are convertible to a string, and the conversion will be performed upon setting the property. This is typically the right behavior, since the value with a type conversion is the class that should be able to decide that it can be interpeted as a string. However, if you find that you wish to disallow any datatype from being assigned to the property unless it is the precise type to start with, then you may need to leverage the setter method instead to enforce that restriction directly.
Have you used the new property validation features? I hope so. Actually there is much more richness to property validation than I've shown here, check out the documentation. I've hinted at size/shape validation but there is also the ability to pass validation functons to the value being set, as well as a rich library of validation functions in MATLAB for your benefit. Let us know what you think!....oh, and
holidays = enumeration('Holidays'); for idx = 1:numel(holidays) greeting(2).PlainValue = holidays(idx); if strcmp(holidays(idx), 'Christmas') % There's always a special case specialGreeting = greeting; specialGreeting(1).PlainValue = "Merry"; disp(join([specialGreeting.DecoratedValue])); continue; end disp(join([greeting.DecoratedValue])); end
Merry Christmas ! Happy Hanukkah ! Happy Kwanzaa ! Happy Ramadan ! Happy WinterSolstice ! Happy NewYears !
- 类别:
- OOAD,
- Software Design
评论
要发表评论,请点击 此处 登录到您的 MathWorks 帐户或创建一个新帐户。