零基础鸿蒙应用开发第二十三节:抽象类的场景应用
【学习目标】
- 理解抽象类的核心设计意义,掌握ArkTS中
abstract关键字的使用规则,解决普通父类可实例化无业务意义对象的问题; - 掌握抽象方法的声明与实现规则,强制子类差异化实现核心业务逻辑(如折后价计算),解决普通父类方法默认实现的局限性;
- 掌握抽象类+多态的进阶应用,实现抽象类引用管理所有商品对象;
- 从语法、业务、工程化三个维度,吃透抽象类与普通类的核心差异,能在实际开发中正确选型。
【学习重点】
- 抽象类的核心特性:不能直接实例化,可包含普通属性/方法+抽象方法,是“半抽象半具体”的基类;
- 抽象方法的强制实现规则:抽象类中的抽象方法无方法体,子类必须用
override实现(显式重写),否则编译报错; - 鸿蒙电商场景下的实战改造:将
Goods普通类改为AbstractGoods抽象类,子类(数码、图书)实现抽象的折后价计算方法; - 抽象类+多态:通过抽象类引用统一管理商品列表;
- 抽象类与普通类的差异对比:重点关注实例化限制、方法强制实现、业务约束性三个核心维度。
一、工程结构
基于上一节ClassObjectDemo_1复制一份重命名为ClassObjectDemo_2,核心修改仅将父类改为抽象类:
ClassObjectDemo_2/
├── entry/ # 应用主模块
│ ├── src/
│ │ ├── main/
│ │ │ ├── ets/ # ArkTS代码根目录
│ │ │ │ ├── pages/ # 页面代码目录
│ │ │ │ │ └── Index.ets # 测试页面(新增UI渲染逻辑)
│ │ │ │ └── model/ # 业务模型类目录
│ │ │ │ ├── AbstractGoods.ets # 父类:抽象商品基类(替换原Goods.ets)
│ │ │ │ ├── DigitalGoods.ets # 子类:数码商品类(修改实现抽象方法)
│ │ │ │ └── BookGoods.ets # 子类:图书商品类(修改实现抽象方法)
│ │ │ ├── resources/ # 资源目录(非本节重点)
│ │ │ └── module.json5 # 模块配置文件
│ │ └── oh-package.json5 # 工程依赖配置
└── hvigorfile.ts # 构建脚本(默认生成)
二、抽象类的核心意义:解决普通父类的局限性
上一节我们发现普通父类Goods存在两大核心问题:
- 业务逻辑缺陷:可直接实例化
new Goods(...)创建无业务意义的“纯商品”对象,违背电商中“商品必须是具体类型(数码、图书)”的业务规则; - 技术约束缺陷:父类
calculateFinalPrice方法是固定实现(售价×折扣),无法强制子类根据业务场景差异化实现(如数码叠加会员折扣、图书叠加满减折扣),易导致业务逻辑不统一。
抽象类的核心价值:
- 从语法层面禁止抽象类的直接实例化,解决“无意义对象”问题;
- 从约束层面通过抽象方法强制子类实现核心业务逻辑,解决“逻辑统一化”问题;
- 保留普通类的代码复用特性(可包含普通属性、普通方法),是普通类与接口之间的“中间层”。
三、抽象类与抽象方法的语法规则
ArkTS中通过abstract关键字定义抽象类和抽象方法,核心语法与规则如下:
3.1 改造 Goods 类为抽象类
文件重命名:
- 右击原
Goods.ets文件名→选择Rename重命名为AbstractGoods.ets; - 类名
Goods修改为AbstractGoods,并添加abstract关键字; - 把
calculateFinalPrice改为抽象方法,保留原有普通属性和方法(如printBaseInfo、getter/setter)。
// model/AbstractGoods.ets
export abstract class AbstractGoods {
// 公共属性
public name: string;
public category: string = "商品分类";
// 保护属性:子类可访问,类外不可访问
protected costPrice: number;
// 私有属性:仅当前类可访问
private _price: number;
private _stock: number;
private _discount: number = 1;
constructor(
name: string,
price: number,
stock: number,
costPrice: number,
discount?: number,
category?: string
) {
this.name = name.trim() || "未命名商品";
this._price = Math.max(price, 0);
this._stock = Math.max(stock, 0);
this.costPrice = Math.max(costPrice, 0);
// 直接赋值私有属性,避免setter依赖未完全初始化的实例
if (discount && discount > 0 && discount <= 1) {
this._discount = discount;
}
this.category = category?.trim() || this.category;
}
// 售价的getter/setter
set price(newPrice: number) {
if (newPrice < 0) {
console.warn(`【${this.name}】售价不能为负,修改失败`);
return;
}
this._price = newPrice;
console.log(`【${this.name}】售价修改为:${newPrice}元`);
}
get price() {
return this._price;
}
// 折扣的getter/setter
set discount(newDiscount: number) {
if (newDiscount > 0 && newDiscount <= 1) {
this._discount = newDiscount;
console.log(`【${this.name}】折扣调整为:${newDiscount * 10}折`);
} else {
console.warn(`【${this.name}】折扣无效,需0~1之间,修改失败`);
}
}
get discount() {
return this._discount;
}
// 库存的getter/setter
set stock(newStock: number) {
if (newStock < 0) {
console.warn(`【${this.name}】库存不能为负,修改失败`);
return;
}
this._stock = newStock;
console.log(`【${this.name}】库存修改为:${newStock}件`);
}
get stock() {
return this._stock;
}
// 普通方法:打印商品基础信息
printBaseInfo(): void {
// 调用抽象方法(子类已强制实现,无运行时错误)
const finalPrice = this.calculateFinalPrice();
const showDiscount = this.discount * 10;
console.log(`
商品名称:${this.name}
商品分类:${this.category}
商品原价:${this.price}元
商品折扣:${showDiscount}折
折后售价:${finalPrice}元
商品库存:${this.stock}件
`);
}
// 抽象方法:强制子类实现差异化折后价计算
public abstract calculateFinalPrice(): number;
}
3.2 抽象类核心规则
- 抽象类不能直接实例化:
new AbstractParent("测试", 100)会直接编译报错,只能作为父类被继承; - 抽象方法必须在抽象类中:普通类中不能定义抽象方法,否则编译报错;
- 子类必须实现所有抽象方法:若子类未实现抽象类的任意一个抽象方法,子类也必须声明为抽象类(示例:
abstract class UnfinishedGoods extends AbstractParent {}); - 抽象类可包含普通成员:抽象类不是“全抽象”的,可包含普通属性、普通方法、构造函数,用于代码复用;
- 抽象方法无方法体:抽象方法仅定义方法签名(方法名、参数、返回值),具体实现由子类完成。
3.3 修改子类实现抽象方法
DigitalGoods数码商品类继承抽象类
新增会员折扣逻辑(折后价=售价×折扣×0.95),强制实现抽象方法calculateFinalPrice,并优化空值处理:
// model/DigitalGoods.ets
import { AbstractGoods } from './AbstractGoods';
export class DigitalGoods extends AbstractGoods {
// 专属属性:品牌、保修年限(私有变量+getter/setter)
private _brand: string;
private _warranty: number;
/**
* 构造函数:初始化父类属性+专属属性
*/
constructor(
name: string,
price: number,
stock: number,
costPrice: number,
brand: string,
warranty: number,
discount?: number,
category?: string
) {
// 补充默认分类,贴合电商业务逻辑
super(name, price, stock, costPrice, discount, category || "数码产品");
// 空值保护:避免brand为undefined时trim报错
this._brand = (brand || "").trim() || "未知品牌";
this._warranty = Math.max(warranty, 1); // 保修年限至少1年
}
// 品牌的getter/setter
set brand(newBrand: string) {
this._brand = (newBrand || "").trim() || "未知品牌";
}
get brand(): string {
return this._brand;
}
set warranty(newWarranty: number) {
if (newWarranty < 1) {
console.warn(`【${this.name}】保修期至少1年,修改失败`);
}
this._warranty = Math.max(newWarranty, 1);
}
get warranty(): number {
return this._warranty;
}
// 专属方法:计算单件利润
calculateProfit(): number {
const finalPrice = this.calculateFinalPrice();
return parseFloat((finalPrice - this.costPrice).toFixed(2));
}
// 重写普通方法:扩展商品信息打印
public override printBaseInfo(): void {
super.printBaseInfo(); // 复用父类逻辑
console.log(`
品牌:${this.brand}
保修年限:${this.warranty}年
单件利润:${this.calculateProfit()}元
`);
}
// 必须实现抽象方法:数码商品叠加会员折扣(额外95折)
public override calculateFinalPrice(): number {
const baseFinalPrice = this.price * this.discount;
const memberDiscountPrice = baseFinalPrice * 0.95; // 会员额外95折
return parseFloat(memberDiscountPrice.toFixed(2));
}
}
BookGoods图书商品类
新增满减逻辑(折后价≥50减10),规范私有属性+getter/setter:
// model/BookGoods.ets
import { AbstractGoods } from './AbstractGoods';
export class BookGoods extends AbstractGoods {
// 专属属性:作者、出版日期(私有变量+getter/setter)
private _author: string;
private _publishDate: string;
constructor(
name: string,
price: number,
stock: number,
costPrice: number,
author: string,
publishDate: string,
discount?: number,
category?: string
) {
// 补充默认分类,贴合电商业务逻辑
super(name, price, stock, costPrice, discount, category || "图书");
this._author = author.trim() || "未知作者";
this._publishDate = publishDate.trim() || "未知日期";
}
// 作者的getter/setter
set author(newAuthor: string) {
this._author = (newAuthor || "").trim() || "未知作者";
}
get author(): string {
return this._author;
}
// 出版日期的getter/setter
set publishDate(newDate: string) {
this._publishDate = (newDate || "").trim() || "未知日期";
}
get publishDate(): string {
return this._publishDate;
}
// 专属方法:计算图书利润
calculateBookProfit(): number {
const finalPrice = this.calculateFinalPrice();
return parseFloat((finalPrice - this.costPrice).toFixed(2));
}
// 重写普通方法:扩展商品信息打印
public override printBaseInfo(): void {
super.printBaseInfo();
console.log(`
作者:${this._author}
出版日期:${this._publishDate}
单件利润:${this.calculateBookProfit()}元
`);
}
// 必须实现抽象方法:图书商品满50减10
public override calculateFinalPrice(): number {
let finalPrice = this.price * this.discount;
// 满50减10
if (finalPrice >= 50) {
finalPrice -= 10;
}
return parseFloat(finalPrice.toFixed(2));
}
// 专属方法:获取作者信息
getAuthorInfo(): string {
return `《${this.name}》的作者是${this._author}`;
}
}
四、主页面:抽象类+多态
在Index.ets中,使用AbstractGoods抽象类引用统一管理所有子类对象,结合鸿蒙ArkUI实现极简商品列表渲染(UI仅为示例,核心聚焦抽象类+多态逻辑):
// pages/Index.ets
import { AbstractGoods } from '../model/AbstractGoods';
import { DigitalGoods } from '../model/DigitalGoods';
import { BookGoods } from '../model/BookGoods';
@Entry
@Component
struct Index {
// 商品列表:抽象类引用(多态核心)
@State goodsList: AbstractGoods[] = [];
// 页面加载时初始化商品数据
aboutToAppear(): void {
// 初始化商品数据(抽象类无法直接实例化,仅能创建子类对象)
const digital = new DigitalGoods("鸿蒙Mate70 Pro 手机", 5999, 100, 4000, "华为", 2, 0.8, "数码产品");
const book = new BookGoods("鸿蒙开发基础实战", 69, 500, 30, "散修", "2025-12-23", 0.7, "图书");
// 报错:Cannot create an instance of an abstract class. <ArkTSCheck>
// 抽象类无法直接实例化,仅能创建子类对象
// const abstrct:AbstractGoods = new AbstractGoods("鸿蒙开发基础实战", 69, 500, 30, "图书");
// 抽象类引用管理子类对象
this.goodsList = [digital, book];
// 控制台打印商品信息(验证抽象方法实现)
this.goodsList.forEach((goods, index) => {
console.log(`\n===== 商品${index + 1}信息 =====`);
goods.printBaseInfo();
});
}
// 批量更新库存方法
private batchUpdateStock(num: number): void {
this.goodsList.forEach(goods => {
goods.stock = goods.stock + num;
});
// 刷新UI(基础阶段简易实现)
const goodsList = this.goodsList
this.goodsList = [...goodsList];
}
build() {
Column() {
// 页面标题
Text("鸿蒙电商商品管理-抽象类与多态")
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ top: 20, bottom: 20 });
// 批量更新库存按钮
Button("批量增加50件库存")
.width(200)
.height(40)
.margin({ bottom: 20 })
.onClick(() => {
this.batchUpdateStock(50);
});
// 商品列表渲染(仅作示例演示)
List({ space: 10 }) {
ForEach(
this.goodsList,
(goods: AbstractGoods) => {
ListItem() {
Column({ space: 8 }) {
// 通用信息
Text(`商品名称:${goods.name}`)
.fontSize(18)
.fontWeight(FontWeight.Medium);
Text(`分类:${goods.category} | 原价:${goods.price}元 | 折扣:${goods.discount * 10}折`)
.fontSize(14);
Text(`折后价:${goods.calculateFinalPrice()}元 | 库存:${goods.stock}件`)
.fontSize(14)
.fontColor(Color.Green);
// 子类专属信息(类型判断)
if (goods instanceof BookGoods) {
Text(goods.getAuthorInfo())
.fontSize(14)
.fontColor(Color.Blue);
}
if (goods instanceof DigitalGoods) {
Text(`品牌:${goods.brand} | 保修:${goods.warranty}年`)
.fontSize(14)
.fontColor(Color.Orange);
}
}
.padding(15)
.backgroundColor(Color.White)
.borderRadius(8)
.shadow({ radius: 3, color: Color.Grey, offsetX: 2, offsetY: 2 })
.width('100%')
}.padding(15)
.width('100%')
}
);
}
.width('100%')
.layoutWeight(1);
}
.width('100%')
.height('100%')
.backgroundColor("#f5f5f5");
}
}
4.1 核心说明
- 抽象类多态体现:无论子类是数码还是图书,都可通过
AbstractGoods类型列表统一管理,调用calculateFinalPrice时自动执行子类的差异化实现; - 类型判断规则:调用子类专属方法/属性时,需通过
instanceof判断类型(编译器自动做静态检查,无需额外类型断言); - UI定位说明:本节UI仅为“抽象类数据层”的可视化示例,基础阶段无需深入ArkUI语法,核心掌握“抽象类+多态”的逻辑即可。
4.2 运行效果说明
- 控制台会打印两类商品的完整信息,包含基础信息、子类专属信息和差异化计算的折后价;
- 点击“批量增加50件库存”按钮,库存会更新并刷新UI;
- 抽象类无法直接实例化的语法限制会在编译阶段生效,避免创建无业务意义的对象。
4.3 抽象类与普通类的核心差异
| 对比维度 | 普通类(原Goods类) | 抽象类(AbstractGoods类) |
|---|---|---|
| 实例化 | 可直接new创建对象 |
不可直接实例化,仅能被继承 |
| 方法包含 | 仅能包含有方法体的普通方法 | 可包含普通方法+无方法体的抽象方法 |
| 子类约束 | 子类可选择是否重写父类方法 | 子类必须实现所有抽象方法(否则为抽象类) |
| 业务约束性 | 弱(可创建无意义的纯商品对象) | 强(强制子类为具体商品类型,实现差异化逻辑) |
| 工程化推荐场景 | 独立的业务实体(无子类扩展) | 作为基类统一管理子类(如商品基类) |
五、常见问题解答
5.1 抽象类可以有构造函数吗?
可以。抽象类的构造函数不能直接调用(因抽象类无法实例化),但子类构造函数必须通过super()调用抽象父类的构造函数,用于初始化父类属性,是代码复用的核心方式。
5.2 抽象方法可以有访问修饰符吗?
可以。抽象方法通常用public或protected(private无意义,子类无法访问),子类实现时的访问修饰符权限不能低于父类(如父类是public,子类不能改为protected)。
5.3 为什么不直接用接口代替抽象类?
ArkTS中接口仅能定义属性签名、方法签名,无法包含方法实现、实例属性初始化和构造函数;抽象类可同时包含“可复用的普通逻辑”和“需子类实现的抽象逻辑”,更适合作为有通用属性/方法的业务基类(如商品基类)。
六、内容总结
- 抽象类通过
abstract关键字定义,无法直接实例化,可包含普通属性/方法和抽象方法,解决了普通父类创建无意义对象的问题; - 抽象方法无方法体,子类必须用
override实现,强制子类差异化处理核心业务逻辑(如不同商品的折后价计算); - 抽象类+多态可统一管理所有子类对象,结合极简ArkUI可实现商品列表的可视化渲染(UI仅为示例,核心是抽象类逻辑);
- 抽象类是“半抽象半具体”的基类,兼顾代码复用与业务约束,是鸿蒙开发中管理多子类场景的核心选型。
七、代码仓库
- 工程名称:
ClassObjectDemo_2 - 仓库地址:https://gitee.com/juhetianxia321/harmony-os-code-base.git
八、下节预告
本节通过抽象类解决了商品类“无意义实例化”和“核心逻辑强制实现”的问题,但当前设计仍存在两大痛点:
- 构造函数参数零散,易出现顺序/类型错误,后期新增字段维护成本高;
- 通用属性与子类独有属性混编,缺乏统一规范,扩展时易出现命名混乱。
下一节将聚焦商品类重构拓展,基于属性契约接口优化架构:
- 定义通用属性契约接口(
IGoodsBase),规范所有商品的公共属性; - 设计子类独有属性契约接口,通过
extends继承通用接口,实现属性分层管理; - 用结构化接口对象替代零散参数,从根源上规避传参错误;
- 重构商品类并完成实战验证,实现“逻辑与数据”解耦,提升代码健壮性。
浙公网安备 33010602011771号