Developer Zone

Advanced Software Development with MATLAB

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.




Published with MATLAB® R2022b

|
  • print

コメント

コメントを残すには、ここ をクリックして MathWorks アカウントにサインインするか新しい MathWorks アカウントを作成します。