08 ArkTS 类与静态方法设计:单例、DAO 与设计令牌
前言

图:08 ArkTS 类与静态方法设计:单例、DAO 与设计令牌 运行效果截图(HarmonyOS NEXT)
ArkTS 完整支持 TypeScript 的面向对象特性------类(class)、继承(extends)、接口(implements)、泛型(generics)。但在鸿蒙应用开发中,类的使用场景有其特有的模式:单例模式管理有状态的服务 (如数据库连接)、纯静态类承载设计令牌 (如颜色、字体常量)、全静态方法类封装数据操作(如 DAO 类)。
本文以"鹿鹿"项目中的三类典型类设计------DatabaseService(单例)、AppColors(静态常量)、UserDao(全静态方法)------为例,深入讲解 ArkTS 类的设计模式与最佳实践。
ArkTS 类定义文档:developer.huawei.com
项目源码:harmony-app GitHub

图:三种类设计模式------单例类(DatabaseService)、静态常量类(AppColors)、全静态方法类(DAO)
一、ArkTS 类的基础特性
1.1 类语法概览
typescript
class Animal {
// 成员变量(属性)
name: string;
private age: number;
protected species: string = 'Unknown';
readonly birthYear: number;
// 构造函数
constructor(name: string, age: number) {
this.name = name;
this.age = age;
this.birthYear = new Date().getFullYear() - age;
}
// 实例方法
speak(): string {
return `${this.name} says hello`;
}
// 静态方法
static create(name: string): Animal {
return new Animal(name, 0);
}
// Getter
get info(): string {
return `${this.name}, age ${this.age}`;
}
}
1.2 访问修饰符
| 修饰符 | 可见范围 | 用途 |
|---|---|---|
public(默认) |
任何地方 | 对外暴露的接口 |
private |
类内部 | 内部实现细节(如 _instance) |
protected |
类及子类 | 子类可以访问的属性 |
readonly |
任何地方(只读) | 初始化后不可修改的常量 |
static |
通过类名访问 | 类级别(无需实例)的属性/方法 |
二、单例模式:DatabaseService
2.1 为什么需要单例
DatabaseService 管理着 RDB 数据库连接,整个应用只需要一个数据库连接实例。使用单例模式确保:
- 连接复用:避免重复创建数据库文件和连接
- 全局访问 :任何 DAO 类都可以通过
DatabaseService.getInstance()获取同一个连接 - 初始化控制:确保数据库只初始化一次,防止并发初始化导致的数据库文件损坏
#mermaid-svg-Xg6qvMConXvhJone{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Xg6qvMConXvhJone .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Xg6qvMConXvhJone .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Xg6qvMConXvhJone .error-icon{fill:#552222;}#mermaid-svg-Xg6qvMConXvhJone .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Xg6qvMConXvhJone .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Xg6qvMConXvhJone .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Xg6qvMConXvhJone .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Xg6qvMConXvhJone .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Xg6qvMConXvhJone .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Xg6qvMConXvhJone .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Xg6qvMConXvhJone .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Xg6qvMConXvhJone .marker.cross{stroke:#333333;}#mermaid-svg-Xg6qvMConXvhJone svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Xg6qvMConXvhJone p{margin:0;}#mermaid-svg-Xg6qvMConXvhJone .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Xg6qvMConXvhJone .cluster-label text{fill:#333;}#mermaid-svg-Xg6qvMConXvhJone .cluster-label span{color:#333;}#mermaid-svg-Xg6qvMConXvhJone .cluster-label span p{background-color:transparent;}#mermaid-svg-Xg6qvMConXvhJone .label text,#mermaid-svg-Xg6qvMConXvhJone span{fill:#333;color:#333;}#mermaid-svg-Xg6qvMConXvhJone .node rect,#mermaid-svg-Xg6qvMConXvhJone .node circle,#mermaid-svg-Xg6qvMConXvhJone .node ellipse,#mermaid-svg-Xg6qvMConXvhJone .node polygon,#mermaid-svg-Xg6qvMConXvhJone .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Xg6qvMConXvhJone .rough-node .label text,#mermaid-svg-Xg6qvMConXvhJone .node .label text,#mermaid-svg-Xg6qvMConXvhJone .image-shape .label,#mermaid-svg-Xg6qvMConXvhJone .icon-shape .label{text-anchor:middle;}#mermaid-svg-Xg6qvMConXvhJone .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Xg6qvMConXvhJone .rough-node .label,#mermaid-svg-Xg6qvMConXvhJone .node .label,#mermaid-svg-Xg6qvMConXvhJone .image-shape .label,#mermaid-svg-Xg6qvMConXvhJone .icon-shape .label{text-align:center;}#mermaid-svg-Xg6qvMConXvhJone .node.clickable{cursor:pointer;}#mermaid-svg-Xg6qvMConXvhJone .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Xg6qvMConXvhJone .arrowheadPath{fill:#333333;}#mermaid-svg-Xg6qvMConXvhJone .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Xg6qvMConXvhJone .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Xg6qvMConXvhJone .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Xg6qvMConXvhJone .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Xg6qvMConXvhJone .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Xg6qvMConXvhJone .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Xg6qvMConXvhJone .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Xg6qvMConXvhJone .cluster text{fill:#333;}#mermaid-svg-Xg6qvMConXvhJone .cluster span{color:#333;}#mermaid-svg-Xg6qvMConXvhJone div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Xg6qvMConXvhJone .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Xg6qvMConXvhJone rect.text{fill:none;stroke-width:0;}#mermaid-svg-Xg6qvMConXvhJone .icon-shape,#mermaid-svg-Xg6qvMConXvhJone .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Xg6qvMConXvhJone .icon-shape p,#mermaid-svg-Xg6qvMConXvhJone .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Xg6qvMConXvhJone .icon-shape .label rect,#mermaid-svg-Xg6qvMConXvhJone .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Xg6qvMConXvhJone .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Xg6qvMConXvhJone .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Xg6qvMConXvhJone :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} EntryAbility
DatabaseService.getInstance
UserDao
ArchiveDao
HandwritingDao
ReportDao
lulu_handwriting.db
2.2 完整单例实现
typescript
// DatabaseService.ets
import relationalStore from '@ohos.data.relationalStore';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { common } from '@kit.AbilityKit';
const TAG = 'DatabaseService';
const DB_NAME = 'lulu_handwriting.db';
const DB_VERSION = 2;
export class DatabaseService {
// ① 私有静态实例(核心:单例容器)
private static _instance: DatabaseService | null = null;
// ② 私有实例属性
private store: relationalStore.RdbStore | null = null;
private initPromise: Promise<void> | null = null; // 防重入锁
// ③ 公开静态获取方法(懒汉式)
static getInstance(): DatabaseService {
if (!DatabaseService._instance) {
DatabaseService._instance = new DatabaseService();
}
return DatabaseService._instance;
}
// ④ 异步初始化方法
async init(context: common.UIAbilityContext): Promise<void> {
// 幂等性:已初始化则直接返回
if (this.store) {
hilog.info(0x0000, TAG, 'DB already initialized');
return;
}
// 防重入:正在初始化时返回同一个 Promise
if (this.initPromise) {
return this.initPromise;
}
// 开始初始化
this.initPromise = this.doInit(context);
return this.initPromise;
}
private async doInit(context: common.UIAbilityContext): Promise<void> {
try {
const config: relationalStore.StoreConfig = {
name: DB_NAME,
securityLevel: relationalStore.SecurityLevel.S2
};
this.store = await relationalStore.getRdbStore(context, config);
hilog.info(0x0000, TAG, 'RDB store created, version=%{public}d', DB_VERSION);
// 执行 DDL(创建表 / 版本升级)
await this.createTables();
} catch (e) {
hilog.error(0x0000, TAG, 'DB init failed: %{public}s', JSON.stringify(e));
this.initPromise = null; // 失败后清空,允许重试
throw e;
}
}
// ⑤ 安全获取 store
getStore(): relationalStore.RdbStore {
if (!this.store) {
throw new Error('DatabaseService 未初始化,请先调用 init()');
}
return this.store;
}
// ⑥ 状态查询
isReady(): boolean {
return this.store !== null;
}
// ⑦ 创建表(DDL)
private async createTables(): Promise<void> {
const ddl = [
`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
open_id TEXT, name TEXT NOT NULL,
avatar TEXT, created_at INTEGER NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS archives (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL, name TEXT NOT NULL,
emoji TEXT, relation TEXT, color TEXT,
last_record_at INTEGER, record_count INTEGER DEFAULT 0,
created_at INTEGER NOT NULL
)`,
// ... 其他表
];
for (const sql of ddl) {
await this.store!.executeSql(sql);
}
hilog.info(0x0000, TAG, 'All tables created/verified');
}
}
2.3 防重入初始化的设计
initPromise 字段是防止并发初始化的关键:
typescript
// 场景:多个 DAO 同时在启动阶段调用(竞态)
// 时刻 0: EntryAbility 调用 init()
// 时刻 1: UserDao.ensureLocalUser() 也调用了 init()(因为 DatabaseService 未就绪)
// 没有 initPromise 时:
// → 两次 getRdbStore() 并发执行 → 可能创建两个数据库文件 → 数据混乱
// 有 initPromise 时:
// 时刻 0: initPromise = doInit() ← 开始初始化
// 时刻 1: return this.initPromise ← 返回同一个 Promise,等待完成
// 时刻 2: 两个调用者都等到同一个初始化完成
三、静态常量类:AppColors
3.1 纯静态常量类设计
AppColors 是一个没有实例状态的静态工具类 ,所有成员都是 static readonly:
typescript
// AppColors.ets --- 完整颜色令牌定义
export class AppColors {
// 背景色系
static readonly BG: string = '#F1ECE2'; // 页面背景(米色)
static readonly CARD: string = '#F9F5EF'; // 卡片背景(浅米)
static readonly PAPER: string = '#F5EFE4'; // 纸张感(标签背景)
static readonly BG_DEEP: string = '#E8E0D5'; // 稍深背景
// 品牌色系(暖棕)
static readonly PRIMARY: string = '#A8907A'; // 主色(中棕)
static readonly PRIMARY_DEEP: string = '#8B7264'; // 深主色(深棕)
static readonly PRIMARY_LIGHT: string = '#C4AA8E'; // 浅主色(浅棕)
static readonly WARM: string = '#D9A689'; // 暖橙色
// 情绪色系(用于情绪可视化)
static readonly QUIET: string = '#7B96C2'; // 沉静蓝
static readonly ROSE: string = '#D4849A'; // 玫瑰粉
static readonly DEEP: string = '#6B5D52'; // 深棕
// 文字色系(三级层次)
static readonly TEXT: string = '#3D352E'; // 主文字(最深)
static readonly TEXT_2: string = '#8B7E72'; // 次要文字
static readonly TEXT_3: string = '#B5A99A'; // 第三级文字(最浅)
// 线条/边框
static readonly LINE: string = '#E8E0D5'; // 分割线、边框
}
3.2 使用方式
typescript
// 直接通过类名访问(无需实例化)
Text('标题').fontColor(AppColors.TEXT)
Row().backgroundColor(AppColors.CARD)
Circle().fill(AppColors.PRIMARY)
Column().borderColor(AppColors.LINE)
// 对比:不使用设计令牌的魔数写法(禁止)
// Text('标题').fontColor('#3D352E') // ❌ 禁止硬编码颜色
3.3 abstract class vs 普通 class
ArkTS 中可以用 abstract class 来禁止实例化:
typescript
// 使用 abstract class 明确禁止实例化
export abstract class AppAnimations {
static readonly FADE_IN_DURATION: number = 500;
static readonly RADAR_DRAW_DURATION: number = 800;
static readonly CURVE_SMOOTH: Curve = Curve.FastOutSlowIn;
}
// 使用普通 class(也可以,但理论上可以 new)
export class AppColors {
static readonly BG: string = '#F1ECE2';
// 如果不想被实例化,可以加私有构造函数:
// private constructor() {}
}
四、全静态方法类:UserDao
4.1 UserDao 设计
UserDao 使用全静态方法设计,无实例状态:
typescript
// UserDao.ets
import relationalStore from '@ohos.data.relationalStore';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { DatabaseService } from './DatabaseService';
import { UserEntity } from './Models';
const TAG = 'UserDao';
const TABLE = 'users';
export class UserDao {
// 私有 getStore:强制通过 DatabaseService 获取连接
private static getStore(): relationalStore.RdbStore {
return DatabaseService.getInstance().getStore();
}
// 1. 确保本地用户存在(首次启动时调用)
static async ensureLocalUser(defaultName: string): Promise<UserEntity> {
try {
const store = UserDao.getStore();
const predicates = new relationalStore.RdbPredicates(TABLE);
predicates.equalTo('open_id', 'local');
const rs = await store.query(predicates, ['id', 'open_id', 'name', 'created_at']);
if (rs.rowCount > 0) {
rs.goToFirstRow();
const user = UserDao.parseRow(rs);
rs.close();
return user;
}
rs.close();
// 不存在则创建
return await UserDao.createLocalUser(defaultName);
} catch (e) {
hilog.error(0x0000, TAG, 'ensureLocalUser failed: %{public}s', JSON.stringify(e));
throw e;
}
}
// 2. 创建本地用户
static async createLocalUser(name: string): Promise<UserEntity> {
const store = UserDao.getStore();
const entity: UserEntity = {
open_id: 'local',
name,
created_at: Date.now()
};
const values: relationalStore.ValuesBucket = {
open_id: 'local',
name,
created_at: entity.created_at
};
const id = await store.insert(TABLE, values);
return { ...entity, id };
}
// 3. 按华为账号 Open ID 查找
static async findByOpenId(openId: string): Promise<UserEntity | null> {
try {
const store = UserDao.getStore();
const predicates = new relationalStore.RdbPredicates(TABLE);
predicates.equalTo('open_id', openId);
const rs = await store.query(predicates);
if (rs.rowCount === 0) {
rs.close();
return null;
}
rs.goToFirstRow();
const user = UserDao.parseRow(rs);
rs.close();
return user;
} catch (e) {
hilog.error(0x0000, TAG, 'findByOpenId failed: %{public}s', JSON.stringify(e));
return null;
}
}
// 4. 更新用户名
static async updateName(id: number, name: string): Promise<void> {
const store = UserDao.getStore();
const values: relationalStore.ValuesBucket = { name };
const predicates = new relationalStore.RdbPredicates(TABLE);
predicates.equalTo('id', id);
await store.update(values, predicates);
}
// 私有解析方法
private static parseRow(rs: relationalStore.ResultSet): UserEntity {
return {
id: rs.getLong(rs.getColumnIndex('id')),
open_id: rs.getString(rs.getColumnIndex('open_id')),
name: rs.getString(rs.getColumnIndex('name')),
avatar: rs.isColumnNull(rs.getColumnIndex('avatar')) ? undefined
: rs.getString(rs.getColumnIndex('avatar')),
created_at: rs.getLong(rs.getColumnIndex('created_at'))
};
}
}
五、三种类设计模式对比
5.1 选型决策矩阵
| 场景 | 模式 | 示例 | 关键特征 |
|---|---|---|---|
| 管理有状态的系统资源 | 单例 | DatabaseService |
private static _instance |
| 承载只读配置常量 | 纯静态常量类 | AppColors、AppFonts、AppAnimations |
所有成员 static readonly |
| 封装无状态的数据操作 | 全静态方法类 | UserDao、ArchiveDao |
所有方法 static,无实例 |
| 管理 UI 组件状态 | @Component struct |
HandwritingRadar、HmTitleBar |
ArkUI 组件,有 build() |
5.2 单例 vs 全静态类
typescript
// 单例模式:需要持有状态(store 是实例状态)
class DatabaseService {
private store: RdbStore | null = null; // 实例状态
static getInstance(): DatabaseService { ... }
async init(): Promise<void> { ... }
getStore(): RdbStore { ... }
}
// 全静态类:无需持有状态(通过单例获取 store)
class UserDao {
// 没有任何实例变量
static async create(item: UserEntity): Promise<number> {
const store = DatabaseService.getInstance().getStore(); // 每次方法调用时获取
return store.insert('users', toValuesBucket(item));
}
}
六、继承与多态
6.1 抽象类与继承
typescript
// 抽象基类(不可直接实例化)
abstract class BaseAiService {
protected isInitialized: boolean = false;
// 抽象方法(子类必须实现)
abstract init(): Promise<boolean>;
// 模板方法
async ensureInit(): Promise<void> {
if (!this.isInitialized) {
this.isInitialized = await this.init();
}
}
}
// 具体实现类
class OcrService extends BaseAiService {
async init(): Promise<boolean> {
// OCR 模型加载逻辑
return true;
}
async recognize(imagePath: string): Promise<string> {
await this.ensureInit();
// OCR 识别逻辑
return '';
}
}
6.2 接口实现(implements)
typescript
interface Closeable {
close(): void;
}
interface Queryable<T> {
findById(id: number): Promise<T | null>;
listAll(): Promise<T[]>;
}
// 实现多个接口
class ResourceManager implements Closeable, Queryable<string> {
close(): void { /* 关闭资源 */ }
findById(id: number): Promise<string | null> { return Promise.resolve(null); }
listAll(): Promise<string[]> { return Promise.resolve([]); }
}
七、注意事项与常见问题
7.1 开发注意事项
在实际开发过程中,需特别注意以下几点:
- API 兼容性:部分接口仅在特定 HarmonyOS NEXT 版本中可用,需做版本条件判断
- 权限模型:采用静态声明(module.json5)+ 动态申请(requestPermissionsFromUser)的两阶段授权
- 生命周期 :合理使用
aboutToAppear()和aboutToDisappear()管理资源初始化与释放 - 状态同步 :跨页面数据通过 AppStorage 共享,组件内状态使用
@State/@Prop/@Link装饰器
7.2 常见错误与解决方案
常见问题快速排查表:
| 问题类型 | 排查方向 | 参考方法 |
|---|---|---|
| 应用崩溃 | 查看 hilog 错误日志 | hilog.error(TAG, "...", e.message) |
| 状态丢失 | 检查 AppStorage 键名拼写 | 统一使用常量管理键名 |
| 动画不流畅 | 避免在 animateTo 回调中执行 I/O | 动画与数据操作分离 |
八、最佳实践与性能优化
8.1 代码组织规范
以下规范可显著提升代码可读性与可维护性:
- 分层解耦:UI 逻辑(pages/)与数据操作(DAO/Service)严格分离,不在 build() 中调用数据库
- 组件拆分 :单个
@Component保持 100~200 行以内,大型组件拆分为@Builder子函数 - 令牌约束 :颜色、字号、间距统一引用
AppColors/AppFonts/AppAnimations,禁止魔数 - 错误处理 :异步方法统一返回
Promise<T | null>,异常时hilog.error记录并返回null
8.2 性能优化要点
以下是常见性能优化措施的对比分析:
| 优化维度 | 优化前 | 优化后 | 效果 |
|---|---|---|---|
| 列表渲染 | ForEach 全量渲染 |
LazyForEach 按需渲染 |
内存降低 40-60% |
| 图片加载 | 不指定尺寸 | 指定 width/height |
减少布局重计算 |
| 数据库查询 | 每次重新查询 | 合理缓存 + 精确 Predicates | 查询速度提升 5-10x |
| 动画实现 | 逐帧手动绘制 | animateTo 属性动画 |
稳定 60fps |
| 状态更新 | 全局刷新 | @State 精细化作用域 |
减少无效渲染 |
总结
ArkTS 类的设计在鸿蒙开发中形成了三种典型模式:
- 单例模式 (
DatabaseService):私有静态实例 +getInstance()+ 异步初始化 + 防重入锁 - 静态常量类 (
AppColors):所有成员static readonly,无实例状态,直接类名访问 - 全静态方法类 (
UserDao):所有方法static,依赖单例获取状态,无需实例化
每种模式对应不同的使用场景,合理选择能让代码更简洁、可维护。
下一篇预告 :第09篇 ArkTS 异步编程:Promise 与 async/await
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源: