こんなところにも DRY 原則

2020年1月19日

プログラミングで重視されている価値観の一つに、DRY 原則と呼ばれるものがあります。要するに「同じことを二度書くな」という意味です。

大抵はこの後、コードの重複は関数にすることで共通化して……と話が続くのですが、これは初歩中の初歩なので、説明は他のサイトに任せます。ここでは、DRY 原則は「思った以上に」コードの細部に宿るという話をします。

コード例

ここでは実際のコード例を3つ挙げますが、もちろんこれがすべてではありません。というよりも、DRY 原則はコードの隅々にまで行き渡らせるものなので、一つ一つ挙げていくことはできないのです。ですから、この先あなた自身が新しい DRY を発見することもあるに違いありません。

ビット演算

組み込み屋にはおなじみビット演算です。次のコードは、あるレジスタの特定のビットを抽出する処理です。このコードに、どのような感想を持たれますか?

bool isPoweredOn()
{
  return (PORT_A & 0x02) == 0x02;
}

上のコードは、細かいレベルで DRY に反しています。0x02 が2回登場するからです。細かすぎるでしょうか。でも、この両辺のうち片方を修正し忘れたというバグを、私は少なくとも2回見たことがあります。DRY に従えば、こうなります。

bool isPoweredOn()
{
  return (PORT_A & 0x02) != 0;
}

バグはいつでも現れる隙をうかがっています。DRY に反するコードというのは結局のところ、そのチャンスをみすみす与えているのと同じことです。

配列の要素数

次のコードは、DRY に反するコードの典型例です。

int g_values[12];

int getSum()
{
  int sum = 0;
  for (int i = 0; i < 12; ++i)
  {
    sum += g_values[i];
  }
  return sum;
}

次に、一般に DRY に従っているとされるコードを示します。

static const int NUM_VALUES = 12;
int g_values[NUM_VALUES];

int getSum()
{
  int sum = 0;
  for (int i = 0; i < NUM_VALUES; ++i)
  {
    sum += g_values[i];
  }
  return sum;
}

間違ってはいませんが、私は個人的にこの解決策はあまりよくないと感じています。理由は3つあります。

  • 本当に正しいラベルを参照しているのかという不安が拭えない
  • コードの断片を見ても実サイズがわからず、たらい回しにされる
  • 開発規模が膨らむと NUM 何とかというラベルが入り乱れることになる

私は sizeof 演算子による解決を推奨したいです。

int g_values[12];

int getSum()
{
  int sum = 0;
  for (int i = 0; i < sizeof(g_values) / sizeof(*g_values); ++i)
  {
    sum += g_values[i];
  }
  return sum;
}

これなら先に挙げた3つの問題はすべて解消されます。

プロトタイプ宣言

次に挙げるのは、プロトタイプ宣言を使ったプログラムの例です。

static int add(int a, int b);

int main()
{
  printf("%d\n", add(1, 2));
  return 0;
}

static int add(int a, int b) { return a + b; }

プロトタイプ宣言は、それ自体が DRY に反するコードの典型であり、無用の長物です。それどころか、引数の型の不一致をコンパイラが見逃して、バグにつながる場合すらあります。

プロトタイプ宣言は、関数を「呼び出されるものから先に」並べるだけで、要らなくなります。

static int add(int a, int b) { return a + b; }

int main()
{
  printf("%d\n", add(1, 2));
  return 0;
}

このように順番を並び替えることで失うものは、何もありません。