BLOG

2021-03-05

CLEANコードを書こう。

どうも、畜産ペンギンです。

 

花粉症が辛くなって来た今日この頃。

 

今日はCLEANコードという物を語って行こうと思います。

 

以前「保守性の高いコードを書くのに必要な事という記事で触れましたが、そこの詳細となります。まぁ要は質の高いコードを書く為のプラクティスのような物です。

 

では、れっつとらい。

 

目次

・Loosely Coupled(疎結合)

・Encapsulated(カプセル化)

・Assertive(断定的)

・Nonredundant(非冗長)

・Cohesive(凝集性)

・まとめ

※Cohesive(凝集性)はあえて最後に説明しています。

 

Loosely Coupled(疎結合)

 

「疎結合」という言葉の意味はよくわかりませんが、直接呼び出すのではなく抽象クラスを使用する等してコードに対して間接的にしか依存しない形が良しとされています。
抽象クラスがどんな物か知らない人もいるかもしれませんが、そこも併せて説明するのでご安心を。

 

では、何故いいのでしょう?

 

例えば、次のような処理を作るとします。

 

1-1.Toyotaというクラスに車名を設定する。
1-2.Toyotaというクラスに乗車人数を設定する。
1-3.Toyotaというクラスに速度を設定する。
1-4.Toyotaというクラスの性能を説明する。
2-1.Nissanというクラスに車名を設定する。
2-2.Nissanというクラスに乗車人数を設定する。
2-3.Nissanというクラスに速度を設定する。
2-4.Nissanというクラスの性能を説明する。

 

これを「疎結合」を意識しないで書くと以下のような形になります。

 

public class Toyota {

  private String name; // 車名
  private int count; // 乗車人数
  private int speed; // 速度

  // 車名を設定する
  public void setName(String name) {
     this.name = name;
  }

  // 乗車人数を設定する
  public void setCount(int count) {
    this.count = count;
  }

  // 速度を設定する
  public void setSpeed(int speed) {
    this.speed = speed;
  }

  // 性能を説明する
  public void description() {
    System.out.println(“車名:” + name);
    System.out.println(“乗車人数:” + count);
    System.out.println(“速度:” + speed);
  }
}

 

public class Nissan {

  private String name; // 車名
  private int count; // 乗車人数
  private int speed; // 速度

  // 車名を設定する
  public void setName(String name) {
    this.name = name;
  }

  // 乗車人数を設定する
  public void setCount(int count) {
    this.count = count;
  }

  // 速度を設定する
  public void setSpeed(int speed) {
    this.speed = speed;
  }

  // 性能を説明する
  public void description() {
    System.out.println(“車名:” + name);
    System.out.println(“乗車人数:” + count);
    System.out.println(“速度:” + speed);
  }
}

 

public class LooselyCoupledBadLogic {

  public void logic() {

    Toyota t = new Toyota();
    t.setName(“トヨタ”);
    t.setCount(4);
    t.setSpeed(180);
    t.description();

    Nissan n = new Nissan();
    n.setName(“ニッサン”);
    n.setCount(6);
    n.setSpeed(100);
    n.description();
  }

}

 

※今回は簡易化している為ToyotaとNissanで処理が同じになっているのでクラスを分ける必要がありませんが実際はそれぞれのメソッドで中の処理は異なると考えて下さい。

 

「疎結合」を意識して書くと以下のような形になります。

 

public abstract class Car {

  // 車名を設定する
  abstract public void setName(String name);

  // 乗車人数を設定する
  abstract public void setCount(int count);

  // 速度を設定する
  abstract public void setSpeed(int speed);

  // 性能を説明する
  abstract public void description();
}

 

public class Nissan extends Car {

  private String name; // 車名
  private int count; // 乗車人数
  private int speed; // 速度

  // 車名を設定する
  @Override
  public void setName(String name) {
    this.name = name;
  }

  // 乗車人数を設定する
  @Override
  public void setCount(int count) {
    this.count = count;
  }

  // 速度を設定する
  @Override
  public void setSpeed(int speed) {
    this.speed = speed;
  }

  // 性能を説明する
  @Override
  public void description() {
    System.out.println(“車名:” + name);
    System.out.println(“乗車人数:” + count);
    System.out.println(“速度:” + speed);
  }
}

 

public class Toyota extends Car {

  private String name; // 車名
  private int count; // 乗車人数
  private int speed; // 速度

  // 車名を設定する
  @Override
  public void setName(String name) {
    this.name = name;
  }

  // 乗車人数を設定する
  @Override
  public void setCount(int count) {
    this.count = count;
  }

  // 速度を設定する
  @Override
  public void setSpeed(int speed) {
    this.speed = speed;
  }

  // 性能を説明する
  @Override
  public void description() {
    System.out.println(“車名:” + name);
    System.out.println(“乗車人数:” + count);
    System.out.println(“速度:” + speed);
  }
}

 

public class LooselyCoupledGoodLogic {

  public void logic() {

    Toyota t = new Toyota();
    t.setName(“トヨタ”);
    t.setCount(4);
    t.setSpeed(180);
    t.description();

    Nissan n = new Nissan();
    n.setName(“ニッサン”);
    n.setCount(6);
    n.setSpeed(100);
    n.description();
  }

}

 

Carという抽象クラスが定義され、Toyota、NissanはCarを継承しています。
こうすると何が良いかと言うと、
まず前提として抽象クラスを継承したクラスは、抽象メソッドを全て実装しないとコンパイルエラーとなります。

 

ですので、例えばToyota、Nissanクラスを作る人をそれぞれ別の人に割り振るとしましょう。
その際どんな機能を作るべきか、それぞれの人に認識違いが起きる可能性があります。
ですが、Carという抽象クラスを予め定義しておいて、そこのメソッドに書いてある機能をそれぞれ作っておいてくれという形で指示する事により、機能の統一化を図る事が出来ます。

 

Encapsulated(カプセル化)

 

詳細な実装は外部から隠し、データに関して外からアクセスは出来ないような形が良しとされています。

 

では、何故いいのでしょう?

 

例えば、次のような処理を作るとします。

 

1.速度を変数に持つToyotaというクラスを作成する。
2.速度に0以上100未満の乱数を足す。(加速処理)
3.速度が100を超えている場合速度を100とする。
4.速度を出力する。
5.1~4を3回繰り返す。

これを「カプセル化」を意識しないで書くと以下のような形になります。

 

public class Toyota {

  public int speed; //速度

}

 

public class EncapsulatedBadLogic {

  public void logic() {

    Toyota t = new Toyota();
    Random r = new Random(); // 乱数を生成するクラス

    for (int i = 0; i < 3; i++){
      t.speed = t.speed + r.nextInt(100); // 0以上、100未満の乱数をプラス
      if (t.speed > 100) {
        t.speed = 100;
      }
      System.out.println(“速度:” + t.speed);
    }

  }

}

 

「カプセル化」を意識して書くと以下のような形になります。

 

public class Toyota {

  private int speed; //速度

  public int getSpeed() {
    return speed;
  }

  public void acceleration() {
    Random r = new Random(); // 乱数を生成するクラス
    speed = speed + r.nextInt(100); // 0以上、100未満の乱数をプラス
    if (speed > 100) {
      speed = 100;
    }
  }
}

 

public class EncapsulatedGoodLogic {

  public void logic() {

    Toyota t = new Toyota();

    for (int i = 0; i < 3; i++){
      t.acceleration();
      System.out.println(“速度:” + t.getSpeed());
    }

  }

}

 

 

速度には直接アクセス出来ず、加速処理をToyotaクラスに渡しました。
こうすると何が良いかと言うと、
例えば加速処理を色んな所でやるとなると、100を超える場合100とする処理が抜けて無限に加速する暴走車になってしまうかもしれません。
これを防ぐ為に直接速度にはアクセスさせず、速度というデータを持つルールを定義した中でのみ扱う事ができます。

また、Javaの場合仕様書のような物を出力するJavadocにはpublicの物しか出力しないといった指定も出来たりします。
その場合加速処理の詳細は見せずに公開する事も可能となります。

 

Assertive(断定的)

 

オブジェクトに独立性があり、管理するのは自分自身とする形が良しとされています。

 

では、何故いいのでしょう?

 

例えば、次のような処理を作るとします。

 

1.預金を変数に持つBankというクラスを作成する。
2.預金に10000を代入する。
3.預金に1.05を掛ける。(利子処理)
4.預金を出力する。

 

これを「断定的」を意識しないで書くと以下のような形になります。

 

public class Bank {

  public static double deposit; //預金

}

 

public class Interest {

  public void interest() {
    Bank.deposit = Bank.deposit * 1.05;
  }

}

 

public class AssertiveBadLogic {

  public void logic() {

    Bank.deposit = 10000;

    Interest i = new Interest();
    i.interest();

    System.out.println(Bank.deposit);
  }

}

 

「断定的」を意識して書くと以下のような形になります。

 

public class Bank {

  private double deposit; //預金

  public double getDeposit() {
    return deposit;
  }

  public void setDeposit(double deposit) {
    this.deposit = deposit;
  }

  public void interest() {
    deposit = deposit * 1.05;
  }

}

 

public class AssertiveGoodLogic {

  public void logic() {

    Bank b = new Bank();

    b.setDeposit(10000);

    b.interest();

    System.out.println(b.getDeposit());
  }

}

 

カプセル化に少し似ていますが、意味合いが違います。
預金に関する処理は、全てBankの中に集約しています。
こうすると何が良いかと言うと、
預金に関する処理は全部Bankクラスの中で見えると言う事です。
オブジェクト指向を意識して機能分割を誤った方向に考えすぎてしまうと、前者のように利子処理を分けようと考えてしまうケースが出てきます。

ですが、預金を管理しているクラス以外で預金の処理を記載してしまうと気付かない所で預金が変更されている、という事態が起きてしまう可能性が出てきます。
ですので、Bankクラスのフィールドで扱っている預金はBank内で変更を完結させるのが良しとされています。
利子処理を他に分ける場合、あくまでBankクラスのinterest()メソッドから呼び出して委託する形にすると良いでしょう。

 

Nonredundant(非冗長)

 

同じ処理は一度にまとめ、繰り返さない形が良しとされています。

 

では、何故いいのでしょう?

 

例えば、次のような処理を作るとします。

 

1-1.変数aを10と定義する。
1-2.aに1を足す。
1-3.aに2を足す。
1-4.aに3を足す。
1-5.aを出力する。
2-1.変数bを20と定義する。
2-2.bに1を足す。
2-3.bに2を足す。
2-4.bに3を足す。
2-5.bを出力する。

 

これを「非冗長」を意識しないで書くと以下のような形になります。

 

public class NonredundantBadLogic {

  public void logic() {

    int a = 10;
    a = a + 1;
    a = a + 2;
    a = a + 3;
    System.out.println(a);

    int b = 20;
    b = b + 1;
    b = b + 2;
    b = b + 3;
    System.out.println(b);
  }

}

 

ですが、「非冗長」を意識し機能を分割すると以下のような形になります。

 

public class NonredundantGoodLogic {

  public void logic() {

    int a = 10;
    System.out.println(Calculation.addition(a));

    int b = 20;
    System.out.println(Calculation.addition(b));
  }

}

 

public class Calculation {

  public static int addition(int x) {

    x = x + 1;
    x = x + 2;
    x = x + 3;
    return x;

  }

}

 

このようになります。
こうすると何が良いかと言うと、
まず変数aとbどちらも計算式は同じですがその内容を一つにまとめる事ができて簡素になりました。
また、計算式の仕様変更があっても後者の方が便利です。
例えば、以下のように変更されたとします。

 

1-1.変数aを10と定義する。
1-2.aに1を足す。
1-3.aに2を足す。
1-4.aに3を足す。
1-5.aに4を足す。
1-6.aを出力する。
2-1.変数bを20と定義する。
2-2.bに1を足す。
2-3.bに2を足す。
2-4.bに3を足す。
2-5.bに4を足す。
2-5.bを出力する。

 

この場合、最初に書いた例だと以下のように修正します。

 

public class NonredundantBadLogic {

  public void logic() {

    int a = 10;
    a = a + 1;
    a = a + 2;
    a = a + 3;
    a = a + 4;
    System.out.println(a);

    int b = 20;
    b = b + 1;
    b = b + 2;
    b = b + 3;
    b = b + 4;
    System.out.println(b);
  }

}

 

後者の場合、以下のように修正します。

 

public class NonredundantGoodLogic {

  public void logic() {

    int a = 10;
    System.out.println(Calculation.addition(a));

    int b = 20;
    System.out.println(Calculation.addition(b));
  }

}

 

public class Calculation {

  public static int addition(int x) {

    x = x + 1;
    x = x + 2;
    x = x + 3;
    x = x + 4;
    return x;

  }

}

 

このように、前者の場合修正箇所が2か所ですが後者の場合1か所で済みます。
部品部品に分けて考えるいかにもオブジェクト指向って感じですね。

 

今回は例なので単純にしていますが、実際のプロジェクトだと量が膨大となる事が多い為、前者のように作られていると修正箇所を洗い出すだけでも大変です。

 

Cohesive(凝集性)

 

「凝集性」という言葉の意味はよくわかりませんが、要はクラスや関数等がそれぞれ持つ機能は単一とし、限られた機能を持ったクラスを沢山持つ形が良しとされています。

 

これまで記載してきた中で、色んな形で機能を分割して記載してきましたが実際のアプリケーションではコードの量が膨大となります。
参画したてのプロジェクトだと、目的のコードを探してもどこにあるのか全然わからんというのはありがちです。
ですがそれぞれ持つ機能を限らせ明確に分けられていれば、見つける事、どんなコードを書くべきか決めるのが容易となります。

 

まとめ

 

さて、いかがでしたでしょうか。
今回紹介した例は基礎的な内容で、入門書やWebで入門サイトを調べた人は似た内容を見た事あるかもしれません。
ですが実際にどう役立つかは記載されていない事も多く、今回背景も含めて書いてみました。

 

今回はここまでです。

 

さようなら。

The following two tabs change content below.

畜産ペンギン

主にネットワーク系、開発系を経験しているエンジニアです。 大きな業界単位のキーワードから細かい実作業の単位まで とりあえずは気まぐれに書いていこうかなと。

最新記事 by 畜産ペンギン (全て見る)