你的代码是否按照高内聚、低耦合的原则来设计的?

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
可观测可视化 Grafana 版,10个用户账号 1个月
简介: 你的代码是否按照高内聚、低耦合的原则来设计的?

      我们一直强调软件开发中要按照高内聚、低耦合的设计原则来做代码结构设计。c语言和c++不同,c语言面向过程、c++面向对象。

       真正的项目中,要对业务升级,原来的业务函数需要保留,要保证老的功能继续维持,不能直接删除,这时候c语言面向过程,通常使用回调的方法。c++面向对象,要实现高内聚、低耦合,需要使用接口技术。

什么是耦合性

耦合性其实就是程序之间的相关性。程序之间绝对没有相关性是不可能的,否则也不可能在一个程序中启动,如下图:

image.gif编辑

这是一个Linux中socket TCP编程的程序流程图,在图中的TCP服务器端,socket()、bind()接口、listen()接口、accept()接口之间肯定存在着相关(就是要调用下一个接口程序必需先调用前一个接口),也就是耦合,否则整个TCP服务器端就建立不起来,以及改变了bind()中的传入的数据,比如端口号,那么接下来的listen()监听的端口,accept()接收连接的端口也会改变,所以它们之间有很强的相关性,属于紧耦合。

耦合的形式

(1)数据之间耦合

   在同一个结构体或者类中,如:

typedef struct Person
{ 
int age; 
char* name; 
}Person; 
class Person 
{ 
    private: 
        int age_m; 
        bool m_setname; 
        std::string m_name; 
};

image.gif

在上面的结构体和类中,年龄和名字两个基本数据单元组合成了一个人数据单元,这两个数据之间就有了耦合,因为它们互相知道,在一些操作中可能需要互相配合操作,当然这两种数据耦合性是比较低的,但是m_setname是判断m_name是否存在的数据,所以这两个数据之间耦合性就高很多了。

(2)函数之间的耦合性

   函数如果在一个类中也会互相存在耦合性,比如下面例子:

class Person 
{ 
    Public: 
        int getAge(){return m_age;}; 
        void setAge(int age){m_age= age;}; 
        std::string getName(){return m_name;}; 
        void setName(std::string name){m_name= name;}; 
    Private: 
        int m_age; 
        std::string m_name; 
};

image.gif

其中的getAge()和setAge()接口操作的是同一个数据,能够互相影响,存在着很明显的耦合,但是getName()和getAge()两个接口相关性就不明显的,但是也会存在耦合性,因为getName()能够访问的类中数据,getAge()也能访问,如果程序员编写代码不注意,也会把在两个接口中调用到了相同数据,互相造成了影响。

除了封装在一个类中的函数之间有耦合性,外部的函数也会根据业务需要产生耦合,比如刚开始说的网络编程的例子中,socket()、listen()、bind()、accept()之间就产生了很强的耦合。

以及在两个类中,比如:

class Fruit 
{}; 
class Apple:Fruit 
{}; 
class FruitFactory 
{ 
    Public: 
        Furit* getFruit(){Fruit* fruit_p = new Apple(); return fruit_p; } 
};
 class Person
 {
     Public: 
        Void eatFruit(Fruit* furit);
 };
 FruitFactory fruitFactory; 
Fruit* fruit = fruitFactory.getFruit(); 
Person person; 
if (fruit != NULL)
 {
 person.eatFruit(fruit); 
}

image.gif

   上面的FruitFactory和Person两个类之间产生了数据耦合,而getFruit()和eatFruit()两个接口之间也产生了耦合。

(3)数据与函数之间的耦合

   从(2)中的程序也能看出,eatFruit()这个接口和Fruit这个数据产生了耦合,如果不先创建Fruit,那么接下来的eatFruit()操作也没有意义,如果强制调用,甚至可能造成程序崩溃,产生coredump。

   上面例子的耦合还是比较明显的,有一些不明显的耦合,如下:

Speaker speaker;

speaker.PowerOn() ;

speaker.PlayMusic() ;

表面上是 PlayMusic()对PowerOn()有依赖性,是函数之间的耦合,但背后的原因是 PowerOn()函数让播放器处于通电状态:

PowerOn(){

this.isPowerOn = true;

}

//只有通了电,播放器才能正常播放音乐

PlayMusic() {

if(this.isPowerOn)

Play();

}

这两个函数是通过 this .isPowerOn 这个数据进行沟通的 。这本质上还是数据和函数之间的耦合。

如何降低耦合性

或者说怎么解耦?

   (1)贯彻面向接口编码的原则

   程序不可能没有改动的,但是尽量把改动放在一个模块的内部,接口不要变,就算需要改变,最好使用适配器模式增加一个适配程序。因为接口就是一个程序与外部的关联处,保持接口不变,就是保持该模块和外部模块的耦合性不变,这样才能保证它的可移植性可重用以及不被外部模块的修改而影响。

   (2)保证一个模块的可测试(单元测试)

   如果一个模块是可以单独进行单元测试的,意味着它可以移植到其他程序上,耦合性低。

   (3)可以学习一下设计模式的设计思想。

   (4)让模块对内有完整的逻辑

   解耦的根本目的是拆除元素之间不必要的联系,一个核心原则就是让每个模块的逻辑独立而完整。其中包含两点,一是对内有完整的逻辑 , 而所依赖的外部资源尽可能是不变量;二是对外体现的特性也是“不变量”(或者尽可能做到不变量),让别人可以放心地依赖我。有的函数光明磊落,它和外界数据的沟通仅限于函数的参数和返回值,那么这种函数给人的感觉可以用两个字形容:靠谱。它把自己所需要的数据都明确标识在参数列表里,把自己能提供的全集中在返回值里。如果你需要的某项数据不在参数里,你就会侬赖上别人,因为你多半需要指名道姓地标明某个第三方来特供;同理,如果你提供的数据不全在返回值和参数里,别人会依赖上你 。有的函数让人觉得神秘莫测,规律难寻:它所需要的数据不全部体现在参数列表里,有的隐藏在函数内部,这种不可靠的变量行为很难预测;它的产出也不集中在返回值,而可能是修改了藏在某个不起眼角落里的资源。这样的函数需要人们在使用过程中和它不断地磨合,才能掌握它的特性。前者使用起来放心,而且是可移植、可复用的,后者使用时需要小心翼翼 ,而且很难移植。

C语言为例:

软件通常有后台日志的记录功能,用log函数实现,主业务用business函数表示:

void log()
{
  printf("Logging...\n");
}
void business()
{
  while(1)
  {
    sleep(1);
    printf("Deal Business...\n");
    log();
  }
}
int main()
{
  business();
  return 0;
}

image.gif

现在需要对后台日志功能进行升级,该如何实现?

一般人的想法是这样:再写一个函数log2,然后business中log改为log2,这样不就可以了?

但是你想想,主业务代码怎能轻易改动?因为一个小小的功能而要改变主要的业务代码,这样不是显得智商很捉急?

换一种思路,使用回调:

#include <stdio.h>
#include <unistd.h>
void log1()
{
  printf("1 Logging...\n");
}
void log2()
{
  printf("2 Logging...\n");  
}
void business( void (*f)() )
{
  while(1)
  {
    sleep(1);
    printf("Deal Business...\n");
    f();
  }
}
int main()
{
  business(log1);
  return 0;
}

image.gif

business函数接受一个函数指针,该指针指向的函数没有参数,返回值为void,符合log函数的原型。business中只要f()即可调用相应的函数。

当需要使用log1时,向business传log1、要使用升级后的log2时,传入log2即可。

C++为例:

C++中强调面向对象的思想。

#include <iostream>
using namespace std; 
class Log
{
public:
  void log()
{
    cout << "logging..." << endl;
  }
};
class Business
{
private:
        Log *l;
public:
        Business(Log *l = NULL)
        {}
  void business()
{
    while(1)
    {
      sleep(1);
      cout << "Deal Business..." << endl;
      l->log();
    }
  }
};
int main()
{
       Business b(new Log);
       b.business();
       return 0;
}

image.gif

现在,我们需要对后台日志功能升级,怎么做?有人想到了C++中的重载,在Log类中重载一个函数log2;

也有人想到了继承Log类,覆写log函数等等,但是这几种方法,都需要对Business类中的代码进行变动。如何解决呢?于是C++中的接口技术就派上用处了。

记住,接口强调的是方法,接口里的方法定义为纯虚函数,接口不能实例化、也不需要实例化,需要接口里的功能的类只需要继承该接口即可!下面给出示例:

#include <iostream>
using namespace std;
class Log
{
public:
  virtual void log() = 0;//纯虚函数
};
class Log1 : public Log//继承接口
{
public:
  void log()
{
    cout << "1 logging..." << endl;
  }
};
class Log2 : public Log//继承接口
{
public:
  void log()
{
    cout << "2 logging..." << endl;
  }
};
class Business
{
public:
  void business(Log * f)//函数参数只要Log指针,具体传入的是Log1还是Log2的实例,由多态进行实现
{
    while(1)
    {
      sleep(1);
      cout << "Deal Business..." << endl;
      f->log();
    }
  }
};
int main()
{
  Business b;
  b.business(new Log2);//会调用Log2类中的log日志函数!
  return 0;
}

image.gif

此时,对日志业务升级就不会影响business的代码了,只需将不同的日志实例化传入business中即可。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
2月前
模块功能高内聚低耦合
模块功能高内聚低耦合
14 1
|
4月前
软件设计原则:耦合与内聚
软件设计原则:耦合与内聚
56 0
|
9月前
|
设计模式 关系型数据库 数据安全/隐私保护
软件架构设计原则之单一职责原则
单一职责(Simple Responsibility Pinciple,SRP)是指不要存在多于一个导致类变更的原因。假设我们有一个类负责两个职责,一旦发生需求变更,修改其中一个职责的逻辑代码,有可能导致另一个职责的功能发生故障。这样一来,这个类就存在两个导致类变更的原因。如何解决这个问题呢?将两个职责用两个类来实现,进行解耦。后期需求变更维护互不影响。这样的设计,可以降低类的复杂度,提高类的可读性,提高系统的可维护性,降低变更引起的风险。总体来说,就是一个类、接口或方法只负责一项职责。
66 0
软件架构设计原则之单一职责原则
|
10月前
|
设计模式 算法
原则的重要性(单一职责原则-开放封闭原则)一
原则的重要性(单一职责原则-开放封闭原则)一
65 0
|
11月前
|
设计模式 Java 测试技术
【Java设计模式 规范与重构】 三 大型重构的手段:高内聚,低耦合
【Java设计模式 规范与重构】 三 大型重构的手段:高内聚,低耦合
105 0
|
12月前
|
程序员 测试技术
面向对象设计五个基本原则
只有聪明人才能看见的简介~( ̄▽ ̄~)~
78 0
|
设计模式
单一职责原则|设计原则
本文带大家学习和了解第一种设计原则,单一职责原则
|
数据安全/隐私保护
软件架构设计原则--单一职责原则
> 本专栏内容参考自:咕泡学院Tom老师的《Spring5核心原理与30个类手写实战》,仅作个人学习记录使用,如有侵权,联系速删
软件架构设计原则--单一职责原则
|
设计模式 网络协议 程序员
没项目经历的安酱,连低耦合高内聚都不懂...
没项目经历的安酱,连低耦合高内聚都不懂...
没项目经历的安酱,连低耦合高内聚都不懂...
|
设计模式 Java 程序员
代码设计原则
代码设计原则
354 0
代码设计原则