跳转至

架构设计

学习目标

完成本模块后,你将能够: - 理解软件架构的重要性和基本概念 - 掌握常见的架构模式和设计原则 - 学会设计分层架构和模块化系统 - 了解接口设计和组件通信 - 应用架构设计到医疗器械软件开发

前置知识

  • 软件工程基础
  • 面向对象设计
  • 需求工程基础
  • C/C++编程经验

软件架构概述

什么是软件架构?

软件架构是软件系统的高层结构,定义了: - 系统的主要组件 - 组件之间的关系 - 组件的职责和接口 - 设计决策和约束

架构的重要性

在医疗器械软件开发中,良好的架构至关重要: - 安全性:隔离关键功能,降低风险 - 可维护性:便于理解、修改和扩展 - 可测试性:支持单元测试和集成测试 - 可追溯性:清晰的组件映射到需求 - 合规性:满足IEC 62304等标准要求

架构设计过程

graph LR
    A[需求分析] --> B[架构设计]
    B --> C[架构评审]
    C --> D{通过?}
    D -->|否| B
    D -->|是| E[详细设计]

说明: 这是架构设计流程图。展示了从需求分析到架构设计、架构评审、详细设计的迭代过程。如果评审不通过,需要返回架构设计阶段进行修改,确保架构质量。

架构设计原则

SOLID原则

S - 单一职责原则(Single Responsibility Principle): - 一个模块只负责一个功能 - 降低耦合,提高内聚 - 便于理解和维护

示例

// 不好的设计:一个模块做太多事情
typedef struct {
    void (*read_sensor)(void);
    void (*process_data)(void);
    void (*display_result)(void);
    void (*save_to_storage)(void);
} AllInOne;

// 好的设计:职责分离
typedef struct {
    void (*read)(void);
} SensorModule;

typedef struct {
    void (*process)(uint8_t* data);
} DataProcessor;

typedef struct {
    void (*display)(uint8_t* result);
} DisplayModule;

O - 开闭原则(Open-Closed Principle): - 对扩展开放,对修改关闭 - 通过抽象和多态实现 - 降低修改风险

L - 里氏替换原则(Liskov Substitution Principle): - 子类可以替换父类 - 保持接口一致性 - 确保多态正确性

I - 接口隔离原则(Interface Segregation Principle): - 接口应该小而专注 - 客户端不应依赖不需要的接口 - 避免"胖"接口

D - 依赖倒置原则(Dependency Inversion Principle): - 依赖抽象而非具体实现 - 高层模块不依赖低层模块 - 通过接口解耦

其他重要原则

关注点分离(Separation of Concerns): - 不同功能分离到不同模块 - 降低复杂度 - 提高可维护性

高内聚低耦合: - 模块内部紧密相关 - 模块之间松散连接 - 便于独立开发和测试

最小知识原则(Law of Demeter): - 模块只与直接朋友通信 - 减少依赖关系 - 降低耦合

分层架构

三层架构

┌─────────────────────────────────────┐
│      表示层 (Presentation Layer)     │
│  - 用户界面                          │
│  - 输入验证                          │
│  - 显示格式化                        │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│      业务逻辑层 (Business Layer)     │
│  - 业务规则                          │
│  - 数据处理                          │
│  - 算法实现                          │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│      数据访问层 (Data Access Layer)  │
│  - 数据存储                          │
│  - 数据检索                          │
│  - 数据持久化                        │
└─────────────────────────────────────┘

优点: - 清晰的职责划分 - 易于理解和维护 - 支持独立测试 - 便于团队协作

缺点: - 可能增加复杂度 - 性能开销 - 过度设计风险

医疗器械典型分层架构

┌─────────────────────────────────────┐
│      应用层 (Application Layer)      │
│  - 用户界面                          │
│  - 用户交互逻辑                      │
│  - 显示控制                          │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│      服务层 (Service Layer)          │
│  - 业务逻辑                          │
│  - 数据处理算法                      │
│  - 警报管理                          │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│      硬件抽象层 (HAL)                │
│  - 传感器接口                        │
│  - 执行器接口                        │
│  - 通信接口                          │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│      硬件层 (Hardware Layer)         │
│  - 传感器                            │
│  - 执行器                            │
│  - 处理器                            │
└─────────────────────────────────────┘

示例代码

// 硬件抽象层接口
typedef struct {
    int (*init)(void);
    int (*read)(uint8_t* buffer, size_t len);
    int (*write)(const uint8_t* buffer, size_t len);
    int (*deinit)(void);
} HAL_Interface;

// 传感器HAL实现
static HAL_Interface sensor_hal = {
    .init = sensor_init,
    .read = sensor_read,
    .write = NULL,  // 传感器只读
    .deinit = sensor_deinit
};

// 服务层使用HAL
int service_read_sensor_data(uint8_t* data) {
    // 通过HAL读取,不直接访问硬件
    return sensor_hal.read(data, DATA_SIZE);
}

模块化设计

模块定义

模块特征: - 明确的职责 - 定义良好的接口 - 信息隐藏 - 独立编译和测试

模块划分策略

按功能划分

modules/
├── sensor/          # 传感器模块
├── algorithm/       # 算法模块
├── display/         # 显示模块
├── storage/         # 存储模块
├── communication/   # 通信模块
└── alarm/          # 警报模块

按层次划分

modules/
├── application/     # 应用层
├── service/        # 服务层
├── hal/            # 硬件抽象层
└── driver/         # 驱动层

模块接口设计

接口设计原则: - 简单明了 - 完整性 - 一致性 - 最小化

接口示例

// sensor_module.h
#ifndef SENSOR_MODULE_H
#define SENSOR_MODULE_H

#include <stdint.h>
#include <stdbool.h>

// 传感器状态
typedef enum {
    SENSOR_OK = 0,
    SENSOR_ERROR_INIT,
    SENSOR_ERROR_READ,
    SENSOR_ERROR_TIMEOUT
} SensorStatus;

// 传感器数据
typedef struct {
    uint16_t value;
    uint32_t timestamp;
    bool valid;
} SensorData;

// 公共接口
SensorStatus sensor_init(void);
SensorStatus sensor_read(SensorData* data);
SensorStatus sensor_calibrate(void);
SensorStatus sensor_deinit(void);

#endif // SENSOR_MODULE_H

模块间通信

直接调用

// 模块A直接调用模块B
void moduleA_function(void) {
    moduleB_function();
}

回调函数

// 模块B注册回调到模块A
typedef void (*callback_t)(void);

void moduleA_register_callback(callback_t cb) {
    // 保存回调函数
}

void moduleA_trigger_event(void) {
    // 调用回调函数
    if (callback != NULL) {
        callback();
    }
}

消息队列

// 模块间通过消息队列通信
typedef struct {
    uint8_t type;
    uint8_t data[MAX_DATA_SIZE];
} Message;

void moduleA_send_message(Message* msg) {
    queue_push(msg);
}

void moduleB_receive_message(void) {
    Message msg;
    if (queue_pop(&msg)) {
        // 处理消息
    }
}

常见架构模式

1. 管道-过滤器模式(Pipe-Filter)

适用场景:数据处理流水线

结构

输入 → 过滤器1 → 过滤器2 → 过滤器3 → 输出

示例:信号处理

// 过滤器接口
typedef struct {
    void (*process)(uint8_t* input, uint8_t* output);
} Filter;

// 滤波器1:去噪
void denoise_filter(uint8_t* input, uint8_t* output) {
    // 去噪算法
}

// 滤波器2:平滑
void smooth_filter(uint8_t* input, uint8_t* output) {
    // 平滑算法
}

// 管道
void signal_pipeline(uint8_t* raw_data, uint8_t* processed_data) {
    uint8_t temp[DATA_SIZE];
    denoise_filter(raw_data, temp);
    smooth_filter(temp, processed_data);
}

2. 发布-订阅模式(Publish-Subscribe)

适用场景:事件驱动系统

结构

发布者 → 事件总线 → 订阅者1
                  → 订阅者2
                  → 订阅者3

示例:警报系统

// 事件类型
typedef enum {
    EVENT_ALARM_HIGH,
    EVENT_ALARM_LOW,
    EVENT_SENSOR_ERROR
} EventType;

// 订阅者回调
typedef void (*subscriber_callback_t)(EventType event, void* data);

// 订阅
void event_subscribe(EventType event, subscriber_callback_t callback);

// 发布
void event_publish(EventType event, void* data);

// 使用示例
void alarm_handler(EventType event, void* data) {
    if (event == EVENT_ALARM_HIGH) {
        // 处理高警报
    }
}

void init_alarm_system(void) {
    event_subscribe(EVENT_ALARM_HIGH, alarm_handler);
}

3. 状态机模式(State Machine)

适用场景:复杂状态管理

结构

stateDiagram-v2
    [*] --> Idle
    Idle --> Measuring: Start
    Measuring --> Processing: DataReady
    Processing --> Displaying: Complete
    Displaying --> Idle: Done
    Measuring --> Error: Fault
    Error --> Idle: Reset

示例:测量设备状态机

// 状态定义
typedef enum {
    STATE_IDLE,
    STATE_MEASURING,
    STATE_PROCESSING,
    STATE_DISPLAYING,
    STATE_ERROR
} DeviceState;

// 事件定义
typedef enum {
    EVENT_START,
    EVENT_DATA_READY,
    EVENT_COMPLETE,
    EVENT_DONE,
    EVENT_FAULT,
    EVENT_RESET
} DeviceEvent;

// 状态机结构
typedef struct {
    DeviceState current_state;
    void (*state_handlers[5])(DeviceEvent event);
} StateMachine;

// 状态处理函数
void idle_state_handler(DeviceEvent event) {
    if (event == EVENT_START) {
        // 转换到测量状态
        state_machine.current_state = STATE_MEASURING;
    }
}

void measuring_state_handler(DeviceEvent event) {
    if (event == EVENT_DATA_READY) {
        state_machine.current_state = STATE_PROCESSING;
    } else if (event == EVENT_FAULT) {
        state_machine.current_state = STATE_ERROR;
    }
}

// 状态机执行
void state_machine_execute(DeviceEvent event) {
    state_handlers[current_state](event);
}

4. 分层状态机(Hierarchical State Machine)

适用场景:复杂嵌套状态

优点: - 减少状态转换数量 - 提高可维护性 - 支持状态继承

接口设计

接口类型

硬件接口: - 传感器接口 - 执行器接口 - 通信接口

软件接口: - 模块间接口 - API接口 - 回调接口

用户接口: - 图形界面 - 命令行界面 - 物理按钮

接口设计最佳实践

1. 使用抽象接口

// 抽象存储接口
typedef struct {
    int (*open)(const char* name);
    int (*read)(void* buffer, size_t size);
    int (*write)(const void* buffer, size_t size);
    int (*close)(void);
} StorageInterface;

// 具体实现:Flash存储
static StorageInterface flash_storage = {
    .open = flash_open,
    .read = flash_read,
    .write = flash_write,
    .close = flash_close
};

// 具体实现:SD卡存储
static StorageInterface sd_storage = {
    .open = sd_open,
    .read = sd_read,
    .write = sd_write,
    .close = sd_close
};

2. 错误处理

// 返回错误码
typedef enum {
    SUCCESS = 0,
    ERROR_INVALID_PARAM = -1,
    ERROR_TIMEOUT = -2,
    ERROR_NO_MEMORY = -3
} ErrorCode;

ErrorCode module_function(int param) {
    if (param < 0) {
        return ERROR_INVALID_PARAM;
    }
    // 正常处理
    return SUCCESS;
}

3. 参数验证

int sensor_read(SensorData* data) {
    // 参数验证
    if (data == NULL) {
        return ERROR_INVALID_PARAM;
    }

    // 范围检查
    if (data->value > MAX_VALUE) {
        return ERROR_OUT_OF_RANGE;
    }

    // 正常处理
    return SUCCESS;
}

架构文档

架构设计文档内容

1. 架构概述: - 系统目标 - 架构风格 - 关键设计决策

2. 架构视图: - 逻辑视图:组件和关系 - 物理视图:部署结构 - 开发视图:模块组织 - 过程视图:运行时行为

3. 组件描述: - 组件职责 - 组件接口 - 组件依赖

4. 设计决策: - 决策理由 - 替代方案 - 权衡分析

UML图示

组件图

┌─────────────┐
│   UI Layer  │
└──────┬──────┘
┌──────▼──────┐
│ Service Layer│
└──────┬──────┘
┌──────▼──────┐
│     HAL     │
└─────────────┘

序列图

sequenceDiagram
    participant UI
    participant Service
    participant HAL
    participant Sensor

    UI->>Service: Request Data
    Service->>HAL: Read Sensor
    HAL->>Sensor: Read
    Sensor-->>HAL: Raw Data
    HAL-->>Service: Processed Data
    Service-->>UI: Display Data

说明: 这是组件交互的时序图示例。展示了UI、Service、HAL和Sensor之间的交互流程:UI请求数据,Service读取传感器,HAL与传感器通信,数据逐层返回并最终显示。这种图有助于理解系统的动态行为。

架构评审

评审检查表

功能性: - [ ] 架构满足所有功能需求 - [ ] 组件职责清晰 - [ ] 接口定义完整

质量属性: - [ ] 性能要求可满足 - [ ] 可靠性设计充分 - [ ] 安全性考虑完整 - [ ] 可维护性良好

合规性: - [ ] 符合IEC 62304要求 - [ ] 满足风险控制措施 - [ ] 可追溯到需求

可实现性: - [ ] 技术可行 - [ ] 资源充足 - [ ] 时间合理

架构评审方法

ATAM(Architecture Tradeoff Analysis Method): - 识别架构方法 - 分析质量属性 - 识别权衡点 - 评估风险

场景评审: - 定义使用场景 - 评估架构响应 - 识别问题和风险

最佳实践

架构设计建议

  1. 从需求出发:架构应满足功能和质量需求
  2. 保持简单:避免过度设计,KISS原则
  3. 模块化:高内聚低耦合
  4. 分层清晰:明确的层次划分
  5. 接口抽象:依赖抽象而非具体实现
  6. 文档化:清晰的架构文档
  7. 持续演进:架构应随需求演进

常见陷阱

注意事项

  1. 过度设计:不必要的复杂性
  2. 层次混乱:跨层调用,破坏分层
  3. 紧耦合:模块间依赖过多
  4. 接口不稳定:频繁修改接口
  5. 缺少抽象:直接依赖具体实现
  6. 忽视非功能需求:只关注功能
  7. 文档缺失:缺少架构文档

实践练习

  1. 为一个血压监测器设计分层架构
  2. 设计一个传感器模块的接口
  3. 使用状态机模式设计测量流程
  4. 绘制一个组件图和序列图

自测问题

问题1:什么是分层架构?有什么优缺点?
答案

分层架构:将系统划分为多个层次,每层有特定职责,上层依赖下层。

典型分层: - 表示层:用户界面 - 业务逻辑层:业务规则和算法 - 数据访问层:数据存储和检索

优点: 1. 清晰的职责划分 2. 易于理解和维护 3. 支持独立开发和测试 4. 便于团队协作 5. 可替换性好(如更换UI)

缺点: 1. 可能增加复杂度 2. 性能开销(层间调用) 3. 过度设计风险 4. 可能限制灵活性

问题2:SOLID原则是什么?如何应用?
答案

SOLID原则

  1. S - 单一职责原则:一个模块只负责一个功能
  2. 应用:将传感器读取、数据处理、显示分离到不同模块

  3. O - 开闭原则:对扩展开放,对修改关闭

  4. 应用:使用接口和多态,添加新功能不修改现有代码

  5. L - 里氏替换原则:子类可以替换父类

  6. 应用:确保派生类不改变基类的行为

  7. I - 接口隔离原则:接口应该小而专注

  8. 应用:不要创建"胖"接口,将大接口拆分为小接口

  9. D - 依赖倒置原则:依赖抽象而非具体实现

  10. 应用:高层模块通过接口调用低层模块
问题3:什么是硬件抽象层(HAL)?为什么重要?
答案

硬件抽象层(HAL):在硬件和软件之间的抽象层,提供统一的硬件访问接口。

作用: 1. 隔离硬件依赖:上层软件不直接访问硬件 2. 提高可移植性:更换硬件只需修改HAL 3. 简化测试:可以模拟HAL进行测试 4. 降低风险:硬件变更不影响上层软件

示例

// HAL接口
typedef struct {
    int (*init)(void);
    int (*read)(uint8_t* data);
} SensorHAL;

// 具体硬件实现
static SensorHAL sensor_a_hal = {
    .init = sensor_a_init,
    .read = sensor_a_read
};

// 上层使用HAL,不关心具体硬件
void read_sensor(uint8_t* data) {
    sensor_hal->read(data);
}

问题4:状态机模式适用于什么场景?如何实现?
答案

适用场景: - 系统有多个状态 - 状态之间有明确的转换条件 - 状态转换逻辑复杂

示例场景: - 测量设备的工作流程 - 通信协议的状态管理 - 用户界面的状态切换

实现方法

  1. 查表法

    typedef enum { IDLE, MEASURING, PROCESSING } State;
    typedef enum { START, DATA_READY, COMPLETE } Event;
    
    State state_table[3][3] = {
        // IDLE, MEASURING, PROCESSING
        {MEASURING, IDLE, IDLE},        // START
        {IDLE, PROCESSING, PROCESSING}, // DATA_READY
        {IDLE, MEASURING, IDLE}         // COMPLETE
    };
    
    State current_state = IDLE;
    
    void handle_event(Event event) {
        current_state = state_table[event][current_state];
    }
    

  2. 函数指针法

    typedef void (*state_handler_t)(Event event);
    
    void idle_handler(Event event);
    void measuring_handler(Event event);
    
    state_handler_t state_handlers[] = {
        idle_handler,
        measuring_handler,
        processing_handler
    };
    
    void execute_state_machine(Event event) {
        state_handlers[current_state](event);
    }
    

问题5:如何设计一个好的模块接口?
答案

设计原则

  1. 简单明了
  2. 接口应该易于理解和使用
  3. 避免复杂的参数和返回值

  4. 完整性

  5. 提供所有必要的功能
  6. 不遗漏关键操作

  7. 一致性

  8. 命名规范一致
  9. 参数顺序一致
  10. 错误处理一致

  11. 最小化

  12. 只暴露必要的接口
  13. 隐藏内部实现

  14. 健壮性

  15. 参数验证
  16. 错误处理
  17. 边界检查

示例

// 好的接口设计
typedef enum {
    SENSOR_OK = 0,
    SENSOR_ERROR_INIT = -1,
    SENSOR_ERROR_READ = -2
} SensorStatus;

// 初始化
SensorStatus sensor_init(void);

// 读取数据(带参数验证)
SensorStatus sensor_read(uint8_t* data, size_t* len);

// 去初始化
SensorStatus sensor_deinit(void);

问题6:架构设计文档应该包含哪些内容?
答案

架构设计文档内容

  1. 架构概述
  2. 系统目标和范围
  3. 架构风格(分层、微服务等)
  4. 关键设计决策

  5. 架构视图

  6. 逻辑视图:组件和关系
  7. 物理视图:部署结构
  8. 开发视图:模块组织
  9. 过程视图:运行时行为

  10. 组件描述

  11. 每个组件的职责
  12. 组件的接口定义
  13. 组件之间的依赖关系

  14. 接口规格

  15. 接口定义
  16. 数据格式
  17. 通信协议

  18. 设计决策

  19. 为什么选择这个架构
  20. 考虑过的替代方案
  21. 权衡分析

  22. 质量属性

  23. 性能考虑
  24. 安全性设计
  25. 可靠性措施

  26. 风险和约束

  27. 技术风险
  28. 资源约束
  29. 时间约束

  30. 追溯性

  31. 架构到需求的映射
  32. 风险控制措施的实现

相关资源

参考文献

  1. 书籍:《Software Architecture in Practice》by Len Bass, Paul Clements, Rick Kazman
  2. 书籍:《Clean Architecture》by Robert C. Martin
  3. 书籍:《Design Patterns: Elements of Reusable Object-Oriented Software》by Gang of Four
  4. IEC 62304:2006+AMD1:2015 - Medical device software - Software life cycle processes
  5. IEEE 1471-2000 - Recommended Practice for Architectural Description of Software-Intensive Systems

内容模块


💬 讨论区

欢迎在这里分享您的想法、提出问题或参与讨论。需要 GitHub 账号登录。