- Mastering TypeScript 3
- Nathan Rozentals
- 929字
- 2021-07-02 12:42:54
Abstract classes
Another fundamental principle of object-oriented design is the concept of abstract classes. An abstract class is a definition of a class that cannot be instantiated. In other words, it is a class that is designed to be derived from. The abstract classes, sometimes referred to as abstract base classes, are often used to provide a set of basic functionality or properties that are shared among a group of similar classes. They are similar to interfaces in that they cannot be instantiated, but they can have function implementations, which interfaces cannot.
Abstract classes are a technique that allows for code reuse among groups of similar objects. Consider the following two classes:
class Employee { public id: number | undefined; public name: string | undefined; printDetails() { console.log(`id: ${this.id}` + `, name ${this.name}`); } } class Manager { public id: number | undefined; public name: string | undefined; public Employees: Employee[] | undefined; printDetails() { console.log(`id: ${this.id} ` + `, name ${this.name}, ` + ` employeeCount ${this.Employees.length}`); } }
We start with a class named Employee that has an id and name property, as well as a function called printDetails. The next class is named Manager, and is very similar to the Employee class. It also has an id and name property, but it also has an extra property named Employees that is a list of employees that this manager oversees. There is a lot of code that is common to these two classes. Both have an id and name property, and both have a printDetails function. Using an abstract base class for both of these classes overcomes this problem with common properties and code. Let's rewrite these two classes, and introduce the concept of an abstract base class, as follows:
abstract class AbstractEmployee { public id: number | undefined; public name: string | undefined; abstract getDetails(): string; public printDetails() { console.log(this.getDetails()); } } class NewEmployee extends AbstractEmployee { getDetails(): string { return `id : ${this.id}, name : ${this.name}`; } } class NewManager extends NewEmployee { public Employees: NewEmployee[] | undefined; getDetails() : string { return super.getDetails() + `, employeeCount ${this.Employees.length}`; } }
Here, we have defined an abstract class named AbstractEmployee that includes an id and name property, which is common to both managers and employees. We then define what is known as an abstract function named getDetails. Using an abstract function means that any class that derives from this abstract class must implement this function. We then define a printDetails function to log details of this AbstractEmployee class to the console. Note how we are calling the abstract function getDetails from within the abstract class. This means that our code in the printDetails function will call the actual implementation of the function in the derived class.
Our second class, named NewEmployee, extends the AbstractEmployee class. As such, it must implement the getDetails function that has been marked as abstract in the base class. This getDetails function returns a string representation of the id and name properties of the NewEmployee class.
Next, we have a class named NewManager that derives from NewEmployee. This NewManager class, therefore, also has an id and name property, but has an extra property named Employees. Because this class already derives from NewEmployee, it does not necessarily need to define the getDetails function again. It could simply use the version of the getDetails function that the NewEmployee class provides. Note, however, that we have actually defined this getDetails function within the NewManager class. This function calls the base class getDetails function, through the super keyword, and then adds some extra information about its Employees property. Let's take a look at what happens when we create and use these classes, as follows:
var employee = new NewEmployee(); employee.id = 1; employee.name = "Employee Name"; employee.printDetails();
Here, we have created an instance of the NewEmployee class, named employee, set its id and name properties, and called the printDetails function from the abstract class. Recall that the abstract class will then call the implementation of the getDetails function that we provided in the NewEmployee class, and therefore output the following to the console:
id: 1, name : Employee Name
Now, let's use our NewManager class in a similar way:
var manager = new NewManager(); manager.id = 2; manager.name = "Manager Name"; manager.Employees = []; manager.printDetails();
Here, we have created an instance of the NewManager class, named manager, and set its id and name properties as before. Because this class also has an array of Employees, we are setting the Employees property to a blank array. Note what happens, however, when we call the abstract class printDetails function on the last line. The output will be as follows:
id: 2, name : Manager Name, employeeCount 0
The abstract class implementation of the printDetails function calls the getDetails function of the derived class. Because the NewManager class also defines a getDetails function, the abstract class will call this function on the NewManager instance. The getDetails function on the NewManager instance then calls through to the base class implementation, that is, the NewEmployee instance of the getDetails function, as seen in the code super.getDetails(). It then appends some information regarding its employee count.
Using abstract classes and inheritance allows us to write our code in a cleaner and more reusable way. Abstraction, inheritance, polymorphism, and encapsulation are the foundations of good object-oriented design principles. As we have seen, the TypeScript language gives us the ability to incorporate each of these principles to help write good, clean JavaScript code.