Embedded C Language: Cohesion and Coupling

1 Principles

Low coupling refers to making modules as independent as possible. While it is impossible for modules to have no connections, the interfaces between modules should be minimal and simple. Thus, high cohesion from the internal characteristics of each module in the entire program, and low coupling from the relationships between various modules in the program, impose requirements on our design.Many technologies and design principles that have emerged during the development of program design and software engineering can be interpreted from the perspective of cohesion and coupling. As beginners in C language programming, we will explore how to achieve high cohesion and low coupling based on our current understanding of functions.Regarding low coupling, the lowest level of coupling is non-direct coupling, which means that the connection between two functions is entirely realized through the control and invocation of a common calling function, resulting in the weakest coupling and the strongest independence of functions. However, it is clearly unrealistic for a group of functions to have no data transfer; thus, we pursue data coupling, where only simple data parameters are passed between the calling function and the called function, such as functions using value passing.Some functions, when called, use the method of passing addresses through formal parameters, allowing the function body to modify storage units outside its scope via pointers, which constitutes a stronger coupling known as feature coupling. Here, the connection between functions is established through address identifiers. Additionally, if two functions open the same file for operations, this also constitutes a form of feature coupling.A stronger coupling is external coupling, where a group of modules accesses the same global variable without passing information about that global variable through parameter tables. When an abnormal program execution result is discovered, it is difficult to locate which function has gone wrong. Many beginners find parameter passing cumbersome and define the data to be processed as global variables as much as possible, simplifying the interfaces between functions but creating a highly coupled structure.In C language, static local variables can also share data between two calls of the same program, which can also be seen as a form of external coupling. However, the scope of static local variables is limited to the function, and their impact is also confined to the function, making the coupling degree much weaker than using global variables. Thus, we can understand the principle of “use when appropriate, do not misuse” proposed when using global variables and static local variables.Regarding high cohesion, the highest level of cohesion is functional cohesion, where all elements within a module exist to complete the same function, collectively achieving a single function, making the module indivisible. Such functions have very clear and defined functionalities and generally appear at lower levels in the program structure diagram.The next level is sequential cohesion, where the various processing elements in a function are closely related to the same function, usually with the output of the previous processing element being the input of the next processing element. For such functions, if high coupling is not to occur, two functions can be separated for implementation.In some functions, different processing functionalities are associated merely because they access a common data, which is referred to as communication cohesion and informational cohesion, with the level of cohesion further decreasing. Lower levels of cohesion will not be listed one by one; the worst is accidental cohesion, where there is no connection between the processing elements within a function, merely being coincidentally grouped together.One can imagine such a module as a disorganized group, corresponding to low quality. In summary, when dividing functions to solve problems, the principle of “one function, one feature” should be followed, striving to achieve functional cohesion in modules.To achieve high cohesion and low coupling, the key is to spend some time on design before writing code. In the following example, we will discuss how to consider the above factors in conjunction with specific problems.

2 Examples

This example is inspired by Teacher Qiu Zongyan’s “From Problem to Program – Introduction to Program Design and C Language”.Task: Output perfect squares within 200 (a number is called a perfect square if it is the square of another integer, also known as a square number), requiring a newline after every 5 data points.Solution and Comments: For this simple task, we completed it in a main function. The program is as follows in Solution 1:

// Solution 1: Implementation with high cohesion in a single module
#include <stdio.h>
int main() {
    int m, num = 0;
    for (m = 1; m * m <= 200; m++) {
        printf("%d ", m * m);
        num++;
        if (num % 5 == 0) printf("\n");
    }
    return 0;
}

Due to the simplicity of the task, implementing it in a single main function results in a cohesion level close to functional cohesion, which is already quite high. For the sake of helping readers understand the technical aspects of module quality, we will attempt to further improve the cohesion of the program and then examine various solutions with different coupling levels.To enhance the cohesion of the function in the above solution (which only has the main function), we consider the functional aspects of “finding perfect squares and outputting them”—”finding perfect squares” and “outputting” are two functionalities in themselves, and when further detailing the output, there is also the requirement of “5 data points per line”. These functional implementation details are all contained within one function, indicating that there is room to further improve the level of cohesion.In practical applications, almost all processing can be decomposed into an “input-computation-output” pattern. Excellent solutions often require at least these three modules to be independent. For the “computation” module, it should not include input and output, but rather accept input data and return results after computation. Of course, for complex problems, further decomposition may be necessary at each stage.Next, we will explore a solution that separates the implementation of “finding perfect squares and outputting them” from “new line after every 5 data points”. Such decomposition helps improve cohesion, while the coupling level between the two decomposed modules becomes the focus of our attention.Now, we will keep the functionality of “finding perfect squares and outputting them” in the main function (it could also be made a separate function, but it is not necessary), while designing a function named format for the functionality of “new line after every 5 data points”, which outputs a space as a separator between two perfect squares each time it is called, and outputs a newline every fifth call.Between these two modules, there is a need to pass information about “which call number it is”; it is impossible to use the loosest coupling of non-direct coupling. We consider data coupling, using simple formal parameters to pass values, resulting in Solution 2.

// Solution 2: A solution with low coupling but unable to meet functional requirements
#include <stdio.h>
void format(int);
int main() {
    int m, num = 0;
    for (m = 1; m * m <= 200; m++) {
        printf("%d", m * m);
        format(num);
    }
    return 0;
}
void format(int n) {
    n++;
    if (n % 5 == 0) printf("\n");
    else printf(" ");
    return;
}

In this program structure, the coupling level between format and main is data coupling. In main, the local variable num is reasonably initialized to 0 before any output. When calling format, num is passed as the formal parameter n, indicating the number of outputs (which perfect square it is), and n is incremented by 1 to control the output of spaces or newlines.However, analysis and execution of the program reveal that the functionality of “outputting a newline after every 5 data points” has not been achieved. This is because the change in the formal parameter n within the format function corresponds to a different memory space than the actual parameter num, and the modification of n++ does not affect num, leading to the loss of important information about “which output it is” in the next call.A remedy is to have format return the changed value of n as a return value back to the main function, resulting in the program of Solution 3:

// Solution 3: Utilizing return values to increase coupling but achieve functionality
#include <stdio.h>
int format(int);
int main() {
    int m, num = 0;
    for (m = 1; m * m <= 200; m++) {
        printf("%d", m * m);
        num = format(num);
    }
    return 0;
}
int format(int n) {
    n++;
    if (n % 5 == 0) printf("\n");
    else printf(" ");
    return n;
}

Maintaining the original function’s return value as void, we can change the parameter to pass by address, resulting in the following program of Solution 4. This solution has a higher coupling level but still achieves functionality.

// Solution 4: Passing by address to achieve functionality, with increased coupling
#include <stdio.h>
void format(int*);
int main() {
    int m, num = 0;
    for (m = 1; m * m <= 200; m++) {
        printf("%d", m * m);
        format(&num);
    }
    return 0;
}
void format(int *p) {
    (*p)++;
    if ((*p) % 5 == 0) printf("\n");
    else printf(" ");
    return;
}

Some may have thought of using global variables as a solution. This way, num can be defined as a global variable, and its lifespan is no longer dependent on function calls, allowing its value to remain unchanged between function calls (as long as it is not assigned a new value in between), thus completing the task of passing information. At this point, format can be designed as a no-parameter function, resulting in the following program of Solution 5:

// Solution 5: The solution with the highest coupling level using global variables
#include <stdio.h>
void format();
int num = 0;
int main() {
    int m;
    for (m = 1; m * m <= 200; m++) {
        printf("%d", m * m);
        format();
    }
    return 0;
}
void format() {
    num++;
    if (num % 5 == 0) printf("\n");
    else printf(" ");
    return;
}

This is the solution with the highest coupling level for this problem. Defining num as an external variable means that if there are other functions, num can be modified by any function, making it difficult to locate errors when format counts incorrectly, and modifications may lead to errors elsewhere. In such a short program, this solution may still be acceptable, but as the scale of the program increases, the potential issues must be taken seriously. Therefore, in practical applications, it is emphasized that global variables should be used cautiously (not avoided).Considering that num is a private data used in format—only format cares about which data point it is, and main does not need to care about it. Thus, we can consider defining num as a static local variable within format, resulting in the program of Solution 6:

// Solution 6: Using static local variables, with higher coupling but the best encapsulation
#include <stdio.h>
void format();
int main() {
    int m;
    for (m = 1; m * m <= 200; m++) {
        printf("%d", m * m);
        format();
    }
    return 0;
}
void format() {
    static int num = 0;
    num++;
    if (num % 5 == 0) printf("\n");
    else printf(" ");
    return;
}

Here, the scope of the static local variable num is local, defined within the function body, providing the best encapsulation among all solutions, thus ensuring information concealment and avoiding unintentional access by other functions. However, the lifespan of num is global, allowing it to pass information between different calls of the function, resulting in a higher coupling degree (self-coupling), but achieving the ideal coupling level between the main function and the format function, ensuring both functional correctness and the safety of local data, demonstrating the advantages of static local variables. In summary, there are many solutions to a problem. Solution 1 is acceptable, but we hope to improve cohesion; Solution 2 achieves low coupling with a simple parameter passing method but unfortunately cannot meet functionality; among other solutions, the preferred order for this problem is:Solution 6, Solution 3 > Solution 4 > Solution 5Readers are encouraged to review the previous content and consider the reasons for this ordering. Throughout the exploration of various solutions, we should realize that in the process of improving programming skills, continuously learning new technologies and understanding new evaluation standards is a process of broadening one’s perspective. In later exercises, it is advisable to think of multiple solutions and evaluate their pros and cons from a professional perspective, ultimately achieving a level of expertise where the best solution is instinctively chosen.[Paid] C Language Tutorial MaterialsSource: https://helijian.blog.csdn.net/article/details/79401703

Leave a Comment