MATLAB Language Inheriting from classes and abstract classes


Example

Disclaimer: the examples presented here are only for the purpose of showing the use of abstract classes and inheritance and may not necessarily be of a practical use. Also, there is no sich thing as polymorphic in MATLAB and therefore the use of abstract classes is limited. This example is to show who to create a class, inherit from another class and apply an abstract class to define a common interface.

The use of abstract classes is rather limited in MATLAB but it still can come useful on a couple of occasions.

Let's say we want a message logger. We might create a class similar to the one below:

classdef ScreenLogger
    properties(Access=protected)
        scrh;
    end
    
    methods
        function obj = ScreenLogger(screenhandler)
            obj.scrh = screenhandler;
        end
        
        function LogMessage(obj, varargin)
            if ~isempty(varargin)
                varargin{1} = num2str(varargin{1});
                fprintf(obj.scrh, '%s\n', sprintf(varargin{:}));
            end
        end
    end
end

Properties and methods

In short, properties hold a state of an object whilst methods are like interface and define actions on objects.

The property scrh is protected. This is why it must be initialized in a constructor. There are other methods (getters) to access this property but it is out of cope of this example. Properties and methods can be access via a variable that holds a reference to an object by using dot notation followed by a name of a method or a property:

mylogger = ScreenLogger(1);                         % OK
mylogger.LogMessage('My %s %d message', 'very', 1); % OK
mylogger.scrh = 2;                                  % ERROR!!! Access denied

Properties and methods can be public, private, or protected. In this case, protected means that I will be able to access to scrh from an inherited class but not from outside. By default all properties and methods are public. Therefore LogMessage() can freely be used outside the class definition. Also LogMessage defines an interface meaning this is what we must call when we want an object to log our custom messages.

Application

Let's say I have a script where I utilize my logger:

clc;
% ... a code
logger = ScreenLogger(1);
% ... a code
logger.LogMessage('something');
% ... a code
logger.LogMessage('something');
% ... a code
logger.LogMessage('something');
% ... a code
logger.LogMessage('something');

If I have multiple places where I use the same logger and then want to change it to something more sophisticated, such as write a message in a file, I would have to create another object:

classdef DeepLogger
    properties(SetAccess=protected)
        FileName
    end
    methods
        function obj = DeepLogger(filename)
            obj.FileName = filename;
        end
        
        function LogMessage(obj, varargin)
            if ~isempty(varargin)
                varargin{1} = num2str(varargin{1});
                fid = fopen(obj.fullfname, 'a+t');
                fprintf(fid, '%s\n', sprintf(varargin{:}));
                fclose(fid);
            end
        end
    end 
end

and just change one line of a code into this:

clc;
% ... a code
logger = DeepLogger('mymessages.log');

The above method will simply open a file, append a message at the end of the file and close it. At the moment, to be consistent with my interface, I need to remember that the name of a method is LogMessage() but it could equally be anything else. MATLAB can force developper to stick to the same name by using abstract classes. Let's say we define a common interface for any logger:

classdef MessageLogger
    methods(Abstract=true)
        LogMessage(obj, varargin);
    end
end

Now, if both ScreenLogger and DeepLogger inherit from this class, MATLAB will generate an error if LogMessage() is not defined. Abstract classes help to build similar classes which can use the same interface.

For the sake of this exmaple, I will make slightly different change. I am going to assume that DeepLogger will do both logging message on a screen and in a file at the same time. Because ScreenLogger already log messages on screen, I am going to inherit DeepLogger from the ScreenLoggger to avoid repetition. ScreenLogger doesn't change at all apart from the first line:

classdef ScreenLogger < MessageLogger
// the rest of previous code 

However, DeepLogger needs more changes in the LogMessage method:

classdef DeepLogger < MessageLogger & ScreenLogger
    properties(SetAccess=protected)
        FileName
        Path
    end
    methods
        function obj = DeepLogger(screenhandler, filename)
            [path,filen,ext] = fileparts(filename);
            obj.FileName = [filen ext];
            pbj.Path     = pathn;
            obj = obj@ScreenLogger(screenhandler);
        end
        function LogMessage(obj, varargin)
            if ~isempty(varargin)
                varargin{1} = num2str(varargin{1});
                LogMessage@ScreenLogger(obj, varargin{:});
                fid = fopen(obj.fullfname, 'a+t');
                fprintf(fid, '%s\n', sprintf(varargin{:}));
                fclose(fid);
            end
        end
    end
end

Firstly, I simply initialize properties in the constructor. Secondly, because this class inherits from ScreenLogger I have to initialize this parrent object as well. This line is even more important because ScreenLogger constructor requires one parameter to initalize its own object. This line:

obj = obj@ScreenLogger(screenhandler);

simply says "call the consructor of ScreenLogger and initalize it with a screen handler". It is worth noting here that I have defined scrh as protected. Therefore, I could equally access this property from DeepLogger. If the property was defined as private. The only way to intialize it would be using consuctor.

Another change is in section methods. Again to avoid repetition, I call LogMessage() from a parent class to log a message on a screen. If I had to change anything to make improvements in screen logging, now I have to do it in one place. The rest code is the same as it is a part of DeepLogger.

Because this class also inherits from an abstract class MessageLogger I had to make sure that LogMessage() inside DeepLogger is also defined. Inheritting from MessageLogger is a little bit tricky here. I think it cases redefinition of LogMessage mandatory--my guess.

In terms of the code where the a logger is applied, thanks to a common interface in classes, I can rest assure ther changin this one line in the whole code would not make any issues. The same messages will be log on screen as before but additionally the code will write such messages to a file.

clc;
% ... a code
logger = DeepLogger(1, 'mylogfile.log');
% ... a code
logger.LogMessage('something');
% ... a code
logger.LogMessage('something');
% ... a code
logger.LogMessage('something');
% ... a code
logger.LogMessage('something');

I hope these examples explained the use of classes, the use of inheritance, and the use of abstract classes.


PS. The solution for the above problem is one of many. Another solution, less complex, would be to make ScreenLoger to be a component of another logger like FileLogger etc. ScreenLogger would be held in one of the properties. Its LogMessage would simply call LogMessage of the ScreenLogger and show text on a screen. I have chosen more complex approach to rather show how classes work in MATLAB. The example code below:

classdef DeepLogger < MessageLogger
    properties(SetAccess=protected)
        FileName
        Path
        ScrLogger
    end
    methods
        function obj = DeepLogger(screenhandler, filename)
            [path,filen,ext] = fileparts(filename);
            obj.FileName     = [filen ext];
            obj.Path         = pathn;
            obj.ScrLogger    = ScreenLogger(screenhandler);
        end
        function LogMessage(obj, varargin)
            if ~isempty(varargin)
                varargin{1} = num2str(varargin{1});
                obj.LogMessage(obj.ScrLogger, varargin{:}); % <-------- thechange here
                fid = fopen(obj.fullfname, 'a+t');
                fprintf(fid, '%s\n', sprintf(varargin{:}));
                fclose(fid);
            end
        end
    end
end