見出し画像

#1 オブジェクト指向って何だっけ

Java、VB.NET、C#などオブジェクト指向言語で開発していましたが、新人時代正直良く分かっていませんでした。今も自信はありません。
本記事では改めてオブジェクト指向について振り返りつつ、自分なりの理解を書いていこうと思います。

先日、ChatGPT4を使えるようになったので、本記事ではChatGPTにオブジェクト指向について説明してもらって、それに対して自分なりの補足を入れていこうと思います。

では、ChatGPT先生、オブジェクト指向についてメリットを踏まえて説明をお願いします。

オブジェクト指向プログラミングは、プログラムを「オブジェクト」という基本単位で考える方法です。オブジェクトとは、データ(プロパティ)とそれを操作する手続き(メソッド)を一つにまとめたものです。

オブジェクト指向の特徴
1.カプセル化(Encapsulation):
・データ(属性)とそのデータを操作するメソッド(関数)を一つのオブジェクト内にまとめます。
・オブジェクトの詳細な実装は隠されており、外部からは公開されているインターフェースを通じてのみアクセスできます。
2.継承(Inheritance):
・あるオブジェクト(子クラス)が別のオブジェクト(親クラス)の属性やメソッドを引き継ぐことができます。
・これにより、既存のコードの再利用や拡張が容易になります。
3.ポリモーフィズム(Polymorphism):
・異なるオブジェクトが同じインターフェース(メソッド)を持つことができ、同じ操作を異なる方法で実行することが可能です。
・これにより、オブジェクトの型に依存しない柔軟なプログラムを書くことができます。
4.抽象化(Abstraction):
・複雑な実装の詳細を隠し、よりシンプルなインターフェースを提供します。
・ユーザーは複雑な内部処理を理解することなく、オブジェクトを使用できます。

オブジェクト指向のメリット
1.再利用性:
・一度作成したオブジェクトやクラスは、異なるプログラムで再利用することが容易です。
2.保守性と拡張性:
・プログラムの特定部分を独立させることで、変更や拡張が容易になります。
3.柔軟性と効率性:
・ポリモーフィズムにより、異なるオブジェクトを同一の方法で扱うことができます。
4.モジュール性:
・オブジェクトごとに分割されているため、チームでの開発がしやすくなります。
5.理解しやすさ:
・現実世界のオブジェクトを模倣するため、直感的に理解しやすい構造を持っています。

オブジェクト指向プログラミングはこれらの特徴とメリットにより、特に大規模なアプリケーション開発や複数人でのプロジェクトで効果を発揮します。また、プログラミング初心者にとっても、システマチックで整理された方法でプログラムを学ぶのに役立つでしょう。

ChatGPT4

ありがとうございます。オブジェクト指向について聞いたら、オブジェクト指向プログラミングについて回答がありました。オブジェクト指向は考え方で、オブジェクト指向はその考え方を仕組みとして採用したプログラミング言語なので実は微妙に違うのですが、ここではスルーします。
ではあれこれ書いていきます。


1.概要部分(オブジェクト指向以前)

オブジェクト指向プログラミングは、プログラムを「オブジェクト」という基本単位で考える方法です。オブジェクトとは、データ(プロパティ)とそれを操作する手続き(メソッド)を一つにまとめたものです。

概要部分

1つのプログラムを如何に分けるかがとても大事。オブジェクト指向では、その単位を「オブジェクト」としています。ただ、新人時代理解し切れていませんでした。

ここでは、「犬、猫、象にそれぞれ鳴き声をあげてもらう。1000メートル走してもらい一位を表示する」プログラムを例に、オブジェクト指向以前の考え方でプログラムを書いていこうと思います。
まずはmain関数内に処理を全て書いた場合から関数化しつつ徐々にオブジェクト指向に向けて変更していきます。
※以下、簡単に済ませようと思いましたが、想定以上に長くなり過ぎました。

1-1.処理を全てmain関数内に実装した場合

プログラムの開始位置であるmain関数内に処理を全て書いた例が以下です。
ちなみにdartという言語で書いています。
DartPadにコピペして実行させると動きます。(言語仕様が変わってエラーとなっている可能性があります。)

<ソースコード>

// 犬のデータ
String dogName = "犬";
String dogCry = "わん";
int dogSpeed = 5;
// 猫のデータ
String catName = "猫";
String catCry = "にゃん";
int catSpeed = 4;
// 象のデータ
String elephantName = "象";
String elephantCry = "ぱおん";
int elephantSpeed = 20;

void main() {
  // ----------------------------------------------
  // 動物に鳴き声を出力する
  // ----------------------------------------------
  print("${dogName}の鳴き声は「${dogCry}」");
  print("${catName}の鳴き声は「${catCry}」");
  print("${elephantName}の鳴き声は「${elephantCry}」");
  
  // ----------------------------------------------
  // 動物に1000メートル走してもらい一番を決める
  // ----------------------------------------------
  const int runDistance = 1000;
  double dogTime = runDistance / dogSpeed;
  double catTime = runDistance / catSpeed;
  double elephantTime = runDistance / elephantSpeed;
  // 最も速い動物の判定
  String fastestAnimalName = "";
  double fastestTime = double.maxFinite;
  if (dogTime < fastestTime) {
    fastestTime = dogTime;
    fastestAnimalName = dogName;
  }
  if (catTime < fastestTime) {
    fastestTime = catTime;
    fastestAnimalName = catName;
  }
  if (elephantTime < fastestTime) {
    fastestTime = elephantTime;
    fastestAnimalName = elephantName;
  }
  print("1000メートル走で最も早いのは${fastestAnimalName}です。");
}

<実行結果>

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

<このプログラムのメリット>

この位の小さなプログラムなら、main関数内を見るだけで、ある程度把握しやすいと思います。
後述する関数化やオブジェクト化など、プログラム設計する時間を省略できることが出来ます。
1人で時間をかけずに作る簡単なツールを作る際に実施する方法と思います。

<このプログラムのデメリット>

①規模が大きくなるにつれ理解し辛い
ソースコードが例のレベルでは良いですが、数万ステップになると、main関数を見ても、どこで何の処理を処理をしているか探すだけで大変です。

②プログラムを修正する際の影響範囲が大きい
例えば、鳴き声を出力する処理を少しだけ変えるとします。今回は後ろに「です。」を付けるだけです。
前)XXの鳴き声は「YY」
後)XXの鳴き声は「YY」です。
影響箇所は以下の3行です(修正済です)。

  print("${dogName}の鳴き声は「${dogCry}」です。");
  print("${catName}の鳴き声は「${catCry}」です。");
  print("${elephantName}の鳴き声は「${elephantCry}」です。");

たかが3行ですが、プログラムの規模が大きくなるにつれて修正範囲が一気に広がります。修正漏れも起きやすくなり、テストも大変になります。

特に動物の種類が増えた場合は大変です。データから処理まで全て変更が必要となります。

上記デメリットを少しずつ変えていこうと思います。

1-2.共通の処理を関数化する

プログラムには処理を1つにまとめる方法があります。その単位を「関数」や「メソッド」など呼ばれます。本当は使い分ける必要があるのでしょうが、割愛。
まずは鳴き声を出力する処理ですが、関数化します。

void animalCry(String name, String cry) {
  print("${name}の鳴き声は「${cry}」");
}

作った関数を使用するよう修正します。

  // ----------------------------------------------
  // 動物に鳴き声を出力する
  // ----------------------------------------------
  animalCry(dogName, dogCry);
  animalCry(catName, catCry);
  animalCry(elephantName, elephantCry);

これで、鳴き声を出力する処理が変わっても修正範囲は比較的少なくなります。今回の例では関数内の1行だけ変えるだけで、犬・猫・象の鳴き声の出力内容全て変更されます。

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

他にも、1000メートル走のタイムを計算する処理は関数化出来そうです。
ということで関数化します。

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

これで、タイムの計算式が複雑になっても、この関数内の処理を変えるだけでよさそうです。
関数化したソースコードは以下となります。

<ソースコード>

// 犬のデータ
String dogName = "犬";
String dogCry = "わん";
int dogSpeed = 5;
// 猫のデータ
String catName = "猫";
String catCry = "にゃん";
int catSpeed = 4;
// 象のデータ
String elephantName = "象";
String elephantCry = "ぱおん";
int elephantSpeed = 20;

void main() {
  // ----------------------------------------------
  // 動物に鳴き声を出力する
  // ----------------------------------------------
  animalCry(dogName, dogCry);
  animalCry(catName, catCry);
  animalCry(elephantName, elephantCry);
  
  // ----------------------------------------------
  // 動物に1000メートル走してもらい一番を決める
  // ----------------------------------------------
  const int runDistance = 1000;
  double dogTime = calcRunTime(runDistance, dogSpeed);
  double catTime = calcRunTime(runDistance, catSpeed);
  double elephantTime = calcRunTime(runDistance, elephantSpeed);
  // 最も速い動物の判定
  String fastestAnimalName = "";
  double fastestTime = double.maxFinite;
  if (dogTime < fastestTime) {
    fastestTime = dogTime;
    fastestAnimalName = dogName;
  }
  if (catTime < fastestTime) {
    fastestTime = catTime;
    fastestAnimalName = catName;
  }
  if (elephantTime < fastestTime) {
    fastestTime = elephantTime;
    fastestAnimalName = elephantName;
  }
  print("1000メートル走で最も早いのは${fastestAnimalName}です。");
}

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

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

<実行結果>

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

動物ごとの処理を関数で共通化することで、修正箇所が減りました。

ただ、これだけだと「動物が増えた場合」の解決が出来ていません。次はこの辺り対応してみます。

1-3.動物のデータを1つにまとめたクラスを作成する

例えば、犬・猫・象の他に鳥を加えようとした場合、まずはデータ部分に影響があります。

// 犬のデータ
String dogName = "犬";
String dogCry = "わん";
int dogSpeed = 5;
// 猫のデータ
String catName = "猫";
String catCry = "にゃん";
int catSpeed = 4;
// 象のデータ
String elephantName = "象";
String elephantCry = "ぱおん";
int elephantSpeed = 20;
// 鳥のデータ
String birdName = "鳥"; // 追加 
String birdCry = "ぴぃ"; // 追加
int birdSpeed = 100; // 追加

このままだと、動物が増えるたびに「名前」「鳴き声」「速度」のセットを追加することになります。
このセットは一つにまとめます。

// 動物データ
class AnimalData {
  String name = "";
  String cry = "";
  int speed = 0;
}

AnimalDataというクラスを作りました。クラスについての説明はひとまず省くとして、「名前」「鳴き声」「速度」を1つにまとめた器を作った形です。その器に各動物のデータを設定するためAnimalDataと名付けました。AnimalDataクラスを使用した形にソースコードを修正します。

<ソースコード>

// 動物データ
class AnimalData {
  String name = "";
  String cry = "";
  int speed = 0;
}

void main() {
  // 犬のデータ
  AnimalData dogData = AnimalData();
  dogData.name = "犬";
  dogData.cry = "わん";
  dogData.speed = 5;
  // 猫のデータ
  AnimalData catData = AnimalData();
  catData.name = "猫";
  catData.cry = "にゃん";
  catData.speed = 4;
  // 象のデータ
  AnimalData elephantData = AnimalData();
  elephantData.name = "象";
  elephantData.cry = "ぱおん";
  elephantData.speed = 20;  
  
  // ----------------------------------------------
  // 動物に鳴き声を出力する
  // ----------------------------------------------
  animalCry(dogData.name, dogData.cry);
  animalCry(catData.name, catData.cry);
  animalCry(elephantData.name, elephantData.cry);
  
  // ----------------------------------------------
  // 動物に1000メートル走してもらい一番を決める
  // ----------------------------------------------
  const int runDistance = 1000;
  double dogTime = calcRunTime(runDistance, dogData.speed);
  double catTime = calcRunTime(runDistance, catData.speed);
  double elephantTime = calcRunTime(runDistance, elephantData.speed);
  // 最も速い動物の判定
  String fastestAnimalName = "";
  double fastestTime = double.maxFinite;
  if (dogTime < fastestTime) {
    fastestTime = dogTime;
    fastestAnimalName = dogData.name;
  }
  if (catTime < fastestTime) {
    fastestTime = catTime;
    fastestAnimalName = catData.name;
  }
  if (elephantTime < fastestTime) {
    fastestTime = elephantTime;
    fastestAnimalName = elephantData.name;
  }
  print("1000メートル走で最も早いのは${fastestAnimalName}です。");
}

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

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

ただ、上記の対応だけでは、鳥を追加したとしてもかなり書き加えることになります。

<実行結果>

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

1-4.For文使ってロジック見直す

動物のデータをクラスとしてまとめたことで、Listとfor文を駆使してたロジックに修正出来ます。
※本当はListではなく配列を使うべきなのですが、今回は説明の都合で外しています。
まずは犬・猫・象のデータをListに格納していきます。

  // 犬のデータ
  AnimalData dogData = AnimalData();
  dogData.name = "犬";
  dogData.cry = "わん";
  dogData.speed = 5;
  // 猫のデータ
  AnimalData catData = AnimalData();
  catData.name = "猫";
  catData.cry = "にゃん";
  catData.speed = 4;
  // 象のデータ
  AnimalData elephantData = AnimalData();
  elephantData.name = "象";
  elephantData.cry = "ぱおん";
  elephantData.speed = 20;
  // 動物のデータをリストに格納していく
  List<AnimalData> animals = []; // 追加 
  animals.add(dogData); // 追加
  animals.add(catData); // 追加
  animals.add(elephantData); // 追加

Listに格納したことで、後続の処理を幾つかfor文で処理します。
まずは鳴き声を出力する処理から。

  // ----------------------------------------------
  // 動物に鳴き声を出力する
  // ----------------------------------------------
  for (int i = 0; i < animals.length; i++) {
    animalCry(animals[i].name, animals[i].cry);  
  }

次に1000メートル走の処理を見直します。一気に変わります。

  // ----------------------------------------------
  // 動物に1000メートル走してもらい一番を決める
  // ----------------------------------------------
  const int runDistance = 1000;
  String fastestAnimalName = "";
  double fastestTime = double.maxFinite;
  for (int i = 0; i < animals.length; i++) {
    double runTime = calcRunTime(runDistance, animals[i].speed);
    if (runTime < fastestTime) {
      fastestTime = runTime;
      fastestAnimalName = animals[i].name;
    }
  }

修正した結果のソースコードが以下。

<ソースコード>

// 動物データ
class AnimalData {
  String name = "";
  String cry = "";
  int speed = 0;
}

void main() {
  // 犬のデータ
  AnimalData dogData = AnimalData();
  dogData.name = "犬";
  dogData.cry = "わん";
  dogData.speed = 5;
  // 猫のデータ
  AnimalData catData = AnimalData();
  catData.name = "猫";
  catData.cry = "にゃん";
  catData.speed = 4;
  // 象のデータ
  AnimalData elephantData = AnimalData();
  elephantData.name = "象";
  elephantData.cry = "ぱおん";
  elephantData.speed = 20;
  // 動物のデータをリストに格納していく
  List<AnimalData> animals = [];
  animals.add(dogData);
  animals.add(catData);
  animals.add(elephantData);
  
  // ----------------------------------------------
  // 動物に鳴き声を出力する
  // ----------------------------------------------
  for (int i = 0; i < animals.length; i++) {
    animalCry(animals[i].name, animals[i].cry);  
  }
  
  // ----------------------------------------------
  // 動物に1000メートル走してもらい一番を決める
  // ----------------------------------------------
  const int runDistance = 1000;
  String fastestAnimalName = "";
  double fastestTime = double.maxFinite;
  for (int i = 0; i < animals.length; i++) {
    double runTime = calcRunTime(runDistance, animals[i].speed);
    if (runTime < fastestTime) {
      fastestTime = runTime;
      fastestAnimalName = animals[i].name;
    }
  }
  print("1000メートル走で最も早いのは${fastestAnimalName}です。");
}

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

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

<実行結果>

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

ロジックを見直したことで幾分か処理がすっきりした感がありますが、ここからが本題。
このプログラムに鳥を加えてみます。どこを修正すれば良いでしょうか。

答えは以下。コメントで追加と書いている行が対象です。
変更したのは5行だけですね。特に他の処理は変えず、ただデータを追加しただけです。

  // 犬のデータ
  AnimalData dogData = AnimalData();
  dogData.name = "犬";
  dogData.cry = "わん";
  dogData.speed = 5;
  // 猫のデータ
  AnimalData catData = AnimalData();
  catData.name = "猫";
  catData.cry = "にゃん";
  catData.speed = 4;
  // 象のデータ
  AnimalData elephantData = AnimalData();
  elephantData.name = "象";
  elephantData.cry = "ぱおん";
  elephantData.speed = 20;
  // 鳥のデータ
  AnimalData birdData = AnimalData(); // 追加
  birdData.name = "鳥"; // 追加
  birdData.cry = "ぴぃ"; // 追加
  birdData.speed = 100; // 追加
  // 動物のデータをリストに格納していく
  List<AnimalData> animals = [];
  animals.add(dogData);
  animals.add(catData);
  animals.add(elephantData);
  animals.add(birdData); // 追加

1-5.まとめ

ここまで、オブジェクト指向が始まる前のプログラミング言語の考え方をざっくりではありますが書いてみました。
サンプルに比較的新しいオブジェクト指向であるDartを使っていますが、関数化やデータを1つにまとめることで、プログラムを保守しやすくなったと思います。

ただ、それでもプログラムの開発規模が大きくなってきたことで、新しい考え方がうまれました。
次回、サンプルをオブジェクト指向向けに変更しようと思います。

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