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

实验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());
    }
}

实验二 面向对象编程

1.实验1回顾

实验一中,我们实现了前进,左转,右转三个指令,利用了多个if else 语句进行判断,并完成了测试用例编写

2.项目实践

2.1 新功能扩展——加速指令

Executor组件增加支持执行:

F: 加速指令,接收到该指令,车进入加速状态,该状态下:

  • M: 前进2格(不能跳跃,只能一格一格前进)
  • L: 先前进1格,然后左转90度
  • R: 先前进1格,然后右转90度

再接收一次F指令,对应的状态取消

F指令实现

  1. 首先需要在ExecutorImpl添加一个实例数据成员,记录当前是不是位于加速状态
    添加私有数据成员bool isFast
    并在初始化列表中初始其为false
    ExecutorImpl::ExecutorImpl(const Pose &pose) noexcept :pose(pose), isFast(false)

  2. 其次需要修改ExecutorImpl::Execute中M、L、R指令的处理逻辑,添加条件判断是否处于加速状态

if(!isFast)//如果不是出于加速状态,跟以前一样
{
}
else//否则
{
}

圈复杂度高,代码重复如何优化代码?

代码就更复杂了,需求的迭代,对代码的扩展性提出了强烈的诉求

代码圈复杂度(Cyclomatic Complexity)是用来衡量代码复杂性的一种指标。简单来说,它表示代码中有多少条不同的执行路径。路径越多,代码就越复杂。

想象一下,你在一个迷宫里,每次遇到一个岔路口(比如if语句或for循环),你就有了一个新的选择。代码圈复杂度就是计算这些岔路口的数量。数值越高,说明迷宫越复杂,走出去的难度也越大。

为什么重要?

  • 可维护性:复杂的代码更难理解和修改。
  • 测试难度:复杂的代码需要更多的测试用例来覆盖所有可能的路径。
  • 错误风险:复杂的代码更容易出错。

2.2 面向对象封装

(1)代码分析和优化思路

我们要把容易互相影响的、关联程度紧密的元素,都封装在一个类内部(而这正是我们老生常谈的封装变化的动机);同时让类之间的关联紧密程度尽可能降低,以让类间尽可能不要相互影响。从而最终做到局部化影响

首先进入我们射程的就是重复代码。编写重复代码不仅仅会让有追求的程序员感到乏味。真正致命的是:“重复”极度违背高内聚、低耦合原则,从而会大幅提升软件的长期维护成本。因而,对于完全重复的代码进行消除,合二为一,会让系统更加高内聚、低耦合

  1. 指令处理划分:根据指令处理逻辑的不同,将M、L、R三个指令处理的逻辑分别抽取为Move、TurnLeft、TurnRight三个成员函数。

  2. 抽象,消减重复代码:为了提高代码的可维护性和扩展性,建议使用面向对象的封装方法。将上述三个方法统一到一个基类Icommand的DoOperate抽象方法中。通过创建MoveCommand、TurnLeftCommand和TurnRightCommand的子类,并在每个子类中实现具体的操作,利用多态性来简化代码结构。

(2)移动指令行为抽取为Move方法

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 final : 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() const noexcept override;
        // 第二阶段新增加的纯虚函数,执行一个用字符串表示的指令,是父类抽象方法Execute的具体实现
        void Execute(const std::string &command) noexcept override;

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

        bool isFast;

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

        void Fast(void) noexcept;         // 切换加速状态
        bool IsFast(void) const noexcept; // 查询当前是否处于加速状态
    private:
        class ICommand
        {
        public:
            // 请在这里给出析构函数和纯虚函数DoOperate的声明
            virtual ~ICommand() = default;                                     // 虚析构函数,确保子类正确析构
            virtual void DoOperate(ExecutorImpl &executor) const noexcept = 0; // 纯虚函数,执行指令
        };

        // 定义一个嵌套类MoveCommand,完成前进动作(M指令)
        class MoveCommand final : public ICommand
        {
        public:
            // 执行Move动作,委托ExecutorImpl&执行器来完成动作
            void DoOperate(ExecutorImpl &executor) const noexcept override
            {
                if (executor.IsFast()) // 如果是加速状态
                {
                    executor.moveForward(); // 加速状态下,前进两格
                    executor.moveForward();
                }
                else
                {
                    executor.moveForward(); // 正常前进一格
                }
               
            }
        };
        // 定义一个嵌套类TurnLeftCommand,完成左转动作(L指令)
        class TurnLeftCommand final : public ICommand
        {
        public:
            // 执行TurnLeft动作,需要委托ExecutorImpl&执行器来完成动作
            void DoOperate(ExecutorImpl &executor) const noexcept override
            {
                if (executor.IsFast()) // 如果是加速状态
                {
                    executor.moveForward(); // 加速状态下,前进1格再左转
                    executor.turnLeft();
                }
                else
                {
                    executor.turnLeft(); // 正常左转
                }
                
            }
        };
        // 定义一个嵌套类TurnRightCommand,完成右转动作(R指令)
        class TurnRightCommand final : public ICommand
        {
        public:
            // 执行TurnRight动作,需要委托ExecutorImpl&执行器来完成动作
            void DoOperate(ExecutorImpl &executor) const noexcept override
            {
                if(executor.IsFast()) // 如果是加速状态
                {
                    executor.moveForward(); // 加速状态下,前进1格再右转
                    executor.turnRight();
                }
                else
                {
                    executor.turnRight(); // 正常右转
                }
            }
        };

        // 加速指令类
        class FastCommand final : public ICommand
        {
        public:
            void DoOperate(ExecutorImpl &executor) const noexcept override
            {
                // 切换加速状态
                executor.Fast();
            }
        };
    };

} // namespace adas

4.3 ExecutorImpl.cpp

#include "ExecutorImpl.hpp"
#include <iostream>
#include <new>
#include <memory>
namespace adas
{

    ExecutorImpl::ExecutorImpl(const Pose &pose) noexcept : pose(pose), isFast(false) {} // 构造函数初始化 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'; // 南转西
        }
    }

    // Fast 方法实现
    void ExecutorImpl::Fast(void) noexcept
    {
        isFast = !isFast; // 切换加速状态
    }

    // IsFast 方法实现
    bool ExecutorImpl::IsFast(void) const noexcept
    {
        return isFast; // 返回当前是否处于加速状态
    }

    // Execute()方法用于执行指令,根据指令调用相应的处理方法
    void ExecutorImpl::Execute(const std::string &commands) noexcept
    {
        for (const auto cmd : commands) // const auto cmd : commands是C++11的特性,用于遍历字符串
        {
            // 声明一个ICommand类型的智能指针
            std::unique_ptr<ICommand> cmder;
            if (cmd == 'M')
            {
                // 智能指针指向MoveCommand实例,不用担心delete了
                cmder = std::make_unique<MoveCommand>();
            }
            else if (cmd == 'L')
            {
                // 智能指针指向TurnLeftCommand实例
                cmder = std::make_unique<TurnLeftCommand>();
            }
            else if (cmd == 'R')
            {
                // 智能指针指向TurnRightCommand实例
                cmder = std::make_unique<TurnRightCommand>();
            }
            else if(cmd =='F')
            {
                //切换状态
                Fast();
            }
            else
            {
                cmder = nullptr;
            }
            if (cmder)
            {
                cmder->DoOperate(*this); // 执行指令
                //*this即ExecutorImpl对象
            }
           
        }
    }
} // namespace adas

4.4 ExecutorFastTest.cpp

#include <gtest/gtest.h>
#include "PoseEq.hpp"
#include "../include/Executor.hpp"

namespace adas
{
    TEST(ExecutorFastTest, should_return_x_plus_2_given_status_is_fast_command_is_M_and_facing_is_E)
    {
        // given
        std::unique_ptr<Executor> executor(Executor::NewExecutor({0, 0, 'E'}));
        // when
        executor->Execute("FM"); // FM: F状态下Move
        // then
        const Pose target{2, 0, 'E'};
        ASSERT_EQ(target, executor->Query());
    }
    TEST(ExecutorFastTest, should_return_N_and_x_plus_1_given_status_is_fast_command_is_L_and_facing_is_E)
    {
        // 命令是FL,起始状态{0,0,’E’}
        // given
        std::unique_ptr<Executor> executor(Executor::NewExecutor({0, 0, 'E'})); // 起始状态{0, 0, 'E'}
        // when
        executor->Execute("FL"); // FL: 向前移动然后左转
        // then
        const Pose target{1, 0, 'N'}; // 结果应为{1, 0, 'N'}
        ASSERT_EQ(target, executor->Query());
    }
    TEST(ExecutorFastTest, should_return_S_and_x_plus_1_given_status_is_fast_given_command_is_R_and_facing_is_E)
    {
        // 命令是FR,起始状态{0,0,’E’}
        // given
        std::unique_ptr<Executor> executor(Executor::NewExecutor({0, 0, 'E'})); // 起始状态{0, 0, 'E'}
        // when
        executor->Execute("FR"); // FR: 向前移动然后右转
        // then
        const Pose target{1, 0, 'S'}; // 结果应为{1, 0, 'S'}
        ASSERT_EQ(target, executor->Query());
    }
    TEST(ExecutorFastTest, should_return_y_plus_1_given_command_is_FFM_and_facing_is_N)
    {
        // given
        std::unique_ptr<Executor> executor(Executor::NewExecutor()); // 默认起始状态是{0,0,'N'}
        // when
        executor->Execute("FFM"); // FFM等价于M
        // then
        const Pose target{0, 1, 'N'};
        ASSERT_EQ(target, executor->Query());
    }
} // namespace adas

4.5 PoseEq.cpp

#include "PoseEq.hpp"
#include <tuple>

// 重载Pose的==操作符,用于比较两个Pose对象是否相等
namespace adas
{
    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);
    }
}
//以下为PoseEq.hpp
#pragma once
#include "../include/Executor.hpp"

namespace adas
{
    bool operator==(const Pose &lhs, const Pose &rhs);
}

实验三 面向对象编程和函数式编程

1 实验二回顾

2 面向对象项目实践

3 函数式项目实践

4 总结

实验四

1 实验三回顾

2 项目实践

2.1 面向接口编程

(1) 实训需求:支持掉头功能

Executor组件可以执行掉头(Turn Round)指令:

  • TR:掉头指令,收到该指令后,左转90度->向前进1格->左转90度
  • TR指令不受倒车状态B影响
  • TR指令会受到加速状态F影响
  • 加速状态下,收到TR指令后,向前进1格->左转90度->向前进1格->左转90度
  • TR一起出现时才有效,不考虑异常输入场景

(2)面向接口编程

  • 目标
    • 团队分工合作:在企业环境中,团队成员通常会分工合作,各自负责开发系统的一部分。通过将指令处理逻辑抽取到Command层,不同的开发人员可以独立地扩展和维护各自的指令处理模块
    • 指令令处理的扩展性:指令添加无需修改ExecutorImpl,还允许指令处理逻辑脱离ExecutorImpl进行独立测试,从而提高测试覆盖率
  • 设计思想
    • 定义和实现分离,保持代码的高内聚低耦合

(3)Command层设计思路

在表驱动时,每次调用时都会生成一个cmderMap,导致效率问题。此外,新增指令时需要修改Executor、command两个地方,增加了维护成本。

 // 表驱动
        std::unordered_map<char, std::function<void(PoseHandler &)>> cmderMap{
            {'M', MoveCommand()},      // 前进
            {'L', TurnLeftCommand()},  // 左转
            {'R', TurnRightCommand()}, // 右转
            {'F', FastCommand()},      // 快速
        };
  • 解决方案:
    • 指令对象生成与执行分开:先生成指令列表,再执行指令
    • 抽取指令对象处理为command层:在command层中处理所有与指令对象相关的操作
    • 使用单体对象:在command层中只生成一个cmderMap,避免重复生成

(4)统一接口:基于模板的单体类-Singleton

使 用 泛 型 来 实 现 单 例 模 式 ( Singleton Pattern),有几个显著的优势:

  1. 线程安全:由于instance 是一个静态局部变量,C++11及以后的标准保证了其初始化是线程安全的。
  2. 懒加载:实例只有在第一次调用Instance()法时才会被创建,这样可以避免不必要的资源消耗。
  3. 防止拷贝和赋值:通过删除拷贝构造函数和赋运算符,确保了单例对象不能被复制或赋值,从而保证了单例的唯一性。
  4. 泛型支持:使用模板类可以使单例模式适用于同类型,而不需要为每种类型单独实现单例模式。
namespace adas
{
    template <typename T>
    class Singleton final
    {
        // 单例模式
    public:
        static T &Instance(void) noexcept // 单体类
        {
            static T instance;
            return instance;
        }
        // 删除构造函数
        Singleton(const Singleton &) = delete;            // 删除拷贝构造函数
        Singleton &operator=(const Singleton &) = delete; // 删除赋值构造函数
    private:
        Singleton() noexcept = default;  // 默认构造函数
        ~Singleton() noexcept = default; // 默认析构函数
    }; //

} // namespace adas

请首先创建src/Singleton.hpp,代码自己输入一遍,加深理解

(5)统一接口:单体工厂类-CmderFactory

namespace adas
{
    class CmderFactory final // 命令工厂
    {
    public:
        CmderFactory(void) = default;  // 默认构造函数
        ~CmderFactory(void) = default; // 默认析构函数

        CmderFactory(const CmderFactory &) = delete; // 删除拷贝构造函数
        CmderFactory &operator=(const CmderFactory &) = delete;

    public:
        std::list<std::function<void(PoseHandler &poseHandler)>> GetCmders(const std::string &commands) const noexcept; // 获取命令

    private:
        const std::unordered_map<char, std::function<void(PoseHandler &poseHandler)>> cmderMap{
            // 命令表
            {'M', MoveCommand()},
            {'L', TurnLeftCommand()},
            {'R', TurnRightCommand()},
            {'F', FastCommand()},
            {'B', ReverseCommand()},
        };
    };
}
  • 创 建 src/CmderFactory.hpp , 请 自 行 实 现 src/CmdFactory.cpp , 提 示 :
    std::list 类 型 使用 push_back 函 数添加元素
  • 删除构造和赋值函数,防止生成多个实例
  • 需要在GetCmders里面首先构造std::list对象,然后遍历cmdermap里面每个键值对,得到其中命
    对象,加到std::list对象

(6)统一接口

修改src/ExecutorImpl.cpp,不再直接使用Command中的指令了,改用指令工厂和单例类,下面使用的std::for_each函数需要引入头文件algorithm

#include "ExecutorImpl.hpp"
#include "CmderFactory.hpp"
#include "Singleton.hpp"
#include <algorithm>
 // Execute()方法用于执行指令,根据指令调用相应的处理方法
    void ExecutorImpl::Execute(const std::string &commands) noexcept
    {
        const auto cmders = Singleton<CmderFactory>::Instance().GetCmders(commands); // 使用指令工厂获取指令字符串对应的操作列表

        std::for_each( // std::for_each 是一个标准库算法,用于对给定范围内的每个元素执行某个操作
            cmders.begin(),
            cmders.end(),
            [this](const std::function<void(PoseHandler & poseHandler)> &cmder) noexcept // 使用 lambda 表达式调用指令列表中的每个指令
            {
                cmder(poseHandler);
            });

(7) using特性

使用using可以为某类型定义别名,还可以定义新的模板
一切使用typedef的场合都建议改用using,因为其语法可读性更好
1.与typdef不同,using的语法类似于变量定义:

typedef int Dword;
// 与上一行等价
using Dword = int;

2.using定义定义函数指针类型时的语法示例:

using Fp = void (*)(int, const std::string&);
// 如果用typedef,等价的定义如下:
typedef void (*Fp)(int, const std::string&);

推荐用法:

typedef std::unique_ptr<std::unordered_map<std::string, std::string>> UPtrMapSS;
// 好的写法
using UPtrMapSS = std::unique_ptr<std::unordered_map<std::string, std::string>>;

使用using定义新的模板可以有效的替代以往需要复杂的模板特例化才能实现的功能

template<typename T>
using MyAllocList = std::list<T, MyAlloc<T>>;
MyAllocList<Widget> lw; // 等价于std::list<Widget, MyAlloc<Widget>>

使用using特性进一步简化代码
修改CmderFactory.hpp,请自行修改CmderFactory.cpp和ExecutorImpl.cpp

using Cmder = std::function<void(PoseHandler &poseHandler)> ;
using CmderList = std::list<Cmder>;

本节小结

  • 引入单体类,将command指令处理逻辑抽取出来,实现了面向接口编程( Interface-Oriented Programming ) 。 ( 请 对 比 第 12 页ExecutorImpl和第21页ExecutorImpl)
  • 所有指令的处理(指将命令字符串映射到命令对象)都集中在command层,这种设计不仅简化了代码结构,还提升了代码的可维护性和可扩展性。
  • 利用using特性,进一步提高了代码的可读性 ,使得代码更加简洁明了。

2.2 任务编排

思考:掉头指令如何实现?
TR指令实现复杂性:

  • 多步骤操作:头指令需要多个操作完成,如前进、左转等
  • 状态依赖:头指令的执行受车速和倒车状态影响,不同状态下操作步骤不同
  • 条件分支处理步骤操作和状态依赖导致条件分支多,处理复杂

(1)解决方案:任务编排

  • 原子操作的组合:掉头指令由多个操作组成。在执行过程中,这些操作按照预定义的顺序依次执行,完成复杂的业务任务
  • 失败处理:企业软件开发中,可能会有某些操作失败。为确保系统能够有效地处理这些失败情况,通过重试、回滚或补偿机制,保证业务流程的完整性和一致性
  • 实现步骤:
    • 子指令抽象
      • 定义原子指令:明确每个原子操作的输入、输出和执行逻辑。例如,左转90度、向前进1格等
      • 抽象接口:为每个原子指令定义统一的接口,使得不同的原子指令可以通过相同的方式调用
    • command指令编排:将多个原子指令按照业务需求进行组合,形成一个完整的任务。例如,TR指令在加速状态下的顺序是:向前进1格->左转90度->向前进1格->左转90度
    • 子指令批量操作调用:高效地执行编排好的原子指令列表,将编排好的原子指令列表传递给执行器,执行器按顺序调用每个原子指令

(2)原子指令抽象:识别8种基本操作

(3)原子指令定义:代码实现

创建src/ActionGroup
创建src/ActionGroup.cpp,请自行实现

enum class ActionType : uint16_t
    {
        FORWARD_1_STEP_ACTION = 0,
        BACKWARD_1_STEP_ACTION,
        TURN_LEFT_ACTION,
        REVERSE_TURN_LEFT_ACTION,
        TURN_RIGHT_ACTION,
        REVERSE_TURN_RIGHT_ACTION,
        BE_FAST_ACTION,
        BE_REVERSE_ACTION,
    };

(4)任务编排:原子指令列表管理、批执行功能实现

修改ActionGroup.cpp,添加ActionGroup.hpp中DoOperate函数的实现

void ActionGroup::DoOperate(PoseHandler &poseHandler) const noexcept// 执行动作
    {
        static std::vector<std::function<void(PoseHandler & poseHandler)>> actionVec = {
            ForwardAction(),
            BackwardAction(),
            TurnLeftAction(),
            ReverseTurnLeftAction(),
            TurnRightAction(),
            ReverseTurnRightAction(),
            BeFastAction(),
            BeReverseAction()};

        std::for_each(actions.begin(), actions.end(), [&poseHandler](const ActionType &actionType) mutable noexcept// 使用 lambda 表达式调用指令列表中的每个指令
        { actionVec[static_cast<uint16_t>(actionType)](poseHandler); });
    }
  • 将原子指令操作添加到vector容器中,注意添加顺序与ActionType枚举类型的顺序一致
  • 遍历actions里面的每个action,执行action。以FORWARD_1_STEP_ACTION指令为例,其ActionType枚举值为0,actionVec[0]对应操作为ForwardAction,ForwardAction(poseHandler)将执行向前1步操作

(5)任务编排:src/Command.hpp里MoveCommand实现

修改Command.hpp,请大家自行实现剩余代码

class MoveCommand final
    {
    public:
        // 现在MoveCommand不再直接调用PoseHandler执行动作
        // 而是返回一个ActionGroup对象,里面包含了可执行的命令
        // 要执行命令需要通过枚举ActionType来表示
        ActionGroup operator()(PoseHandler &poseHandler) const noexcept
        {
            // 创建ActionGroup对象
            ActionGroup actionGroup;

            // 如果当前是倒车状态,action为ActionType::BACKWARD_1_STEP_ACTION
            // 如果当前不是倒车状态,action为ActionType::FORWARD_1_STEP_ACTION
            const auto action = poseHandler.IsReverse() ? ActionType::BACKWARD_1_STEP_ACTION 
            : ActionType::FORWARD_1_STEP_ACTION;
            // 如果是加速状态,则额外执行一次action
            // 所以需要额外将actionGroup添加到ActionGroup
            // 因为ActionGroup已经覆盖了执行Action的序列
            if (poseHandler.IsFast())
            {
                actionGroup.PushAction(action);
            }

            // 无论是否为加速状态,一个Action是必须要加入到ActionGroup去执行
            actionGroup.PushAction(action);

            return actionGroup;
        }
    };

(6)任务编排:Command需返回原子指令列表

(7)任务编排:指令编排后统一执行

2.3 TR指令实现

(1)测试用例设计

创建tests/ExecutorTurnRoundTest.cpp,请自行实现剩余测试用例

// 测试输入:TR
    TEST(ExecutorTurnRoundTest, should_normal_tr_build_left_forward_left)
    {
        // given
        std::unique_ptr<Executor> executor(Executor::NewExecutor({0, 0, 'E'}));

        // when
        executor->Execute("TR");

        // then
        const Pose target{0, 1, 'W'};
        ASSERT_EQ(target, executor->Query());
    }

(2)创建TR指令任务编排类

修改Command.hpp

class TurnRoundCommand final
    {
    public:
        ActionGroup operator()(PoseHandler &poseHandler) const noexcept
        {
            // 如果是倒车状态
            if (poseHandler.IsReverse())
            {
                return ActionGroup(); // 在倒车状态下,什么都不做
            }
            else
            {
                if (poseHandler.IsFast())
                {
                    return ActionGroup({
                        // 在F状态下,TR指令需要四个原子Action
                        ActionType::FORWARD_1_STEP_ACTION, // 向前一步
                        ActionType::TURN_LEFT_ACTION,       // 左转
                        ActionType::FORWARD_1_STEP_ACTION, // 向前一步
                        ActionType::TURN_LEFT_ACTION,       // 左转
                    });
                }
                else
                { // 正常状态
                    return ActionGroup({
                        // 在正常状态下,TR指令需要三个原子Action
                        ActionType::TURN_LEFT_ACTION,       // 左转
                        ActionType::FORWARD_1_STEP_ACTION, // 向前一步
                        ActionType::TURN_LEFT_ACTION,       // 左转
                    });
                }
            }
        }
    };

以原子指令的组合编排形式完成TR指令任务

(3)指令映射

在CmdFactory.hpp中,指令都是一个字符,指令映射表key为char字符类型,如何映射TR指令?

  1. 方案1 修改映射表key为std:string
    添加TR->TurnRoundCommand对象映射
    指令的处理遍历匹配M\L\R\B\F\TR搜索,需考虑的细节较多
  2. 方案2 推荐方案
    映射表key不修改,仍然使用char
    指令字符串先通过字符串替换处理,对字符串中的TR替换为Z
    映射表添加Z->TurnRoundCommand对象映射

修改CmdFactory.hpp

private:
        const std::unordered_map<char, Cmder> cmderMap{
            // 命令表
            {'M', MoveCommand()},
            {'L', TurnLeftCommand()},
            {'R', TurnRightCommand()},
            {'F', FastCommand()},
            {'B', ReverseCommand()},
            {'Z', TurnRoundCommand()},
        };
    private:
    //将字符串中的"TR"替换为'Z'
    std::string parseCommandString(std::string_view commands) const noexcept;
    //string_view表示一个字符串的视图,不拥有字符串的所有权,只是一个字符串的引用
    void ReplaceAll(std::string &inout, std::string what, std::string with)const noexcept;

字符串替换, string_view表示该类型不会为数据分配存储空间,而且该数据类型只能用来读。实际上该数据类型的实例不会具体存储原数据,仅仅存储指向的数据的起始指针和长度,所以开销非常小。

(4)使用Z替换TR,将所有指令统一为一个字符

  std::string CmderFactory::parseCommandString(std::string_view commands) const noexcept
    {
        std::string result(commands);
        ReplaceAll(result, "TR", "Z");
        return result;
    }
    void CmderFactory::ReplaceAll(std::string &inout, std::string what, std::string with) const noexcept
    {
        for (
            std::string::size_type pos{};//std::string::size_type 是 std::string 的 size 类型
            inout.npos != (pos = inout.find(what.data(), pos, what.length()));
            pos += with.length())
        {
            inout.replace(pos, what.length(), with.data(), with.length());
        }
    }

本节小结

  • 原子层抽象:
    • 自顶向下设计:将复杂的掉头指令操作逐层分解为更小、更简单的原子指令。例如,掉头指令可以分解为前进、左转、后退等原子指令。
    • 单一职责原则:每个原子指令只负责一个独立的功能,如前进指令只负责车辆前进,左转指令只负责车辆左转。这样确保了指令功能独立且可复用。
  • 任务编排:
    • 合设计模式:
      • 将复杂的指令处理转换为原子指令的编排。例如,掉头操作可以通过组合前进、左转、后退等原子指令来实现。
      • 通过不同的原子指令组合,可以实现各种复杂的功能需求,如在不同状态下执行不同的掉头步骤。

2.4 目录结构设计

3 总结

4 实现

4.1 src/cmder

4.1.1 ActionGroup.hpp

点击查看代码
#pragma once

#include <list>
#include "../src/core/PoseHandler.hpp"

namespace adas
{
    enum class ActionType : uint16_t
    {
        FORWARD_1_STEP_ACTION = 0,
        BACKWARD_1_STEP_ACTION,
        TURN_LEFT_ACTION,
        REVERSE_TURN_LEFT_ACTION,
        TURN_RIGHT_ACTION,
        REVERSE_TURN_RIGHT_ACTION,
        BE_FAST_ACTION,
        BE_REVERSE_ACTION,
    };

    class ActionGroup final
    {
    public:
        ActionGroup(void) = default;// 默认构造函数
        explicit ActionGroup(const std::list<ActionType> &actions) noexcept; // 有参构造函数,explicit代表显示构造函数
        ~ActionGroup() = default;// 默认析构函数

        void PushAction(const ActionType &ActionType) noexcept;// 添加动作
        void DoOperate(PoseHandler &poseHandler) const noexcept;// 执行动作

    private:
        std::list<ActionType> actions;// 动作列表
    };
}

4.1.2 ActionGroup.cpp

点击查看代码
#include "ActionGroup.hpp"
#include "CmderFactory.hpp"

#include <vector>
#include <algorithm>

namespace adas
{

    class ForwardAction final
    {
    public:
        void operator()(PoseHandler &poseHandler) const noexcept
        {
            poseHandler.Forward();
        }
    };

    class BackwardAction final
    {
    public:
        void operator()(PoseHandler &poseHandler) const noexcept
        {
            poseHandler.Backward();
        }
    };

    class TurnLeftAction final
    {
    public:
        void operator()(PoseHandler &poseHandler) const noexcept
        {
            poseHandler.TurnLeft();
        }
    };

    class ReverseTurnLeftAction final
    {
    public:
        void operator()(PoseHandler &poseHandler) const noexcept
        {
            poseHandler.TurnRight();
        }
    };

    class TurnRightAction final
    {
    public:
        void operator()(PoseHandler &poseHandler) const noexcept
        {
            poseHandler.TurnRight();
        }
    };

    class ReverseTurnRightAction final
    {
    public:
        void operator()(PoseHandler &poseHandler) const noexcept
        {
            poseHandler.TurnLeft();
        }
    };

    class BeFastAction final
    {
    public:
        void operator()(PoseHandler &poseHandler) const noexcept
        {
            poseHandler.Fast();
        }
    };

    class BeReverseAction final
    {
    public:
        void operator()(PoseHandler &poseHandler) const noexcept
        {
            poseHandler.Reverse();
        }
    };
    ActionGroup::ActionGroup(const std::list<ActionType> &actions) noexcept : actions(actions) {}// 有参构造函数

    void ActionGroup::PushAction(const ActionType &ActionType) noexcept// 添加动作
    {
        actions.push_back(ActionType);
    }

    void ActionGroup::DoOperate(PoseHandler &poseHandler) const noexcept// 执行动作
    {
        static std::vector<std::function<void(PoseHandler & poseHandler)>> actionVec = {
            ForwardAction(),
            BackwardAction(),
            TurnLeftAction(),
            ReverseTurnLeftAction(),
            TurnRightAction(),
            ReverseTurnRightAction(),
            BeFastAction(),
            BeReverseAction()};

        std::for_each(actions.begin(), actions.end(), [&poseHandler](const ActionType &actionType) mutable noexcept// 使用 lambda 表达式调用指令列表中的每个指令
        { actionVec[static_cast<uint16_t>(actionType)](poseHandler); });
    }
}

4.1.3 CmderFactory.hpp

点击查看代码
#pragma once

#include <functional>
#include <list>
#include <unordered_map>
#include <map>
#include "Command.hpp"
#include"ActionGroup.hpp"

namespace adas
{ // std::function 是个模板,类型实参是函数参数及返回值,
    // std::function<ActionGroup(PoseHandler& poseHandler)> 表示这样一个函数:
    // 参数为 PoseHandler& poseHandler,返回类型为 ActionGroup。
    // 作为 std::function 的类型实参,但不光是一个函数可以作为 std::function 的类型实参,
    // 重载了 ActionGroup operator()(PoseHandler& poseHandler) 的函数对象
    // 以及输入参数为 PoseHandler& poseHandler,返回类型为 ActionGroup 的 lambda 表达式
    // 都可以作为类型实参。

    // ActionGroup.cpp 里面的 ForwardAction 等不都是重载了
    // ActionGroup operator()(PoseHandler& poseHandler) 的函数对象吗?

    // std::function 模板定义了一个可调用对象:函数,函数对象,lambda 表达式,函数指针都是可调用对象。
    // 它将不同类型的可调用对象统一起来
    using Cmder = std::function<ActionGroup(PoseHandler & poseHandler)>;
    using CmderList = std::list<Cmder>;
    class CmderFactory final
    {
    public:
        CmderFactory(void) = default;  // 默认构造函数
        ~CmderFactory(void) = default; // 默认析构函数

        CmderFactory(const CmderFactory &) = delete; // 删除拷贝构造函数
        CmderFactory &operator=(const CmderFactory &) = delete;

    public:
        CmderList GetCmders(const std::string &commands) const noexcept; // 获取命令

    private:
        const std::unordered_map<char, Cmder> cmderMap{
            // 命令表
            {'M', MoveCommand()},
            {'L', TurnLeftCommand()},
            {'R', TurnRightCommand()},
            {'F', FastCommand()},
            {'B', ReverseCommand()},
            {'Z', TurnRoundCommand()},
        };
    private:
    //将字符串中的"TR"替换为'Z'
    std::string parseCommandString(std::string_view commands) const noexcept;
    //string_view表示一个字符串的视图,不拥有字符串的所有权,只是一个字符串的引用
    void ReplaceAll(std::string &inout, std::string what, std::string with)const noexcept;
    };
}

4.1.4 CmderFactory.cpp

点击查看代码
#include "CmderFactory.hpp"

namespace adas
{
    CmderList CmderFactory::GetCmders(const std::string &commands) const noexcept
    {
        // 需要在 GetCmders 里面首先构造 std::list 对象,然后遍历 cmderMap 里面每个键值对,得到其中命令对象,加到 std::list 对象:
        CmderList cmderList;
        for (const auto cmd : parseCommandString(commands))
        {
            const auto it = cmderMap.find(cmd);
            if (it != cmderMap.end())
            {
                cmderList.push_back(it->second); // std::list 类型使用 push_back 函数添加元素
            }
        }
        return cmderList;
    }
    std::string CmderFactory::parseCommandString(std::string_view commands) const noexcept
    {
        std::string result(commands);
        ReplaceAll(result, "TR", "Z");
        return result;
    }
    void CmderFactory::ReplaceAll(std::string &inout, std::string what, std::string with) const noexcept
    {
        for (
            std::string::size_type pos{};//std::string::size_type 是 std::string 的 size 类型
            inout.npos != (pos = inout.find(what.data(), pos, what.length()));
            pos += with.length())
        {
            inout.replace(pos, what.length(), with.data(), with.length());
        }
    }
}

4.1.5 Command.hpp

点击查看代码
#pragma once

#include "ExecutorImpl.hpp"
#include "../src/core/PoseHandler.hpp"
#include <functional>
#include "ActionGroup.hpp"
namespace adas
{
    class MoveCommand final
    {
    public:
        // 现在MoveCommand不再直接调用PoseHandler执行动作
        // 而是返回一个ActionGroup对象,里面包含了可执行的命令
        // 要执行命令需要通过枚举ActionType来表示
        ActionGroup operator()(PoseHandler &poseHandler) const noexcept
        {
            // 创建ActionGroup对象
            ActionGroup actionGroup;

            // 如果当前是倒车状态,action为ActionType::BACKWARD_1_STEP_ACTION
            // 如果当前不是倒车状态,action为ActionType::FORWARD_1_STEP_ACTION
            const auto action = poseHandler.IsReverse() ? ActionType::BACKWARD_1_STEP_ACTION 
            : ActionType::FORWARD_1_STEP_ACTION;
            // 如果是加速状态,则额外执行一次action
            // 所以需要额外将actionGroup添加到ActionGroup
            // 因为ActionGroup已经覆盖了执行Action的序列
            if (poseHandler.IsFast())
            {
                actionGroup.PushAction(action);
            }

            // 无论是否为加速状态,一个Action是必须要加入到ActionGroup去执行
            actionGroup.PushAction(action);

            return actionGroup;
        }
    };

    class TurnLeftCommand final
    {
    public:
        
        ActionGroup operator()(PoseHandler &poseHandler) const noexcept
        {
            ActionGroup actionGroup;

            if (poseHandler.IsFast())//如果是加速状态
            {
                //如果是倒车状态,执行倒车动作
                const auto moveAction = poseHandler.IsReverse() ? 
                ActionType::BACKWARD_1_STEP_ACTION
                  : ActionType::FORWARD_1_STEP_ACTION;
                actionGroup.PushAction(moveAction);
            }
            //如果是倒车状态,执行右转动作,否则执行左转动作
            const auto turnAction = poseHandler.IsReverse() ? ActionType::REVERSE_TURN_LEFT_ACTION
            : ActionType::TURN_LEFT_ACTION;
            actionGroup.PushAction(turnAction);

            return actionGroup;
        }
    };

    class TurnRightCommand final
    {
    public:
    
        ActionGroup operator()(PoseHandler &poseHandler) const noexcept
        {
            ActionGroup actionGroup;

            if (poseHandler.IsFast())//如果是加速状态
            {
                const auto moveAction = poseHandler.IsReverse() ? ActionType::BACKWARD_1_STEP_ACTION
                : ActionType::FORWARD_1_STEP_ACTION;
                actionGroup.PushAction(moveAction);
            }

            const auto turnAction = poseHandler.IsReverse() ? ActionType::REVERSE_TURN_RIGHT_ACTION
            : ActionType::TURN_RIGHT_ACTION;
            actionGroup.PushAction(turnAction);

            return actionGroup;
        }
    };

    class FastCommand final
    {
    public:
        ActionGroup operator()(PoseHandler &poseHandler) const noexcept
        {
            ActionGroup actionGroup;

            actionGroup.PushAction(ActionType::BE_FAST_ACTION);//切换加速状态
            return actionGroup;
        }
    };

    class ReverseCommand final
    {
    public:
        ActionGroup operator()(PoseHandler &poseHandler) const noexcept
        {
            ActionGroup actionGroup;
            actionGroup.PushAction(ActionType::BE_REVERSE_ACTION);//切换倒车状态
            return actionGroup;
        }
    };
    class TurnRoundCommand final
    {
    public:
        ActionGroup operator()(PoseHandler &poseHandler) const noexcept
        {
            // 如果是倒车状态
            if (poseHandler.IsReverse())
            {
                return ActionGroup(); // 在倒车状态下,什么都不做
            }
            else
            {
                if (poseHandler.IsFast())
                {
                    return ActionGroup({
                        // 在F状态下,TR指令需要四个原子Action
                        ActionType::FORWARD_1_STEP_ACTION, // 向前一步
                        ActionType::TURN_LEFT_ACTION,       // 左转
                        ActionType::FORWARD_1_STEP_ACTION, // 向前一步
                        ActionType::TURN_LEFT_ACTION,       // 左转
                    });
                }
                else
                { // 正常状态
                    return ActionGroup({
                        // 在正常状态下,TR指令需要三个原子Action
                        ActionType::TURN_LEFT_ACTION,       // 左转
                        ActionType::FORWARD_1_STEP_ACTION, // 向前一步
                        ActionType::TURN_LEFT_ACTION,       // 左转
                    });
                }
            }
        }
    };
} // namespace adas

4.2 src/core/Singleton.hpp

点击查看代码
#pragma once

namespace adas
{
    template <typename T>
    class Singleton final
    {
        // 单例模式
    public:
        static T &Instance(void) noexcept // 单体类
        {
            static T instance;
            return instance;
        }
        // 删除构造函数
        Singleton(const Singleton &) = delete;            // 删除拷贝构造函数
        Singleton &operator=(const Singleton &) = delete; // 删除赋值构造函数
    private:
        Singleton() noexcept = default;  // 默认构造函数
        ~Singleton() noexcept = default; // 默认析构函数
    }; //

} // namespace adas

4.3 ExecutorImpl.cpp

点击查看代码
#include "ExecutorImpl.hpp"
#include "../src/cmder/CmderFactory.hpp"
#include "../src/core/Singleton.hpp"
#include <algorithm>

namespace adas
{

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

    /*
        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 下有效
    }

    // Execute()方法用于执行指令,根据指令调用相应的处理方法
    void ExecutorImpl::Execute(const std::string &commands) noexcept
    {
        // 使用指令工厂获取字符串对应的命令列表
        // Cmders 类型是 std::list<Cmder>
        // Cmder类型是 std::function<void(PoseHandler& poseHandler)>
        const auto cmders = Singleton<CmderFactory>::Instance().GetCmders(commands);

        // 遍历命令列表里的每个命令cmder
        std::for_each(
            cmders.begin(),
            cmders.end(),
            [this](const Cmder&cmder) noexcept {
            cmder(poseHandler).DoOperate(poseHandler); // 执行cmder(poseHandler)就是进行移动或转向操作
            }
        );
    } 
    // 原子指令抽象前 Cmder 对应了 std::function<void(PoseHandler&)>,
    //执行 cmder(poseHandler) 就是进行移动或转向操作;
    //原子指令抽象后 Cmder 对应了 std::function<ActionGroup(PoseHandler&)>,
    //执行 cmder(poseHandler).DoOperate(poseHandler) 才是进行移动或转向操作。

} // namespace adas

4.4 ExecutorTurnRoundTest.cpp

点击查看代码
#include <gtest/gtest.h>
#include "Executor.hpp"
#include "PoseEq.hpp"

namespace adas
{

    // 测试输入:TR
    TEST(ExecutorTurnRoundTest, should_normal_tr_build_left_forward_left)
    {
        // given
        std::unique_ptr<Executor> executor(Executor::NewExecutor({0, 0, 'E'}));

        // when
        executor->Execute("TR");

        // then
        const Pose target{0, 1, 'W'};
        ASSERT_EQ(target, executor->Query());
    }

    // 测试输入:FTR
    TEST(ExecutorTurnRoundTest, should_fast_tr_build_left_forward_left)
    {
        // given
        std::unique_ptr<Executor> executor(Executor::NewExecutor({0, 0, 'E'}));

        // when
        executor->Execute("FTR");

        // then
        const Pose target{1, 1, 'W'};
        ASSERT_EQ(target, executor->Query());
    }

    // 测试输入:BTR
    TEST(ExecutorTurnRoundTest, in_the_B_state_the_reverse_command_will_be_ignored)
    {
        // given
        std::unique_ptr<Executor> executor(Executor::NewExecutor({0, 0, 'E'}));

        // when
        executor->Execute("BTR");

        // then
        const Pose target{0, 0, 'E'};
        ASSERT_EQ(target, executor->Query());
    }
}

作业

posted @ 2024-10-16 16:21  Losyi  阅读(540)  评论(1)    收藏  举报