見出し画像

#3 カプセル化

オブジェクト指向に関して自分なりの理解を記事にしています。
前回記事ではオブジェクト指向における「オブジェクト」とは何かを書きました。
今回は「カプセル化」についてです。前回作成したサンプルコードを修正していきます。

<前回作成したサンプルコード>
「犬、猫、象にそれぞれ鳴き声をあげてもらう。1000メートル走してもらい一位を表示する」プログラム(一部オブジェクト化したVer)

// 犬
class Dog {
  String name = "犬";
  String cry = "わん";
  int speed = 5;
  // 鳴く
  String toCry() {
    return cry;
  }
  // 走る
  double run(int distance) {
    return distance / speed; 
  }
}
// 猫
class Cat {
  String name = "猫";
  String cry = "にゃん";
  int speed = 4;
  // 鳴く
  String toCry() {
    return cry;
  }
  // 走る
  double run(int distance) {
    return distance / speed; 
  }
}
// 象
class Elephant {
  String name = "象";
  String cry = "ぱおん";
  int speed = 20;
  // 鳴く
  String toCry() {
    return cry;
  }
  // 走る
  double run(int distance) {
    return distance / speed; 
  }
}

void main() {
  Dog dog = Dog();
  Cat cat = Cat();
  Elephant elephant = Elephant();
  
  // ----------------------------------------------
  // 動物に鳴き声を出力する
  // ----------------------------------------------
  animalCry(dog.name, dog.toCry());
  animalCry(cat.name, cat.toCry());
  animalCry(elephant.name, elephant.toCry());
  
  // ----------------------------------------------
  // 動物に1000メートル走してもらい一番を決める
  // ----------------------------------------------
  const int runDistance = 1000;
  double dogTime = dog.run(runDistance);
  double catTime = cat.run(runDistance);
  double elephantTime = elephant.run(runDistance);
  // 最も速い動物の判定
  String fastestAnimalName = "";
  double fastestTime = double.maxFinite;
  if (dogTime < fastestTime) {
    fastestTime = dogTime;
    fastestAnimalName = dog.name;
  }
  if (catTime < fastestTime) {
    fastestTime = catTime;
    fastestAnimalName = cat.name;
  }
  if (elephantTime < fastestTime) {
    fastestTime = elephantTime;
    fastestAnimalName = elephant.name;
  }
  print("1000メートル走で最も早いのは$fastestAnimalNameです。");
}

// 鳴き声を出力する関数
void animalCry(String name, String cry) {
  print("$nameの鳴き声は「$cry」です。");
}

// 距離ごとのタイムを計算する関数
double calcRunTime(int runDistance, int speed) {
  return runDistance / speed;
}

ChatGPTから得た解説に対して補足する形で進めていきます。


3.カプセル化

1.カプセル化(Encapsulation):
・データ(属性)とそのデータを操作するメソッド(関数)を一つのオブジェクト内にまとめます。
・オブジェクトの詳細な実装は隠されており、外部からは公開されているインターフェースを通じてのみアクセスできます。

ChatGPTより

ChatGPTのカプセル化に関する説明です。
前者は前回記事にさせていただいたので、後者について書いていきます。

3-1.カプセル化とは

カプセル化は上記説明そのままですが、「オブジェクト内にまとめたデータとメソッドを外部へ必要な部分のみ公開する機能」です。
外部へ公開する部分以外隠す機能」とも言えます。

※今回から、この「データ」という表現を変え「フィールド」と呼びます。エンジニアにはこっちの方が違和感ないと思うので。前回のソースコードではDogクラスのname,cry,speedがフィールドにあたります。

  String name = "犬";
  String cry = "わん";
  int speed = 5;

前回のサンプルでは、このカプセル化を使っていません。そのため、全フィールドとメソッドに外部からアクセス出来ます。
以下、前回サンプルを少し変えて外部からフィールドにアクセスし変更してみました。カプセル化が何故必要か含めて書いていきます。

<ソースコード>

// 犬
class Dog {
  String name = "犬";
  String cry = "わん";
  int speed = 5;
  // 鳴く
  String toCry() {
    return cry;
  }
  // 走る
  double run(int distance) {
    return distance / speed; 
  }
}

void main() {
  Dog dog = Dog();
  // データ部分に外部からアクセス
  dog.name = "いぬぬ";
  dog.cry = "わぉ";
  
  // ----------------------------------------------
  // 動物に鳴き声を出力する
  // ----------------------------------------------
  animalCry(dog.name, dog.toCry());
}

// 鳴き声を出力する関数
void animalCry(String name, String cry) {
  print("$nameの鳴き声は「$cry」です。");
}

<実行結果>

いぬぬの鳴き声は「わぉ」です。

犬のオブジェクトの「名前」と「鳴き声」をmain関数から変更出来ました。
実行結果がちょっと意図しない動きをしていることが分かります。
この「外部からのオブジェクトへのアクセスで意図しない動きを防ぐ」がカプセル化で重要な部分となります。

3-2.実装方法

カプセル化の実装方法はプログラミング言語で少し違うのですが、前に「private」を付けることが多いと思います。公開する場合は「public」。

private String name; // 外部からアクセス出来ない
public String name; // 外部からアクセス出来る

ただ、今回はDart言語を使用しているので少し違います。Dart言語の場合は変数名の前に「_」を付けます。

// 犬
class Dog {
  String _name = "犬";
  String _cry = "わん";
  int _speed = 5;
  // 鳴く
  String toCry() {
    return _cry;
  }
  // 走る
  double run(int distance) {
    return distance / _speed; 
  }
}
  dog._name = "いぬぬ";
  dog._cry = "わぉ";

。。。あれ、エラーが出るはずだったのに動く。何でだろう。
どうも、Dartでは同じファイル内であれば「_」付けてもアクセス出来るっぽいです。
この記事を通しで使用しているDartPadだとファイル分割厳しかったので、VSCodeで作り直しました。
animal.dartとmain.dartの2ファイルに分けて実装しています。

<ソースコード>

// 犬
class Dog {
  String _name = "犬";
  String _cry = "わん";
  int _speed = 5;
  // 鳴く
  String toCry() {
    return _cry;
  }

  // 走る
  double run(int distance) {
    return distance / _speed;
  }
}

animal.dart

import 'animal.dart';

void main() {
  Dog dog = Dog();
  // データ部分に外部からアクセス
  dog._name = "いぬぬ";
  dog._cry = "わぉ";

  // ----------------------------------------------
  // 動物に鳴き声を出力する
  // ----------------------------------------------
  animalCry(dog._name, dog.toCry());
}

// 鳴き声を出力する関数
void animalCry(String name, String cry) {
  print("$nameの鳴き声は「$cry」です。");
}

main.dart

_nameが見つからないよエラー

無事?エラーが出ました。カプセル化はこの様に行います。メソッドに対しても同様にメソッド名に「_」を付ければ同様のことが出来ます。

3-3.フィールドの公開方法

3-2でフィールドを外部からアクセス出来ないよう隠しましたが、main関数でnameフィールドを使用しているので対応が必要です。
こういう場合はgetterというフィールドを読み取り専用で使える方法を取るのが一般的かと思います。

String get name => _name;

nameというgetterを作りました。これでmain関数から取得できます。
それでは、前回のソースコードをカプセル化を使用し作り直します。

<ソースコード>

// 犬
class Dog {
  String _name = "犬";
  String _cry = "わん";
  int _speed = 5;
  String get name => _name; // getter
  // 鳴く
  String toCry() {
    return _cry;
  }
  // 走る
  double run(int distance) {
    return distance / _speed;
  }
}

// 猫
class Cat {
  String _name = "猫";
  String _cry = "にゃん";
  int _speed = 4;
  String get name => _name; // getter
  // 鳴く
  String toCry() {
    return _cry;
  }
  // 走る
  double run(int distance) {
    return distance / _speed;
  }
}

// 象
class Elephant {
  String _name = "象";
  String _cry = "ぱおん";
  int _speed = 20;
  String get name => _name; // getter
  // 鳴く
  String toCry() {
    return _cry;
  }
  // 走る
  double run(int distance) {
    return distance / _speed;
  }
}

animal.dart

import 'animal.dart';

void main() {
  Dog dog = Dog();
  Cat cat = Cat();
  Elephant elephant = Elephant();

  // ----------------------------------------------
  // 動物に鳴き声を出力する
  // ----------------------------------------------
  animalCry(dog.name, dog.toCry());
  animalCry(cat.name, cat.toCry());
  animalCry(elephant.name, elephant.toCry());

  // ----------------------------------------------
  // 動物に1000メートル走してもらい一番を決める
  // ----------------------------------------------
  const int runDistance = 1000;
  double dogTime = dog.run(runDistance);
  double catTime = cat.run(runDistance);
  double elephantTime = elephant.run(runDistance);
  // 最も速い動物の判定
  String fastestAnimalName = "";
  double fastestTime = double.maxFinite;
  if (dogTime < fastestTime) {
    fastestTime = dogTime;
    fastestAnimalName = dog.name;
  }
  if (catTime < fastestTime) {
    fastestTime = catTime;
    fastestAnimalName = cat.name;
  }
  if (elephantTime < fastestTime) {
    fastestTime = elephantTime;
    fastestAnimalName = elephant.name;
  }
  print("1000メートル走で最も早いのは$fastestAnimalNameです。");
}

// 鳴き声を出力する関数
void animalCry(String name, String cry) {
  print("$nameの鳴き声は「$cry」です。");
}

// 距離ごとのタイムを計算する関数
double calcRunTime(int runDistance, int speed) {
  return runDistance / speed;
}

main.dart

<実行結果>

犬の鳴き声は「わん」です。
猫の鳴き声は「にゃん」です。
象の鳴き声は「ぱおん」です。
1000メートル走で最も早いのは象です。

出来ました。これで、main関数からは犬・猫・像それぞれのオブジェクトから必要なフィールドしか見えなくなりました。

3-4.カプセル化は必要?

ここまでカプセル化の記事を書きましたが、改めてカプセル化は必要か考えます。
私は絶対必要というスタンスで仕事してはいますが、もう少し踏み込むとこう考えています。

複数人で開発する場合、そのプログラムが保守される可能性がある場合は必須。一時的な自作ツールなどは速度優先であえて無視。

開発は仕事上、一人で開発することは少ないと思います。自分が作ったクラスを予期しない使い方されないためにも必要だと思います。
オブジェクト指向は作ったその人の思想にかなり左右されます。他者が思わぬ使い方をすることもあります。公開するフィールドやメソッドは限定するのが吉です。

でも、カプセル化に限らずオブジェクト指向を意識して設計するのは結構大変なんですよね。
今回の簡単な例でもフィールドに対してgetterを用意するなど少し手間かけますし。
なので、速度優先で使うのが一時的なツールなどはオブジェクト指向にこだわって作る必要はないと思います。

どんな状況でもオブジェクト指向が必須というわけではないよ、とだけ書いて今回の記事を終わります。

次回は「継承」です。

この記事が気に入ったらサポートをしてみませんか?