#4 継承
オブジェクト指向に関して自分なりの理解を記事にしています。
前回記事ではオブジェクト指向における「カプセル化」とは何かを書きました。
今回は「継承」についてです。前回作成したサンプルコードを修正していきます。
<前回作成したサンプルコード>
「犬、猫、象にそれぞれ鳴き声をあげてもらう。1000メートル走してもらい一位を表示する」プログラム(カプセル化を適用したVer)
// 犬
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
ChatGPTから得た解説に対して補足する形で進めていきます。
4.継承
今回は前者をメインで説明していこうと思います。
4-1.実装方法その1
継承について私にはChatGPT以上の説明が難しいので、いきなり実装します。
ポイントはDog,Cat,Elephantの3クラス。この3クラスは全て動物で、フィールドやメソッドが共通しています。
それらの親クラスとしてAnimalクラスを作ります。
// 動物
class Animal {
String _name;
String _cry;
int _speed;
// コンストラクタ
Animal(this._name, this._cry, this._speed);
// gettetr
String get name => _name;
// 鳴く
String toCry() {
return _cry;
}
// 走る
double run(int distance) {
return distance / _speed;
}
}
aminal.dart
出来ました。各動物の共通部分を抜き出してAnimalクラスに実装しています。
ただ、Animalクラスのフィールドの値は子クラスが決める必要があります。今回はクラス初期化時に呼ばれるコンストラクタを利用しフィールドを設定するようにしています。
では、子クラスを実装します。
// 犬
class Dog extends Animal {
Dog() : super("犬", "わん", 5);
}
// 猫
class Cat extends Animal {
Cat() : super("猫", "にゃん", 4);
}
// 象
class Elephant extends Animal {
Elephant() : super("象", "ぱおん", 20);
}
animal.dart
「exends Animal」という部分がAnimalクラスを継承している部分です。
今回、必要な処理のほとんどはAnimalクラスに実装しているので、各子クラスで実装するのは親クラスのコンストラクタを使って値を渡しています。
4-2.実装方法その2
4-1では親クラスにフィールドやメソッドを実装していますが、子クラス内に実装させることも可能です。
今回は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;
}
}
animal.dart
まずはAnimalクラスから。toCryメソッドを定義だけして実装をなくしました。toCryメソッドの実装は子クラスでします。
加えて、Animalクラスの前に「abstract」と付けています。Dartの言語仕様というのもあるのですが、Animalが一部実装のないクラスになったことで実体化出来ない抽象クラスとなったためです。
// 犬
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
子クラスを実装しました。toCry関数をそれぞれで実装しています。親クラスのメソッドを子クラスで実装することをオーバーライドと呼びます。
ソース内の「@override」はアノテーションと呼ばれるもので、オーバーライドであることを明示しています。
4-1の様に親クラスに実装の比重を多くとるか、4-2の様に子クラスで実装するかは悩みの種です。
クラス分けの思想次第ですが、個人的にはあまり親クラスに多く実装は多くしない派です。
どんどん親クラスの実装が膨れ上がり、俗にいう「神クラス」が出来てしまう恐れがあるからです。
この辺りを語ると長くなるので割愛しちゃいます。
以上がChatGPTで説明されている子クラスが親クラスから引き継げる点の説明となります。概念というより実装ベースでの説明となっていますね。。。
4-3.継承を使用してのコードの再利用・拡張について
これについては、あまりしっくりきていない。。。
私自身の解釈を書いていきます。
<再利用性>
例えば、今回のサンプルで言えば「チーター」や「猿」を追加する際にAnimalクラスを継承すれば差分のみの実装で済みます。
新しく実装する必要ないので、再利用が容易といえます。
<拡張>
子クラスでメソッドをオーバーライドすれば親クラスの処理を拡張することが可能です。
<個人的な継承の再利用・拡張に対する考え>
今までの経験で継承を「再利用」「拡張」を目的にして利用するとあまり良い思い出が出てきません。。。
親クラスを「再利用」を主目的として利用すると妙な継承が出来上がります。
例えば、Aminalクラスを何故かロボットクラスが継承する的な実装を見ることがあります。実装者曰く親クラスのメソッドが使えるから。
でも、継承は基本「is a」の関係が成り立つかなんですよね。犬は動物である、猫は動物である。的な。
これが崩れると、ちょっとした仕様変更に弱くなっちゃいます。
Animalクラスにロボットの実装が入り込んだりとか。
拡張についても、それ中心に継承を考えると大変な思い出があります。
継承の階層が多くなりつつオーバーライドも複雑化し、どのクラス実装があるか分からなくなるケースです。
犬クラスを更に柴犬クラスが継承して、更に信州柴犬クラスが継承するようなイメージ。
継承の階層はほどほどにしつつ、ヘルパークラスなど他クラスに処理を委譲するなど、バランスが大切と思っています。
まぁ、エンジニア特有のこだわりです。結局動けばいいですし、担当者が保守しやすければそれが正義です。
4-4.継承は何のため?
4-3であれこれ言いましたが、私なりの考えとしては「継承とは多態性(ポリモーフィズム)を実現するためにある」と考えています。
少し言い換えると、クラスを使う側にとって中の実装を意識させない作りをするために継承はあるという考えです。
これによって、例えば「依存性の注入」を利用して実態とモックを入れ替えることが実現できます。
急になにそれ、と思われるような新しい単語を出していますが。。。この辺りは改めて別記事で書くかと思います。
継承であれこれ悩んできた身として、最後勢いで書いてしまっています。
次の記事では「多態性(ポリモーフィズム)」について書こうと思います。
最後に今回のソースコード全体を載せて終わります。
<ソースコード>
// 動物
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
<実行結果>
犬の鳴き声は「わん」です。
猫の鳴き声は「にゃん」です。
象の鳴き声は「ぱおん」です。
1000メートル走で最も早いのは象です。
この記事が気に入ったらサポートをしてみませんか?