零基础鸿蒙应用开发第二十四节:商品类重构属性契约接口

零基础鸿蒙应用开发学习计划表

【学习目标】

  1. 针对抽象类设计的构造函数参数零散、属性缺乏统一规范两大核心痛点,掌握属性契约接口的定义与使用方法;
  2. 学会通过extends实现接口分层继承,构建“通用属性+子类独有属性”的属性规范体系;
  3. 掌握结构化接口对象替代零散参数的实战技巧,从根源上规避传参顺序、类型错误;
  4. 结合枚举状态(上下架)实现商品状态管理;
  5. 基于属性契约接口重构抽象类与子类,实现代码的健壮性与可维护性提升。

【学习重点】

  • 核心设计:通过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 上一节遗留的核心痛点

  1. 传参痛点:子类构造函数参数零散(如DigitalGoods有8个参数),参数顺序写错会导致业务逻辑错误,新增字段需修改所有调用处;
  2. 属性规范痛点:通用属性(name/price)与子类独有属性(brand/author)混编,无统一约束,新增商品类型易出现属性命名混乱;
  3. 状态管理痛点:缺少商品上下架状态管理,无法适配电商核心场景,若用布尔值表示状态语义化差;
  4. 商品管理痛点:缺少商品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 运行效果说明

属性契约接口

  1. 控制台输出:每个商品初始化时会打印唯一ID和完整基础信息,包含状态、折扣、利润等;
  2. UI交互
    • 点击“批量增加50件库存”:所有商品库存增加,UI实时刷新;
    • 点击“上架/下架商品”:库存为0时上架会触发Toast提示,状态切换后文字颜色同步变化;
    • 点击“清空库存”:库存清0后商品自动下架,状态文字变为红色;
  3. 语法校验:传参时缺少接口必填属性(如数码商品的brand)会直接编译报错,提前拦截错误。

七、核心优势对比(重构前 vs 重构后)

对比维度 上一节(抽象类+零散参数) 本节(抽象类+属性契约接口)
传参方式 多个零散参数,需记忆顺序,易出错 结构化接口对象,无需记顺序,静态校验类型
属性规范 通用属性与独有属性混编,无统一约束 接口分层管理,属性规范统一,扩展方便
状态管理 无上下架状态,无法适配电商场景 枚举状态语义化强,「库存为0自动下架」联动逻辑
维护成本 新增字段需修改所有构造函数调用处 新增字段仅需扩展接口,无需修改调用逻辑
数据校验 分散在构造函数中,无统一校验方法 集中在validateProps,逻辑更清晰
类型安全 传参类型全靠人工保障,易出现类型不匹配 接口强类型约束,编译期校验参数类型/完整性
商品标识 无唯一ID,无法精准管理 UUID生成唯一ID,适配后续商品管理场景

八、内容总结

  1. 本节通过属性契约接口分层设计,解决了抽象类构造函数参数零散、属性缺乏统一规范的核心痛点,结构化传参大幅降低传参错误风险;
  2. 新增GoodsStatus枚举实现商品状态的语义化管理,补充「库存为0自动下架」逻辑,完善电商场景核心功能;
  3. 新增goodsId只读属性并通过UUID生成唯一值,为后续商品统一管理、精准查找提供可靠标识,低侵入性融入现有逻辑;
  4. 重构后保留抽象类核心价值(强制子类实现抽象方法、属性复用),同时通过接口实现属性规范统一,兼顾复用性与可维护性;
  5. 接口的强类型约束实现编译期静态校验,相比零散传参大幅降低运行时错误概率。

九、代码仓库

十、下节预告

本节通过属性契约接口完成了商品类属性层的架构优化,下一节将聚焦接口的行为契约能力,突破抽象类单继承的限制:

  1. 设计IDiscountable(可打折)、IFullReduction(可满减)、IReturnable(可退换)等标准化行为契约接口;
  2. 设计促销活动规则属性契约,支持动态修改促销时间、折扣/满减规则;
  3. 演示商品子类按需实现多个行为接口,实现业务行为“按需扩展”;
  4. 构建“抽象类负责属性复用+接口负责行为规范”的电商架构,兼顾复用性与扩展性。

通过下一节学习,你将掌握ArkTS接口的核心实战场景,设计高扩展性的代码架构。

posted @ 2026-01-22 12:34  鸿蒙-散修  阅读(0)  评论(0)    收藏  举报