2024华中科技大学计算机学院C++程序设计实验企业开发实践华为

losyi / 2024-10-21 / 原文

实验1-高质量编程基础

前言

欢迎参加C++企业软件开发实践课程。本课程旨在通过完整开发案例,分享企业
软件开发中的实践、经验和要求,帮助在校学生提升软件开发技能,养成良好的
软件开发习惯。
实践课程共有4次实验,本课程为实验1,您将学习高质量编程的基础知识,包括:
⚫ 开发者测试:掌握确保代码正确性和需求完整性的测试方法
⚫ 命名:了解提高代码可读性的命名原则、规范和最佳实践
⚫ 代码版本管理:学习使用git进行代码版本管理的规范和实践
期待您在课程中的精彩表现!

目标

通过本课程的学习,您将能够:
⚫ 掌握测试的基本理论和方法
⚫ 实践有效的开发者测试技术
⚫ 掌握使用git进行代码版本管理的规范和实践
了解C++编程的可读性要求,建立高质量编程的意识,养成良好的开发习惯

1 课程导读

企业软件开发的特点

  • 企业高质量产品对软件开发的要求:企业软件规模大、生命周期长、可靠性要求高,软件代码不仅满足功能和性能要求,还需要满足代码质量和安全要求,软件开发人员需要掌握良好的软件开发技能,遵循企业软件开发规范,应用软件代码实现的最佳实践。

  • 《企业软件开发实战》系列课程通过完整的需求开发,帮助学生提升满足企业软件开发要求的软件实践能力:

    • 企业软件开发工程化思维,软件开发不仅仅是编写代码,还包括版本管理、开发者测试、代码审查、自动化构建等;
    • 语言编程实践动手能力,遵循编码规范,合理运用语言特性,学习编写高质量代码(可读、可维护、安全、高效等)的方法,养成良好的编程习惯。
业务特点 对软件技术的诉求 高质量代码要求
质量要求高 1. 可靠性、稳定性要求高,异常处理完善;
2. 并发性能高,资源利用率高;
安全、可靠性、高效
客户众多,全球化分布 1. 适应不同的环境和场景
2. 快速地响应不同用户的差异化需求
可维护—可修改/扩展、可复用、可测试
产品生命周期长,迭代演进 1. 长期开发和运营,需要不断地进行迭代和优化,以适应市场的变化,保持产品的竞争力;
2. 软件规模大,需要能够方便地进行修改和扩展,能够有效地进行复用和测试,能够高效地进行维护和更新;
可读、可维护、可复用、可测试

课程整体框架

Level1 课程模块 Level2 课程目录 学时(h) Level3 主要内容 内容要点
实验1 高质量编程基础 4 开发者自测试 如何确保代码的正确性和需求的完整性
命名 代码可读性的原则、规范、实践
代码版本管理 使用git进行代码版本管理的规范和实践
实验2 面向对象编程 4 封装 掌握面向对象编程的基本概念,包括类和对象的封装,以及如何通过封装实现代码的复用性
继承与多态 掌握面向对象中,继承、多态等特性,实现代码的良好扩展性
实验3 面向对象编程 4 表驱动和STL容器 有效地使用STL容器和智能指针,提升代码的可扩展性和灵活性
解耦循环依赖 如何通过分层设计和抽象化来有效地解耦代码中的循环依赖
实验4 函数式编程 4 函数式编程 掌握C++新特性带来的代码简洁性收益,写出简洁优雅的代码
对象生命周期安全 关注C++中使用过程中编译器附带可能导致的问题,对背后原理有深根,学习C++特性整体体系
面向接口编程 通过C++泛型编程技术,提升代码效率和可扩展性
任务编排 将复杂问题分解为原子操作,逐步实现,提升代码扩展性
目录结构规划 理解和应用物理设计的原则,优化代码结构和组织
课后作业 课程内容巩固 0.5~4 all 课程内容巩固

2 项目实践

2.1 需求澄清

  • ADAS系统介绍
    Advanced Driver Assistance System,ADAS是辅助驾驶员
    安全驾驶车辆的系统,可以增加车辆安全和道路安全,同时可以明
    显减轻汽车驾驶的操作负担。
  • ADAS Executor组件主要功能
    -由Config组件进行初始化及配置
    • 接收Controller组件的各种移动控制命令(如前进、转向等),
      维护车的位置状态

课程实训需求1 支持基本控制指令

设计一个C++程序,模拟自动驾驶车辆的执行器Executor组件功能。
Executor组件提供初始化接口,负责将车初始化在指定位置,输入数据为:
(x, y, heading),其中:

  • x,y对应地点位置(均为int32类型)
  • heading对应四个方向(N、S、E、W)(均为char类型)
    Executor组件可以执行如下的移动指令
  • M: 前进,1次移动1格
    Executor组件可以执行如下的转向指令
  • L: 左转90度,位置不变
  • R: 右转90度,位置不变
    Executor组件提供获取车当前的坐标位置和朝向接口,如果Executor未被
    初始化位置,则接口默认返回(0,0,N)。 X轴移动的方向为EW方向,Y轴移动的方向为NS方向。

要求

  • 设计Executor组件对外的接口,
  • 设计测试用例,构建上述功能的测试防护网

约束

  • 整个过程中坐标(x,y)的值都在
    int32范围内,不考虑整数溢出场景
  • 调用者保证了传给Executor接口
    的参数都是合法的,不考虑非法参
    数场景

2.2 接口设计

2.3 命名实践

2.4 开发者测试

2.5 代码版本管理

2.6 编码实践

3 总结

4 实现

4.1 Executor.hpp

#pragma once
#include <string>

namespace adas
{

    struct Pose // 位置和朝向
    {
        int x;
        int y;
        char heading;
    };
    // Executor 类是一个抽象类,定义了执行指令和查询当前位置的接口
    class Executor
    {
    public:
        // 工厂方法,创建 Executor 实例,默认初始位置为 (0, 0, 'N')
        static Executor *NewExecutor(const Pose &pose = {0, 0, 'N'}) noexcept;

        // 默认构造函数和析构函数
        Executor(void) = default;
        virtual ~Executor(void) = default;

        // 不能拷贝
        Executor(const Executor &) = delete;
        // 不能赋值
        Executor &operator=(const Executor &) = delete;

    public:
        // 执行指令的纯虚函数接口
        virtual void Execute(const std::string &command) noexcept = 0;
        // 获取当前位置和朝向的纯虚函数接口
        virtual Pose Query(void) const noexcept = 0;
    };

} // namespace adas

4.2 ExecutorImpl.hpp

#pragma once

#include "Executor.hpp"
#include <string>

namespace adas
{

    /*
        Executor的具体实现
    */
    class ExecutorImpl : public Executor
    {
    public:
        // 构造函数
        explicit ExecutorImpl(const Pose &pose) noexcept;
        // 默认析构函数
        ~ExecutorImpl() noexcept = default;

        // 不能拷贝
        ExecutorImpl(const ExecutorImpl &) = delete;
        // 不能赋值
        ExecutorImpl &operator=(const ExecutorImpl &) = delete;

    public:
        // 查询当前的车辆姿态,是父类抽象方法Query的具体实现
        Pose Query(void) const noexcept override;
        // override是C++11的特性,用于显式地声明一个函数是重载了基类中的虚函数
        //  第二阶段新增加的纯虚函数,执行一个用字符串表示的指令,是父类抽象方法Execute的具体实现
        void Execute(const std::string &command) noexcept override;

    private:
        // 私有数据成员,记录当前车辆的姿态
        Pose pose;

    private:
        // 私有方法,处理前进、左转、右转指令
        void moveForward();
        void turnLeft();
        void turnRight();
    };

} // namespace adas

4.3 ExecutorImpl.cpp

#include "ExecutorImpl.hpp"
#include <iostream>

#include <new>

namespace adas
{

    ExecutorImpl::ExecutorImpl(const Pose &pose) noexcept : pose(pose) {} // 构造函数初始化 pose
    // Query()方法用于查询当前位置和朝向
    Pose ExecutorImpl::Query(void) const noexcept
    {
        return pose;
    }

    /*
        std::nothrow 是 C++ 标准库的一个常量,用于指示在分配内存时不抛出任何异常。
        它是 std::nothrow_t 类型的实例,通常用在 new 运算符和 std::nothrow 命名空间中,
        以请求内存分配器在分配失败时返回一个空指针,而不是抛出 std::bad_alloc 异常。
    */
    Executor *Executor::NewExecutor(const Pose &pose) noexcept
    {
        return new (std::nothrow) ExecutorImpl(pose); // 只在 C++17 下有效
    }
    // moveForward()方法用于处理前进指令,根据当前朝向更新车辆的位置
    void ExecutorImpl::moveForward()
    {
        if (pose.heading == 'E')
        {
            pose.x += 1; // 向东前进
        }
        else if (pose.heading == 'W')
        {
            pose.x -= 1; // 向西前进
        }
        else if (pose.heading == 'N')
        {
            pose.y += 1; // 向北前进
        }
        else if (pose.heading == 'S')
        {
            pose.y -= 1; // 向南前进
        }
    }
    // turnLeft()方法用于处理左转指令,根据当前朝向更新车辆的朝向
    void ExecutorImpl::turnLeft()
    {
        if (pose.heading == 'E')
        {
            pose.heading = 'N'; // 东转北
        }
        else if (pose.heading == 'W')
        {
            pose.heading = 'S'; // 西转南
        }
        else if (pose.heading == 'N')
        {
            pose.heading = 'W'; // 北转西
        }
        else if (pose.heading == 'S')
        {
            pose.heading = 'E'; // 南转东
        }
    }
    // turnRight()方法用于处理右转指令,根据当前朝向更新车辆的朝向
    void ExecutorImpl::turnRight()
    {
        if (pose.heading == 'E')
        {
            pose.heading = 'S'; // 东转南
        }
        else if (pose.heading == 'W')
        {
            pose.heading = 'N'; // 西转北
        }
        else if (pose.heading == 'N')
        {
            pose.heading = 'E'; // 北转东
        }
        else if (pose.heading == 'S')
        {
            pose.heading = 'W'; // 南转西
        }
    }
    // Execute()方法用于执行指令,根据指令调用相应的处理方法
    void ExecutorImpl::Execute(const std::string &commands) noexcept
    {
        for (const auto cmd : commands) // const auto cmd : commands是C++11的特性,用于遍历字符串
        {
            if (cmd == 'M')
            {
                moveForward(); // 前进
            }
            else if (cmd == 'L')
            {
                turnLeft(); // 左转
            }
            else if (cmd == 'R')
            {
                turnRight(); // 右转
            }
            else
            {
                std::cout << "未知指令: " << cmd << std::endl;
            }
        }
    }
} // namespace adas

4.4 ExecutorTest.cpp

剩下的自己写哦

#include <gtest/gtest.h>
#include <memory>
#include <tuple>
#include "../include/Executor.hpp"

namespace adas
{

    // 重载Pose的==操作符,用于比较两个Pose对象是否相等
    bool operator==(const Pose &lhs, const Pose &rhs)
    {
        return std::tie(lhs.x, lhs.y, lhs.heading) == std::tie(rhs.x, rhs.y, rhs.heading);
    }

    // 测试返回初始化的 Pose
    TEST(ExecutorTest, should_return_init_pose_when_without_command)
    {
        // given 给定测试条件
        // 测试条件是就是调用Executor的静态方法NewExecutor返回一个指向 Executor 对象的智能指针 executor,这样我们就不需要delete了
        std::unique_ptr<Executor> executor(Executor::NewExecutor({0, 0, 'E'})); // 给了初始姿态

        // when

        // then
        const Pose expectedPose = {0, 0, 'E'}; // 构造一个姿态对象,其内容为(0, 0, 'E')
        // 既然构造对象时的初始姿态是(0, 0, 'E'),那么调用Query()方法返回的姿态也应该是(0, 0, 'E')
        // 所以这里用了断言,executor->Query()返回的姿态应该等于expectedPose,否则测试失败,说明Query()方法有问题
        ASSERT_EQ(expectedPose, executor->Query()); // 内部调用了重载的pose的==操作符
    }

    // 测试返回默认的 Pose
    TEST(ExecutorTest, should_return_default_pose_when_without_init_and_command)
    {
        // given 给定测试条件

        std::unique_ptr<Executor> executor(Executor::NewExecutor());

        // when
        // 不给初始姿态,也不给指令
        // then
        const Pose expectedPose = {0, 0, 'N'}; // 构造一个姿态对象,其内容为(0, 0, 'N')
        // 由于没有给定初始姿态,所以调用Query()方法返回的姿态应该是默认的(0, 0, 'N')
        ASSERT_EQ(expectedPose, executor->Query());
    }
}