MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript实现接口与扩展抽象类的选择

2021-09-044.9k 阅读

TypeScript中的接口

在TypeScript里,接口是一种强大的类型定义工具,它主要用于对对象的形状(shape)进行描述。接口定义了对象必须包含的属性和方法,不过它只关注对象的结构,而不关心其具体实现。

基本接口定义与使用

先来看一个简单的接口示例,定义一个表示人的接口 Person

interface Person {
    name: string;
    age: number;
}

function greet(person: Person) {
    return `Hello, ${person.name}! You are ${person.age} years old.`;
}

let tom: Person = { name: 'Tom', age: 25 };
console.log(greet(tom)); 

在上述代码中,Person 接口定义了 name(字符串类型)和 age(数字类型)两个属性。greet 函数接受一个符合 Person 接口的对象作为参数,并返回问候语。变量 tom 被声明为 Person 类型,并按照接口的要求进行了初始化。

可选属性

接口中的属性也可以是可选的,这在某些属性不是必须存在时非常有用。例如,我们可以给 Person 接口添加一个可选的 address 属性:

interface Person {
    name: string;
    age: number;
    address?: string;
}

function greet(person: Person) {
    let message = `Hello, ${person.name}! You are ${person.age} years old.`;
    if (person.address) {
        message += ` You live in ${person.address}.`;
    }
    return message;
}

let tom: Person = { name: 'Tom', age: 25 };
console.log(greet(tom)); 

let mary: Person = { name: 'Mary', age: 30, address: 'New York' };
console.log(greet(mary)); 

这里 address 属性后面跟着一个 ?,表示它是可选的。tom 对象没有 address 属性,而 mary 对象有,greet 函数能够正确处理这两种情况。

只读属性

有时候我们希望对象的某些属性只能在对象创建时被赋值,之后不能再修改,这就可以使用只读属性。例如,给 Person 接口添加一个只读的 id 属性:

interface Person {
    readonly id: number;
    name: string;
    age: number;
}

let tom: Person = { id: 1, name: 'Tom', age: 25 };
// tom.id = 2; // 这行代码会报错,因为id是只读属性

上述代码中,id 属性被声明为只读,一旦 tom 对象被初始化,就不能再修改 id 的值。

函数类型接口

接口不仅可以描述对象的属性,还可以描述函数的类型。比如,定义一个表示加法函数的接口 AddFunction

interface AddFunction {
    (a: number, b: number): number;
}

let add: AddFunction = function (a: number, b: number): number {
    return a + b;
};

console.log(add(3, 5)); 

AddFunction 接口描述了一个接受两个 number 类型参数并返回一个 number 类型值的函数。变量 add 被声明为 AddFunction 类型,并实现了符合该接口的函数。

TypeScript中的抽象类

抽象类是一种特殊的类,它不能被直接实例化,主要用于为其他类提供一个通用的基类。抽象类可以包含抽象方法和具体方法。

抽象类的定义与基本使用

定义一个抽象类 Animal

abstract class Animal {
    constructor(public name: string) {}

    abstract makeSound(): void;

    move(): void {
        console.log(`${this.name} is moving.`);
    }
}

class Dog extends Animal {
    makeSound(): void {
        console.log('Woof!');
    }
}

class Cat extends Animal {
    makeSound(): void {
        console.log('Meow!');
    }
}

let dog = new Dog('Buddy');
dog.makeSound(); 
dog.move(); 

let cat = new Cat('Whiskers');
cat.makeSound(); 
cat.move(); 

在上述代码中,Animal 是一个抽象类,它有一个构造函数和两个方法。makeSound 方法被声明为抽象方法,意味着它没有具体的实现,子类必须实现这个方法。move 方法是一个具体方法,有自己的实现。DogCat 类继承自 Animal 类,并实现了 makeSound 方法。

抽象类中的抽象属性

抽象类还可以包含抽象属性,这些属性同样需要子类去实现。例如,修改 Animal 抽象类,添加一个抽象属性 color

abstract class Animal {
    constructor(public name: string) {}

    abstract color: string;

    abstract makeSound(): void;

    move(): void {
        console.log(`${this.name} is moving.`);
    }
}

class Dog extends Animal {
    color = 'Brown';
    makeSound(): void {
        console.log('Woof!');
    }
}

class Cat extends Animal {
    color = 'Gray';
    makeSound(): void {
        console.log('Meow!');
    }
}

let dog = new Dog('Buddy');
console.log(`${dog.name} is ${dog.color}`); 

let cat = new Cat('Whiskers');
console.log(`${cat.name} is ${cat.color}`); 

这里 Animal 抽象类中的 color 属性被声明为抽象属性,DogCat 子类分别实现了这个属性。

接口与抽象类的区别

  1. 定义和本质
    • 接口:主要用于定义对象的形状,它是一种类型定义,只关心对象的结构,不包含任何实现代码。接口可以被类、对象字面量等实现,一个类可以实现多个接口。
    • 抽象类:是一种特殊的类,它可以包含抽象方法和具体方法,以及属性和构造函数。抽象类不能被直接实例化,主要为子类提供一个通用的基类,子类通过继承抽象类来获得其部分实现并实现抽象方法。一个类只能继承一个抽象类。
  2. 实现方式
    • 接口实现:类使用 implements 关键字来实现接口,必须实现接口中定义的所有属性和方法。例如:
interface Shape {
    area(): number;
}

class Circle implements Shape {
    constructor(public radius: number) {}
    area(): number {
        return Math.PI * this.radius * this.radius;
    }
}
- **抽象类继承**:类使用 `extends` 关键字来继承抽象类,必须实现抽象类中的抽象方法。例如:
abstract class Shape {
    abstract area(): number;
}

class Circle extends Shape {
    constructor(public radius: number) {}
    area(): number {
        return Math.PI * this.radius * this.radius;
    }
}
  1. 成员类型
    • 接口:只能包含属性和方法的签名,不能包含具体的实现代码、构造函数、私有成员等。接口中的属性默认是公开的。
    • 抽象类:可以包含具体的方法实现、抽象方法、属性、构造函数以及私有成员等。抽象类中的成员可以有不同的访问修饰符,如 publicprivateprotected 等。例如:
abstract class AbstractClass {
    private secret: string = 'This is a secret';
    protected shared: string = 'This is shared with subclasses';

    constructor() {}

    abstract doSomething(): void;

    someConcreteMethod(): void {
        console.log('This is a concrete method');
    }
}

class SubClass extends AbstractClass {
    doSomething(): void {
        console.log(this.shared); 
        // console.log(this.secret); // 这行代码会报错,因为secret是私有成员
    }
}
  1. 多继承特性
    • 接口:一个类可以实现多个接口,从而实现类似多继承的效果。这使得类可以从多个不同的接口获取不同的功能定义。例如:
interface Flyable {
    fly(): void;
}

interface Swimmable {
    swim(): void;
}

class Duck implements Flyable, Swimmable {
    fly(): void {
        console.log('Duck is flying');
    }
    swim(): void {
        console.log('Duck is swimming');
    }
}
- **抽象类**:一个类只能继承一个抽象类,不支持多继承。这是因为多继承可能会导致代码的复杂性增加,出现菱形继承等问题。

何时选择接口,何时选择抽象类

  1. 当需要定义对象的形状,而不关心实现细节时选择接口
    • 场景:在定义一些通用的数据结构或者服务契约时,接口非常有用。比如,在一个电商系统中,定义一个表示商品的接口 Product
interface Product {
    id: number;
    name: string;
    price: number;
    description: string;
}

function displayProduct(product: Product) {
    console.log(`Name: ${product.name}, Price: ${product.price}`);
}

let phone: Product = { id: 1, name: 'Smartphone', price: 500, description: 'A high - end smartphone' };
displayProduct(phone); 

这里 Product 接口定义了商品必须具备的属性,displayProduct 函数依赖于这个接口来展示商品信息,而不关心商品具体是如何实现的,不同的商品类只要实现了 Product 接口,就可以被 displayProduct 函数处理。 2. 当需要提供一些通用的实现,并且希望子类继承并扩展这些实现时选择抽象类 - 场景:在开发图形绘制库时,如果有多种图形,如圆形、矩形、三角形等,它们都有一些共同的操作,如绘制、计算面积等。可以定义一个抽象类 Shape 来提供这些通用操作的部分实现,并将一些特定于每种图形的操作定义为抽象方法。

abstract class Shape {
    constructor(public x: number, public y: number) {}

    abstract draw(): void;

    abstract calculateArea(): number;

    move(dx: number, dy: number): void {
        this.x += dx;
        this.y += dy;
        console.log(`Shape moved to (${this.x}, ${this.y})`);
    }
}

class Circle extends Shape {
    constructor(x: number, y: number, public radius: number) {
        super(x, y);
    }

    draw(): void {
        console.log(`Drawing a circle at (${this.x}, ${this.y}) with radius ${this.radius}`);
    }

    calculateArea(): number {
        return Math.PI * this.radius * this.radius;
    }
}

class Rectangle extends Shape {
    constructor(x: number, y: number, public width: number, public height: number) {
        super(x, y);
    }

    draw(): void {
        console.log(`Drawing a rectangle at (${this.x}, ${this.y}) with width ${this.width} and height ${this.height}`);
    }

    calculateArea(): number {
        return this.width * this.height;
    }
}

let circle = new Circle(10, 10, 5);
circle.draw(); 
circle.move(5, 5); 
console.log(`Circle area: ${circle.calculateArea()}`); 

let rectangle = new Rectangle(20, 20, 10, 5);
rectangle.draw(); 
rectangle.move(3, 3); 
console.log(`Rectangle area: ${rectangle.calculateArea()}`); 

在这个例子中,Shape 抽象类提供了 move 方法的具体实现,以及 drawcalculateArea 抽象方法。CircleRectangle 子类继承自 Shape 抽象类,并实现了抽象方法,同时可以复用 move 方法。 3. 当需要实现类似多继承的功能时选择接口 - 场景:假设有一个游戏角色,它既可以攻击敌人(Attacker 接口),又可以治疗队友(Healer 接口)。

interface Attacker {
    attack(target: string): void;
}

interface Healer {
    heal(target: string): void;
}

class Paladin implements Attacker, Healer {
    attack(target: string): void {
        console.log(`Paladin attacks ${target}`);
    }
    heal(target: string): void {
        console.log(`Paladin heals ${target}`);
    }
}

let paladin = new Paladin();
paladin.attack('Dragon'); 
paladin.heal('Warrior'); 

通过实现多个接口,Paladin 类获得了攻击和治疗的功能,模拟了多继承的效果。 4. 当需要限制实例化,并且希望在类层次结构中共享一些状态或行为时选择抽象类 - 场景:在一个权限管理系统中,有不同类型的用户,如普通用户、管理员用户等。可以定义一个抽象类 User 来管理用户的基本信息和一些通用行为,如登录、注销等,同时限制不能直接创建 User 实例,只能创建具体的用户子类实例。

abstract class User {
    constructor(public username: string, public password: string) {}

    abstract hasPermission(permission: string): boolean;

    login(): void {
        console.log(`${this.username} has logged in.`);
    }

    logout(): void {
        console.log(`${this.username} has logged out.`);
    }
}

class RegularUser extends User {
    hasPermission(permission: string): boolean {
        return false;
    }
}

class AdminUser extends User {
    hasPermission(permission: string): boolean {
        return true;
    }
}

// let user = new User('test', 'test'); // 这行代码会报错,因为User是抽象类
let regularUser = new RegularUser('regularUser', 'password');
regularUser.login(); 
console.log(`Regular user has permission: ${regularUser.hasPermission('admin:create')}`); 

let adminUser = new AdminUser('adminUser', 'password');
adminUser.login(); 
console.log(`Admin user has permission: ${adminUser.hasPermission('admin:create')}`); 

在这个例子中,User 抽象类定义了用户的基本信息和通用行为,具体的用户子类 RegularUserAdminUser 继承自 User 并实现了 hasPermission 方法,同时可以复用 loginlogout 方法。

接口和抽象类在实际项目中的应用案例

  1. 前端开发中的应用
    • 接口的应用:在 React 项目中,经常使用接口来定义组件的 props 类型。例如,定义一个 Button 组件,其 props 可以用接口来描述:
import React from'react';

interface ButtonProps {
    label: string;
    onClick: () => void;
    disabled?: boolean;
}

const Button: React.FC<ButtonProps> = ({ label, onClick, disabled = false }) => {
    return (
        <button disabled={disabled} onClick={onClick}>
            {label}
        </button>
    );
};

export default Button;

这里 ButtonProps 接口定义了 Button 组件所需的属性,包括 label(按钮显示的文本)、onClick(点击按钮时执行的函数)和可选的 disabled(是否禁用按钮)属性。这种方式可以在开发过程中提供类型检查,确保组件的使用符合预期。 - 抽象类的应用:在一些复杂的前端应用中,可能会有多个视图组件继承自一个抽象的视图基类。例如,定义一个抽象的 View 类,包含一些通用的方法,如初始化视图、更新视图等,具体的视图组件如 HomeViewAboutView 等继承自这个抽象类。

abstract class View {
    constructor(public element: HTMLElement) {}

    abstract render(): void;

    initialize(): void {
        console.log('Initializing view');
    }

    update(): void {
        console.log('Updating view');
    }
}

class HomeView extends View {
    render(): void {
        this.element.innerHTML = '<h1>Home Page</h1>';
    }
}

class AboutView extends View {
    render(): void {
        this.element.innerHTML = '<h1>About Page</h1>';
    }
}

let homeElement = document.createElement('div');
document.body.appendChild(homeElement);
let homeView = new HomeView(homeElement);
homeView.initialize(); 
homeView.render(); 

let aboutElement = document.createElement('div');
document.body.appendChild(aboutElement);
let aboutView = new AboutView(aboutElement);
aboutView.initialize(); 
aboutView.render(); 
  1. 后端开发中的应用
    • 接口的应用:在 Node.js 的 Express 应用中,可以使用接口来定义路由处理函数的参数类型。例如,定义一个处理用户登录的路由,其请求体数据可以用接口来描述:
import express from 'express';

interface LoginRequest {
    username: string;
    password: string;
}

const app = express();
app.use(express.json());

app.post('/login', (req, res) => {
    const { username, password }: LoginRequest = req.body;
    // 处理登录逻辑
    if (username === 'admin' && password === '123456') {
        res.json({ message: 'Login successful' });
    } else {
        res.status(401).json({ message: 'Login failed' });
    }
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

这里 LoginRequest 接口定义了登录请求体中必须包含的 usernamepassword 属性,使得在处理登录请求时能够进行类型检查,提高代码的健壮性。 - 抽象类的应用:在一个基于 Node.js 的数据库访问层中,可以定义一个抽象类 Database 来提供一些通用的数据库操作方法,如连接数据库、查询数据等,具体的数据库实现类如 MySQLDatabaseMongoDatabase 等继承自这个抽象类。

abstract class Database {
    abstract connect(): void;
    abstract query(sql: string): Promise<any>;

    close(): void {
        console.log('Closing database connection');
    }
}

class MySQLDatabase extends Database {
    connect(): void {
        console.log('Connecting to MySQL database');
    }
    query(sql: string): Promise<any> {
        // 实际的查询逻辑
        return Promise.resolve({ data: 'Query result' });
    }
}

class MongoDatabase extends Database {
    connect(): void {
        console.log('Connecting to MongoDB database');
    }
    query(sql: string): Promise<any> {
        // 实际的查询逻辑
        return Promise.resolve({ data: 'Query result' });
    }
}

let mySQLDB = new MySQLDatabase();
mySQLDB.connect(); 
mySQLDB.query('SELECT * FROM users').then(result => {
    console.log(result);
    mySQLDB.close(); 
});

let mongoDB = new MongoDatabase();
mongoDB.connect(); 
mongoDB.query('find({})').then(result => {
    console.log(result);
    mongoDB.close(); 
});

在实际项目中,正确地选择接口和抽象类对于代码的可维护性、可扩展性和可复用性至关重要。需要根据具体的业务需求和场景,仔细权衡两者的特点,以达到最佳的代码设计效果。同时,随着项目规模的扩大和功能的增加,可能会结合使用接口和抽象类,充分发挥它们各自的优势。例如,在一个大型的企业级应用中,可能会使用抽象类来构建业务逻辑的层次结构,提供通用的实现和状态管理,而使用接口来定义不同模块之间的交互契约,确保模块之间的松散耦合。通过合理地运用接口和抽象类,能够提高代码的质量,降低开发和维护的成本。