Learning TypeScript 0x3 面向对象编程

本文深入探讨SOLID原则在软件设计中的应用,包括单一职责、开闭、里氏替换、接口隔离及依赖反转原则,阐述如何通过这些原则提升代码质量,避免God对象,以及在实际开发中如何遵循SOLID原则。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

SOLID原则

  • 单一职责原则(SRP):组件(函数、类、模块)必须专注于单一的任务
  • 开/闭原则(OCP):设计时考虑扩展性,但是扩展时最少地修改原有代码
  • 里氏替换原则(LSP): 只要继承的是同一个接口,程序里任意一个类都可以被其他的类替换。在替换完成后,不需要其他额外的工作程序就能像原来一样运行。
  • 接口隔离原则(ISP):将非常大的接口拆分成小的具体的接口,使客户端只需要关心它们需要到接口
  • 依赖反转原则(DIP):表明一个方法应遵从依赖于抽象(接口) 而不是一个实例(类)的概念

class Person {
    public name : string
    public surname : string
    public email : string
    constructor(name: string, surname: string, email: string) {
        this.email = email
        this.name = name
        this.surname = surname
    }
    greet() {
        console.log('Hi')
    }
}
const me : Person = new Person('Remo', 'Jansen', 'remo.jasen@qq.com')

constructor是一个特殊的方法,在用new关键字创建一个类的实例的时候用到。声明了一个变量me,用来存储Person类的实例。new关键字使用了Person类的constructor方法返回一个类型为Person的对象

当一个类不遵循SRP,拥有太多属性或者太多方法,称这种对象为God对象。这里如果要对email进行验证最好的方法不是在Person类中加上validateEmail方法,而是通过声明一个Email类。

class Person {
    public name : string
    public surname : string
    public email : Email
    constructor(name: string, surname: string, email: Email) {
        this.email = email
        this.name = name
        this.surname = surname
    }
    greet() {
        console.log('Hi')
    }
}
const me : Person = new Person('Remo', 'Jansen', 'remo.jasen@qq.com')

class Email {
    public email : string
    constructor(email : string){
        if (this.validateEmail(email)) {
            this.email = email
        } else {
            throw new Error('Invalid email'))
        }
    }
    validateEmail(email : string){
        var re = /\S+@\S+\.\S+/
        return re.test(email)
    }
}

确保每一个类都只有单一的职责,可以更容易看出一个类做了哪些事情、如何去扩展/改进他,可以通过提高其的抽象等级来改进。如,当使用Email类时,不需要意识到validateEmail方法的存在,那么这个方法就是外部不可见。

class Email {
    private email : string
    constructor(email : string){
        if (this.validateEmail(email)) {
            this.email = email
        } else {
            throw new Error('Invalid email'))
        }
    }
    private validateEmail(email : string){
        var re = /\S+@\S+\.\S+/
        return re.test(email)
    }
    get() : string {
        return this.email
    }
}
var email = new Email('remo.jasen@qq.com')

接口

在面向对象的语言中,interface常被用来定义一个不包含数据和逻辑代码但函数签名定义了行为的抽象类型

实现一个接口可以看做是签署了一份协议。接口好比协议,但我们签署他(实现)时,必须遵守他的规则。接口的规则是方法和属性的签名,必须实现他们。

TS中接口不是严格遵守上面协议:

  • 接口可以扩展其他接口或者类
  • 接口可以定义数据和行为而不只是行为

关联、聚合和组合

关联

那些有联系但是他们的对象有独立的生命周期,并且没有从属关系的类之间的关系,称关联。如老师和学生,二者可以有关联关系但是都有自己的生命周期(都可以被独立地创建和删除),所以当老师离开学校时,不必删除任何学生,学生们离开学校时也不必删除任何老师

聚合

将拥有独立生命周期,但是有从属关系,并且子对象不能从属于其他对象的关系成为聚合。如手机和电池,手机停止工作可以删除但是电池不需要删除,可以继续用。

组合

指没有独立生命周期,父对象被删除后子对象也被删除的对象间的关系。如问题和答案,一个问题可以有多个答案,并且一个答案不可以属于多个问题。如果删除问题,答案也会被自动删除。生命周期中依赖其他对象的也被称作弱实体

继承

继承即扩展已有的类。允许我们创建一个类(子类),从已有的类(父类) 上继承所有的属性和方法,子类可以包含父类中没有的属性和方法。

class Person {
    public name : string
    public surname : string
    public email : Email
    constructor(name: string, surname: string, email: Email) {
        this.email = email
        this.name = name
        this.surname = surname
    }
    greet() {
        console.log('Hi')
    }
}
class Teacher extends Person {
    teach() {
        console.log('Welcome to class')
    }
}

可以使用super达到子类能提供父类同名方法的特殊实现,即方法重写。

class Teacher extends Person {
    public subjects : string[]
    constructor(name : string, surname : string, email : Eamil, subjects : string[]) {
        super(name, surname, email)
        this.subjects = subjects
    }
    greet() {
        super.greet()
        console.log(`I teach ${this.subjects}`)
    }
    teach() {
        console.log('Welcome to class')
    }
}
const teacher = new Teacher('remo', 'jansen', new Email('remo.jasen@qq.com'), ['math', 'physics'])

可以声明一个类继承一个已经继承别的类的类,可以访问其父类的所有方法和属性

class SchoolPrincipal extends Teacher {
    mangeTeachers() {
        console.log('We need to help students to get better result!')
    }
}
const principal = new SchoolPrincipal('remo', 'jansen', new Eamil('remo.jasen@qq.com'), ['math', 'physics'])
principal.greet()
principal.teach()
principal.mangeTeachers()

不推荐有过多层级的继承。维护开发会十分复杂。一般推荐继承树深度(DIT)在0-4之间。

混合

TS不支持多重继承,即一个类只能继承自一个类。这种设计会导致钻石问题,如在多个父类中存在相同方法,调用会有歧义。

引入混合,混合是多重继承的替代,但是功能有一些限制

class Mammal { // 哺乳动物
    breathe() : string {
        return 'I am alive'
    }
}
class WingedAnimal { // 飞行动物
    fly() : string {
        return 'I can fly'
    }
}
class Bat implements Mammal, WingedAnimal {
    breathe : () => string
    fly : () => string
}

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

applyMixins(Bat, [Mammal, WingedAnimal])

const bat = new Bat()
bat.breathe()
bat.fly()

 限制:

  • 只能在继承树上继承一级的方法和属性
  • 如果多个父类有同名方法,只会继承最后一个类中的方法(即之前继承会被后面的覆盖)

范型类

范型类可以办我们避免重复代码

class User {
    public name : string
    public password : string
}

class NotGenericUserRepository {
    private _url : string
    constructor(url :string) {
        this._url = url
    }
    public getAsync() {
        return Q.Promise((resolve : (users : User[]) => void, reject) => {
            $.ajax({
                url: this._url,
                type: 'GET',
                dataType: 'json',
                success: data => {
                    var users = <User[]>data.item
                    resolve(users)
                },
                error: e => {
                    reject(e)
                }
            })
        })
    }
}

var notGenericUserRepository = new NotGenericUserRepository('./demo/shared/user.json')
notGenericUserRepository.getAsync()
    .then(function(users : User[]){
        console.log('notGenericUserRepository => ', users)
    })

如果还要请求一个不同于User的其他类型实例的列表,会重复写很多代码。可能会想到用any类型,但是这样的话就失去了TypeScript在编译器中提供的类型检查的保护。更好的方式是创建一个范型。

class GenericRepository<T> {
    private _url  : string
    constructor(url : string) {
        this._url = url
    }
    public getAsync() {
        return Q.Promise((resolve: (entities : T[]) => void, reject) => {
            $.ajax({
                url: this._url,
                type: 'GET',
                dataType: 'json',
                sucess: data => {
                    var list = <T[]>data.items
                    resolve(list)
                },
                error: e => {
                    reject(e)
                }
            })
        })
    }
}
var userRepository = new GenericRepository<User>('./demos/shared/user.json')
userRepository.getAsync()
    .then((users : User[]) => {
        console.log('userRepository => ', users)
    })
var talkRepository = new GenericRepository<Talk>('./demos/shared/talks.json')
talkRepository.getAsync()
    .then((talk : Talk[]) => {
        console.log('talkRepository => ', talks)
    })

范型约束

对于一些新需求,需要增加一些变更来验证通过Ajax请求到的数据并且只有在验证有效后返回。一些可行的解决方案是在范型或函数内使用typeof操作符来验证参数范型T的类型。

//...
success:data => {
    var list : T[]
    var items = <T[]>data.items
    for(var i = 0;i < items.length; i++) {
        if(items[i] instanceof User) {
            //validate user
        }
        if(items[i] instanceof Talk) {
            // validate talk
        }
    }
    resolve(list)
}
//...

上面的解法会导致每增加一个新的有效实例,就必须修改增加额外的逻辑。一个更好的解决方案是给要获取的实例增加一个isValid方法,它在实例通过的时候返回true

//...
success:data => {
    var list : T[]
    var items = <T[]>data.items
    for(var i = 0;i < items.length; i++) {
        if(items[i].isValid()){//error
            //..
        }
    }
    resolve(list)
}
//...

还有一种方式遵循SOLID原则中的开/闭原则。通过范型约束解决,范型约束会约束允许作为范型参数中的T的类型。

// 声明接口
interface ValidatableInterface {
    isValid() : boolean
}
//实现接口,并实现isValid方法
class User implements ValidatableInterface {
    public name : string
    public password : string
    public isValid() : boolean {
        // user validation
        return true
    }
}
class Talk implements ValidatableInterface {
    public title : string
    public description : string
    public language : string
    public url : string
    public year : string
    public isValid() : boolean {
        // talk validation
        return true
    }
}
// 声明一个范型仓库并加上范型约束
class GenericRepositorWithConstraint<T extends ValidatableInterface> {
    private _url = : string
    constructor(url : string) {
        this._url = url
    }
    public getAsync() {
        return Q.Promise((resolve : (talks : T[]) => void, reject) => {
            $.ajax({
                url: this._url,
                type: 'GET',
                dataType: 'json',
                success: data => {
                    var items = <T[]>data.items
                    for(var i = 0; i < items.length; i++) {
                        if(items[i].isValid()) {
                            list.push(items[i])
                        }
                    }
                    resolve(list)
                },
                error: e => {
                    reject(e)
                }
            })
        })
    }
}
// 现在可以创建想要的任意多的仓库
var userRepository = new GenericRepositorWithConstraint<User>('./users.json')
userRepository.getAsync()
    .then(function(users : User[]) {
        console.log(users)
    })

在范型约束中使用多重类型

interface IMyInterface {
    doSomething()
}
interface IMySecondInterface {
    doSomethingElse()
}
// 转变为超接口
interface IChildInterface extends IMyInterface, IMySecondInterface {
    
}
class Example<T extends IChildInterface> {
    private genericProperty : T
    useT() {
        this.genericProperty.doSomething()
        this.genericProperty.doSomethingElse()
    }
}

范型中new操作

function factoryNotWorking<T>() : T {
    return new T()// 找不到标识T,编译错误
}

//要通过范型代码来创建新的对象,需要声明范型T拥有构造函数
// 即用type: { new(): T }代替type: T
function factory<T>() : T {
    var type: { new() : T }
    return new type()
}
var myClass: MyClass = factory<MyClass>()

遵循SOLID原则

里氏替换原则

派生类对象能够替换其基类对象被使用

// 将一些对象持久化到某种存储中
interface PersistanceServiceInterface {
    save(entity : any) : number
}
// 实现接口,使用cookie作为存储介质
class CookiePersitanceService implements PersistanceServiceInterface {
    save(entity : any) : number {
        var id = Math.floor((Math.random() * 100) + 1)
        // Cookie持久化逻辑
        return id
    }
}
//一个基于PersistanceServiceInterface依赖的类
class FavouritesController {
    private _persistanceService : PersistanceServiceInterface
    constructor(persistanceService : PersistanceServiceInterface) {
        this._persistanceService = persistanceService
    }
    public saveAsFavourite(articleId : number) {
        return this._persistanceService.save(articleId)
    }
}

//创建实例
var favController = new FavouritesController(new CookiePersitanceService())

//LSP允许将依赖换成其他的实现,只要实现是基于同一个基类的
// 存储介质使用H5本地存储
class LocalStoragePersitanceService implements PersistanceServiceInterface {
    save(entity : any) :number {
        var id = Math.floor((Math.random() * 100) + 1)
        //本地存储持久化逻辑
        return id
    }
}

// 现在可以在不需要对FavouritesController控制类做任何修改的情况下用他替换
var favController2 = new FavouritesController(new LocalStoragePersitanceService())

接口隔离原则

接口被用来声明两个或更多的应用组件间是如何互相操作和交换信息的。

接口隔离原则代表客户端不应强制依赖于他没有使用到的方法。在应用组件内声明API时,声明多个针对特定客户端的接口,要好于声明一个大而全的接口。


interface VehicleInterface {
    getSpeed() : number
    getVehicleType() : string
    istaxPayed() : boolean
    isLightsOn() : boolean
    isLightsOff() : boolean
    startEngine() : void
    acelerate() : number
    stopEngine() : void
    startRadio() : void
    playCd : void
    stopRadio() : void
}

优化方案

interface VehicleInterface {
    getSpeed() : number
    getVehicleType() : string
    istaxPayed() : boolean
    isLightsOn() : boolean
}
interface LightsInterface {
    isLightsOn() : boolean
    isLightsOff() : boolean
}
interface RadioInterface {
    startRadio() : void
    playCd() : void
    stopRadio() : void
}

依赖反转原则

一个方法应该遵从依赖于抽象而不是一个实例。

命名空间

如果在写一个大型应用,在代码量增加的时候需要引入某种代码组织方案避免命名冲突,并使代码更加容易跟踪和理解。可以用命名空间包裹那些有联系的接口、类和对象。 

namespace app {
    export namespace models {
        //可以简写 app.models
        export class UserModel {
            //...
        }
        export class TalkModel {
            //...
        }
    }
}

var user = new app.models.UserModel()
var talk = new app.models.TalkModel()

模块

与命名空间的区别:在声明了所有的模块之后,我们不会使用<script>标签引入它们,而是通过模块加载器来加载。

模块加载器是在模块加过程中为我们提供更好控制能力的工具,可以优化加载任务(如异步加载或合并多个模块到单一文件)。

使用<script>标签的方式并不被推荐,因为当浏览器发现一个<script>标签时,它不会使用异步请求加载这个文件。应该尽可能地尝试异步加载文件,因为这样能显著提高Web程序的网络性能。

常用模块加载器:

  • RequireJS:  RequireJS使用了一个被称作异步模块定义的语法(AMD)
  • Browserify:该语法被称作CommonJS
  • SystemJS:一个通用模块加载器。支持所有的模块定义语法(ES6、AMD、UMD)

TS允许选择在运行环境中使用哪一种模块定义语法

tsc --module commonjs main.ts // CommonJS
tsc --module amd main.ts // AMD
tsc --module umd main.ts // UMD
tsc --module system main.ts // SystemJS

在程序设计阶段只能是选择两种模块定义语法

  • 外部模块语法
  • ES6模块语法

设计阶段和运行时使用的模块语法可以不一样

ES6模块

class UserModel {
    //...
}
export { UserModel }

 或

export class UserModel {
    //...
}

别名输出

class UserModel {
    //...
}
export { UserModel as User } // UserModel输出为User

一个export输出所有同名定义

interface UserModel {
    //...
}
class UserModel {
    //...
}
export { UserModel } //输出接口和函数

引入

import { UserModel } from './models.js'

导出多个实体

class UserValidator {
    //...
}
class TalkValidator {
    //...
}
export { UserValidator, TalkValidator }

导入


import { UserValidator, TalkValidator } from './validation.ts'

外部模块语法——仅在设计阶段可用

一旦编译成JS,将会被转换成AMD、CommonJS、UMD或SystemJS。应避免使用这种语法而应使用新的ES6语法代替。

导入

import User = require('./user_class')

直接导出

export class User {
    // ...
}

赋值导出

class User {
    //...
}
export = User

AMD模块定义语法——仅在运行时使用

初始的外部模块语法编译成AMD

define(['require', 'exports'], function(require, exports) {
    var UserModel = (function () {
        function UserModel() {}
        return UserModel
    })()
    return UserModel
})

define函数第一个参数为数组,包含了依赖的模块名列表。第二个参数是一个回调函数,这个函数将在所有依赖全部加载完成时执行一次。

CommonJS模块定义语法——仅在运行时使用

class User {
    //...
}
export = User

生成CommonJS

var UserModel = (function () {
    function UserModel() {
        //...
    }
    return UserModel
})()
module.exports = UserModel

上面的CommonJS模块不需要任何修改就能被NodeJS程序使用import和require关键字加载

import UserModel = require('./UserModel')
var user = new UserModel()

当尝试在浏览器中使用require时,会抛出异常,因为require未被定义。可以使用Browserify解决

1.安装

npm i -g browserify

2.将所有的CommonJS模块打包成一个JS文件

browserify main.js -o bundle.js

3.在<script>标签中引入bundle.js

UMD模块定义语法——仅在运行时使用

如果要发布一个JS库或者框架,需要将TS编译成CommonJS和AMD。

定义通用模块

(function (root, factory) {
    if (typeof exports === 'object') {
        module.exports = factory(require('b)) //CommonJS
    } else if (typeof define === 'function' && define.amd) {
        // AMD
        define(['b'], function(b) {
            return (root.returnExportGlobal = factory(b))
        })
    } else {
        //全局变量
        root.returnExportGlobal = factory(root.b)
    }
}(this, function(b) {
    //真正的模块
    return {}
}))

其他方法实现UMD

1.使用--compile umd标识

2.使用模块加载器,比如Browserify

SystemJS模块定义——仅在运行时使用

SystemJS可以使在不兼容ES6的浏览器上,更加贴近它们语义地使用ES6模块定义方法。

循环依赖

类似A依赖B,B依赖A

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值