Subclasses in MATLAB
I'm pleased to have Dave Foti back for a discussion of subclassing and class hierarchies in MATLAB. Dave manages the group responsible for object-oriented programming features in MATLAB.
In computer science, a class represents a set of objects that share a common definition, usually including data and functions or behaviors. A subclass represents a subset of objects that share a more specific definition usually by adding or specializing data and/or functions defined in a superclass. In practice subclassing has been used to reuse functionality for a new and more specialized purpose. In recent years, this usage has become largely discouraged due to the tendency for introducing errors in programs that reuse superclasses (often called base classes) for applications not anticipated by the original class. A class author hides the inner workings of a class to make the class useful as an abstract representation. That same hiding of details can also hinder the ability of a programmer to anticipate how the class will function in a new role. Let's explore some cases where subclassing does make sense in MATLAB and how to avoid some common programming pitfalls.
Contents
Modeling Physical Systems
One of the most obvious uses for subclassing in MATLAB is a situation requiring a model of a real physical system that has already been classified. In such a situation, it might make sense to build a software model that closely resembles the real physical classification system. For example, one could create objects based on the Standard Model of particle physics [1]. In this way, the classification system already exists and can simply be represented in software. There is still design work to be done in how best to represent a physical system in software for a specific purpose, but all the classes can be built at roughly the same time by the same person or team and thus avoid the pitfalls of reusing an existing class later on after its details have been forgotten. Here is what a superclass might look like for elementary particles in the Standard Model:
type ElemParticle
classdef ElemParticle % Intrinsic properties of the particle properties(SetAccess = immutable) Spin Charge Mass end % State variables for a particular particle properties Position = [0 0 0]; Velocity = [0 0 0]; end methods function p = ElemParticle(spin, charge, mass) p.Spin = spin; p.Charge = charge; p.Mass = mass; end end end
We can then create subclasses for fermions and bosons:
type Fermion type Boson
classdef Fermion < ElemParticle methods function p = Fermion(charge, mass) p@ElemParticle(.5, charge, mass); end end end classdef Boson < ElemParticle methods function p = Boson(mass) p@ElemParticle(0, 0, mass); end end end
We can divide the Fermions into Leptons and Quarks:
type Lepton type Quark
classdef Lepton < Fermion methods function p = Lepton(charge, mass) p@Fermion(charge, mass); end end end classdef Quark < Fermion properties Color end methods function p = Quark(charge, mass, color) p@Fermion(charge, mass); p.Color = color; end end end
In the above example, the superclasses and subclasses were designed as a single collection of classes so there is no need for superclasses to anticipate the needs of unknown future subclasses. Any future evolution of the superclasses needs to consider the whole system, but it is a bounded list of classes.
Systems of Abstract Data Structures
Even if one is not modeling a physical system with an existing classification model that makes sense in software, there are still other examples where a system of classes can be designed at roughly the same time to fulfill some purpose such as managing a variety of data formats or anaysis techniques. For example, we might have a need to visualize some data as points. We might know that we have two kinds of data sets: one that is two-dimensional and one that is three-dimensional. We design a class that handles 2-D data and a sub-class that adds support for a third dimension. Here is what the 2-D class looks like:
type PointList
classdef PointList properties X Y end methods(Sealed) function show(pc) figure; data = getData(pc); opts = {'+', 'MarkerSize', 3}; if numel(data) == 3 plot3(data{:},opts{:}); else plot(data{:},opts{:}); end end end methods(Access = protected) function data = getData(pc) data = {pc.X, pc.Y}; end end end
Because we know that we want to support 3-D data, we design our plotting routine to be able to handle 2-D or 3-D data. This is an advantage of knowing about all the subclasses in the beginning. Then when we write the 3-D subclass:
type PointList3D
classdef PointList3D < PointList properties Z end methods(Access = protected) function data = getData(pc) data = {pc.X, pc.Y, pc.Z}; end end end
it is very easy to support 3-D plots without rewriting the whole show method. Here we produce a 3-D point cloud for the 3rd harmonic of the L-shaped membrane:
L = membrane(3); [X, Y] = meshgrid(1:length(L), 1:length(L)); pts = PointList3D; pts.Z = L(:); pts.X = X(:); pts.Y = Y(:); show(pts)
Mixins
Another good reason to use subclassing is when a superclass is designed specifically for reuse. A common design pattern [2] is called the mixin pattern [3]. In this pattern a class is often used to implement one specialized piece of functionality. This class represents all objects that have this common functionality even though such classes may do other things very differently. A mixin class should expect nothing of a subclass except that it uses the mixin functionality. A common example of a mixin class in MATLAB is the class called matlab.mixin.CustomDisplay which is used to customize the MATLAB default object display while retaining appropriate consistency with other objects. Here is an example of this mixin being used on our ElemParticle superclass. To separate this version from the previous example, I've put the new version of the classes in a package called particle.
type +particle/ElemParticle
classdef ElemParticle < matlab.mixin.CustomDisplay % Intrinsic properties of the particle properties(SetAccess = immutable) Spin Charge Mass end % State variables for a particular particle properties Position = [0 0 0]; Velocity = [0 0 0]; end methods function p = ElemParticle(spin, charge, mass) p.Spin = spin; p.Charge = charge; p.Mass = mass; end end methods(Access = protected, Sealed) function propgroups = getPropertyGroups(p) propgroups = matlab.mixin.util.PropertyGroup; propgroups(1).Title = 'Intrinsic'; propgroups(1).PropertyList = getIntrinsicPropertyNames(p); propgroups(2).Title = 'State'; propgroups(2).PropertyList = {'Position','Velocity'}; end end methods(Access = protected) function pnames = getIntrinsicPropertyNames(~) pnames = {'Spin','Charge','Mass'}; end end end
Note that whenever functionality is designed for a superclass such as this, it is important to implement the functionality for the whole class of objects including all instances of subclasses. Thus, the display should not just show what is common to ElemParticle, but should provide for specialization in subclasses. In this case, we add one method that can be specialized by subclasses to provide lists of intrinsic properties. Since we know about all the subclasses, we don't need to worry about what kinds of property groups a future subclass might need. We can see why it is important for superclasses to be designed with the knowledge that subclasses will be created and why it is very helpful if the subclasses are already known during this design. In this case, we know the family of subclasses of ElemParticle and thus we know that they will sometimes need to add new properties, but not whole new groups of properties. We choose the right amount of generality in the superclass so that each subclass can specialize display with minimal effort. In fact Quark is the only particle that needs additional code as shown here:
type +particle/Quark
classdef Quark < particle.Fermion properties Color end methods function p = Quark(charge, mass, color) p@particle.Fermion(charge, mass); p.Color = color; end end methods(Access = protected) function pnames = getIntrinsicPropertyNames(p) pnames = [getIntrinsicPropertyNames@particle.Fermion(p), 'Color']; end end end
We can now create and display an electron and a top quark:
e = particle.Lepton(-1, 5.11e-4)
e = Lepton with properties: Intrinsic Spin: 0.5 Charge: -1 Mass: 0.000511 State Position: [0 0 0] Velocity: [0 0 0]
t = particle.Quark(2/3, 173.07, 'red')
t = Quark with properties: Intrinsic Spin: 0.5 Charge: 0.66667 Mass: 173.07 Color: 'red' State Position: [0 0 0] Velocity: [0 0 0]
Other Patterns
Other common patterns where classes are designed for unbounded subclassing include interfaces [4] and frameworks [5]. Interface classes define how two pieces of code can interact with each other without actually defining how those interactions are accomplished. Because MATLAB is a dynamically typed language, it is possible to write functions to an interface that isn't actually defined as a class. For example many functions in MATLAB can work with any object that implements the size, subsref, and subsasgn functions to perform reshaping operations on arrays. In some cases, it may be useful to define interfaces explicitly so that it is easier to determine what kinds of objects will work with a particular function and so that it is easier to see the capabilities of a class in the explicit interfaces it supports.
Design Considerations
When designing a class for extension in the future, it is a good idea to think about how subclasses might restrict evolution of the base class. Some questions to consider are:
- Is it clear that there will be no need to add any properties or methods to the base class in the future?
- Is it clear that only internal implementation details will need to change over time and not the fundamental meaning and behavior of the class?
When designing methods in the base class, it is important to consider how subclasses will reuse and as necessary adapt these methods while maintaining the principle that subclass instances can be substituted for base class instances [6]. Some questions to ask are:
- What methods might need to be specialized?
- How can base class methods be best organized to allow subclasses to reuse as much base class implementation as possible?
- Are there ways to better ensure that subclasses still behave consistently with the intended behavior of the class?
Once a class has been presented as a base class, its fundamental definition should not change or else subclasses could become invalid or produce erroneous results. The new or changed behaviors and capabilities may not be appropriate for all subclasses. The subclass authors could not possibly anticipate changes to the superclass when the subclasses were created. This is one of the reasons for adhering to the open/closed design principle [7].
Preventing Unexpected Subclassing
Given the importance of considering how a class will be extended by subclasses, MATLAB provides ways to signal that a class wasn't designed for subclassing. Classes with the Sealed attribute set can't be used as superclasses. If an entire class hierarchy was designed together but not intended for extension with additional subclasses, then it can be sealed using the Sealed attribute on the leaf classes and the AllowedSubclasses attribute on the superclasses. MathWorks will at times seal classes or class hierarchies of widely used classes to indicate that these classes have not been designed to support subclasses.
These are some thoughts on subclassing in MATLAB, but I'd like to hear your thoughts as well. Have you used any of the mixin MATLAB classes like matlab.mixin.CustomDisplay or matlab.mixin.Heterogeneous? Do you have suggestions for mixin classes that you would like to see in MATLAB?
References
- http://en.wikipedia.org/wiki/Standard_Model
- http://en.wikipedia.org/wiki/Design_pattern_(computer_science)
- http://en.wikipedia.org/wiki/Mixin
- http://en.wikipedia.org/wiki/Interface_(computer_science)
- http://en.wikipedia.org/wiki/Framework_(computer_science)
- http://en.wikipedia.org/wiki/Liskov_substitution_principle
- http://en.wikipedia.org/wiki/Open/closed_principle
- 类别:
- Object-oriented
评论
要发表评论,请点击 此处 登录到您的 MathWorks 帐户或创建一个新帐户。