鸿蒙ArkTS面向对象实战:类、继承、接口与泛型全解析

发布时间:2026/9/10 8:17:12
鸿蒙ArkTS面向对象实战:类、继承、接口与泛型全解析 Ark这个词现在指什么基本不用多解释就是ArkTS/ArkUI这一整套鸿蒙应用开发体系。挺多人把它当成TypeScript的鸿蒙版上手就开始写页面但真正做复杂业务的时候才会发现面向对象的底子不过关代码很快就烂成一锅粥。我在几个Ark项目里最大的感受是ArkTS的面向对象既不是摆设也不是照搬Java它有自己的脾气和习惯。这篇文章就把我实际写过的类、继承、接口、泛型以及踩过的坑都摊开聊一聊适合刚入坑Ark开发、或者从Java/C/Python/C#转过来的同学尤其如果你正打算做那种带数据模型、文件读写结构的课程设计比如股票交易系统之类的这篇会有直接帮助。1. ArkTS的类与对象先别急着写页面把数据模型想清楚很多人学Ark开发一上来就奔着EntryComponent写UI忙着调布局、调动画结果业务一复杂就发现页面里塞了一大堆逻辑改一个字段要翻好几个文件。这其实是把面向对象这层给跳过了。ArkTS说到底还是一门现代静态类型语言它的核心优势就是可以用class构建清晰的数据模型把界面和业务拆开。1.1 类和对象的定义比普通TS多了一点“正式感”ArkTS定义类的方式和TypeScript很像但又有一些自己的约束。先看一段最朴素的写法// DataModel.ets export class Stock { private code: string ; private name: string ; private price: number 0; constructor(code: string, name: string, price: number) { this.code code; this.name name; this.price price; } public getCode(): string { return this.code; } public getName(): string { return this.name; } public getPrice(): number { return this.price; } // 计算市值A股一手是100股这里按手算更符合交易场景 public marketValue(hands: number): number { return this.price * hands * 100; } }这里有几个细节值得说。第一属性默认值不是必须的但建议写避免构造器里漏初始化导致后面到处是undefined判断。第二所有成员必须显式标注类型ArkTS在这点上比JS严格得多不允许any满天飞。第三构造器里this.code code这种赋值是刚需别想着像Python那样用kwargs偷懒。你要是从Java转过来这套东西几乎是零成本迁移。你要是从C转过来除了没有析构函数和运算符重载其他都很眼熟。你要是从Python转过来会明显感觉“变量还得写类型真麻烦”但项目大了之后这种麻烦恰恰是保护你的。1.2 访问修饰符、getter/setter和静态成员把内部状态看管好ArkTS对访问控制的支持是完整的public默认、private私有、protected受保护。这块在传统面向对象里是常识但在实际Ark开发中经常被当成摆设很多人直接class Stock { code: string }然后到处stock.code xxx短期爽长期痛。我给一个更完整的写法把price的修改做一层控制免得外部随意改出负价格export class Stock { private code: string ; private name: string ; private price: number 0; constructor(code: string, name: string, price: number) { this.code code; this.name name; this.setPrice(price); } public getPrice(): number { return this.price; } public setPrice(price: number): void { if (price 0) { throw new Error(价格不能为负数); } this.price price; } }有人说ArkUI的State不是可以直接改吗这里要分清State是UI层的状态装饰器它面对的是页面渲染数据Stock这种业务模型类应该自己守好自己的边界。UI层直接透传改业务对象的内部字段一旦校验逻辑变了你得把所有改价格的地方全翻一遍。集中到setPrice()里只改一处。静态成员也是面向对象的一部分适合放常量或纯工具方法。比如export class MarketRules { public static readonly LOT_SIZE: number 100; public static readonly DEFAULT_COMMISSION_RATE: number 0.0003; public static calcCommission(amount: number): number { // 最低佣金5元不足按5元收这是实际交易里的规则 const commission amount * MarketRules.DEFAULT_COMMISSION_RATE; return commission 5 ? 5 : commission; } }这种类你不需要实例化纯粹是借用class的组织能力来分组管理常量和方法比散落一地的const变量好维护太多。2. 继承、多态和接口在组件复用与事件回调里用起来类只是面向对象的第一层。真正让你代码活起来的是继承、多态和接口。在Ark开发里这些不是纸面概念——你的页面之间有很多公共逻辑可以抽你的数据源可能有多种实现这些都离不开这几个特性。2.1 用继承抽取公共逻辑一个BaseViewModel的例子很多新手写Ark页面每个页面里都重复一套初始化逻辑比如读取本地缓存、处理加载状态、统一的错误弹窗。这种地方就是继承发挥价值的地方。假设我的项目里有多个页面都需要管理股票列表我可以写一个基类export abstract class BaseStockViewModel { protected loading: boolean false; protected errorMessage: string ; public async loadStockList(): Promisevoid { this.loading true; this.errorMessage ; try { await this.fetchFromRemote(); } catch (e) { this.errorMessage 加载失败请稍后重试; } finally { this.loading false; } } // 子类必须实现真正拉数据的逻辑 protected abstract fetchFromRemote(): Promisevoid; }然后不同的页面视图模型去继承它export class HotStockViewModel extends BaseStockViewModel { private hotStocks: Stock[] []; protected async fetchFromRemote(): Promisevoid { // 这里可以调用网络接口也可以从本地json读取 this.hotStocks await getHotStockList(); } public getHotStocks(): Stock[] { return this.hotStocks; } }这里有个关键点BaseStockViewModel把“加载中/报错/重试”这套通用流程固定下来子类只需要关心“怎么拉数据”。你用Java/C时怎么用抽象类这里就怎么用。ArkTS并没有因为它长了一张TS的脸就在面向对象上打折。2.2 接口与多态数据源可以随时换接口在ArkTS里是纯契约不包含实现。它的意义在于调用方只管依赖接口不依赖具体类这样换实现的时候调用方代码一个字都不用改。拿股票行情来说我可以定义一个行情数据源接口export interface IMarketDataSource { fetchStock(code: string): PromiseStock; subscribe(code: string, callback: (stock: Stock) void): void; }然后写两个实现一个是真实网络数据源一个是测试用的Mock数据源class MockMarketDataSource implements IMarketDataSource { async fetchStock(code: string): PromiseStock { // 模拟网络延迟 return new Stock(code, 测试股票 code, 10.5); } subscribe(code: string, callback: (stock: Stock) void): void { // 模拟推送实际项目里这里可能是WebSocket } }业务层只需要依赖接口不在乎你给他的是Mock还是真实数据源export class TradeService { private dataSource: IMarketDataSource; constructor(dataSource: IMarketDataSource) { this.dataSource dataSource; } public async getStockInfo(code: string): PromiseStock { return await this.dataSource.fetchStock(code); } }我把这个模式叫“插拔式数据源”。前端开发里大家经常提依赖注入在ArkTS里最简单的实践就是构造器接收接口而不是接收具体类。这样测试的时候塞Mock进去上线的时候塞真实实现进去其他代码一律不用动。3. 泛型、枚举与扩展面向对象配合泛型才不低水平重复如果只用继承和接口你的代码已经挺面向对象了。但真正写出不重复代码的秘诀是泛型。在ArkTS里泛型和class结合起来可以帮你封装掉大量重复逻辑。3.1 泛型仓库类一个Repository管所有数据我经常看到这种情况一个股票App有股票列表、自选列表、持仓列表三个列表的结构差不多于是有人复制粘贴了三份增删改查代码。这就是典型的“面向对象等于写了class但没写出抽象能力”。用泛型解决是这样的export class RepositoryT { private items: T[] []; public add(item: T): void { this.items.push(item); } public remove(item: T): boolean { const index this.items.indexOf(item); if (index -1) { this.items.splice(index, 1); return true; } return false; } public getList(): T[] { return this.items; } public find(predicate: (item: T) boolean): T | undefined { return this.items.find(predicate); } public clear(): void { this.items []; } }然后创建具体仓库就非常简单const stockRepo new RepositoryStock(); const positionRepo new RepositoryPosition(); stockRepo.add(new Stock(000001, 平安银行, 10.5)); const found stockRepo.find((stock) stock.getCode() 000001);泛型的核心价值在于RepositoryT只需要写一遍不管里面装的是Stock、Position还是别的什么类增删改查逻辑都复用。这个模式在Java里叫泛型类在C里叫模板类Python里大家喜欢用TypeVar思路完全一样。3.2 枚举和扩展方法让业务状态可读性翻倍面向对象的设计里枚举不是必须的但它在描述业务状态时确实比字符串好太多。比如一个订单的状态export enum TradeStatus { PENDING PENDING, SUCCESS SUCCESS, FAILED FAILED }在账户类里交易结果可以直接返回状态而不是返回一个暧昧的布尔值export class Account { private status: TradeStatus TradeStatus.PENDING; public getTradeStatus(): TradeStatus { return this.status; } public setTradeStatus(status: TradeStatus): void { this.status status; } }这时候如果你从Java转过来会发现枚举在ArkTS里就是个普通对象不必对它抱有太多幻想但该用还是得用至少比散落的字符串强。扩展方法这块ArkTS继承TS的能力可以给已有的类加方法但说实话实际项目里我反而建议少用。原因很简单ArkTS在编译期的限制比纯TS多扩展方法一旦过度使用出问题的时候排查链路会很奇怪。我更推荐用组合或者工具类替代比如上面的MarketRules.calcCommission()就是工具类方式比给Number.prototype挂方法安全得多。4. 从Java/C/Python/C#转过来几个想当然的坑这部分是干货中的干货。我在Ark项目里踩过的坑几乎都来自“老语言经验直接套用”结果被现实打脸。列出来希望你能绕开。4.1 State与class对象改了属性页面为什么不刷新这是新手最容易问的问题也是从其他语言转过来最不理解的地方。你写了一个class对象用State装饰然后在某个事件里改了对象内部属性页面纹丝不动。原因在于State的观察能力默认是浅观察它观察的是“这个变量有没有被重新赋值”而不是“这个对象内部的某个字段有没有变”。你直接写this.user.age 21;State根本感知不到这个变化。你需要要么给整个对象重新赋值const newUser new User(张三, 21); this.user newUser;要么用ArkUI提供的深度观察方案给类加Observed装饰器在子组件里用ObjectLink接收Observed export class User { name: string ; age: number 0; }Component struct UserCard { ObjectLink user: User; build() { Row() { Text(${this.user.name} - ${this.user.age}) } } }这个坑的根源是你以为在写Java/C的普通对象实际上你在和UI框架的状态管理机制打交道。面向对象在Ark里不只是数据组织方式还牵扯到状态的可观察性。4.2 struct是UI壳别把业务对象硬塞进去ArkUI里的页面组件是struct比如Entry Component struct Index {}。很多从Java Swing或C Qt转过来的同学会下意识觉得struct就是class的兄弟于是把大量业务逻辑直接写进struct的成员方法里甚至想用继承来复用struct。这里要说清楚ArkUI的struct是一个专属于UI描述层的结构它和业务class有本质区别。struct之间的继承能力是很弱的你没法像class那样让一个struct去继承另一个struct然后把公共逻辑美美地抽出来。组件的复用官方给的路线是自定义组件、Reusable或者把公共逻辑下沉到普通的class/ViewModel中而不是硬搞UI继承。我的建议是struct里只留UI状态和事件绑定任何需要被多处复用的业务行为都放到普通class里。这样你的struct薄如纸业务class厚如书项目才能扛得住迭代。4.3 对象引用、深拷贝和数组变更Python程序员可能对“对象传引用”的坑很熟Java程序员应该也不陌生。在ArkTS里如果你把一个class对象直接丢进数组然后修改这个对象数组里也会跟着变因为它们是同一个引用。这个特性本身不坑坑的是你把它当值类型用。比如const stock new Stock(000001, 平安银行, 10.5); const list: Stock[] [stock]; stock.setPrice(11.2); console.log(list[0].getPrice()); // 输出11.2如果你希望list里的数据是快照不被外部修改影响就得做深拷贝。ArkTS没有内置的深拷贝函数可以手写或用JSON序列化但要注意class实例经过JSON序列化再解析回来会丢失方法变成纯数据对象。const copy JSON.parse(JSON.stringify(stock)) as Stock;这种方法适合纯数据模型如果你依赖class里的方法就得考虑别的方式比如为Stock单独实现一个clone方法public clone(): Stock { return new Stock(this.code, this.name, this.price); }这是从C拷贝构造函数那里带过来的好习惯在ArkTS里同样适用。5. 实战拆解用面向对象重做股票交易系统的领域模型热搜里出现了“c股票交易系统 课程设计 控制台 面向对象 文件读写”这个关键词说明很多人在做类似的课设/练手项目。我直接用ArkTS把这套系统重做一遍。你对照着看会发现面向对象的设计思路是跨语言的。5.1 需求梳理与类设计一个控制台版的股票交易系统核心需求通常是几件事账户有资金、可以买入股票、可以卖出股票、查看当前持仓和收益。我们用面向对象思维拆解至少需要下面几个类类名职责核心成员依赖的OOP特性Stock描述一只股票的基础信息、当前价格code, name, price封装Position持仓记录包含股票和持有数量stock, volume, avgCost组合Account资金账户负责买卖动作和资产核算balance, positions封装、聚合TradeService交易入口依赖行情源account, dataSource接口、依赖注入这个表格看起来简单但它是整个系统的骨架。你写任何面向对象程序第一件事不是打开编辑器而是先在纸上把类和类之间的关系画明白。5.2 交易、持仓、账户三个核心对象先写业务规则再考虑UI下面是核心类的实现。不要一上来就写界面把业务对象先跑通。export class Position { private stock: Stock; private volume: number 0; private avgCost: number 0; constructor(stock: Stock, volume: number, avgCost: number) { this.stock stock; this.volume volume; this.avgCost avgCost; } public getStock(): Stock { return this.stock; } public getVolume(): number { return this.volume; } public getAvgCost(): number { return this.avgCost; } // 当前市值 public getMarketValue(): number { return this.stock.getPrice() * this.volume; } // 浮动盈亏 public getProfit(): number { return (this.stock.getPrice() - this.avgCost) * this.volume; } }一眼就能看出来Position知道如何计算自己的市值和盈亏不需要外部去拼公式。这就是封装的价值——业务规则收拢在对象内部外部只用管“这个持仓现在赚了还是亏了”。Account类负责资金和持仓的变动它必须保证“钱不够不能买”这个规则不被绕过export class Account { private balance: number 0; private positions: Position[] []; constructor(balance: number) { if (balance 0) { throw new Error(初始资金不能为负数); } this.balance balance; } public getBalance(): number { return this.balance; } public getPositions(): Position[] { return this.positions; } // 买入余额充足则扣款、加仓否则返回false public buy(stock: Stock, price: number, volume: number): boolean { const cost price * volume; if (cost this.balance) { return false; } this.balance - cost; const existing this.positions.find((p) p.getStock().getCode() stock.getCode()); if (existing) { // 这里只是简单演示实际上还要重新计算摊薄成本 const newAvgCost (existing.getAvgCost() * existing.getVolume() cost) / (existing.getVolume() volume); this.positions[this.positions.indexOf(existing)] new Position( stock, existing.getVolume() volume, newAvgCost ); } else { this.positions.push(new Position(stock, volume, price)); } return true; } }Account把“余额不足就不让买”这条规则锁死在buy方法里任何UI操作、任何外部调用都不可能绕过这个约束。这比把校验逻辑写在按钮点击事件里靠谱一万倍因为后者只需要多一个入口就崩了。5.3 用接口隔离行情源后续替换不再改业务代码整个交易系统最可能变的地方是行情数据从哪来。今天可以用Mock明天可能接真实行情API后天可能换成WebSocket推送。这就是第2章IMarketDataSource接口发挥作用的地方。TradeService作为交易入口只认接口export class TradeService { private account: Account; private dataSource: IMarketDataSource; constructor(account: Account, dataSource: IMarketDataSource) { this.account account; this.dataSource dataSource; } public async buyStock(code: string, volume: number): Promisestring { const stock await this.dataSource.fetchStock(code); if (this.account.buy(stock, stock.getPrice(), volume)) { return 买入成功${stock.getName()} ${volume}股; } return 买入失败资金不足; } public async sellStock(code: string, volume: number): Promisestring { const stock await this.dataSource.fetchStock(code); const position this.account.getPositions().find( (p) p.getStock().getCode() code ); if (!position || position.getVolume() volume) { return 卖出失败持仓不足; } this.account.sell(code, stock.getPrice(), volume); return 卖出成功${stock.getName()} ${volume}股; } }这套代码跑起来之后你再回头去看那些把股票、账户、交易逻辑全塞在一个文件里的写法就会发现面向对象的“分而治之”到底在解决什么问题每个类只操心自己的一亩三分地改动的影响范围被限制到最小。做课设的时候这种设计也特别加分因为注释都省了——类名本身就是文档。6. 从面向对象到面向场景我的一点个人体会写了这么多Ark的面向对象实践最想说的一点是面向对象不是目的而是让代码能活着走到下个版本的手段。ArkTS作为Ark开发的主力语言它的class、继承、接口、泛型这些能力完全够用关键是你愿不愿意在写UI之前先花点时间把模型层理清楚。我自己在这个项目里最大的体会是设计阶段多花半小时后期重构能少花五个小时。现在拿到一个Ark开发任务我的习惯是先别动手写Entry先在项目里建一个model目录把Stock、Position、Account这些业务类落好再谈页面。这个顺序看起来反直觉但真能帮你在代码越来越复杂的时候少薅点头发。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询