简体   繁体   English

通过使用正确的 function 名称在 C 语言中实现面向对象的设计

[英]Achieving Object-Oriented Design in the C Language by using proper function names

I am writing a MISRA compliant code that runs on a microcontroller, and this program must be written in the C language.我正在编写一个在微控制器上运行的符合 MISRA 的代码,并且该程序必须用 C 语言编写。 I would like to design my software according to the object-oriented design.我想根据面向对象的设计来设计我的软件。 However, the C-language lacks OOP support.但是,C 语言缺乏 OOP 支持。 Therefore, I decided to create " classes " and " packages " through C-files and Folders, respectively.因此,我决定分别通过 C 文件和文件夹创建“”和“”。 Currently, all legacy function names were having the following names: <Module_Name>_f_<Function_Name>_<Return type> .目前,所有旧版 function 名称都具有以下名称: <Module_Name>_f_<Function_Name>_<Return type>

This function naming convention works as long as there is only a single Module .只要只有一个Module ,这个 function 命名约定就可以工作。 However, if I add SubModules or even SubSubModules , then the function name might get confusing.但是,如果我添加SubModules甚至是SubSubModules ,那么 function 名称可能会令人困惑。 For example, having Module , Submodule , and a SubSubModule might end up in one the following function names:例如,拥有ModuleSubmodule 和 SubSubModule可能会出现以下 function 名称之一:

  1. <Module_Name><SubModule_Name><SubSubModule_Name>_f_<Function_Name>_<Return type>

  2. <Module_Name>_<SubModule_Name>_<SubSubModule_Name>_f_<Function_Name>_<Return type>

  3. <Module_Name>_f_<SubModule_Name>_<SubSubModule_Name>_<Function_Name>_<Return type>

    ... ...

What would be a good name for such functions, and their respective C-files?此类函数及其各自的 C 文件的好名称是什么? I would like to have a naming convention that one can read and still understand the "class"/"package" structure?我想要一个可以阅读并仍然理解“类”/“包”结构的命名约定?

To make it more clear, we can take more concrete example with the following file structure containing Folders and C-files:为了更清楚,我们可以用以下包含文件夹和 C 文件的文件结构来举一个更具体的例子:

Module (Folder)
  - SubModule_1 (Folder)
     - SubSubModule_1_1.c
     - SubSubModule_1_2.c
     - SubSubSubModule_1_2_1.c (Maybe also put in a seperate Sub-Folder?)
     - SubSubSubModule_1_2_2.c (Maybe also put in a seperate Sub-Folder?)
     ...

  - SubModule_n (Folder)
     - SubSubModule_n_1.c
     - SubSubModule_n_2.c
     ...

The above file structure might look like this in an OOP pseudocode:上述文件结构在 OOP 伪代码中可能如下所示:

class Module:
  begin Module;

    # Field Declarations
    SubModule_1 subModule_1_Instance;
    SubModule_2 subModule_2_Instance;
    ...
    # Function declarations
    Module_f_<Function_Name>_<return type>;
    ...

  end Module;


class SubModule_1:
  begin SubModule_1;

    # Field Declarations
    SubSubModule_1_1 subSubModule_1_1_Instance;
    SubSubModule_1_2 subSubModule_1_2_Instance;
    ...
    # Function declarations
    ModuleSubModule1_f_<Function_Name>_<return type>;
    OR 
    Module_SubModule1_f_<Function_Name>_<return type>;
    OR
    Module_f_SubModule1_f_<Function_Name>_<return type>;
    ...

  end SubModule_1;


class SubSubModule_1_1:
  begin SubSubModule_1_1;

    # Function declarations
    ModuleSubModule1SubModuleSubModule11_f_<Function_Name>_<return type>;
    OR 
    Module_SubModule1_SubModule11_f_<Function_Name>_<return type>;
    OR
    Module_f_SubModule1_SubModule11__f_<Function_Name>_<return type>;
    ...

  end SubSubModule_1_1;

So for the SubSubModule_1_1 , I might end up with:所以对于SubSubModule_1_1 ,我最终可能会得到:

  1. ModuleSubModule1SubModuleSubModule11_f_<Function_Name>_<return type>;
  2. Module_SubModule1_SubModule11_f_<Function_Name>_<return type>;
  3. Module_f_SubModule1_SubModule11__f_<Function_Name>_<return type>;

Is there maybe a better way to name those functions?有没有更好的方法来命名这些函数? I am looking forward to Your replays/alternatives.我期待着您的重播/替代方案。 Thank you in advance.先感谢您。

Sticking to an OO design is almost always a good idea, but you need to boil down OO to the things that matter.坚持 OO 设计几乎总是一个好主意,但您需要将 OO 归结为重要的事情。 Namely:即:

  • Autonomous objects that only know of their designated purpose and know nothing about unrelated things.只知道其指定用途而对无关事物一无所知的自主对象。

    For example in an embedded system, your SPI driver shouldn't and needn't know anything about the LCD you are using, even though you are communicating with the LCD through SPI.例如在嵌入式系统中,即使您通过 SPI 与 LCD 通信,您的 SPI 驱动程序也不应该也不需要了解您正在使用的 LCD 的任何信息。

  • Private encapsulation that hides information away to reduce complexity, tight coupling and namespace collisions.隐藏信息以降低复杂性、紧密耦合和命名空间冲突的私有封装。

  • In some cases, inheritance.在某些情况下,inheritance。

    For example if you are writing a portable HAL that should function the same no matter the underlying microcontroller hardware.例如,如果您正在编写一个便携式 HAL,那么无论底层微控制器硬件如何,function 都应该相同。 (Like for example a SPI driver.) (例如 SPI 驱动程序。)

All of the above OO can be achieved in C and the language directly or indirectly has language support for it.上述所有的OO都可以在C中实现,并且该语言直接或间接地对其有语言支持。 There's misc other concepts like "RAII", which are handy but not necessary.还有诸如“RAII”之类的其他概念,它们很方便但不是必需的。 Unfortunately we can't get automatically called constructors/destructors in C, so we have to live with calling them explicitly.不幸的是,我们无法在 C 中自动调用构造函数/析构函数,因此我们必须明确调用它们。

The main thing to concider when doing OO in C (and other languages) is to do it on a file level.在 C(和其他语言)中进行 OO 时要考虑的主要事情是在文件级别上进行。 The header file should contain the public interface - everything that the caller needs to know, that you would normally have declared public in a language with keyword support. header 文件应该包含公共接口 - 调用者需要知道的所有内容,您通常会在支持关键字的语言中声明public接口。 Each header file contains a corresponding.c file containing the private implementation details.每个 header 文件都包含一个对应的.c 文件,其中包含私有实现细节。

It's a good idea to have a strict naming policy like in your examples, so that the caller knows where a certain function belongs.像您的示例中那样有一个严格的命名策略是个好主意,这样调用者就知道某个 function 属于哪里。 The functions belonging to the SPI driver spi.h should be named spi_init , spi_transceive and so on, with the source code prefix first.属于 SPI 驱动程序spi.h的函数应命名为spi_initspi_transceive等,源代码前缀在前。

Not sure if I like the SubSubModule idea though, seems a bit burdensome.不确定我是否喜欢SubSubModule的想法,但似乎有点繁琐。 Also, in an embedded system there should be just so many cases where you actually need inheritance, it is a bit of a rare beast rather than the main attraction in most programs.此外,在嵌入式系统中应该有很多情况下您实际上需要 inheritance,它有点稀有,而不是大多数程序的主要吸引力。 Often it can rather be a sign of poor design and over-engineering with far too many abstraction layers.通常,这可能是设计不佳和过度工程的标志,抽象层太多。 It's also important to never let your inheritance API be set in stone.同样重要的是不要让您的 inheritance API 一成不变。 Don't hesitate to change it later on, when you discover new requirements that weren't considered during the initial design.当您发现在初始设计期间未考虑的新需求时,请不要犹豫稍后对其进行更改。

Regarding private encapsulation, C supports that through the static keyword.关于私有封装,C 通过static关键字支持。 Functions declared static in the.c file are truly private and can't be accessed from other files. c 文件中声明的static函数是真正私有的,不能从其他文件访问。 It doesn't work quite as well for variables though.不过,它对变量的效果并不好。 You can use static file scope variables as a "poor man's private", that's in fact how it is done most of the time in embedded systems.可以static文件 scope 变量用作“穷人的私有”,实际上在嵌入式系统中大部分时间都是这样做的。 static variables have some limitations though: they force the object to become a "singleton pattern" with only one instance possible. static变量有一些限制:它们强制 object 成为只有一个实例可能的“单例模式”。 Which is fine if you only need one instance of the SPI driver, but what if the MCU comes with 5 different SPI peripherals, all behaving identically?如果您只需要一个 SPI 驱动程序实例,这很好,但如果 MCU 带有 5 个不同的 SPI 外设,它们的行为都一样呢?

As a side note, static variables aren't thread-safe in larger, multi-process/multi-thread programs.作为旁注, static变量在较大的多进程/多线程程序中不是线程安全的。 Could become relevant in case of RTOS.在 RTOS 的情况下可能变得相关。

It is however possible to take OO one step further in C, by using the concept known as opaque type / opaque pointers .然而,通过使用称为不透明类型/不透明指针的概念,可以在 C 中将 OO 更进一步。 Examples . 例子 This allows you to create multi-instance classes, fully encapsulated, or optionally with some public parts.这允许您创建多实例类,完全封装,或可选地使用一些公共部分。 It can be used to model inheritance and polymorphism, by letting the first object of the inherited class contain a struct instance of its parent.它可以用于 model inheritance 和多态性,通过让继承的 ZA2F2ED4F8DC40AB6 的结构实例的第一个 object 包含一个struct实例。 Function pointers enable "virtual" inherited functions, where calling a function through a base class pointer invokes the corresponding function in the caller. Function pointers enable "virtual" inherited functions, where calling a function through a base class pointer invokes the corresponding function in the caller.

An object declared as opaque through pointers to incomplete type cannot be allocated by the caller, they can only declare pointers to them.通过指向不完整类型的指针声明为不透明的 object 不能由调用者分配,它们只能声明指向它们的指针。 From the caller's perspective they work essentially just the same as abstract base classes in C++.从调用者的角度来看,它们的工作原理与 C++ 中的抽象基类基本相同。 You will have to encapsulate the object allocation inside the init function (constructor).您必须将 object 分配封装在 init function(构造函数)中。 This is a bit of a disadvantage in low-end embedded systems, since sanity demands that we don't use malloc there.这在低端嵌入式系统中有点不利,因为理智要求我们不要在那里使用 malloc。 Instead memory allocation will have to be done through a fixed maximum size static memory pool.相反,memory 分配必须通过固定的最大大小 static memory 池来完成。 Examples: Static allocation of opaque data types示例: Static 分配不透明数据类型

From a MISRA-C perspective, they actually encourage the use of opaque type since MISRA-C:2012 (Dir 4.8).从 MISRA-C 的角度来看,自 MISRA-C:2012 (Dir 4.8) 以来,他们实际上鼓励使用不透明类型。

Do not over-use opaque type though.但是不要过度使用不透明类型。 It makes perfect sense for things like HAL on top of drivers, portable code, protocol handling etc. But not so much for hiding away non-portable, application-specific logic, which doesn't benefit from abstraction layers since you won't be able to re-use or port it anyway.它对于诸如驱动程序之上的 HAL、可移植代码、协议处理等非常有意义。但对于隐藏不可移植的、特定于应用程序的逻辑就没有那么多了,因为你不会从抽象层中受益无论如何都可以重新使用或移植它。

Overall, program design is highly qualified work.总体而言,程序设计是一项高素质的工作。 It takes lots of experience to get it done properly.需要大量经验才能正确完成。 Add too much abstraction and you end up in over-engineered, meta-programming hell.添加太多抽象,你最终会陷入过度设计的元编程地狱。 Add too little and you end up in spaghetti-programming, tight-coupling hell.加得太少,你最终会陷入意大利面条式编程、紧耦合的地狱。

The concept missing from this discussion is the "this" pointer to have instance-specific data.此讨论中缺少的概念是具有特定于实例的数据的“this”指针。 It's implicit in C++, but must be explicit in C.它在 C++ 中是隐含的,但在 C 中必须是显式的。

For example, in a hypothetical module NSMotionController.c:例如,在假设的模块 NSMotionController.c 中:

typedef struct NSMotionControllerStruct {
   float speed__m_s;
} NSMotionController_t;

float NSMotionController_SpeedGet__m_s(NSMotionController_t const * const this) {
     return this->speed__m_s;
}
bool NSMotionController_Initialize(NSMotionController_t * const this, float const speedCurrent__m_s) {
     this->speed__m_s = speedCurrent__m_s;
     return true;
}

We can use this like so:我们可以这样使用它:

int main(int argc, char ** argv) {
    NSMotionController_t motionControllerInstance1;
    NSMotionController_Initialize(motionControllerInstance1, 1.0f);
    NSMotionController_t motionControllerInstance2;
    NSMotionController_Initialize(motionControllerInstance1, 2.0f);
    printf("speed1: %.1f\n", NSMotionController_SpeedGet__m_s(&motionControllerInstance1));
    printf("speed2: %.1f\n", NSMotionController_SpeedGet__m_s(&motionControllerInstance2));
}

As far as naming, I use a two-letter namespace ("NS" above) since C doesn't support namespaces idiomatically.至于命名,我使用两个字母的命名空间(上面的“NS”),因为 C 不习惯性地支持命名空间。 I use the module name, then an underscore to start the method name.我使用模块名称,然后使用下划线来开始方法名称。 I use two underscores to separate a units suffix ("__m_s" above indicates "meters per second").我使用两个下划线分隔一个单位后缀(上面的“__m_s”表示“米每秒”)。

For polymorphism, you can use function pointers.对于多态性,您可以使用 function 指针。 So, augmenting our example with function pointers:因此,用 function 指针扩充我们的示例:

typedef float (*NSMotionControllerInterface_SpeedGet__m_s_t)(void const * const this);

typedef struct NSMotionControllerStruct {
    NSMotionControllerInterface_SpeedGet__m_s_t SpeedGet__m_s;
    float speed__m_s;
} NSMotionController_t;

float NSMotionController_SpeedGet__m_s(void const * const this) {
    NSMotionController_t const * const motionThis = (NSMotionController_t const *) this;
    return motionThis->speed__m_s;
}
bool NSMotionController_Initialize(NSMotionController_t * const this, float const speedCurrent__m_s) {
    this->SpeedGet__m_s = NSMotionController_SpeedGet__m_s;
    this->speed__m_s = speedCurrent__m_s;
    return true;
}

int main(int argc, char ** argv) {
    NSMotionController_t motionControllerInstance1;
    NSMotionController_Initialize(motionControllerInstance1, 1.0f);
    NSMotionController_t motionControllerInstance2;
    NSMotionController_Initialize(motionControllerInstance1, 2.0f);
    printf("speed1: %.1f\n", motionControllerInstance1.SpeedGet__m_s(&motionControllerInstance1));
    printf("speed2: %.1f\n", motionControllerInstance2.SpeedGet__m_s(&motionControllerInstance2));
}

Rather than using polymorphism on a single function, though, you can gather them up in a struct and pass that to other modules.但是,您可以将它们收集在一个结构中并将其传递给其他模块,而不是在单个 function 上使用多态性。

typedef float (*NSMotionControllerInterface_SpeedGet__m_s_t)(void const * const this);
typedef bool (*NSMotionControllerInterface_SpeedSet__m_s_t)(void const * const this, float const speedNew__m_s);
typedef struct NSMotionControllerInterfaceStruct {
    NSMotionControllerInterface_SpeedGet__m_s_t SpeedGet__m_s;
    NSMotionControllerInterface_SpeedSet__m_s_t SpeedSet__m_s;
} NSMotionControllerInterface_t;

typedef struct NSMotionControllerStruct {
    NSMotionControllerInterface_t interface;
    float speed__m_s;
} NSMotionController_t;

float NSMotionController_SpeedGet__m_s(void const * const this) {
    NSMotionController_t const * const motionThis = (NSMotionController_t const *) this;
    return motionThis->speed__m_s;
}

bool NSMotionController_SpeedSet__m_s(void const * const this, float const speedNew__m_s) {
    NSMotionController_t const * const motionThis = (NSMotionController_t const *) this;
    motionThis->speed__m_s = speedNew__m_s;
    return true;
}

bool NSMotionController_Initialize(NSMotionController_t * const this, float const speedCurrent__m_s) {
    this->interface.SpeedGet__m_s = NSMotionController_SpeedGet__m_s;
    this->interface.SpeedSet__m_s = NSMotionController_SpeedSet__m_s;
    this->speed__m_s = speedCurrent__m_s;
    return true;
}

int main(int argc, char ** argv) {
    NSMotionController_t motionControllerInstance1;
    NSMotionController_Initialize(motionControllerInstance1, 1.0f);
    NSMotionController_t motionControllerInstance2;
    NSMotionController_Initialize(motionControllerInstance1, 2.0f);

    NSMotionControllerInterface_t * const interface1 = motionControllerInstance1.interface;
    NSMotionControllerInterface_t * const interface2 = motionControllerInstance2.interface;
    printf("speed1: %.1f\n", interface1->SpeedGet__m_s(&interface1));
    printf("speed2: %.1f\n", interface2->SpeedGet__m_s(&interface2));

    interface1->SpeedSet__m_s(&interface1, 5.0f);
    printf("speed1 (faster): %.1f\n", interface1->SpeedGet__m_s(&interface1));

    /* Example of passing abstract interface */
    NSGroundControl_t groundControl;
    NSGroundControl_Initialize(&groundControl, interface1);
}

In short, never use statics when you can avoid it.简而言之,当你可以避免时,永远不要使用静态。 This will also help unit testing, which I imagine is next (or hopefully first) if you're working in a MISRA environment.这也将有助于单元测试,如果您在 MISRA 环境中工作,我想这是下一个(或希望是第一个)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM