CHAPTER 3

image

Object Orientation in TypeScript

There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies and the other way is to make it so complicated that there are no obvious deficiencies. The first method is far more difficult. It demands the same skill, devotion, insight, and even inspiration as the discovery of the simple physical laws which underlie the complex phenomena of nature.

—Tony Hoare

Object-oriented programming allows concepts from the real world to be represented by code that contains both the data and related behavior. The concepts are normally modelled as classes, with properties for the data and methods for the behavior, and the specific instances of these classes are called objects.

There have been many discussions about object orientation over the years and I’m sure that the debate remains lively enough to continue for many years to come. Because programming is a heuristic process, you will rarely find one absolute answer. This is why you will hear the phrase “it depends” so often in software development. No programming paradigm fits every situation so anyone telling you that functional programming, object-oriented programming, or some other programming style is the answer to all problems hasn’t been exposed to a large enough variety of complex problems. Because of this, programming languages are becoming increasingly multiparadigm.

Object-oriented programming is a formalization of many good practices that emerged early on in computer programming. It supplies the concepts to make these good practices easier to apply. By modelling real-world objects from the problem domain using objects in the code, the program can speak the same language as the domain it serves. Objects also allow encapsulation and information hiding, which prevent different parts of a program from modifying data that another part of the program relies on.

The simplest explanation in favor of programming concepts such as object orientation comes not from the world of software, but from psychology. G. A. Miller published his famous paper, “The Magical Number Seven, Plus or Minus Two” (Psychological Review, 1956) describing the limitations on the number of pieces of information we can hold in short-term memory at any one time. Our information processing ability is limited by this number of between five and nine items of information that we can hold on to concurrently. This is the key reason for any technique of code organization and in object orientation it should drive you toward layers of abstraction that allow you to skim high-level ideas first and dive further into the levels of detail when you need to. If you organize it well, a programmer maintaining the code will need to hold onto less concurrent information when attempting to understand your program.

Robert C. Martin (Uncle Bob) presented this idea in a slightly different way during a group-refactoring session when he said well-written, “polite” code was like reading a newspaper. You could scan the higher level code in the program as if they were headlines. A programmer maintaining the code would scan through the headlines to find relevant areas in the code and then drill down to find the implementation details. The value in this idea comes from small readable functions that contain code at a similar level of abstraction. The newspaper metaphor supplies a clear vision of what clean code looks like, but the principle of reducing the amount of cognitive overhead is still present.

Object Orientation in TypeScript

TypeScript supplies all of the key tools that you need to use object orientation in your program.

  • Classes
  • Instances of classes
  • Methods
  • Inheritance
  • Open recursion
  • Encapsulation
  • Delegation
  • Polymorphism

Classes, instances of classes, methods, and inheritance were discussed in detail in Chapter 1. These are the building blocks of an object-oriented program and are made possible in a simple way by the language itself. All you need for each of these concepts is one or two language keywords.

The other terms in this list are worthy of further explanation, particularly in respect of how they work within the TypeScript type system. The following sections expand on the concepts of open recursion, encapsulation, delegation, and polymorphism along with code examples that demonstrate each concept.

Image Note  Although this chapter discusses object orientation in detail, don’t forget that JavaScript, and therefore TypeScript, is a multiparadigm language.

Open Recursion

Open recursion is a combination of recursion and late binding. When a method calls itself within a class, that call can be forwarded to a replacement defined in a subclass. Listing 3-1 is an example of a class that reads the contents of a directory. The FileReader class reads the contents based on the supplied path. Any files are added to the file tree, but where directories are found, there is a recursive call to this.getFiles. These calls would continue until the entire path including all subfolders are added to the file tree. The fs.reaaddirSync and fs.statSync methods belong to NodeJS, which is covered in more detail in Chapter 6.

Listing 3-1. Open recursion

interface FileItem {
    path: string;
    contents: string[];
}

class FileReader {
    getFiles(path: string, depth: number = 0) {
        var fileTree = [];

        var files = fs.readdirSync(path);

        for (var i = 0; i < files.length; i++) {
            var file = files[i];

            var stats = fs.statSync(file);
            var fileItem;

            if (stats.isDirectory()) {
                // Add directory and contents
                fileItem = {
                    path: file,
                    contents: this.getFiles(file, (depth + 1))
                };
            } else {
                // Add file
                fileItem = {
                    path: file,
                    contents: []
                };
            }

            fileTree.push(fileItem);
        }

        return fileTree;
    }
}

class LimitedFileReader extends FileReader {
    constructor(public maxDepth: number) {
        super();
    }

    getFiles(path: string, depth = 0) {
        if (depth > this.maxDepth) {
            return [];
        }

        return super.getFiles(path, depth);
    }
}

// instatiating an instance of LimitedFileReader
var fileReader = new LimitedFileReader(1);

// results in only the top level, and one additional level being read
var files = fileReader.getFiles('path'),

Image Note  I used the Sync versions of the NodeJS file system calls, readdirSync and statSync, because they make the examples much simpler. In a real program you should consider using the standard equivalents, readdir and stat, which accept a callback function.

The LimitedFileReader is a subclass of the FileReader class. When you create an instance of the LimitedFileReader class, you must specify a number that limits the depth of the file tree represented by the class. This example shows how the call to this.getFiles uses open recursion. If you create a FileReader instance, the call to this.getFiles is a simple recursive call. If you create an instance of the LimitedFileReader, the same call to this.getFiles within the FileReader.getFiles method will actually be dispatched to the LimitedFileReader.getFiles method.

This example of open recursion can be summarized as:

  • When you create a new FileReader
    • fileReader.getFiles is a call to FileReader.getFiles
    • this.getFiles within FileReader is a call to FileReader.getFiles
  • When you create a new LimitedFileReader
    • fileReader.getFiles is a call to LimitedFileReader.getFiles
    • super.getFiles is a call to FileReader.getFiles
    • this.getFiles within FileReader is a call to LimitedFileReader.getFiles

The beauty of open recursion is that the original class remains unchanged and needs no knowledge of the specialization offered by the subclass. The subclass gets to re-use the code from the superclass, which avoids duplication.

Encapsulation

Encapsulation is fully supported in TypeScript. A class instance can contain both properties and methods that operate on those properties; this is the encapsulation of data and behavior. The properties can also be hidden using the private access modifier, which hides the data from code outside of the class instance.

A common use of encapsulation is data hiding; preventing access to data from outside of the class except via explicit operations. The example in Listing 3-2 shows a Totalizer class that has a private total property, which cannot be modified by code outside of the Totalizer class. The property can change when external code calls the methods defined on the class. This removes the risk of

  • External code adding a donation without adding the tax rebate
  • External code failing to validate the amount is a positive number
  • The tax rebate calculation appearing in many places in calling code
  • The tax rate appearing in many places in external code

    Listing 3-2. Encapsulation

    class Totalizer {
        private total = 0;
        private taxRateFactor = 0.2;

        addDonation(amount: number) {
            if (amount <= 0) {
                throw new Error('Donation exception'),
            }

            var taxRebate = amount * this.taxRateFactor;
            var totalDonation = amount + taxRebate;

            this.total += totalDonation;
        }

        getAmountRaised() {
            return this.total;
        }
    }

    var totalizer = new Totalizer();

    totalizer.addDonation(100.00);

    var fundsRaised = totalizer.getAmountRaised();

    // 120
    console.log(fundsRaised);

Encapsulation is the tool that can help you to prevent the largest amount of duplicated code in a program, but it doesn’t do it magically. You have to hide your properties using the private keyword to prevent external code changing the value or controlling the program’s flow using the value. One of the most common kinds of duplication is logical branching, for example the if and switch statements, which control the program based on a property that should have been hidden using the private keyword. When you change the property, you then need to hunt down all of these logical branches, which creates a worrying ripple of change throughout your code.

Delegation

One of the most important concepts in terms of re-use in your program is delegation. Delegation describes the situation where one part of your program hands over a task to another part of the system. In true delegation, the wrapper passes a reference to itself into the delegate, which allows the delegate to call back into the original wrapper, for example WrapperClass would call DelegateClass, passing the keyword this as an argument. DelegateClass can then call methods on WrapperClass. This allows the wrapper and delegate to behave as a subclass and superclass.

Where the wrapper doesn’t pass a reference to itself, the operation is known as forwarding rather than delegation. In both delegation and forwarding you may call a method on one class, but that class hands off the processing to another class, as shown in Listing 3-3. Delegation and forwarding are often good alternatives to inheritance if the relationship between two classes fails the “is a” test.

Listing 3-3. Delegation

interface ControlPanel {
    startAlarm(message: string): any;
}

interface Sensor {
    check(): any;
}

class MasterControlPanel {
    private sensors: Sensor[] = [];

    constructor() {
        // Instantiating the delegate HeatSensor
        this.sensors.push(new HeatSensor(this));
    }

    start() {
        for (var i= 0; i < this.sensors.length; i++) {
            // Calling the delegate
            this.sensors[i].check();
        }

        window.setTimeout(() => this.start(), 1000);
    }

    startAlarm(message: string) {
        console.log('Alarm! ' + message);
    }
}

class HeatSensor {
    private upperLimit = 38;
    private sensor = {
        read: function() { return Math.floor(Math.random() * 100); }
    };

    constructor(private controlPanel: ControlPanel) {
    }

    check() {
        if (this.sensor.read() > this.upperLimit) {
            // Calling back to the wrapper
            this.controlPanel.startAlarm('Overheating!'),
        }
    }
}

var cp = new MasterControlPanel();

cp.start();

Image Note  The “is a” test in object orientation involves describing the relationship between objects to validate that the subclass is indeed a specialized version of the superclass. For example, “a Cat is a Mammal” and “a Savings Account is a Bank Account”. It is usually clear when the relationship is not valid, for example, “a Motor Car is a Chassis” doesn’t work, but “a Car has a Chassis” does. A “has a” relationship requires delegation (or forwarding), not inheritance.

Listing 3-3 is a simple example of delegation. The ControlPanel class passes itself into the HeatSensor constructor, which enables the HeatSensor class to call the startAlarm method on the ControlPanel when required. The ControlPanel can coordinate any number of sensors and each sensor can call back into the ControlPanel to set off the alarm if a problem is detected.

It is possible to expand on this to demonstrate various decision points where either inheritance or delegation may be selected. Figure 3-1 describes the relationships between various car components. The chassis is the plain skeleton that a motor car is built on, the bare framework for a car. When the engine, driveshaft, and transmission are attached to the chassis, the combination is called a rolling chassis.

9781430267911_Fig03-01.jpg

Figure 3-1. Encapsulation and inheritance

For each relationship in the diagram, try reading both the is a and the has a alternatives to see if you agree with the relationships shown.

Polymorphism

In programming, polymorphism refers to the ability to specify a contract and have many different types implement that contract. The code using a class that implements a contract should not need to know the details of the specific implementation. In TypeScript, polymorphism can be achieved using a number of different forms

  • An interface implemented by many classes
  • An interface implemented by many objects
  • An interface implemented by many functions
  • A superclass with a number of specialized subclasses
  • Any structure with many similar structures

The final bullet point, “any structure with many similar structures,” refers to TypeScript’s structural type system, which will accept structures compatible with a required type. This means you can achieve polymorphism with two functions with the same signature and return type (or two classes with compatible structures, or two objects with similar structures) even if they do not explicitly implement a named type, as shown in Listing 3-4.

Listing 3-4. Polymorphism

interface Vehicle {
    moveTo(x: number, y: number);
}

class Car implements Vehicle {
    moveTo(x: number, y: number) {
        console.log('Driving to ' + x + ' ' + y);
    }
}

class SportsCar extends Car {

}

class Airplane {
    moveTo(x: number, y: number) {
        console.log('Flying to ' + x + ' ' + y);
    }
}

function navigate(vehicle: Vehicle) {
    vehicle.moveTo(59.9436499, 10.7167959);
}

var airplane = new Airplane();

navigate(airplane);

var car = new SportsCar();

navigate(car);

Listing 3-4 illustrates polymorphism in TypeScript. The navigate function accepts any type compatible with the Vehicle interface. Specifically, this means any class or object that has a method named moveTo that accepts up to two arguments of type number.

Image Note  It is important to remember that a method is structurally compatible with another if it accepts fewer arguments. In many languages, you would be forced to specify the redundant parameter even though it isn’t used in the method body, but in TypeScript you can omit it. The calling code may still pass the argument if the contract specifies it, which preserves polymorphism.

The navigate function in Listing 3-4 sends the specified Vehicle to the Norwegian Computing Centre is Oslo—where polymorphism was created by Ole-Johan Dahl and Kristen Nygaard.

All of the types defined in the example are compatible with the Vehicle definition; Car explicitly implements the interface, SportsCar inherits from Car so also implements the Vehicle interface. Airplane does not explicitly implement the Vehicle interface, but it has a compatible moveTo method and will be accepted by the navigate function. The acceptance of compatible types based on their structure is a feature of TypeScript’s structural type system, which is described in Chapter 2.

SOLID Principles

Object orientation, as with any programming paradigm, doesn’t prevent confusing or unmaintainable programs. This is the reason for the five heuristic design guidelines commonly referred to as the SOLID principles.

The SOLID principles were cataloged by R. C. Martin (http://www.objectmentor.com/resources/articles/Principles_and_Patterns.pdf, 2000; Agile Principles, Patterns, and Practices in C#. Prentice Hall, 2006), although the “SOLID” acronym was spotted by Michael Feathers. Luckily, the order of the principles isn’t important, so they can be ordered to suit this more memorable form. The principles were intended to be the basic tenets that underpin object-oriented programming and design. In general, the principles provide guidance for creating readable and maintainable code.

It is important to remember that software design is a heuristic process. It is not possible to create rules that can be followed like a checklist. The SOLID principles are guidelines to help you think about your program’s design in terms of object orientation and can help you to make an informed design decision that works in your specific context. The principles also supply a shared language that can be used to discuss designs with other programmers.

The five SOLID principles are

  • Single responsibility principle—a class should have one, and only one, reason to change.
  • Open–closed principle—it should be possible to extend the behavior of a class without modifying it.
  • Liskov substitution principle—subclasses should be substitutable for their superclasses.
  • Interface segregation principle—many small, client-specific interfaces are better than one general-purpose interface.
  • Dependency inversion principle—depends on abstractions not concretions.

The five SOLID principles are discussed individually in the sections that follow.

The Single Responsibility Principle (SRP)

The SRP requires that a class should have only one reason to change. When designing your classes, you should aim to put related features together, ensuring that they are likely to change for the same reason, and keep features apart if they will change for different reasons. A program that follows this principle has classes that perform just a few related tasks. Such a program is likely to be highly cohesive.

The term cohesion refers to a measure of the relatedness of features within a class or module. If features are unrelated, the class has low cohesion and is likely to change for many different reasons. High cohesion results from the application of the SRP.

When you are adding code to your program, you need to make a conscious decision about where it belongs. Most violations of this principle do not come from obvious cases where a method is clearly mismatched to its enclosing class. It is far more common for a class to gradually overstep its original purpose over a period of time and under the care of many different programmers.

You don’t need to limit your thinking to classes when considering the SRP. You can apply the principle to methods, ensuring that they do just one thing and therefore have just one reason to change. You can apply the principle to modules, ensuring that at a general level the module has one area of responsibility.

Listing 3-5 shows a typical violation of the SRP. At first glance all of the methods seem to belong to the Movie class, because they all perform operations using the properties of a movie. However, the appearance of persistence logic blurs the line between the use of the Movie class as an object, and its use as a data structure.

Listing 3-5. Single responsibility principle (SRP) violation

class Movie {
    private db: DataBase;

    constructor(private title: string, private year: number) {
        this.db = DataBase.connect('user:pw@mydb', ['movies']);
    }

    getTitle() {
        return this.title + ' (' + this.year + ')';
    }

    save() {
        this.db.movies.save({ title: this.title, year: this.year });
    }
}

To fix this class before it grows into a bigger problem, the two concerns can be divided between the Movie class that takes care of movie related behavior and a MovieRepository that is responsible for storing the data as shown in Listing 3-6. If features are added to the Movie class, the MovieRepository requires no changes. If you were to change your data storage device, the Movie class wouldn’t need to change.

Listing 3-6. Separate reasons for change

class Movie {
    constructor(private title: string, private year: number) {
    }

    getTitle() {
        return this.title + ' (' + this.year + ')';
    }
}

class MovieRepository {
    private db: DataBase;

    constructor() {
        this.db = DataBase.connect('user:pw@mydb', ['movies']);
    }

    save(movie: Movie) {
        this.db.movies.save(JSON.stringify(movie));
    }
}

// Movie
var movie = new Movie('The Internship', 2013);

// MovieRepository
var movieRepository = new MovieRepository();

movieRepository.save(movie);

Keeping an eye on the class level responsibilities is usually straightforward if you keep in mind the SRP, but it can be even more important at the method level, ensuring that each method performs just one task and is named in a way that reveals the intended behavior of the method. Uncle Bob coined the phrase “extract ’til you drop,” which refers to the practice of refactoring your methods until each one has so few lines it can only do a single thing. This method of refactoring methods extensively is easily worth the effort of reworking the design.

The Open–Closed Principle (OCP)

The OCP is often summed up by the sentence: software entities should be open for extension but closed for modification. In pragmatic terms, no matter how much you design your program up front, it is almost certain that it won’t be entirely protected from modification. However, the risk of changing an existing class is that you will introduce an inadvertent change in behavior. This can be mitigated somewhat (but not entirely) by automated tests, which is described in Chapter 9.

To follow the OCP, you need to consider the parts of your program that are likely to change. For example, you would attempt to identify any class that contains a behavior that you may want to replace or extend in the future. The slight hitch with this is that it is usually not possible to predict the future and there is a danger that if you introduce code intended to pay off later on, it almost always will not. Trying to guess what may happen can be troublesome either because it turns out the code is never needed or because the real future turns out to be incompatible with the prediction. So you will need to be pragmatic about this principle, which sometimes means introducing the code to solve a problem only when you first encounter the problem in real life.

So with these warnings in mind, a common way to follow the OCP is to make it possible to substitute one class with another to get different behaviors. This is a reasonably simple thing to do in most object-oriented languages and TypeScript is no exception. Listing 3-7 shows a reward card point calculation class named RewardPointsCalculator. The standard number of reward points is four points per whole dollar spent in the store. When the decision is made to offer double points to some VIP customers, instead of adding a conditional comment within the original RewardPointsCalculator class, a subclass named DoublePointsCalculator is created to deal with the new behavior. The subclass in this case calls the original getPoints method on the superclass, but it could ignore the original class entirely and calculate the points any way it wishes.

Listing 3-7. Open–closed principle (OCP)

class RewardPointsCalculator {
    getPoints(transactionValue: number) {
        // 4 points per whole dollar spent
        return Math.floor(transactionValue) * 4;
    }
}

class DoublePointsCalculator extends RewardPointsCalculator {
    getPoints(transactionValue: number) {
        var standardPoints = super.getPoints(transactionValue);
        return standardPoints * 2;
    }
}

var pointsCalculator = new DoublePointsCalculator();

alert(pointsCalculator.getPoints(100.99));

If the decision was taken to only give reward points on particular qualifying purchases, a class could handle the filtering of the transactions based on their type before calling the original RewardPointsCalculator—again, extending the behavior of the application rather than modifying the existing RewardPointsCalculator class.

By following the OCP, a program is more likely to contain maintainable and re-usable code. By avoiding changes, you also avoid the shock waves that can echo throughout the program following a change. Code that is known to work is left untouched and new code is added to handle the new requirements.

The Liskov Substitution Principle (LSP)

In Data Abstraction and Hierarchy Barbara Liskov (http://www.sr.ifes.edu.br/~mcosta/disciplinas/20091/tpa/recursos/p17-liskov.pdf, 1988) wrote,

What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

—Barbara Liskov

The essence of this is that if you substitute a subclass for a superclass, the code that uses the class shouldn’t need to know that the substitution has taken place. If you find yourself testing the type of an object in your program, there is a high probability that you are violating the LSP. The specific requirements of this principle are described later, using the example of a super Animal class, and a subclass of Cat that inherits from Animal.

  • Contravariance of method arguments in the subtype: If the superclass has a method accepting a Cat, the subclass method should accept an argument of type Cat or Animal, which is the superclass for Cat.
  • Covariance of return types in the subtype: If the superclass has a method that returns an Animal, the subclass method should return an Animal, or a subclass of Animal, such as Cat.
  • The subtype should throw either the same exceptions as the supertype, or exceptions that are subtypes of the supertype exceptions: In TypeScript, you are not limited to using exception classes; you can simply specify a string to throw an exception. It is possible to create classes for errors in TypeScript, as shown in Listing 3-8. The key here is that if calling code has an exception handling block, it should not be surprised by the exception thrown by a subclass. There is more information on exception handling in Chapter 7.

    Listing 3-8. Error classes

    class ApplicationError implements Error {
        constructor(public name: string, public message: string) {

        }
    }

    throw new ApplicationError('Example Error', 'An error has occurred'),

The LSP supports the OCP by ensuring that new code can be used in place of old code when a new behavior is added to a program. If a subclass couldn’t be directly substituted for a superclass, adding a new subclass would result in changes being made throughout the code and may even result in the program flow being controlled by conditions that branch based on the object types.

The Interface Segregation Principle (ISP)

It is quite common to find that an interface is in essence just a description of an entire class. This is usually the case when the interface was written after the class. Listing 3-9 shows a simple example of an interface for a printer that can copy, print, and staple documents. Because the interface is just a way of describing all of the behaviors of a printer, it grows as new features are added, for example, folding, inserting into envelopes, faxing, scanning, and e-mailing may eventually end up on the Printer interface.

Listing 3-9. Printer interface

interface Printer {
    copyDocument();
    printDocument(document: Document);
    stapleDocument(document: Document, tray: number);
}

The ISP states that we should not create these big interfaces, but instead write a series of smaller, more specific, interfaces that are implemented by the class. Each interface would describe an independent grouping of behavior, allowing code to depend on a small interface that provides just the required behavior. Different classes could provide the implementation of these small interfaces, without having to implement additional unrelated functionality.

The Printer interface from Listing 3-9 makes it impossible to implement a printer that can print and copy, but not staple—or even worse, the staple method be implemented to throw an error that states the operation cannot be completed. The likelihood of a printer satisfying the Printer interface as the interface grows larger decreases over time and it becomes hard to add a new method to the interface because it affects multiple implementations. Listing 3-10 shows an alternative approach that groups methods into more specific interfaces that describe a number of contracts that could be implemented individually by a simple printer or simple copier, as well as by a super printer that could do everything.

Listing 3-10. Segregated interfaces

interface Printer {
    printDocument(document: Document);
}

interface Stapler {
    stapleDocument(document: Document, tray: number);
}

interface Copier {
    copyDocument();
}

class SimplePrinter implements Printer {
    printDocument(document: Document) {
        //...
    }
}

class SuperPrinter implements Printer, Stapler, Copier {
    printDocument(document: Document) {
        //...
    }

    copyDocument() {
        //...
    }

    stapleDocument(document: Document, tray: number) {
        //...
    }
}

When you follow the ISP, client code is not forced to depend on methods it doesn’t intend to use. Large interfaces tend to encourage calling code that is organized in similar large chunks, whereas a series of small interfaces allows the client to implement small maintainable adapters to communicate with the interface.

The Dependency Inversion Principle (DIP)

In a conventional object-oriented program, the high-level components depend on low-level components in a hierarchical structure. The coupling between components results in a rigid system that is hard to change, and one that fails when changes are introduced. It also becomes hard to reuse a module because it cannot be moved into a new program without also bringing along a whole series of dependencies.

Listing 3-11 shows a simple example of conventional dependencies. The high-level LightSwitch class depends on the lower-level Light class.

Listing 3-11. High-level dependency on low-level class

class Light {
    switchOn() {
        //...
    }

    switchOff() {
        //...
    }
}

class LightSwitch {
    private isOn = false;

    constructor(private light: Light) {
    }

    onPress() {
        if (this.isOn) {
            this.light.switchOff();
            this.isOn = false;
        } else {
            this.light.switchOn();
            this.isOn = true;
        }
    }
}

The DIP simply states that high-level modules shouldn’t depend on low-level components, but instead depend on an abstraction. In turn, the abstractions should not depend on details, but on yet more abstractions. In simple terms, you can satisfy the DIP by depending on an interface, rather than a class.

Listing 3-12 demonstrates the first step of DIP in practice, simply adding a LightSource interface to break the dependency between the LightSwitch and Light classes. We can continue this design by abstracting the LightSwitch into a Switch interface, the Switch interface would depend on the LightSource interface, not on the low-level Light class.

Listing 3-12. Implementing the dependency inversion principle (DIP)

interface LightSource {
    switchOn();
    switchOff();
}

class Light implements LightSource {
    switchOn() {
        //...
    }

    switchOff() {
        //...
    }
}

class LightSwitch {
    private isOn = false;

    constructor(private light: LightSource) {
    }

    onPress() {
        if (this.isOn) {
            this.light.switchOff();
            this.isOn = false;
        } else {
            this.light.switchOn();
            this.isOn = true;
        }
    }
}

The DIP extends the concepts of the OCP and the LSP. By depending on abstractions, code is less tightly bound to the specific implementation details of a class. This principle has a big impact, yet it is relatively simple to follow, as all you need to do is supply an interface to depend on rather than a class.

Design Patterns

In software, design patterns provide a catalog of known problems along with a design solution for each problem described. These patterns are not overly prescriptive, instead they provide a set of tools that you can arrange in a different way each time you use them. The definitive source for the most common design patterns is the original “Gang of Four” book, Design Patterns: Elements of Reusable Object-Oriented Software (Gamma, Helm, Johnson, & Vlissides, Addison Wesley, 1995).

It is possible to transfer these design patterns to JavaScript, as shown by Diaz and Harmes (Pro JavaScript Design Patterns, Apress, 2007), and if it can be done in plain JavaScript it can be done in TypeScript. The translation from traditional design pattern examples to TypeScript is more natural in many cases, due to the class-based object orientation offered in TypeScript.

TypeScript is a natural fit for design patterns because it supplies all of the language constructs required to use all of the creational, structural, and behavioral patterns in the original catalog as well as many more documented since then. A small sample of design patterns are described following along with TypeScript code examples.

The following example demonstrates the strategy pattern and the abstract factory pattern. These are just two of the 24 patterns described in the original Gang of Four book. The patterns are described in general below and then used to improve the design of a small program.

Image Note  Although you may have an up-front idea about the design patterns that may improve the design of your program, it is far more common and often more desirable to let the patterns emerge as your program grows. If you predict the patterns that may be required, you could be guessing wrong. If you let the code reveal problems as you extend it, you are less likely to create a large number of unnecessary classes, and you are less likely to get lost down the rabbit hole following the wrong design.

The Strategy Pattern

The strategy pattern allows you to encapsulate different algorithms in a way that makes each one substitutable for another. In Figure 3-2 the Context class would depend on Strategy, which provides the interface for concrete implementations. Any class that implements the interface could be passed to the Context class at runtime.

An example of the strategy pattern is shown in the practical example later in this section.

9781430267911_Fig03-02.jpg

Figure 3-2. The strategy pattern

The Abstract Factory Pattern

The abstract factory pattern is a creational design pattern. It allows you to specify an interface for the creation of related objects without specifying their concrete classes. The aim of this pattern is for a class to depend on the behavior of the abstract factory, which will be implemented by different concrete classes that are either changed at compile time or runtime.

An example of the abstract factory pattern is shown in the practical example in Figure 3-3 and in the following text.

9781430267911_Fig03-03.jpg

Figure 3-3. The abstract factory pattern

Practical Example

To illustrate the use of Strategy and Abstract Factory design patterns we use a car wash example. The car wash is able to run different grades of wash depending on how much the driver spends. Listing 3-13 illustrates the wheel cleaning strategy, which consists of an interface for wheel cleaning classes, and two strategies that provide either a basic or executive clean.

Listing 3-13. Wheel cleaning.

interface WheelCleaning {
    cleanWheels(): void;
}

class BasicWheelCleaning implements WheelCleaning {
    cleanWheels() {
        console.log('Soaping Wheel'),
        console.log('Brushing wheel'),
    }
}

class ExecutiveWheelCleaning extends BasicWheelCleaning {
    cleanWheels() {
        super.cleanWheels();
        console.log('Waxing Wheel'),
        console.log('Rinsing Wheel'),
    }
}

Listing 3-14 shows the strategies for cleaning the bodywork of the car. This is similar to the WheelCleaning example in Listing 3-13, but it does not necessarily need to be. Neither the WheelCleaning nor BodyCleaning code will change when we convert the example to use the abstract factory pattern later.

Listing 3-14. Body cleaning

interface BodyCleaning {
    cleanBody(): void;
}

class BasicBodyCleaning implements BodyCleaning {
    cleanBody() {
        console.log('Soaping car'),
        console.log('Rinsing Car'),
    }
}

class ExecutiveBodyCleaning extends BasicBodyCleaning {
    cleanBody() {
        super.cleanBody();
        console.log('Waxing car'),
        console.log('Blow drying car'),
    }
}

Listing 3-15 shows the CarWashProgram class before it is updated to use the abstract factory pattern. This is a typical example of a class that knows too much. It is tightly coupled to the concrete cleaning classes and is responsible for creating the relevant classes based on the selected program.

Listing 3-15. CarWashProgram class before the abstract factory pattern

class CarWashProgram {
    constructor(private washLevel: number) {

    }

    runWash() {
        var wheelWash: WheelCleaning;
        var bodyWash: BodyCleaning;

        switch (this.washLevel) {
            case 1:
                wheelWash = new BasicWheelCleaning();
                wheelWash.cleanWheels();

                bodyWash = new BasicBodyCleaning();
                bodyWash.cleanBody();

                break;
            case 2:
                wheelWash = new BasicWheelCleaning();
                wheelWash.cleanWheels();

                bodyWash = new ExecutiveBodyCleaning();
                bodyWash.cleanBody();

                break;
            case 3:
                wheelWash = new ExecutiveWheelCleaning();
                wheelWash.cleanWheels();

                bodyWash = new ExecutiveBodyCleaning();
                bodyWash.cleanBody();

                break;
        }
    }
}

The abstract factory itself is an interface that describes the operations each concrete factory can perform. In Listing 3-16 the ValetFactory interface provides method signatures for obtaining the class providing the wheel cleaning feature and the class providing the body cleaning feature. A class that requires wheel cleaning and body cleaning can depend on this interface and remain de-coupled from the classes that specify the actual cleaning.

Listing 3-16. Abstract factory

interface ValetFactory {
    getWheelCleaning() : WheelCleaning;
    getBodyCleaning() : BodyCleaning;
}

In Listing 3-17, three concrete factories are declared that provide either a bronze, silver, or gold level wash. Each factory provides appropriate cleaning classes that match the level of wash required.

Listing 3-17. Concrete factories

class BronzeWashFactory implements ValetFactory {
    getWheelCleaning() {
        return new BasicWheelCleaning();
    }

    getBodyCleaning() {
        return new BasicBodyCleaning();
    }
}

class SilverWashFactory implements ValetFactory {
    getWheelCleaning() {
        return new BasicWheelCleaning();
    }

    getBodyCleaning() {
        return new ExecutiveBodyCleaning();
    }
}

class GoldWashFactory implements ValetFactory {
    getWheelCleaning() {
        return new ExecutiveWheelCleaning();
    }

    getBodyCleaning() {
        return new ExecutiveBodyCleaning();
    }
}

Listing 3-18 shows the updated class with the abstract factory pattern in action. The CarWashProgram class no longer has any knowledge of the concrete classes that will perform the car cleaning actions. The CarWashProgram is now constructed with the appropriate factory that will provide the classes to perform the clean. This could either be done via a compile time mechanism or a dynamic runtime one.

Listing 3-18. Abstract factory pattern in use

class CarWashProgram {
    constructor(private cleaningFactory: ValetFactory) {

    }

    runWash() {
        var wheelWash = this.cleaningFactory.getWheelCleaning();
        wheelWash.cleanWheels();

        var bodyWash = this.cleaningFactory.getBodyCleaning();
        bodyWash.cleanBody();
    }
}

Mixins

Mixins provide an alternate way of composing your application that isn’t explicitly covered in books on design patterns.

Mixins take their name from a customizable ice-cream dessert that was first available at Steve’s Ice Cream in Somerville, Massachusetts. The idea behind the mix-in dessert was that you choose an ice cream and add another product to flavor it, for example, a candy bar. The mix-in, or smoosh-in, ice-cream concept has gone global since its appearance on Steve Herrell’s menu back in 1973.

In programming, mixins are based on a very similar concept. Augmented classes are created by adding together a combination of mixin classes that each provides a small reusable behavior. These mixin classes are partly an interface and partly an implementation.

TypeScript Mixins

Although mixins are not 100% natively supported in TypeScript yet, it is possible to implement them with the aid of just one additional function that performs the wiring. The function to apply the mixins is shown in Listing 3-19. This function walks the instance members of each of the mixin classes passed in the baseCtors array and adds each of them to the derivedCtor class. You will use this function each time you want to apply mixins to a class and you’ll see this function used in the examples throughout this section.

Listing 3-19. Mixin enabler function

function applyMixins(derivedCtor: any, baseCtors: any[]) {
    baseCtors.forEach(baseCtor => {
        Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
            if (name !== 'constructor') {
                derivedCtor.prototype[name] = baseCtor.prototype[name];
            }
        })
    });
}

Once you have added this function somewhere within your program, you are ready to start using mixins. In Listing 3-20 a series of small reusable mixin classes are defined. There is no specific syntax for these classes. In this example, we define a series of possible behaviors, Sings, Dances, and Acts. These classes act as the menu of behaviors that can be mixed together to create different flavors composed of different combinations.

Listing 3-20. Reusable classes

class Sings {
    sing() {
        console.log('Singing'),
    }
}

class Dances {
    dance() {
        console.log('Dancing'),
    }
}

class Acts {
    act() {
        console.log('Acting'),
    }
}

On their own, these classes are too small to be useful, but they adhere very closely to the single responsibility principle. You are not restricted to a single method, but the idea is that each class represents one behavior that you can sum up in the class name. To make these mixins useful, you need to compose them into usable augmented classes.

In TypeScript, you compose your class using the implements keyword, followed by a comma-separated list of mixins. The implements keyword pays homage to the fact that mixins are like interfaces that come with an implementation. You will also need to supply temporary properties that match all of the mixins that you combine, as shown in Listing 3-21. These properties will be replaced when the applyMixins function is called directly after the class declaration.

Listing 3-21. Composing classes

class Actor implements Acts {
    act: () => void;
}

applyMixins(Actor, [Acts]);

class AllRounder implements Acts, Dances, Sings {
    act: () => void;
    dance: () => void;
    sing: () => void;
}

applyMixins(AllRounder, [Acts, Dances, Sings]);

There is nothing to ensure that you call the applyMixins function with the same collection of classes that you listed in the implements statement. You are responsible for keeping the two lists synchronized.

The Actor and AllRounder classes have no real implementation, only placeholders for the implementation that is supplied by the mixins. This means that there is only one place in the program that needs to be changed for any given behavior. Using an augmented class is no different to using any other class in your program, as shown in Listing 3-22.

Listing 3-22. Using the classes

var actor = new Actor();
actor.act();

var allRounder = new AllRounder();
allRounder.act();
allRounder.dance();
allRounder.sing();

Image Note  You may have spotted that mixins look a little bit like multiple inheritance. Multiple inheritance is not permitted in TypeScript. The key to mixins is the use of the implements keyword, rather than the extends keyword, which makes them like interfaces rather than superclasses.

When to Use Mixins

Mixins already have some support in TypeScript—but what should you bear in mind when using them? First and foremost, the mechanism for adding the implementation to the augmented class is not checked, so you have to be very careful about calling the applyMixins function with the correct list of class names. This is one area that you will want to fully test to avoid any nasty surprises.

The decision about whether to use mixins or classical inheritance usually comes down to the relationship between the classes. When deciding between inheritance and delegation, it is common to use the “is a” verses “has a” test. As described earlier in this chapter.

  • A car has a chassis.
  • A rolling chassis is a chassis.

Inheritance would only be used where the “is a” relationship works in a sentence, and delegation would be used where “has a” makes more sense. With mixins, the relationship is best described by a “can do” relationship, for example

  • An actor can do acting.

Or

  • An actor acts.

You can reinforce this relationship by naming your mixins with names such as Acting or Acts. This makes your class read like a sentence, for example, “Actor implements Acting.”

Mixins are supposed to allow small units to be composed into larger ones, so the following scenarios are good candidates for using mixins:

  • Composing classes with optional features, mixins are options.
  • Reusing the same behavior on many classes
  • Creating many variations based on similar lists of features

Restrictions

You cannot use mixins with private members because the compiler will generate an error if the members are not implemented in the augmented class. The compiler will also generate an error if both the mixin and the augmented class define a private member with the same name.

The other restriction on mixins is that although method implementations are mapped to the augmented class, property values are not mapped; this is demonstrated in Listing 3-23. When you implement a property from a mixin you need to initialize it in the augmented class. To avoid confusion, it is best to define a required property in the mixin, but provide no default value.

Listing 3-23. Properties not mapped

class Acts {
    public message = 'Acting';

    act() {
        console.log(this.message);
    }
}

class Actor implements Acts {
    public message: string;
    act: () => void;
}

applyMixins(Actor, [Acts]);

var actor = new Actor();

// Logs 'undefined', not 'Acting'
actor.act();

If the property does not need to be tied to the instance, you can use static properties as these would remain available from within the methods that are mapped from the mixin to the augmented class. Listing 3-24 is an update to Listing 3-23 that solves the problem using a static property. If you do need different values on each instance, the instance property should be initialized within the augmented class.

Listing 3-24. Static properties are available

class Acts {
    public static message = 'Acting';

    act() {
        alert(Acts.message);
    }
}

Summary

The basic building blocks of object orientation are all present in TypeScript. The language tools make it possible to bring all of the principles and practices of object orientation from other languages into your program, using the SOLID principles to guide your composition and design patterns as a reference for well-established solutions to common problems.

Object orientation, in itself doesn’t solve the problems of writing and maintaining a program that solves complex problems. It is just as possible to write poor code using object orientation as it is to write bad code in any other programming paradigm; this is why the patterns and principles are so important. The elements of object orientation in this chapter compliment the unit testing techniques in Chapter 9.

You can practice and improve your object-oriented design skills as well as your unit testing skills using coding katas. These are described in Appendix 4 and there are some example katas for you to try out.

Key Points

  • TypeScript has all of the tools needed to write object-oriented programs.
  • The SOLID principles aim to keep your code malleable and prevent it from rotting.
  • Design patterns are existing well-known solutions to common problems.
  • You don’t have to implement a design pattern exactly as it is described.
  • Mixins provide an alternative mechanism for composition.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.18.198