Harmony之路:实战起航(二)——数据模型与业务逻辑封装

引入

在上一篇中,我们搭建了清晰的项目结构,为应用奠定了坚实的架构基础。今天,我们将深入实战项目的核心——数据模型与业务逻辑封装。如果说项目结构是应用的骨架,那么数据模型就是血肉,业务逻辑则是神经中枢。合理的分层设计能让代码更易维护、测试和扩展,避免"面条式代码"的噩梦。

想象一下,当你的应用需要从本地存储切换到云端存储,或者需要支持多设备数据同步时,如果业务逻辑与数据源紧密耦合,修改成本将呈指数级增长。而通过今天学习的数据层抽象业务逻辑封装,这些问题都能迎刃而解。

讲解

一、数据模型设计原则

在HarmonyOS应用中,数据模型设计遵循**单一数据源(SSOT)**原则,即每种数据类型都应该有且只有一个权威的数据源。这能确保数据一致性,避免数据冲突。

数据模型分层架构:

应用层
├── UI组件(页面、组件)
├── 业务逻辑层(ViewModel、Service)
└── 数据层
    ├── 数据源抽象(Repository)
    ├── 本地数据源(Preferences、数据库)
    └── 远程数据源(网络API)

二、实体类设计

首先定义应用的核心数据模型。以待办事项应用为例:

// models/Todo.ts
export interface Todo {
  id: string;
  title: string;
  description?: string;
  completed: boolean;
  createdAt: Date;
  updatedAt: Date;
}

export interface TodoCreateParams {
  title: string;
  description?: string;
}

设计要点:

  • 使用接口而非类,保持数据不可变性
  • 区分实体类和创建参数类,避免污染实体属性
  • 使用可选属性(?)标记非必填字段

三、数据源抽象层(Repository模式)

Repository模式是数据层设计的核心,它对外提供统一的数据访问接口,隐藏底层数据源的实现细节。

// data/repositories/TodoRepository.ts
export interface TodoRepository {
  getAll(): Promise<Todo[]>;
  getById(id: string): Promise<Todo | null>;
  create(todo: TodoCreateParams): Promise<Todo>;
  update(id: string, updates: Partial<Todo>): Promise<Todo>;
  delete(id: string): Promise<void>;
  toggleComplete(id: string): Promise<Todo>;
}

接口设计的优势:

  • 支持多数据源(本地、网络、内存缓存)
  • 便于单元测试(Mock实现)
  • 支持热切换数据源(如离线优先策略)

四、本地数据源实现

4.1 使用Preferences存储

对于轻量级配置数据,使用Preferences是最佳选择:

// data/sources/LocalTodoSource.ts
import preferences from '@ohos.data.preferences';
import { Todo, TodoCreateParams } from '../models/Todo';

export class LocalTodoSource {
  private static readonly STORE_NAME = 'todos';
  private static readonly KEY_TODOS = 'todos_list';
  
  private prefs: preferences.Preferences | null = null;

  async initialize(context: common.Context): Promise<void> {
    this.prefs = await preferences.getPreferences(context, LocalTodoSource.STORE_NAME);
  }

  async getAll(): Promise<Todo[]> {
    if (!this.prefs) throw new Error('Preferences not initialized');
    
    const json = await this.prefs.get(LocalTodoSource.KEY_TODOS, '[]');
    return JSON.parse(json.toString());
  }

  async saveAll(todos: Todo[]): Promise<void> {
    if (!this.prefs) throw new Error('Preferences not initialized');
    
    await this.prefs.put(LocalTodoSource.KEY_TODOS, JSON.stringify(todos));
    await this.prefs.flush();
  }
}
4.2 使用关系型数据库

对于结构化数据,关系型数据库更合适:

// data/sources/DatabaseTodoSource.ts
import relationalStore from '@ohos.data.relationalStore';
import { Todo, TodoCreateParams } from '../models/Todo';

export class DatabaseTodoSource {
  private static readonly DB_NAME = 'todo.db';
  private static readonly TABLE_NAME = 'todos';
  
  private rdbStore: relationalStore.RdbStore | null = null;

  async initialize(context: common.Context): Promise<void> {
    const config: relationalStore.StoreConfig = {
      name: DatabaseTodoSource.DB_NAME,
      securityLevel: relationalStore.SecurityLevel.S1
    };
    
    this.rdbStore = await relationalStore.getRdbStore(context, config);
    
    // 创建表
    const sql = `
      CREATE TABLE IF NOT EXISTS ${DatabaseTodoSource.TABLE_NAME} (
        id TEXT PRIMARY KEY,
        title TEXT NOT NULL,
        description TEXT,
        completed INTEGER NOT NULL DEFAULT 0,
        createdAt TEXT NOT NULL,
        updatedAt TEXT NOT NULL
      )
    `;
    await this.rdbStore.executeSql(sql);
  }

  async getAll(): Promise<Todo[]> {
    if (!this.rdbStore) throw new Error('Database not initialized');
    
    const predicates = new relationalStore.RdbPredicates(DatabaseTodoSource.TABLE_NAME);
    const result = await this.rdbStore.query(predicates);
    
    const todos: Todo[] = [];
    while (result.goToNextRow()) {
      todos.push({
        id: result.getString(result.getColumnIndex('id')),
        title: result.getString(result.getColumnIndex('title')),
        description: result.getString(result.getColumnIndex('description')),
        completed: result.getLong(result.getColumnIndex('completed')) === 1,
        createdAt: new Date(result.getString(result.getColumnIndex('createdAt'))),
        updatedAt: new Date(result.getString(result.getColumnIndex('updatedAt')))
      });
    }
    return todos;
  }
}

五、网络数据源实现

// data/sources/RemoteTodoSource.ts
import http from '@ohos.net.http';
import { Todo, TodoCreateParams } from '../models/Todo';

export class RemoteTodoSource {
  private static readonly BASE_URL = 'https://api.example.com/todos';
  private httpClient: http.HttpClient;

  constructor() {
    this.httpClient = http.createHttp();
  }

  async getAll(): Promise<Todo[]> {
    const response = await this.httpClient.request(
      `${RemoteTodoSource.BASE_URL}`,
      { method: http.RequestMethod.GET }
    );
    
    if (response.responseCode !== 200) {
      throw new Error(`HTTP ${response.responseCode}: ${response.result}`);
    }
    
    return JSON.parse(response.result.toString());
  }

  async create(todo: TodoCreateParams): Promise<Todo> {
    const response = await this.httpClient.request(
      `${RemoteTodoSource.BASE_URL}`,
      {
        method: http.RequestMethod.POST,
        header: { 'Content-Type': 'application/json' },
        extraData: JSON.stringify(todo)
      }
    );
    
    if (response.responseCode !== 201) {
      throw new Error(`HTTP ${response.responseCode}: ${response.result}`);
    }
    
    return JSON.parse(response.result.toString());
  }
}

六、Repository实现

将多个数据源组合成统一的Repository:

// data/repositories/TodoRepositoryImpl.ts
import { Todo, TodoCreateParams } from '../models/Todo';
import { TodoRepository } from './TodoRepository';
import { LocalTodoSource } from '../sources/LocalTodoSource';
import { RemoteTodoSource } from '../sources/RemoteTodoSource';

export class TodoRepositoryImpl implements TodoRepository {
  private localSource: LocalTodoSource;
  private remoteSource: RemoteTodoSource;
  private isOnline: boolean = false;

  constructor(context: common.Context) {
    this.localSource = new LocalTodoSource();
    this.remoteSource = new RemoteTodoSource();
    
    // 初始化本地数据源
    this.localSource.initialize(context);
    
    // 监听网络状态
    this.checkNetworkStatus();
  }

  async getAll(): Promise<Todo[]> {
    if (this.isOnline) {
      try {
        const todos = await this.remoteSource.getAll();
        await this.localSource.saveAll(todos);
        return todos;
      } catch (error) {
        console.warn('Failed to fetch from remote, falling back to local', error);
      }
    }
    return this.localSource.getAll();
  }

  async create(todo: TodoCreateParams): Promise<Todo> {
    const newTodo: Todo = {
      id: this.generateId(),
      title: todo.title,
      description: todo.description,
      completed: false,
      createdAt: new Date(),
      updatedAt: new Date()
    };

    if (this.isOnline) {
      try {
        const created = await this.remoteSource.create(todo);
        await this.localSource.saveTodo(created);
        return created;
      } catch (error) {
        console.warn('Failed to create on remote, saving locally', error);
      }
    }
    
    await this.localSource.saveTodo(newTodo);
    return newTodo;
  }

  private generateId(): string {
    return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }

  private async checkNetworkStatus(): Promise<void> {
    // 实际项目中应使用@ohos.net.connection监听网络状态
    this.isOnline = true; // 简化实现
  }
}

七、业务逻辑层封装

业务逻辑层负责处理复杂的业务规则,将数据层与UI层解耦:

// business/TodoService.ts
import { Todo, TodoCreateParams } from '../models/Todo';
import { TodoRepository } from '../data/repositories/TodoRepository';

export class TodoService {
  constructor(private repository: TodoRepository) {}

  async getTodos(): Promise<Todo[]> {
    return this.repository.getAll();
  }

  async addTodo(params: TodoCreateParams): Promise<Todo> {
    if (!params.title.trim()) {
      throw new Error('Title cannot be empty');
    }
    
    if (params.title.length > 100) {
      throw new Error('Title cannot exceed 100 characters');
    }
    
    return this.repository.create(params);
  }

  async toggleTodo(id: string): Promise<Todo> {
    const todo = await this.repository.getById(id);
    if (!todo) {
      throw new Error('Todo not found');
    }
    
    return this.repository.update(id, {
      completed: !todo.completed,
      updatedAt: new Date()
    });
  }

  async deleteTodo(id: string): Promise<void> {
    const todo = await this.repository.getById(id);
    if (!todo) {
      throw new Error('Todo not found');
    }
    
    return this.repository.delete(id);
  }
}

八、ViewModel层实现

ViewModel负责管理UI状态,响应式更新UI:

// viewmodels/TodoViewModel.ts
import { Todo, TodoCreateParams } from '../models/Todo';
import { TodoService } from '../business/TodoService';

export class TodoViewModel {
  @State todos: Todo[] = [];
  @State loading: boolean = false;
  @State error: string | null = null;

  constructor(private todoService: TodoService) {}

  async loadTodos(): Promise<void> {
    this.loading = true;
    this.error = null;
    
    try {
      this.todos = await this.todoService.getTodos();
    } catch (err) {
      this.error = err instanceof Error ? err.message : 'Failed to load todos';
    } finally {
      this.loading = false;
    }
  }

  async addTodo(title: string): Promise<void> {
    this.error = null;
    
    try {
      await this.todoService.addTodo({ title });
      await this.loadTodos(); // 重新加载列表
    } catch (err) {
      this.error = err instanceof Error ? err.message : 'Failed to add todo';
    }
  }

  async toggleTodo(id: string): Promise<void> {
    try {
      await this.todoService.toggleTodo(id);
      // 本地更新,避免重新加载
      const index = this.todos.findIndex(todo => todo.id === id);
      if (index !== -1) {
        this.todos[index] = {
          ...this.todos[index],
          completed: !this.todos[index].completed,
          updatedAt: new Date()
        };
      }
    } catch (err) {
      this.error = err instanceof Error ? err.message : 'Failed to toggle todo';
    }
  }
}

九、依赖注入与单例管理

为了便于管理和测试,使用单例模式管理服务实例:

// managers/ServiceManager.ts
import { TodoRepositoryImpl } from '../data/repositories/TodoRepositoryImpl';
import { TodoService } from '../business/TodoService';

export class ServiceManager {
  private static instance: ServiceManager;
  private todoService: TodoService | null = null;

  private constructor() {}

  static getInstance(): ServiceManager {
    if (!ServiceManager.instance) {
      ServiceManager.instance = new ServiceManager();
    }
    return ServiceManager.instance;
  }

  getTodoService(context: common.Context): TodoService {
    if (!this.todoService) {
      const repository = new TodoRepositoryImpl(context);
      this.todoService = new TodoService(repository);
    }
    return this.todoService;
  }
}

十、UI层使用示例

// pages/TodoPage.ets
import { TodoViewModel } from '../viewmodels/TodoViewModel';
import { ServiceManager } from '../managers/ServiceManager';

@Entry
@Component
struct TodoPage {
  private viewModel: TodoViewModel;

  aboutToAppear() {
    const todoService = ServiceManager.getInstance().getTodoService(getContext(this));
    this.viewModel = new TodoViewModel(todoService);
    this.viewModel.loadTodos();
  }

  build() {
    Column() {
      if (this.viewModel.loading) {
        LoadingComponent()
      } else if (this.viewModel.error) {
        ErrorComponent(this.viewModel.error)
      } else {
        TodoListComponent({
          todos: this.viewModel.todos,
          onToggle: (id: string) => this.viewModel.toggleTodo(id)
        })
      }
    }
  }
}

小结

通过本文的学习,我们掌握了HarmonyOS应用数据模型与业务逻辑封装的核心方法。分层架构让我们将数据存储、业务规则和UI展示彻底解耦,每个层各司其职:

数据层:负责数据持久化和网络通信,通过Repository模式提供统一接口

业务层:封装复杂的业务逻辑和验证规则,确保数据一致性

ViewModel层:管理UI状态,响应式更新界面

UI层:专注于界面展示和用户交互

这种架构设计带来了三大优势:

  1. 可测试性:每层都可以独立测试,Mock依赖项
  2. 可维护性:修改数据源或业务逻辑不影响其他层
  3. 可扩展性:轻松添加新功能或切换数据源

在下一篇文章中,我们将进入实战项目的第三环节——UI组件化与重构,教你如何将复杂的UI拆分为可复用的自定义组件,进一步提升代码质量和开发效率。敬请期待!

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐