見出し画像

#5 ポリモーフィズム、抽象化

オブジェクト指向に関して自分なりの理解を記事にしています。
前回記事ではオブジェクト指向における「継承」とは何かを書きました。
今回は「ポリモーフィズム」と「抽象化」についてです。前回作成したサンプルコードを修正していきます。
説明の都合上、前回記事にした「継承」についても更に深堀させていただきます。

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

// 動物
abstract class Animal {
  String _name;
  int _speed;
  // コンストラクタ
  Animal(this._name, this._speed);
  // gettetr
  String get name => _name;
  // 鳴く
  String toCry();
  // 走る
  double run(int distance) {
    return distance / _speed;
  }
}

// 犬
class Dog extends Animal {
  Dog() : super("犬", 5);
  @override
  String toCry() {
    return "わん";
  }
}

// 猫
class Cat extends Animal {
  Cat() : super("猫", 4);
  @override
  String toCry() {
    return "にゃん";
  }
}

// 象
class Elephant extends Animal {
  Elephant() : super("象", 20);
  @override
  String toCry() {
    return "ぱおん";
  }
}

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

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


5.ポリモーフィズム、抽象化

3.ポリモーフィズム(Polymorphism):
・異なるオブジェクトが同じインターフェース(メソッド)を持つことができ、同じ操作を異なる方法で実行することが可能です。
・これにより、オブジェクトの型に依存しない柔軟なプログラムを書くことができます。
4.抽象化(Abstraction):
・複雑な実装の詳細を隠し、よりシンプルなインターフェースを提供します。
・ユーザーは複雑な内部処理を理解することなく、オブジェクトを使用できます。

ChatGPT

ポリモーフィズムは多態性とも呼ばれます。

5-1.ポリモーフィズム

今回もソースコードベースで説明させていただこうと思います。力技です。

・異なるオブジェクトが同じインターフェース(メソッド)を持つことができ、同じ操作を異なる方法で実行することが可能です。

ChatGPT

「異なるオブジェクト」とはサンプルの中で言うと「犬」「猫」「象」です。

// 犬
class Dog extends Animal {
  Dog() : super("犬", 5);
  @override
  String toCry() {
    return "わん";
  }
}

// 猫
class Cat extends Animal {
  Cat() : super("猫", 4);
  @override
  String toCry() {
    return "にゃん";
  }
}

// 象
class Elephant extends Animal {
  Elephant() : super("象", 20);
  @override
  String toCry() {
    return "ぱおん";
  }
}

これらが「同じインターフェースを持つ」とはどういうことか。
同じインターフェースとは、親クラスが持つメソッドのことです。

// 動物
abstract class Animal {
  String _name;
  int _speed;
  // コンストラクタ
  Animal(this._name, this._speed);
  // gettetr
  String get name => _name;
  // 鳴く
  String toCry();
  // 走る
  double run(int distance) {
    return distance / _speed;
  }
}

Animalクラスで言うとtoCry, runメソッドのことです。継承すると子クラスはこれらメソッドをもつこととなります。
異なるオブジェクトが継承を通して同じメソッドをもつ。これをChatGPTは
「同じインターフェースをもつ」と表現していると私は解釈しています。

でも、これだけだと犬クラスと猫クラスに同じ名前のメソッドと定義するのと何が違うの?と思われる方がいらっしゃるかもしれません。

違うのです。継承することで親クラスの型に子クラスを代入することが出来ます。
こんな風に。

  Animal dog = Dog();
  Animal cat = Cat();
  Animal elephant = Elephant();

本来、型が違うと(例外を除き)代入出来ないのですが、子クラスを親クラスへの代入が可能です。Animalクラスとして振る舞います。
では、この状態でtoCryメソッドを呼び出します。結果はどうなるでしょうか。

  print(dog.toCry());
  print(cat.toCry());
  print(elephant.toCry());
わん
にゃん
ぱおん

子クラスで実装した処理が実行されていますね。
これがChatGPTの説明にある「同じ操作を異なる方法で実行することが可能」の部分です。

5-2.抽象化

・複雑な実装の詳細を隠し、よりシンプルなインターフェースを提供します。
・ユーザーは複雑な内部処理を理解することなく、オブジェクトを使用できます。

ChatGPT

オブジェクトに実装をもたないメソッドをもたせ、子クラスに実装を任せることが出来ます。
前回の記事で作成した。Animalクラスが該当します。
toCryメソッドの実装を子クラスに任せています。

// 動物
abstract class Animal {
  String _name;
  int _speed;
  // コンストラクタ
  Animal(this._name, this._speed);
  // gettetr
  String get name => _name;
  // 鳴く
  String toCry();
  // 走る
  double run(int distance) {
    return distance / _speed;
  }
}

この「抽象化」が何に活かせるのか、「継承」「ポリモーフィズム」とあわせて具体例を挙げて書いていきます。

5-2.どうやって活用するの?

・Listに使える
Listは任意の型を入れた順番で格納出来ます。本来異なる型は格納出来ませんが、継承を使えばこの通り。

  List<Animal> animals = [];
  animals.add(Dog());
  animals.add(Cat());
  animals.add(Elephant());

Listに使えるということはFor文も使えます。

  for(int i = 0; i < animals.length; i++) {
    print(animals[i].toCry());
  }

ポリモーフィズムにより、toCryを呼び出すと子クラスの実装が呼び出されます。

サンプルの様な小さいプログラムでは良さが分かりにくいかと思いますが、使われているケースは良くあります。
ここでは解説しないですが、デザインパターンの1つであるCommandパターンなんかではこの方法ありきですし。

・引数に使える
関数やメソッドの引数を親クラスにすると、子クラスであれば共通で利用できます。
例として、サンプルで使っているanimalCry関数、引数をAnimalにしてみます。

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

この関数は引数にDog、Cat、Elephantいずれも渡すことが出来ます。
前述したFor文と組み合わせると効果的です。

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

と、ここまではある意味小手先感があるのですが、私自身が一番重要視している活用方法が次です。

・実装とモックを切り替えることが出来る

なんのこっちゃと思われるかもしれません。抽象化による恩恵です。
今までのサンプルは一旦忘れて、以下のプログラムを考えてみます。

「データベースからSELECT文を発行して画面の一覧に表示する」

これをかなり簡略化して実装してみます。(サンプルは都合上データベースに接続しません。実際に接続してSQLを発行している想定です。)

<データベースへ接続し結果を一覧表示するサンプル>

// データベースから取得したエンティティ
class Entity {
  Entity(String d) {
    data = d;
  }
  String data = "";
}

// データベース制御用クラス
class DatabaseController {
  // 接続
  bool connect() {
    // データベースへ接続
    bool result = true;
    return result;
  }

  // SELECT文発行
  List<Entity> select() {
    //データベースからSQLを発行してデータを取得しているつもり
    List<Entity> list = [];
    list.add(Entity("本番データ1"));
    list.add(Entity("本番データ2"));
    list.add(Entity("本番データ3"));
    list.add(Entity("本番データ4"));
    list.add(Entity("本番データ5"));
    return list;
  }

  // 閉じる
  void close() {
    // データベースを閉じる
  }
}

database.dart

// 一覧表示
class ListView {
  // データベースを元にデータを表示する
  void viewFromDatabase(DatabaseController db) {
    List<Entity> entities = db.select();
    for (int i = 0; i < entities.length; i++) {
      print(entities[i].data);
    }
  }
}

list_view.dart

void main() {
  DatabaseController db = DatabaseController();
  // データベースへ接続
  if (db.connect() == false) {
    print("データベースへの接続失敗しました");
  }
  // データベースの情報を一覧表示
  ListView view = ListView();
  view.viewFromDatabase(db);
  // データベースを閉じる
  db.close();
}

main_database.dart

<実行結果>

本番データ1
本番データ2
本番データ3
本番データ4
本番データ5

データベース用のクラスからSELECT文を発行し、結果を一覧表示用のクラスで表示しています。動作もしますし、特に問題はないと思います。

ここからは私のこだわりにもなってくるのですが、このままだとデータベースが用意されている必要があり、またテーブルにデータが格納されている必要があります。当たり前ですが。
よく、「画面の表示部分だけを動作確認したいのに、データの整合性が合わずなかなか画面まで辿り着かない!」こうした苦い経験を味わってきた私なりに、このプログラムを改造します。

<データベースへ接続し結果を一覧表示するサンプル 改>

// データベースから取得したエンティティ
class Entity {
  Entity(String d) {
    data = d;
  }
  String data = "";
}

// データベース制御用抽象クラス
abstract class DatabaseController {
  bool connect();
  List<Entity> select();
  void close();
}

database.dart

import 'database.dart';

// データベース制御用クラス(データベースアクセスする)
class DatabaseControllerImpl extends DatabaseController {
  // 接続
  @override
  bool connect() {
    // データベースへ接続
    bool result = true;
    return result;
  }

  // SELECT文発行
  @override
  List<Entity> select() {
    // データベースからSQLを発行してデータを取得しているつもり
    List<Entity> list = [];
    list.add(Entity("本番データ1"));
    list.add(Entity("本番データ2"));
    list.add(Entity("本番データ3"));
    list.add(Entity("本番データ4"));
    list.add(Entity("本番データ5"));
    return list;
  }

  // 閉じる
  @override
  void close() {
    // データベースを閉じる
  }
}

database_impl.dart

import 'database.dart';

// 一覧表示
class ListView {
  // データベースを元にデータを表示する
  void viewFromDatabase(DatabaseController db) {
    List<Entity> entities = db.select();
    for (int i = 0; i < entities.length; i++) {
      print(entities[i].data);
    }
  }
}

list_view.dart

import 'database.dart';
import 'database_impl.dart';
import 'list_view.dart';

void main() {
  DatabaseController db = DatabaseControllerImpl();
  // データベースへ接続
  if (db.connect() == false) {
    print("データベースへの接続失敗しました");
  }
  // データベースの情報を一覧表示
  ListView view = ListView();
  view.viewFromDatabase(db);
  // データベースを閉じる
  db.close();
}

main_database.dart

<実行結果>

本番データ1
本番データ2
本番データ3
本番データ4
本番データ5

出来ました。
DatabaseControllerクラスを抽象化し、子クラスとしてDatabaseControlelrImpleクラスを追加しただけです。

それだけ?と思われるかもしれませんが、これが重要です。
更に2つファイルを加えます。

<データベースへ接続し結果を一覧表示するサンプル モック用>

import 'database.dart';

// データベース制御用クラス(データベースアクセスしない)
class DatabaseControllerMock extends DatabaseController {
  // 接続
  @override
  bool connect() {
    // データベースへ接続するふり
    bool result = true;
    return result;
  }

  // SELECT文発行
  @override
  List<Entity> select() {
    // データベースにアクセスしてデータを返すふり
    List<Entity> list = [];
    list.add(Entity("テストデータ1"));
    list.add(Entity("テストデータ2"));
    list.add(Entity("テストデータ3"));
    list.add(Entity("テストデータ4"));
    list.add(Entity("テストデータ5"));
    return list;
  }

  // 閉じる
  @override
  void close() {
    // データベースを閉じるふり
  }
}

database_mock.dart

import 'database.dart';
import 'database_mock.dart';
import 'list_view.dart';

void main() {
  DatabaseController db = DatabaseControllerMock();
  // データベースへ接続
  if (db.connect() == false) {
    print("データベースへの接続失敗しました");
  }
  // データベースの情報を一覧表示
  ListView view = ListView();
  view.viewFromDatabase(db);
  // データベースを閉じる
  db.close();
}

main_mock.dart

<実行結果>

テストデータ1
テストデータ2
テストデータ3
テストデータ4
テストデータ5

どうでしょうか。
DatabaseControllerを継承したDatabaseControllerMockクラスを追加しています。このクラスはデータベースに接続したフリをして、適当なデータを返しています。
main_mockという別ファイルでmain関数を定義し、中でDatabaseControllerImplではなくDatabaseControllerMockを使っています。

この実装の重要なのは
DatabaseControllerを継承した子クラスを切り替えるだけで、表示処理を担っているListViewクラスの実装は何も変えず動いている
点です。

ListViewクラス視点だと、DatabaseControllerの子クラスが何か意識していないのです。

この様に、システムの起動方法を切り替える仕組みを作ることが出来れば、データベースやネットワーク・センサーといった実機に接続するような処理をモック化出来ます。

私はこの点がオブジェクト指向の一番好きな点です。

5-3.まとめ

今回は「ポリモーフィズム」「抽象化」の説明でした。
オブジェクト指向の好きな特徴なので、こだわりの部分が強い記事となりましたが、こういう考え方があるのだと感じていただければ幸いです。


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