在工作中,发现有太多的程序员压根没搞懂什么是模块化,天天叫喊着要让代码模块化,真正做的时候只是把代码放在不同的目录和文件里。

真正的模块化,是逻辑上的概念,而非文本意义上的。一个模块就像一个电路芯片,有定义良好的输入和输出。

最简单的一个模块化的案例就是函数,每个函数都有明确的输入(参数)和输出(返回值),同一个文件里可以包含多个函数,实现模块化并不一定非要将代码分开至多个文件或目录里。我们可以将代码放在同一个文件里,仍然可以写出模块化良好的代码。

所以,千万别再一提模块化就想着怎么拆分文件和目录了。

当然,有些时候确实需要拆分,但是一定不能以为模块化就必须拆分,具体情况具体分析。

要写出模块化良好的代码,我们通常需要搞定以下几个问题:

(1)函数的功能要单一

有些程序员喜欢写一些大而全的函数,这个函数既可以做A又可以做B,甚至还可以做C,函数的内部依据某些变量和条件,来判断这个函数究竟需要做什么。比如,下面这个函数:

void foo() {
  if (getOS().equals("MacOS")) {
    a();
  } else {
    b();
  }
  c();
  if (getOS().equals("MacOS")) {
    d();
  } else {
    e();
  }
}

写这个函数的人,根据系统是否为“MacOS”来做不同的事情。你可以看出这个函数里,其实只有c()是两种系统共有的,而其它的a(), b(), d(), e()都属于不同的分支。

这种复用其实是有很多弊端的。如果一个函数可能做两件事(甚至更多),而且它们之间的共同点少于它们的不同点,那我们最好写两个不同的函数,否则这个函数的逻辑会非常的混乱,且容易出现错误。其实,上面这个函数可以改写成两个函数:

void fooMacOS() {
  a();
  c();
  d();
}

void fooOther() {
  b();
  c();
  e();
}

如果发现两个函数大部分内容相同,只有少数不同,我们则可以把相同的部分提取出去,做成一个辅助函数。比如,下面这个函数:

void foo() {
  a();
  b()
  c();
  if (getOS().equals("MacOS")) {
    d();
  } else {
    e();
  }
}

其中a(),b(),c()都是一样的,只有d()和e()根据系统有所不同。那么我们可以把a(),b(),c()提取出去:

void preFoo() {
  a();
  b()
  c();
}

然后拆分成两个函数:

void fooMacOS() {
  preFoo();
  d();
}

void fooOther() {
  preFoo();
  e();
}

这样一来,我们既共享了代码,又做到了每个函数只做一件简单的事情。这样的代码,逻辑更加清晰,且不容易出错。

(2)封装小的工具函数

如果我们仔细观察代码,通常会发现里面有很多重复的片段。这些常用的代码,不管它有多短,提取出去做成函数,都可能是会有好处的。有些工具函数也许只有两行,然而它却能大大简化主要函数里面的逻辑。

有些人不喜欢使用小函数,想避免函数调用的开销,结果很容易写出几百行的大函数。函数调用引起的开销,其实是一种过时的观念。现代的编译器都能自动的把小的函数内联(inline)到调用它的地方,所以根本不产生函数调用,更不会产生任何多余的开销。

还有一些人,喜欢使用宏(macro)来代替小函数,这也是一种过时的观念。在早期的C语言编译器里,只有宏是静态“内联”的,所以他们使用宏,其实是为了达到内联的目的。然而能否内联,其实并不是宏与函数的根本区别。宏与函数有着巨大的区别,应该尽量避免使用宏。为了内联而使用宏,其实是滥用了宏,这会引起各种各样的麻烦,比如使程序难以理解,难以调试,容易出错等等。

(3)杜绝太长的函数

如果发现函数太大,应该把它拆分成几个更小的。通常我们写的函数最好不超过40行。一般笔记本电脑屏幕所能容纳的代码行数是50行。如果长度不超过40行的话,我们可以一目了然的看完整个函数,而不需要滚屏。另外,只所以是40行而不是50行的原因是,我们的眼球不转的话,最大的视角通常只能看得到40行代码。

如果我们看代码不转眼球,就能把函数的代码完整的映射到脑子里,这样就算忽然闭上眼睛,也能看得见这段代码。这样有利于我们更加有效地处理代码,有利于在脑海中构思这段代码是否还有更好的实现方式。40行并不是一个很大的限制,因为函数里面比较复杂的部分,往往更应该被提取出去,做成更小的函数,然后从原来的函数里面调用。

(4)减少使用全局变量和类成员变量

避免使用全局变量和类成员(class member)来传递数据,尽量使用局部变量和参数。有些人写代码,经常用类成员来传递信息,就像这样:

 class A {
   String x;

   void findX() {
      ...
      x = ...;
   }

   void foo() {
     findX();
     ...
     print(x);
   }
}

首先,使用findX(),把一个值写入成员x。然后,使用x的值。这样,x就变成了findX和print之间的数据通道。由于x属于class A,这样函数就失去了模块化的结构。由于这两个函数依赖于成员x,它们不再有明确的输入和输出,而是依赖全局的数据。findX和foo不再能够离开class A而存在,而且由于类成员还有可能被其他代码改变,代码变得难以维护,难以确保正确性。

如果使用局部变量而不是类成员来传递信息,那么这两个函数就不需要依赖于某一个class,而且更加容易理解,不易出错:

 String findX() {
    ...
    x = ...;
    return x;
 }
 void foo() {
   String x = findX();
   print(x);
 }

添加新评论