零基础鸿蒙应用开发第二十四节:商品类重构属性契约接口
【学习目标】
- 针对抽象类设计的构造函数参数零散、属性缺乏统一规范两大核心痛点,掌握属性契约接口的定义与使用方法;
- 学会通过
extends实现接口分层继承,构建“通用属性+子类独有属性”的属性规范体系; - 掌握结构化接口对象替代零散参数的实战技巧,从根源上规避传参顺序、类型错误;
- 结合枚举状态(上下架)实现商品状态管理;
- 基于属性契约接口重构抽象类与子类,实现代码的健壮性与可维护性提升。
【学习重点】
- 核心设计:通过
IBaseGoods通用属性契约接口规范公共属性,IDigitalGoods/IBookGoods继承通用接口补充独有属性; - 关键优化:将抽象类与子类的构造函数零散参数替换为接口对象,彻底解决传参痛点;
- 状态管理:新增
GoodsStatus枚举(上下架),默认商品状态为下架,实现「手动修改库存为0时自动下架」逻辑; - 商品id:新增商品
goodsId,通过获取UUID保证商品id的唯一性; - 工程落地:新增
interface目录统一管理属性契约接口,保持文件分类规范。
一、工程结构
基于上一节ClassObjectDemo_2复制并重命名为ClassObjectDemo_3,核心调整为新增接口目录、枚举文件,重构抽象类与子类的构造函数传参方式:
ClassObjectDemo_3/
├── entry/ # 应用主模块
│ ├── src/
│ │ ├── main/
│ │ │ ├── ets/ # ArkTS代码根目录
│ │ │ │ ├── pages/ # 页面代码目录
│ │ │ │ │ └── Index.ets # 测试页面(重构后验证)
│ │ │ │ ├── model/ # 抽象类、子类、枚举目录
│ │ │ │ │ ├── AbstractGoods.ets # 抽象商品基类(重构传参方式)
│ │ │ │ │ ├── DigitalGoods.ets # 数码商品子类(重构)
│ │ │ │ │ ├── BookGoods.ets # 图书商品子类(重构)
│ │ │ │ │ └── GoodsStatus.ets # 新增:商品状态枚举(上下架)
│ │ │ │ └── interface/ # 新增:属性契约接口目录
│ │ │ │ ├── IBaseGoods.ets # 新增:通用商品属性契约接口
│ │ │ │ ├── IDigitalGoods.ets # 新增:数码商品属性契约接口
│ │ │ │ └── IBookGoods.ets # 新增:图书商品属性契约接口
│ │ │ ├── resources/ # 资源目录(非本节重点)
│ │ │ └── module.json5 # 模块配置文件
│ │ └── oh-package.json5 # 工程依赖配置
└── hvigorfile.ts # 构建脚本(默认生成)
二、核心痛点回顾与解决方案
2.1 上一节遗留的核心痛点
- 传参痛点:子类构造函数参数零散(如
DigitalGoods有8个参数),参数顺序写错会导致业务逻辑错误,新增字段需修改所有调用处; - 属性规范痛点:通用属性(
name/price)与子类独有属性(brand/author)混编,无统一约束,新增商品类型易出现属性命名混乱; - 状态管理痛点:缺少商品上下架状态管理,无法适配电商核心场景,若用布尔值表示状态语义化差;
- 商品管理痛点:缺少商品id,若后期实现商品统一管理,无法通过id精准查找。
2.2 解决方案思路
- 属性契约接口分层:定义
IBaseGoods管理通用属性,IDigitalGoods/IBookGoods继承通用接口并补充独有属性,形成统一的属性规范; - 结构化传参:将构造函数的零散参数替换为接口对象,传参时无需记忆参数顺序,静态类型校验参数类型;
- 枚举状态:定义
GoodsStatus枚举(仅ON_SHELF/OFF_SHELF),默认商品状态为下架,新增「手动修改库存为0时自动下架」逻辑; - 商品id只读属性:定义
goodsId属性,使用readonly修饰为只读,通过UUID保证商品id唯一性。
三、第一步:定义属性契约接口与状态枚举
3.1 定义商品状态枚举(model/GoodsStatus.ets)
// model/GoodsStatus.ets
// 商品状态枚举:语义化管理商品上下架状态,避免布尔值语义模糊问题
export enum GoodsStatus {
// 上架状态:正常销售
ON_SHELF = 'ON_SHELF',
// 下架状态:人工/系统下架
OFF_SHELF = 'OFF_SHELF'
}
3.2 定义通用属性契约接口(interface/IBaseGoods.ets)
规范所有商品的公共属性,与抽象类的通用属性一一对应,包含状态枚举字段:
// interface/IBaseGoods.ets
import { GoodsStatus } from '../model/GoodsStatus';
// 通用商品属性契约接口:规范所有商品的公共属性
// 注:接口中未显式声明readonly,因商品核心属性(如name/price)需支持运行时修改;
// 若需固定不可变属性,可添加readonly关键字(如readonly id: string;)
export interface IBaseGoods {
// 商品名称(必填)
name: string;
// 商品售价(必填)
price: number;
// 商品库存(必填)
stock: number;
// 商品成本价(必填)
costPrice: number;
// 商品分类(可选)
category?: string;
// 折扣系数(0-1,可选,默认1)
discount?: number;
// 商品状态(可选,默认下架)
status?: GoodsStatus;
}
3.3 定义数码类独有属性契约接口(interface/IDigitalGoods.ets)
继承IBaseGoods并补充数码商品独有属性,保持属性规范的分层管理:
// interface/IDigitalGoods.ets
import { IBaseGoods } from './IBaseGoods';
// 数码商品属性契约接口:继承通用属性,补充独有属性
export interface IDigitalGoods extends IBaseGoods {
// 商品品牌(必填)
brand: string;
// 保修年限(必填)
warranty: number;
}
3.4 定义图书商品属性接口(interface/IBookGoods.ets)
继承IBaseGoods并补充图书商品独有属性:
// interface/IBookGoods.ets
import { IBaseGoods } from './IBaseGoods';
// 图书商品属性契约接口:继承通用属性,补充独有属性
export interface IBookGoods extends IBaseGoods {
// 图书作者(必填)
author: string;
// 出版日期(必填)
publishDate: string;
}
四、重构抽象商品类(model/AbstractGoods.ets)
核心改动:构造函数接收IBaseGoods接口对象,替代零散参数,保留抽象方法、getter/setter、数据校验等核心逻辑,新增「库存为0自动下架」「UUID生成商品id」逻辑:
// model/AbstractGoods.ets
import { IBaseGoods } from '../interface/IBaseGoods';
import { GoodsStatus } from './GoodsStatus';
import { util } from '@kit.ArkTS';
export abstract class AbstractGoods {
// 新增:商品唯一标识(只读,通过UUID生成)
public readonly goodsId: string;
// 公共属性
public name: string;
public category: string = "商品分类";
public status: GoodsStatus; // 新增:商品状态(枚举类型)
// 保护属性:子类可访问,类外不可访问
protected costPrice: number;
// 私有属性
private _price: number;
private _stock: number;
private _discount: number = 1;
/**
* 重构点:构造函数接收IBaseGoods接口对象,替代零散参数
* 结构化传参优势:新增/删减属性时,仅需修改接口定义,无需调整构造函数参数顺序,降低维护成本
* @param props 符合通用属性契约的对象
*/
constructor(props: IBaseGoods) {
// 数据校验:提前拦截非法参数,避免后续逻辑出错
this.validateProps(props);
// 初始化通用属性
this.name = props.name.trim() || "未命名商品";
this._price = Math.max(props.price, 0);
this._stock = Math.max(props.stock, 0);
this.costPrice = Math.max(props.costPrice, 0);
this.category = props.category?.trim() || this.category;
this._discount = props.discount && props.discount > 0 && props.discount <= 1 ? props.discount : 1;
this.status = props.status ?? GoodsStatus.OFF_SHELF; // 默认下架
// 生成唯一商品id(仅模拟器/真机可用,预览模式无此API)
this.goodsId = util.generateRandomUUID();
console.log(`【${this.name}】生成唯一ID:${this.goodsId}`);
}
/**
* 数据校验方法:集中管理参数校验逻辑,提升代码可读性
* 注:数据校验仅在构造函数执行时触发,若需运行时修改属性也校验,可将逻辑迁移至对应属性的setter中
*/
private validateProps(props: IBaseGoods): void {
// 名称非空校验
if (!props.name || props.name.trim() === '') {
throw new Error('商品名称不能为空');
}
// 数值非负校验
if (props.price < 0 || props.stock < 0 || props.costPrice < 0) {
throw new Error('价格、库存、成本价不能为负数');
}
// 状态与库存联动校验:上架状态但库存为0,强制改为下架
if (props.status === GoodsStatus.ON_SHELF && props.stock === 0) {
props.status = GoodsStatus.OFF_SHELF;
}
}
// 售价setter(保留负数校验)
set price(newPrice: number) {
if (newPrice < 0) {
console.warn(`【${this.name}】售价不能为负,修改失败`);
return;
}
this._price = newPrice;
console.log(`【${this.name}】售价修改为:${newPrice}元`);
}
// 售价getter
get price(): number {
return this._price;
}
// 折扣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之间的系数`);
}
}
// 折扣getter
get discount(): number {
return this._discount;
}
// 库存setter(新增「库存为0自动下架」逻辑)
set stock(num: number) {
if (num < 0) {
console.warn(`【${this.name}】库存不能为负,修改失败`);
return;
}
this._stock = num;
console.log(`【${this.name}】库存修改为:${num}件`);
// 核心逻辑:库存为0时自动下架
if (this._stock === 0) {
this.status = GoodsStatus.OFF_SHELF;
console.log(`【${this.name}】库存为0,自动下架`);
}
}
// 库存getter
get stock(): number {
return this._stock;
}
// 打印基础信息(新增状态展示)
public printBaseInfo(): void {
const finalPrice = this.calculateFinalPrice();
console.log(`
===== 商品基础信息 =====
商品ID:${this.goodsId}
商品名称:${this.name}
商品分类:${this.category}
商品原价:${this._price}元
商品折扣:${this._discount * 10}折
折后售价:${finalPrice}元
库存数量:${this._stock}件
商品状态:${this.status === GoodsStatus.ON_SHELF ? '上架' : '下架'}`);
}
// 抽象方法:强制子类实现差异化折后价计算(核心逻辑保留)
public abstract calculateFinalPrice(): number;
}
五、重构商品子类
5.1 重构数码商品子类(model/DigitalGoods.ets)
核心改动:构造函数接收IDigitalGoods接口对象,保留会员折扣逻辑与属性校验:
// model/DigitalGoods.ets
import { AbstractGoods } from './AbstractGoods';
import { IDigitalGoods } from '../interface/IDigitalGoods';
export class DigitalGoods extends AbstractGoods {
// 专属属性(私有变量+getter/setter)
private _brand: string;
private _warranty: number;
/**
* 重构点:构造函数接收IDigitalGoods接口对象
* 结构化传参优势:无需记忆参数顺序,新增属性仅扩展接口即可
* @param props 符合数码商品属性契约的对象
*/
constructor(props: IDigitalGoods) {
super(props); // 传递通用属性给父类
// 初始化独有属性并校验
this._brand = props.brand.trim() || "未知品牌";
this._warranty = Math.max(props.warranty, 1); // 保修年限至少1年
}
// 品牌的getter/setter(保留空值校验)
set brand(newBrand: string) {
this._brand = newBrand.trim() || "未知品牌";
}
get brand(): string {
return this._brand;
}
// 保修年限getter/setter(保留最小值校验)
set warranty(newWarranty: number) {
if (newWarranty < 1) {
console.warn(`【${this.name}】保修期至少1年,修改失败`);
return;
}
this._warranty = newWarranty;
}
get warranty(): number {
return this._warranty;
}
// 实现抽象方法:数码商品会员额外95折(核心逻辑保留)
public override calculateFinalPrice(): number {
const baseFinalPrice = this.price * this.discount;
const memberDiscountPrice = baseFinalPrice * 0.95;
return parseFloat(memberDiscountPrice.toFixed(2));
}
// 计算单件利润(核心逻辑保留)
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()}元
`);
}
}
5.2 重构图书商品子类(model/BookGoods.ets)
核心改动:构造函数接收IBookGoods接口对象,保留满50减10逻辑:
// model/BookGoods.ets
import { AbstractGoods } from './AbstractGoods';
import { IBookGoods } from '../interface/IBookGoods';
export class BookGoods extends AbstractGoods {
// 专属属性(私有变量+getter/setter)
private _author: string;
private _publishDate: string;
/**
* 重构点:构造函数接收IBookGoods接口对象
* @param props 符合图书商品属性契约的对象
*/
constructor(props: IBookGoods) {
super(props); // 传递通用属性给父类
// 初始化独有属性并校验
this._author = props.author.trim() || "未知作者";
this._publishDate = props.publishDate.trim() || "未知日期";
}
// 计算图书单件利润(核心逻辑保留)
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()}元
`);
}
// 获取作者信息(核心逻辑保留)
getAuthorInfo(): string {
return `《${this.name}》的作者是${this._author}`;
}
// 实现抽象方法:图书满50减10(核心逻辑保留)
public override calculateFinalPrice(): number {
let finalPrice = this.price * this.discount;
if (finalPrice >= 50) {
finalPrice -= 10;
}
return parseFloat(finalPrice.toFixed(2));
}
// 作者setter(保留空值校验)
set author(newAuthor: string) {
this._author = newAuthor.trim() || "未知作者";
}
// 作者getter
get author(): string {
return this._author;
}
// 出版日期setter(保留空值校验)
set publishDate(newDate: string) {
this._publishDate = newDate.trim() || "未知日期";
}
// 出版日期getter
get publishDate(): string {
return this._publishDate;
}
}
六、实战验证(pages/Index.ets)
使用结构化接口对象传参创建实例,验证重构后的功能,保留UI渲染逻辑并优化交互:
import { AbstractGoods } from '../model/AbstractGoods';
import { DigitalGoods } from '../model/DigitalGoods';
import { BookGoods } from '../model/BookGoods';
import { IDigitalGoods } from '../interface/IDigitalGoods';
import { GoodsStatus } from '../model/GoodsStatus';
import { IBookGoods } from '../interface/IBookGoods';
import { prompt } from '@kit.ArkUI';
@Entry
@Component
struct Index {
// 商品列表:抽象类引用(多态核心)
@State goodsList: AbstractGoods[] = [];
// 页面初始化:创建商品实例
aboutToAppear(): void {
// 1. 创建数码商品实例:结构化传参(无需记忆参数顺序)
const digitalProps: IDigitalGoods = {
name: "鸿蒙Mate70手机",
price: 5999,
stock: 100,
costPrice: 4000,
discount: 0.8,
category: "数码产品",
status: GoodsStatus.ON_SHELF,
brand: "华为",
warranty: 2
};
const digitalGoods = new DigitalGoods(digitalProps);
// 2. 创建图书商品实例:未传status,默认下架
const bookProps: IBookGoods = {
name: "鸿蒙开发实战",
price: 69,
stock: 500,
costPrice: 30,
discount: 0.7,
category: "图书",
author: "散修",
publishDate: "2025-01"
};
const bookGoods = new BookGoods(bookProps);
// 3. 验证核心功能:打印商品信息
this.goodsList = [digitalGoods, bookGoods];
this.goodsList.forEach(goods => {
goods.printBaseInfo();
});
}
// 批量更新库存方法(浅拷贝触发UI刷新)
private batchUpdateStock(num: number): void {
this.goodsList.forEach(goods => {
goods.stock += num;
});
this.goodsList = [...this.goodsList];
}
// 商品上下架切换(含库存校验)
private toggleStatus(index: number): void {
const newGoodsList = [...this.goodsList];
const targetGoods = newGoodsList[index];
// 业务校验:库存为0禁止上架
if (targetGoods.status === GoodsStatus.OFF_SHELF && targetGoods.stock === 0) {
prompt.showToast({ message: `${targetGoods.name}库存为0,无法上架` });
return;
}
// 切换状态
targetGoods.status = targetGoods.status === GoodsStatus.ON_SHELF
? GoodsStatus.OFF_SHELF
: GoodsStatus.ON_SHELF;
// 刷新UI
this.goodsList = newGoodsList;
}
// 清空商品库存(触发自动下架逻辑)
private clearStock(index: number): void {
const newGoodsList = [...this.goodsList];
newGoodsList[index].stock = 0; // 库存清0会自动触发下架
this.goodsList = newGoodsList;
}
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, index: number) => {
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}件 | 状态:${goods.status === GoodsStatus.ON_SHELF ? '在售' : '下架'}`)
.fontSize(14)
.fontColor(goods.status === GoodsStatus.ON_SHELF ? Color.Green : Color.Red);
// 子类专属信息(类型判断)
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);
}
// 操作按钮
Row(){
Button(goods.status === GoodsStatus.OFF_SHELF ? "上架商品" : "下架商品")
.onClick(() => this.toggleStatus(index))
.backgroundColor(goods.status === GoodsStatus.OFF_SHELF ? Color.Red : Color.Green)
.margin({ right: 10 });
Button("清空库存")
.onClick(() => this.clearStock(index))
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 8 });
}
.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");
}
}
6.1 运行效果说明

- 控制台输出:每个商品初始化时会打印唯一ID和完整基础信息,包含状态、折扣、利润等;
- UI交互:
- 点击“批量增加50件库存”:所有商品库存增加,UI实时刷新;
- 点击“上架/下架商品”:库存为0时上架会触发Toast提示,状态切换后文字颜色同步变化;
- 点击“清空库存”:库存清0后商品自动下架,状态文字变为红色;
- 语法校验:传参时缺少接口必填属性(如数码商品的
brand)会直接编译报错,提前拦截错误。
七、核心优势对比(重构前 vs 重构后)
| 对比维度 | 上一节(抽象类+零散参数) | 本节(抽象类+属性契约接口) |
|---|---|---|
| 传参方式 | 多个零散参数,需记忆顺序,易出错 | 结构化接口对象,无需记顺序,静态校验类型 |
| 属性规范 | 通用属性与独有属性混编,无统一约束 | 接口分层管理,属性规范统一,扩展方便 |
| 状态管理 | 无上下架状态,无法适配电商场景 | 枚举状态语义化强,「库存为0自动下架」联动逻辑 |
| 维护成本 | 新增字段需修改所有构造函数调用处 | 新增字段仅需扩展接口,无需修改调用逻辑 |
| 数据校验 | 分散在构造函数中,无统一校验方法 | 集中在validateProps,逻辑更清晰 |
| 类型安全 | 传参类型全靠人工保障,易出现类型不匹配 | 接口强类型约束,编译期校验参数类型/完整性 |
| 商品标识 | 无唯一ID,无法精准管理 | UUID生成唯一ID,适配后续商品管理场景 |
八、内容总结
- 本节通过属性契约接口分层设计,解决了抽象类构造函数参数零散、属性缺乏统一规范的核心痛点,结构化传参大幅降低传参错误风险;
- 新增
GoodsStatus枚举实现商品状态的语义化管理,补充「库存为0自动下架」逻辑,完善电商场景核心功能; - 新增
goodsId只读属性并通过UUID生成唯一值,为后续商品统一管理、精准查找提供可靠标识,低侵入性融入现有逻辑; - 重构后保留抽象类核心价值(强制子类实现抽象方法、属性复用),同时通过接口实现属性规范统一,兼顾复用性与可维护性;
- 接口的强类型约束实现编译期静态校验,相比零散传参大幅降低运行时错误概率。
九、代码仓库
- 工程名称:
ClassObjectDemo_3 - 仓库地址:https://gitee.com/juhetianxia321/harmony-os-code-base.git
十、下节预告
本节通过属性契约接口完成了商品类属性层的架构优化,下一节将聚焦接口的行为契约能力,突破抽象类单继承的限制:
- 设计
IDiscountable(可打折)、IFullReduction(可满减)、IReturnable(可退换)等标准化行为契约接口; - 设计促销活动规则属性契约,支持动态修改促销时间、折扣/满减规则;
- 演示商品子类按需实现多个行为接口,实现业务行为“按需扩展”;
- 构建“抽象类负责属性复用+接口负责行为规范”的电商架构,兼顾复用性与扩展性。
通过下一节学习,你将掌握ArkTS接口的核心实战场景,设计高扩展性的代码架构。
浙公网安备 33010602011771号