Skip to main content
Engineering LibreTexts

10.2: Transform Self Type Checks

  • Page ID
    32422
  • \( \newcommand{\vecs}[1]{\overset { \scriptstyle \rightharpoonup} {\mathbf{#1}} } \) \( \newcommand{\vecd}[1]{\overset{-\!-\!\rightharpoonup}{\vphantom{a}\smash {#1}}} \)\(\newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\) \( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\) \( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\) \( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\) \( \newcommand{\Span}{\mathrm{span}}\) \(\newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\) \( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\) \( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\) \( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\) \( \newcommand{\Span}{\mathrm{span}}\)\(\newcommand{\AA}{\unicode[.8,0]{x212B}}\)

    Intent Improve the extensibility of a class by replacing a complex conditional statement with a call to a hook method implemented by subclasses.

    Problem

    A class is hard to modify or extend because it bundles multiple possible behaviors in complex conditional statements that test some attribute representing the current “type” of the object.

    This problem is difficult because:

    • Conceptually simple extensions require many changes to the conditional code.

    • Subclassing is next to impossible without duplicating and adapting the methods containing the conditional code.

    • Adding a new behavior always results in changes to the same set of methods and always results in adding a new case to the conditional code.

    Yet, solving this problem is feasible because:

    • Self type checks simulate polymorphism. The conditional code tells you what subclasses you should have instead.

    Solution

    Identify the methods with complex conditional branches. In each case, replace the conditional code with a call to a new hook method. Identify or introduce subclasses corresponding to the cases of the conditional. In each of these subclasses, implement the hook method with the code corresponding to that case in the original case statement.

    Detection

    Most of the time, the type discrimination will jump in your face while you are working on the code, so this means that you will not really need to detect where the checks are made. However, it can be interesting to have simple techniques to quickly assess if unknown parts of a system suffer from similar practices. This can be a valuable source of information to evaluate the state of a system.

    • Look for long methods with complex decision structures on some immutable attribute of the object that models type information. In particular look for attributes that are set in the constructor and never changed.

    • Attributes that are used to model type information typically take on values from some enumerated type, or from some finite set of constant values. Look for constant definitions whose names represent entities or concepts that one would usually expect to be associated to classes (like RetiredEmployee or PendingOrder). The conditionals will normally just compare the value of a fixed attribute to one of these constant values.

    • Especially look for classes where multiple methods switch on the same attribute. This is another common sign that the attribute is being used to simulate a type.

    • Since methods containing case statements tend to be long, it may help to use a tool that sorts methods by lines of code or visualizes classes and methods according to their size. Alternatively, search for classes or methods with a large number of conditional statements.

    • For languages like C++ or Java where it is common to store the implementation of a class in a separate file, it is straightforward to search for and count the incidence of conditional keywords (if, else, case, etc.). On a UNIX system, for example,

      grep 'switch' `find . --name "*.cxx" --print`

      enumerates all the files in a directory tree with extension .cxx that contain a switch. Other text processing tools like agrep offer possibilities to pose finer granularity queries. Text processing languages like Perl may be better suited for evaluating some kinds of queries, especially those that span multiple lines.

    • C/C++: Legacy C code may simulate classes by means of union types. Typically the union type will have one data member that encodes the actual type. Look for conditional statements that switch on such data members to decide which type to cast a union to and which behavior to employ.

      In C++ it is fairly common to find classes with data members that are declared as void pointers. Look for conditional statements that cast such pointers to a given type based on the value of some other data member. The type information may be encoded as an enum or (more commonly) as a constant integer value.

    • Ada: Because Ada 83 did not support polymorphism (or subprogram access types), discriminated record types are often used to simulate polymorphism. Typically an enumeration type provides the set of variants and the conversion to polymorphism is straightforward in Ada95.

    • Smalltalk: Smalltalk provides only a few ways to manipulate types. Look for applications of the methods isMemberOf: and isKindOf:, which signal explicit type-checking. Type checks might also be made with tests like self class = anotherClass, or with property tests throughout the hierarchy using methods like isSymbol, isString, isSequenceable, isInteger.

    Transformation of explicit type check into self polymorphic method calls.
    Figure \(\PageIndex{1}\): Transformation of explicit type check into self polymorphic method calls.

    Steps

    1. Identify the class to transform and the different conceptual classes that it implements. An enumeration type or set of constants will probably document this well.

    2. Introduce a new subclass for each behavior that is implemented (see Figure \(\PageIndex{1}\)). Modify clients to instantiate the new subclasses rather than the original class. Run the tests.

    3. Identify all methods of the original class that implement varying behavior by means of conditional statements. If the conditionals are surrounded by other statements, move them to separate, protected hook methods. When each conditional occupies a method of its own, run the tests.

    4. Iteratively move the cases of the conditionals down to the corresponding subclasses, periodically running the tests.

    5. The methods that contain conditional code should now all be empty. Replace these by abstract methods and run the tests.

    6. Alternatively, if there are suitable default behaviors, implement these at the root of the new hierarchy.

    7. If the logic required to decide which subclass to instantiate is non-trivial, consider encapsulating this logic as a factory method of the new hierarchy root. Update clients to use the new factory method and run the tests.

    Tradeoffs

    Pros

    • New behaviors can now be added in a incremental manner, without having to change a set of methods of a single class containing all the behavior. A specific behavior can now be understood independently from the other variations.

    • A new behavior represents its data independently from the others, thereby minimizing the possible interference and increasing the understandability of the separated behaviors.

    • All behaviors now share a common interface, thereby improving their readability.

    Cons

    • All the behaviors are now dispersed into multiple but related abstractions, so getting an overview of the behavior may be more difficult. However, the concepts are related and share the interface represented by the abstract class reducing then the problem.

    • The larger number of classes makes the design more complex, and potentially harder to understand. If the original conditional statements are simple, it may not be worthwhile to perform this transformation.

    • Explicit type checks are not always a problem and we can sometimes tolerate them. Creating new classes increases the number of abstractions in the applications and can clutter namespaces. Hence, explicit type checks may be an alternative to the creation of new classes when:

      • the set over which the method selection is fixed and will not evolve in the future, and

      • the type check is only made in a few places.

    Combining simple delegation and Transform Self Type Checks when the class cannot be subclassed.
    Figure \(\PageIndex{2}\): Combining simple delegation and Transform Self Type Checks when the class cannot be subclassed.

    Difficulties

    • Since the requisite subclasses do not yet exist, it can be hard to tell when conditionals are being used to simulate multiple types.

    • Wherever instances of the transformed class were originally created, now instances of different subclasses must be created. If the instantiation occurred in client code, that code must now be adapted to instantiate the right class. Factory objects or methods may be needed to hide this complexity from clients.

    • If you do not have access to the source code of the clients, it may be difficult or impossible to apply this pattern since you will not be able to change the calls to the constructors.

    • If the case statements test more than one attribute, it may be necessary to support a more complex hierarchy, possibly requiring multiple inheritance. Consider splitting the class into parts, each with its own hierarchy.

    • When the class containing the original conditionals cannot be sub-classed, Transform Self Type Checks can be composed with delegation. The idea is to exploit polymorphism on another hierarchy by moving part of the state and behavior of the original class into a separate class to which the method will delegate, as shown in Figure \(\PageIndex{2}\).

    When the legacy solution is the solution

    There are some situations in which explicit type-checks may nevertheless be the right solution:

    • The conditional code may be generated from a special tool. Lexical analysers and parsers, for example, may be automatically generated to contain the kind of conditional code we are trying to avoid. In these cases, however, the generated classes should never be manually extended, but simply regenerated from the modified specifications.

    Example

    We worked on a complex system that controls large, physical machines by sending them messages. These messages are represented by the class Message and can be of different types.

    Initial design and source code.
    Figure \(\PageIndex{3}\): Initial design and source code.

    Before

    A message class wraps two different kinds of messages (TEXT and ACTION) that must be serialized to be sent across a network connection as shown in the code and the figure. We would like to be able to send a new kind of message (say VOICE), but this will require changes to several methods of Message as shown in Figure \(\PageIndex{3}\).

    Resulting hierarchy and source code.
    Figure \(\PageIndex{4}\): Resulting hierarchy and source code.

    After

    Since Message conceptually implements two different classes, Text_Message and Action_Message, we introduce these as subclasses of Message, as shown in Figure \(\PageIndex{4}\). We introduce constructors for the new classes, we modify the clients to construct instances of Text_Message and Action_Message rather than Message, and we remove the set_value() methods. Our regression tests should run at this point.

    Now we find methods that switch on the type variable. In each case, we move the entire case statement to a separate, protected hook method, unless the switch already occupies the entire method. In the case of send(), this is already the case, so we do not have to introduce a hook method. Again, all our tests should still run.

    Now we iteratively move cases of the case statements from Message to its subclasses. The TEXT case of Message::send() moves to Text_Message::send() and the ACTION case moves to Action_Message::send(). Every time we move such a case, our tests should still run.

    Finally, since the original send() method is now empty, it can be redeclared to be abstract (i.e., virtual void send(Channel) = 0). Again, our tests should run.

    Rationale

    Classes that masquerade as multiple data types make a design harder to understand and extend. The use of explicit type checks leads to long methods that mix several different behaviors. Introducing new behavior then requires changes to be made to all such methods instead of simply specifying one new class representing the new behavior.

    By transforming such classes to hierarchies that explicitly represent the multiple data types, you improve cohesion by bringing together all the code concerning a single data type, you eliminate a certain amount of duplicated code (i.e., the conditional tests), and you make your design more transparent, and consequently easier to maintain.

    Related Patterns

    In Transform Self Type Checks the condition to be transformed tests type information that is represented as an attribute of the class itself.

    If the conditional tests mutable state of the host object, consider instead applying Factor out State, or possibly Factor out Strategy.

    If the conditional occurs in a client rather than in the provider class itself, consider applying Transform Client Type Checks.

    If the conditional code tests some type attribute of a second object in order to select some third handler object, consider instead applying Transform Conditionals into Registration.


    This page titled 10.2: Transform Self Type Checks is shared under a CC BY-SA license and was authored, remixed, and/or curated by Serge Demeyer, Stéphane Ducasse, Oscar Nierstrasz.

    • Was this article helpful?