読者です 読者をやめる 読者になる 読者になる

グループワークに向けて(2)

今回の目的

今回はグループワークに向けて(0)で上げた問題点のうち、問題2の方についてです。 コードの可読性はグループワークの作業効率に大きく影響すると思います。(あくまで予想ですが)

なので「どのようにコードを書けばわかりやすいのか」を考えていきましょう。

リーダブルコード

リーダブルコードという本を読むことにしました。

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

この本のサブタイトルは『より良いコードを書くためのシンプルで実践的なテクニック』となっています。 ここで言う『良いコード』とは、端的に言ってしまえば読みやすいコードということでしょう。 今回は第一部の『表面上の改善』について、実際にコードを見直して学習していきましょう。

第Ⅰ部 『表面上の改善』

このテーマはすごく大切だ。コードのすべての行に影響するからね。 個々の変更は小さいかもしれないけど、それらを合わせれば、コードに大きな改善をもたらせる。

(「リーダブルコード」第Ⅰ部より)

ここでは出来るだけ短い時間でコードを読めるようにするにはどのようにコードを書けば良いのかについて言及しています。

  • 変数や関数の名前に情報を埋め込む
  • 紛らわしい名前を避ける
    • ex) filter 選択するのか除去するのかわからない
  • 一貫性のある美しいコードを書く
    • 似たような部分を似せる。=,などの位置を揃える。
  • 簡潔で価値のあるコメントを書く
    • コメントを読むために消費する時間に見合うだけのコメントを書こう
    • 関数の引数などをわかりやすく示すのもいい。

大まかに上げればこのような感じです。読んでみると「えっ!?このコードってだめなの!?」と思うことも多々ありました... とても面白い本なので、是非読んでみてください!

実践してみよう

せっかく勉強したので実際にコードを改善してみましょう。

static final int vert = 7;
static final float r = 180;
static final int max_level = 2;
static final float k = 2 / (1 + sqrt(5));

void setup() {
  blendMode(ADD);
  stroke(0, 0, 255);
  smooth();
  noFill();
  size(displayWidth, displayHeight);
  background(0);
  float x = 0;
  float y = 0;
  translate(width/2, height/2);
  rotate(radians(270/vert));
  fractalloop(r, x, y, 0);
  save("fractal.png");
}

void drawShape(float _r, float _x, float _y) {
  beginShape();
  for (int i = 0; i < vert; i++) {
    float x = cos(radians(i * 360 / vert)) * _r + _x;
    float y = sin(radians(i * 360 / vert)) * _r + _y;
    vertex(x, y);
  }
  endShape(CLOSE);
}

void fractalloop(float _r, float _x, float _y, int level) {
  if (level > max_level) {
    return;
  }
  stroke(level * 10, 100 + level * 4, 100 + level * 50, 100 - level * 25);
  for (int i = 0; i < vert; i++) {
    float x = cos(radians(i * 360 / vert)) * _r + _x;
    float y = sin(radians(i * 360 / vert)) * _r + _y;
    drawShape(_r, x, y);

    float r = _r * k;
    fractalloop(r, x, y, level+1);
  }
}

これは僕が実際に書いたフラクタル図形を描くprocessingのプログラムコードです。 実行結果はこんな感じ。

f:id:fms_eraser:20160930000503p:plain

フラクタル図形って綺麗でいいですよね。では、コードを見直していきましょう。

定数

最初は定数の宣言です。vertとかrとかありますが、なんの為の定数なのかが明確ではありません。 また、変数との見分けもつきづらいですね。 rという変数はradiusから来ていると思いますが、直径を代入してしまう人も少なくないでしょう。 その点に注意して書き換えましょう。

// MAX_LEVELはループ回数の上限
// SCALEは辺の縮小率、黄金比を代入
static final int   VERTEX    = 7;
static final int   MAX_LEVEL = 2;
static final float RADIUS    = 180;
static final float SCALE     = 2 / (1 + sqrt(5));

定数はすべて大文字表記に変更しました。 rではなくRADIUSと全文表記することで、半径であることをわかりやすくしています。 SCALEに関しては、どうしてもわかりやすい名前が思いつかなかったので、コメントで補足をしています。

関数

次は関数です。関数は2つありますが、drawShapefractalloopで、なんの関数かわかりづらいですね。 コメントも一切ないので、何をしているのかが全くわかりません。

// 正(VERTEX)角形を書く
void drawPolygon(float _centerX, float _centerY, float _radius) {
  beginShape();

  for (int i = 0; i < VERTEX; i++) {
    float topX = cos(radians(i * 360 / VERTEX)) * _radius + _centerX;
    float topY = sin(radians(i * 360 / VERTEX)) * _radius + _centerY;
    vertex(topX, topY);
  }

  endShape(CLOSE);
}

// フラクタルの描画を制御
void drawFractal(float _centerX, float _centerY, float _radius, int _level) {
  // 再帰ループを脱出
  if (_level > MAX_LEVEL) {
    return;
  }

  stroke(_level*10, 100+_level*4, 100+_level*50, 100-_level*25);

  // 各頂点を求めなおしている -> 改善の余地あり
  for (int i = 0; i < VERTEX; i++) {
    float topX = cos(radians(i * 360 / VERTEX)) * _radius + _centerX;
    float topY = sin(radians(i * 360 / VERTEX)) * _radius + _centerY;
    drawPolygon(topX, topY, _radius);
    float nextRadius = _radius * SCALE;

    // 再帰ループ
    drawFractal(topX, topY, nextRadius, _level+1);
  }
}

書き直した結果がこちらです。関数の引数には規則として_をつけました。 関数の名前は簡潔で正確なものに変え、変数の情報量も増やし、ひと目で分かるように変更しました。 コメントで改善できそうなところを見つけて示しておくと、グループワークでコードを見直したときの変更すべき点がわかりやすいと思います。 for文のiは使用するスコープが小さく、補足するほど重要な変数ではないと考えてそのままにしておきました。

setup()関数

最後はメインのsetup()です。

void setup() {
  size(displayWidth, displayHeight);
  background(0);

  // 色加算合成モードに変更
  blendMode(ADD);
  smooth();
  noFill();
  
  translate(width/2, height/2);
  rotate(radians(270/VERTEX));
  drawFractal(0, 0, RADIUS, 0);

  save("fractal.png");
}

大きな変更点はないですが、それぞれの行をブロックに分けた事によって理解しやすくしています。 また、不要な変数を削除してより見やすくしました。 みんなが知らないような関数があればコメントで何をしているのか書いてあげると丁寧だと思います。

まとめ

すべてをまとめてみましょう。

// MAX_LEVELはループ回数の上限
// SCALEは辺の縮小率、黄金比を代入
static final int   VERTEX    = 7;
static final int   MAX_LEVEL = 2;
static final float RADIUS    = 180;
static final float SCALE     = 2 / (1 + sqrt(5));

void setup() {
  size(displayWidth, displayHeight);
  background(0);

  // 色加算合成モードに変更
  blendMode(ADD);
  smooth();
  noFill();
  
  translate(width/2, height/2);
  rotate(radians(270/VERTEX));
  drawFractal(0, 0, RADIUS, 0);

  save("fractal.png");
}

// 正(VERTEX)角形を書く
void drawPolygon(float _centerX, float _centerY, float _radius) {
  beginShape();

  for (int i = 0; i < VERTEX; i++) {
    float topX = cos(radians(i * 360 / VERTEX)) * _radius + _centerX;
    float topY = sin(radians(i * 360 / VERTEX)) * _radius + _centerY;
    vertex(topX, topY);
  }

  endShape(CLOSE);
}

// フラクタルの描画を制御
void drawFractal(float _centerX, float _centerY, float _radius, int _level) {
  // 再帰ループを脱出
  if (_level > MAX_LEVEL) {
    return;
  }

  stroke(_level*10, 100+_level*4, 100+_level*50, 100-_level*25);

  // 各頂点を求めなおしている -> 改善の余地あり
  for (int i = 0; i < VERTEX; i++) {
    float topX = cos(radians(i * 360 / VERTEX)) * _radius + _centerX;
    float topY = sin(radians(i * 360 / VERTEX)) * _radius + _centerY;
    drawPolygon(topX, topY, _radius);
    float nextRadius = _radius * SCALE;
    // 再帰ループ
    drawFractal(topX, topY, nextRadius, _level+1);
  }
}

結果的に行数は多くなってしまいましたが、読みやすさは上がっていると思います。 これからは『良いコード』を意識してプログラミングをしていきましょう。 次回もリーダブルコードの続きを勉強していきましょう。