概要
Javaのインターフェースを利用して抽象的に表現したコードサンプルを紹介します。
目的
インターフェースを挟むことでメインクラスと個別に実装されたクラスが疎結合となり、クラスの責務が明確になります。またメインクラス側に個別仕様が表現されていないため、個別実装側の仕様変更に影響されることがなくなります。
抽象的でない悪い例
public class Dog {
public void makeSound() {
System.out.println("ワンワン");
}
}
public class Cat {
public void makeSound() {
System.out.println("ニャーニャー");
}
}
public class Elephant {
public void makeSound() {
System.out.println("パオーン");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.makeSound();
Cat cat = new Cat();
cat.makeSound();
Elephant elephant = new Elephant();
elephant.makeSound();
}
}
実行結果は以下のようになります。
ワンワン
ニャーニャー
パオーン
メインクラスでは各動物のインスタンスを生成してメソッドを実行しているので、動物が増えたときなどメインクラスにも修正が入ります。
抽象化したコード例
Dogクラス、Catクラス、Elephantクラスに共通する動物という性質はAnimalクラスを継承することで表現します。また犬、猫、象は鳴くという性質を持っているのでSoundableインターフェースを定義します。
本質的な共通機能の継承関係をクラス、能力的な付加機能の継承関係をインターフェースという住み分けにします。
つまり犬は鳴く動物ですが、ウサギは鳴かない動物です。
象は飛ばない動物ですが、ウサギは飛ぶ動物です。
トビウオは動物ではありませんが、飛びます。
どういうことかコードを例に見てみましょう。
まずは本質的な共通機能と能力的な付加機能をそれぞれ定義します。
public interface Soundable {
public void makeSound();
}
public interface Jumpable {
public void jump();
}
class Animal {
}
class Fish {
}
次に各動物たちに能力インターフェースを適用します。
public class Dog extends Animal implements Soundable, Jumpable {
@Override
public void makeSound() {
System.out.println("ワンワン");
}
@Override
public void jump() {
System.out.println("ビョーン");
}
}
public class Cat extends Animal implements Soundable, Jumpable {
@Override
public void makeSound() {
System.out.println("ニャーニャー");
}
@Override
public void jump() {
System.out.println("ピョーン");
}
}
// 象は飛ばないのでJumpableを継承しない
public class Elephant extends Animal implements Soundable {
@Override
public void makeSound() {
System.out.println("パオーン");
}
}
// うさぎは鳴かないのでSoundableは継承しない
public class Rabbit extends Animal implements Jumpable {
@Override
public void jump() {
System.out.println("ぴょんぴょん");
}
}
// トビウオは動物ではないが飛ぶのでJumpableを継承する
public class FlyingFish extends Fish implements Jumpable {
@Override
public void jump() {
System.out.println("パシャーン");
}
}
またZooクラスも定義します。
public class Zoo {
private static final String[] animalTypes = { "Dog", "Cat", "Elephant", "Rabbit" };
public Animal takeOut(String animalType)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return (Animal) Class.forName(animalType).newInstance();
}
public List<Animal> takeOutAll() {
List<Animal> animals = new ArrayList<>();
for (String animalType : animalTypes) {
try {
animals.add(takeOut(animalType));
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException ex) {
System.out.println("Zoo isn't allowed to have " + animalType);
}
}
return animals;
}
}
これで準備が整いました。メインクラスは以下のようになります。
public class Main {
public static void main(String[] args) {
Zoo zoo = new Zoo();
List<Animal> animals = zoo.takeOutAll();
System.out.println("連れ出した動物が鳴く");
animals.stream()
.filter(Soundable.class::isInstance)
.map(Soundable.class::cast)
.forEach(animal -> animal.makeSound());
System.out.println("連れ出した動物が飛ぶ");
animals.stream()
.filter(Jumpable.class::isInstance)
.map(Jumpable.class::cast)
.forEach(animal -> animal.jump());
}
}
実行結果は前例と同じですが、うさぎは鳴かない動物のため、鳴かない結果になります。
一方飛ぶことに関してはうさぎは飛び、象は飛ばない結果になっています。
連れ出した動物が鳴く
ワンワン
ニャーニャー
パオーン
連れ出した動物が飛ぶ
ビョーン
ピョーン
ぴょんぴょん
まとめ
このように抽象化を意識したコードを書いていくと、自然とクラス間が疎結合になり、責務も明確になっていきます。