Han Solo Revisited
A long time ago in a blog post far, far away… Andy wrote about Han Solo Encapsulation – to keep Jabba’s “system working as designed he needed to encapsulate his system behind a rock solid interface”.
By a stroke of good fortune, or judicious design choices depending on your perspective, a recent refactoring job that I thought could have far reaching consequences turned into a few changes in a single class, ultimately saving much time. Let’s have a look.
My scenario is that I have a DataProvider that accepts a DataRequest and returns some data:
classdef DataProvider methods function data = getData(dataProvider,dataRequest) arguments dataProvider (1,1) DataProvider dataRequest (1,1) DataRequest end % Implementation continues... end end end
The DataRequest looks like this:
classdef DataRequest properties Make (1,1) string = missing Model (1,1) string = missing ManufacturingYear (1,1) datetime = missing end end
It defines relevant properties that the DataProvider needs to serve up the data. All the values are scalar and have default values of missing to represent the fact that they are “unset” by the user.
To form its query, the DataProvider must extract the values of each property. To do this, it needs to know that “unset” is represented by “missing” and therefore it should be ignored from the search condition.
Here’s one possible implementation in DataProvider:
classdef DataProvider methods function data = getData(dataProvider,dataRequest) arguments dataProvider (1,1) DataProvider dataRequest (1,1) DataRequest end searchTerms = {}; propsToGet = ["Make" "ManufacturingYear" "Model"]; for prop = propsToGet if ~ismissing(dataRequest.(prop)) searchTerms = [searchTerms {prop} {dataRequest.(prop)}]; end end result = dataProvider.search(searchTerms{:}); % Implementation continues... end end end
The problem here is that DataProvider, and any other code that makes use of DataRequest, is coupled to how DataRequest represents its “unset” values. That’s something that should be encapsulated within the DataRequest. What my DataProvider really wants is an array of name-value pairs of non-missing property names and values.
(We could also describe this as an instance of the tell don’t ask principle since we’re not actually hiding DataRequest’s properties from the outside world.)
Let’s refactor the DataRequest to add a method that does just that. I’ve called it namedargs2cell due to its similarity to the built-in MATLAB function.
classdef DataRequest properties Make (1,1) string = missing Model (1,1) string = missing ManufacturingYear (1,1) datetime = missing end methods function paramCell = namedargs2cell(dataRequest) arguments dataRequest (1,1) DataRequest end paramCell = {}; propsToGet = ["Make" "ManufacturingYear" "Model"]; for prop = propsToGet if ~ismissing(dataRequest.(prop)) paramCell = [paramCell {prop} {dataRequest.(prop)}]; end end end end end
This makes our DataProvider much simpler:
classdef DataProvider methods function data = getData(dataProvider,request) arguments dataProvider (1,1) DataProvider request (1,1) DataRequest end searchTerms = namedargs2cell(request); result = dataProvider.search(searchTerms{:}); % Implementation continues... end end end
Returning to our encapsulation principle, what do we achieve? In my real-life case, I wanted to change the DataRequest to allow multiple values to be specified for a given property rather than just scalars. It therefore makes sense to represent “unset” with an empty rather that with a scalar missing. Since all knowledge of how “unset” is represented is contained within the DataRequest, the only changes I needed to make were within DataRequest itself. No other code had to be touched:
classdef DataRequest properties Make (1,:) string Model (1,:) string ManufacturingYear (1,:) datetime end methods function paramCell = namedargs2cell(dataRequest) arguments dataRequest (1,1) DataRequest end paramCell = {}; propsToGet = ["Make" "ManufacturingYear" "Model"]; for prop = propsToGet if ~all(isempty(dataRequest.(prop))) paramCell = [paramCell {prop} {dataRequest.(prop)}]; end end end end end
In the above, note how ismissing has become all(isempty(…)).
In conclusion, by providing the right interface to your classes, code changes can become much more limited in scope and easier to implement.
- Category:
- OOAD
Comments
To leave a comment, please click here to sign in to your MathWorks Account or create a new one.