株式会社エス・スリー・フォー

[Java入門]3.9 車とガソリンタンクの共通点

車に乗っていてガス欠になってしまったことはありませんか? バイクならスタンドまで手で押していってもいいのですが、さすがに車を押していくのはちょっと無理です。

こんなとき、何かガソリンを入れられるような缶などがあれば、それだけをもってガソリンスタンドを探しにいけますね。とりあえずそのスタンドにたどり着けるくらいのガソリンを買ってくればいいのですから。

そうすると、車だけでなく、缶やポリタンク等もガソリンスタンドを利用できるということになります。前節で定義したガソリンタンクのクラスを思い出してみてください。そもそもこのタンククラスは、車クラスからガソリンを入れるという機能を取り出したものですから、当然のことながらガソリンを入れることができます。

同じ様に、缶クラスやポリタンククラスを定義してみると、すべてガソリンを入れるという同じ機能を持っていることになるでしょう。

ここで、実際にガソリンを入れるガソリンスタンドの立場になってみると、形態は違ってもみんな同じお客さんですから、同じ様な手順で扱いたいですね。車クラスとタンククラスではこの手順は共通しており、ガソリンタンクの残量を確認するときにはisNotFullメソッドを使い、給油するときにはrefuelメソッドを使っています。

これは、車の持つ機能の一部を取り出してタンククラスを作ったのですから、当然のことかもしれません。しかし、新たに缶クラスやポリタンククラスを作るときにも同じ機能を同じメソッドで同じように扱えるようにするべきでしょう。そうしなければ、ガソリンスタンドは、違う容器が来る度に、給油の方法を変えなければならなくなります。同じ「ガソリンを入れられるもの」なのですから、同じメソッドでガソリンを入れることができた方が何かと便利です。

このためには、ガソリンを入れるために必要なメソッドを、ひとかたまりの決まった枠組みとして定義しておけばよいでしょう、そして、「ガソリンを入れられるもの」のクラスは必ずその枠組みに従ってメソッドを定義するようにすればよいのです。

3.9.1 ガソリンを入れることができるインターフェース

Javaには、まさにこれを実現するための仕組みが用意されています。
それは、インターフェースというものです。つまり、インターフェースはある機能に必要なメソッドの枠組で、なおかつその機能を実現するのに十分なメソッドから構成されています。

それでは実際に、「ガソリンを入れられる」という機能のインターフェースを定義してみます。まず、名前を付けましょう。ガソリンを入れられるのですから、Refuelableにします。このインターフェースに必要なメソッドは、タンククラスが持っているメソッドそのものです。インターフェースではメソッド名だけ決めていればいいので、変数については考えなくてかまいません。まとめると以下のようになります。

Java: Refuelable.java
interface Refuelable {

  boolean isNotFull();
  void refuel(int amount);

}

このソースは、$chapter3/ex8/Refuelable.javaにあります。

メソッドの記述が、クラス定義のときとはちょっと違っています。メソッドの中身がなく、セミコロン(;) があるだけです。

これには理由があります。車クラスとタンククラスのクラス定義を思い出してください。車クラスのisNotFullメソッドと、タンククラスのisNotFullメソッドは、名前は同じでしたが、変数の構成や実際のメソッド中の手続きは異なっていました。ですから、実際の手続きを示す部分までインターフェースで記述することはできないのです。

インターフェースはメソッドの名前を決めるだけで、メソッドの具体的な動作には関知しません。つまりインターフェースという機構は、同じ機能を別なクラスで実現するための枠組みと考えることができるわけです。

C++: Refuelable.h
#ifndef REFUELABLE_H__
#define REFUELABLE_H__

class Refuelable {

public:
  virtual ~Refuelable();
  virtual bool isNotFull() const =0;
  virtual void refuel(int amount) =0;

};

#endif
C++: Refuelable.cpp
#include "Refuelable.h"

Refuelable::~Refuelable() {
}
C#: Refuelable.cs
interface Refuelable {

  bool isNotFull();
  void refuel(int amount);

}
VB: Refuelable.vb
Interface Refuelable

  Function isNotFull() As Boolean
  Sub refuel(ByVal amount As Integer)

End Interface

3.9.2 Refuelable インターフェースの実装

インターフェースに対して、各クラスにおけるメソッドの具体的な動作のことを実装と言います。そして、あるクラスがあるインターフェースで決められた機能をすべて持っているとき、そのクラスはそのインターフェースを実装していると言います。

それでは、インターフェースを実装するクラスは、どのように記述すればよいのでしょうか。ガソリンスタンドの例で見てみましょう。CarクラスもTankクラスもメソッド自体は既に定義されているので、クラスの定義の一箇所だけ変更すればよいでしょう。クラス名の後ろにimplementsに続けて実装するインターフェース名を記述するだけです。この場合であれば、それぞれ次のように記述します。

  class Car implements Refuelable {
    : (省略)
  }

  class Tank implements Refuelable {
    : (省略)
}

なお、それぞれの完全なソースは$chapter3/ex8/Car.java$chapter3/ex8/Tank.javaにあります。前の版と実際に比べて、本当にこれしか変更していないということを確認してください。

この場合はどちらのクラスもスーパークラスを決めていないのでextendsはありませんが、ある場合にはimplementsはスーパークラス名の後ろに書きます。

■ C++:

  class Car : virtual public Refuelable {
    : (省略)
  };

  class Tank : virtual public Refuelable {
    : (省略)
  };

■ C#:

  class Car : Refuelable {
    : (省略)
  }

  class Tank : Refuelable {
    : (省略)
  }

■ VB:

  Class Car
      Implements Refuelable
    : (省略)
    Public Function isNotFull() As Boolean Implements Refuelable.isNotFull
      Return tank_.isNotFull()
    End Function

    Public Sub refuel(ByVal amount As Integer) Implements Refuelable.refuel
      tank_.refuel(amount)
    End Sub
    : (省略)
  End Class

  Class Tank
      Implements Refuelable
    : (省略)
    Public Function isNotFull() As Boolean Implements Refuelable.isNotFull
      Return remain_ < capacity_
    End Function

    Public Sub refuel(ByVal amount As Integer) Implements Refuelable.refuel
      remain_ = remain_ + amount
    End Sub

  End Class

3.9.3 GasStation クラスへの修正

これで車クラスもタンククラスもRefuelableインターフェースとして扱えるようになりましたので、GasStationの方もRefuelableインターフェースを使うようにしましょう。

refuelメソッドでCarクラスを使っていた部分を、すべてRefuelableインターフェースを使うように書き換えます。

  public int refuel(Refuelable aRefuelable) {
    int charged = 0;
    while ( aRefuelable.isNotFull() ) {
      aRefuelable.refuel(1);
      charged = charged + 1;
    }
    return charged;
  }

■ C++

  int GasStation::refuel(Refuelable* aRefuelable) {
    int charged = 0;
    while ( aRefuelable->isNotFull() ) {
      aRefuelable->refuel(1);
      charged = charged + 1;
    }
    return charged;
  }

■ C#

  public int refuel(Refuelable aRefuelable) {
    int charged = 0;
    while ( aRefuelable.isNotFull() ) {
      aRefuelable.refuel(1);
      charged = charged + 1;
    }
    return charged;
  }

■ VB

  Public Function refuel(ByRef aRefuelable As Refuelable) As Integer
    Dim charged As Integer = 0
    While aRefuelable.isNotFull()
      aRefuelable.refuel(1)
      charged = charged + 1
    End While
    Return charged
  End Function

3.9.4 GasStationSimulation クラスへの修正

GasStationSimulationクラスはどうでしょうか。Carクラスを使っていたところを全部Refuelableに直すというわけにはいきません。インターフェースは実装を規定していないため、変数や他のメソッドの構成などがわからないので、オブジェクトを作ることはできないのです。

それでは困るので、お客さんごとに乱数を使って車かタンクのどちらかを自動的に選ぶようにしましょう。現実ならば車の方が圧倒的に多いのでしょうけど、ここでは簡単にほぼ半々だということにしてしまいましょう。乱数の値が負か、0以上かで分けます。タンクについては常に空だということにします。

以上のことを考慮すると、GasStationSimulationクラスは次のようになります。

Java: GasStationSimulation.java
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Random;

class GasStationSimulation {

  private GasStation aGS_;

  private int askInt(String message) {
    try {
      System.out.print(message);
      System.out.flush();
      String line = new BufferedReader(new InputStreamReader(System.in)).readLine();
      return Integer.parseInt(line);
    } catch ( java.io.IOException e ) {
      System.out.println("could not get input. Sorry.");
      return 0;
    }
  }

  private void dealCustomer() {
    Random aRandom = new Random();
    int charged[] = new int[5];
    boolean isCar[] = new boolean[5];
    int totalAmount = 0;
    Refuelable aRefuelable;
    for ( int i = 0;
          aGS_.isNotEmpty() && i < charged.length; i = i + 1 ) {
      if ( aRandom.nextInt() < 0 ) {
        int initial = aRandom.nextInt() % Car.tankCapacity();
        if ( initial < 0 ) {
          initial = -initial;
        }
        aRefuelable = new Car(initial);
        isCar[i] = true;
      } else {
        int capa = aRandom.nextInt() % Car.tankCapacity();
        if ( capa < 0 ) {
          capa = -capa;
        }
        aRefuelable = new Tank(capa, 0);
        isCar[i] = false;
      }
      charged[i] = aGS_.refuel(aRefuelable);
      totalAmount = totalAmount + charged[i];
    }
    System.out.print("No.");
    for ( int i = 0; i < charged.length; i++ ) {
      if ( isCar[i] ) {
        System.out.print("\t" + i + "(Car)");
      } else {
        System.out.print("\t" + i + "(Tank)");
      }
    }
    System.out.print("\namount:");
    for ( int i = 0; i < charged.length; i++ ) {
      System.out.print("\t" + charged[i]);
    }
    System.out.print("\nprice:");
    for ( int i = 0; i < charged.length; i++ ) {
      int price = aGS_.price(charged[i]);
      aGS_.addSales(price);
      System.out.print("\t" + price);
    }
    System.out.println("\ntotal: " + totalAmount + " [liter] / "

                                   + aGS_.totalSales() + " [yen]");
  }

  public void simulate() {
    System.out.println("For all gas stations:");
    int initial = askInt("  initial amount? ");
    int unitPrice = askInt("  unit price? ");
    GasStation.setUnitPrice(unitPrice);
  loop:
    while ( true ) {
      System.out.println("\nSelect command:");
      int type = askInt("  1) Normal  2) Discount  other) exit ");
      switch ( type ) {
      case 1:
        aGS_ = new GasStation(initial);
        break;
      case 2:
        int border = askInt("borderline amount? ");
        aGS_ = new DiscountGasStation(initial, border);
        break;
      default:
        break loop;
      }
      dealCustomer();
    }
  }

  public static void main(String[] args) {
    GasStationSimulation simulation = new GasStationSimulation();
    simulation.simulate();
  }

}

C++: GasStationSimulation.h
#ifndef GASSTATIONSIMULATION_H__
#define GASSTATIONSIMULATION_H__

#include <string>
#include "GasStation.h"

class GasStationSimulation {

private:
  GasStation* aGS_;

  int askInt(const std::string& message);
  void dealCustomer();

public:
  void simulate();

};

#endif
C++: GasStationSimulation.cpp
#include <iostream>
#include <cstdlib> // rand
#include <vector>  // vector

#include "GasStationSimulation.h"

#include "Car.h"
#include "Tank.h"
#include "GasStation.h"
#include "DiscountGasStation.h"

int GasStationSimulation::askInt(const std::string& message) {
  std::cout << message << std::flush;
  int result;
  if ( std::cin >> result ) {
    return result;
  }
  std::cout << "could not get input.  sorry." << std::endl;
  return 0;
}

void GasStationSimulation::dealCustomer() {
  std::vector<int> charged(5);
  typedef std::vector<int>::size_type size_type;
  bool isCar[5];
  int totalAmount = 0;
  Refuelable* aRefuelable;
  for ( size_type i = 0; aGS_->isNotEmpty() && i < charged.size(); ++i ) {
    if ( std::rand() % 2 == 0 ) {
      int initial = std::rand() % Car::tankCapacity();
      aRefuelable = new Car(initial);
      isCar[i] = true;
    } else {
      int capa = std::rand() % Car::tankCapacity();
      aRefuelable = new Tank(capa, 0);
      isCar[i] = false;
    }
    charged[i] = aGS_->refuel(aRefuelable);
    totalAmount = totalAmount + charged[i];
    delete aRefuelable;
  }
  std::cout << "No.";
  for ( size_type i = 0; i < charged.size(); ++i ) {
    if ( isCar[i] ) {
      std::cout << '\t' << static_cast<unsigned>(i) << "(Car)";
    } else {
      std::cout << '\t' << static_cast<unsigned>(i) << "(Tank)";
    }
  }
  std::cout << "\namount:";
  for ( size_type i = 0; i < charged.size(); ++i ) {
    std::cout << '\t' << charged[i];
  }
  std::cout << "\nprice:";
  for ( size_type i = 0; i < charged.size(); ++i ) {
    int price = aGS_->price(charged[i]);
    aGS_->addSales(price);
    std::cout << '\t' << price;
  }
  std::cout << "\ntotal: " << totalAmount << " [liter] / "

            << aGS_->totalSales() << " [yen]" << std::endl;
}

void GasStationSimulation::simulate() {
  std::cout << "For all gas stations:" << std::endl;
  int initial = askInt("  initial amount? ");
  int unitPrice = askInt("  unit price? ");
  GasStation::setUnitPrice(unitPrice);
  bool keepGoing = true;
  while ( keepGoing ) {
    std::cout << "\nSelect command:" << std::endl;
    int type = askInt("  1) Normal  2) Discount  other) exit ");
    switch ( type ) {
    case 1:
      aGS_ = new GasStation(initial);
      break;
    case 2: {
        int border = askInt("borderline amount? ");
        aGS_ = new DiscountGasStation(initial, border);
      }
      break;
    default:
      keepGoing = false;
    }
    if ( keepGoing ) {
      dealCustomer();
      delete aGS_;
      aGS_ = 0;
    }
  }
}
C#: GasStationSimulation.cs
class GasStationSimulation {

  private GasStation aGS_;

  private int askInt(string message) {
    System.Console.Write(message);
    System.Console.Out.Flush();
    try {
      return System.Int32.Parse(System.Console.ReadLine());
    } catch ( System.Exception ) {
      System.Console.WriteLine("could not get input.  sorry.");
      return 0;
    }
  }

  private void dealCustomer() {
    System.Random aRandom = new System.Random();
    int[] charged = new int[5];
    bool[] isCar = new bool[5];
    int totalAmount = 0;
    Refuelable aRefuelable;
    for ( int i = 0; aGS_.isNotEmpty() && i < charged.Length; ++i ) {
      if ( aRandom.Next(2) == 0 ) {
        int initial = aRandom.Next(Car.tankCapacity);
        aRefuelable = new Car(initial);
        isCar[i] = true;
      } else {
        int capa = aRandom.Next(Car.tankCapacity);
        aRefuelable = new Tank(capa, 0);
        isCar[i] = false;
      }
      charged[i] = aGS_.refuel(aRefuelable);
      totalAmount = totalAmount + charged[i];
    }
    System.Console.Write("No.");
    for ( int i = 0; i < charged.Length; ++i ) {
      if ( isCar[i] ) {
        System.Console.Write("\t" + i + "(Car)");
      } else {
        System.Console.Write("\t" + i + "(Tank)");
      }
    }
    System.Console.Write("\namount:");
    for ( int i = 0; i < charged.Length; ++i ) {
      System.Console.Write("\t" + charged[i]);
    }
    System.Console.Write("\nprice:");
    for ( int i = 0; i < charged.Length; ++i ) {
      int price = aGS_.price(charged[i]);
      aGS_.addSales(price);
      System.Console.Write("\t" + price);
    }
    System.Console.WriteLine("\ntotal: " + totalAmount + " [liter] / "

                                         + aGS_.totalSales + " [yen]");
  }

  public void simulate() {
    System.Console.WriteLine("For all gas stations:");
    int initial = askInt("  initial amount? ");
    GasStation.unitPrice = askInt("  unit price? ");
    bool keepGoing = true;
    while ( keepGoing ) {
      System.Console.WriteLine("\nSelect command:");
      int type = askInt("  1) Normal  2) Discount  other) exit ");
      switch ( type ) {
      case 1:
        aGS_ = new GasStation(initial);
        break;
      case 2:
        int border = askInt("borderline amount? ");
        aGS_ = new DiscountGasStation(initial, border);
        break;
      default:
        keepGoing = false;
        break;
      }
      if ( keepGoing ) {
        dealCustomer();
      }
    }
  }

  [System.STAThread]
  static void Main(string[] args) {
    GasStationSimulation simulation = new GasStationSimulation();
    simulation.simulate();
  }

}
VB: GasStationSimulation.vb
Class GasStationSimulation

  Private aGS_ As GasStation

  Private Function askInt(ByVal message As String)
    System.Console.Write(message)
    System.Console.Out.Flush()
    Try
      Return System.Int32.Parse(System.Console.ReadLine())
    Catch e As System.Exception
      System.Console.WriteLine("could not get input.  Sorry.")
      Return 0
    End Try
  End Function

  Private Sub dealCustomer()
    Dim aRandom As System.Random = New System.Random()
    Dim charged(5) As Integer
    Dim isCar(5) As Boolean
    Dim totalAmount As Integer = 0
    Dim aRefuelable As Refuelable
    Dim i As Integer
    For i = 0 To charged.Length - 1
      If aRandom.Next(2) = 0 Then
        Dim initial As Integer = aRandom.Next(Car.tankCapacity)
        aRefuelable = New Car(initial)
        isCar(i) = True
      Else
        Dim capa As Integer = aRandom.Next(Car.tankCapacity)
        aRefuelable = New Tank(capa, 0)
        isCar(i) = False
      End If
      charged(i) = aGS_.refuel(aRefuelable)
      totalAmount = totalAmount + charged(i)
    Next
    System.Console.Write("No.")
    For i = 0 To charged.Length - 1
      If isCar(i) Then
        System.Console.Write(vbTab + CStr(i) + "(Car)")
      Else
        System.Console.Write(vbTab + CStr(i) + "(Tank)")
      End If
    Next
    System.Console.Write(vbCrLf + "amount:")
    For i = 0 To charged.Length - 1
      System.Console.Write(vbTab + CStr(charged(i)))
    Next
    System.Console.Write(vbCrLf + "price:")
    For i = 0 To charged.Length - 1
      Dim price As Integer = aGS_.price(charged(i))
      aGS_.addSales(price)
      System.Console.Write(vbTab + CStr(price))
    Next
    System.Console.WriteLine(vbCrLf + "total: " + CStr(totalAmount) + " [liter] /" _
                                                + CStr(aGS_.totalSales()) + " [yen]")
  End Sub

  Public Sub simulate()
    System.Console.WriteLine("For all gas stations:")
    Dim initial As Integer = askInt("  initial amount? ")
    GasStation.unitPrice = askInt("  unit price? ")
    Dim keepGoing As Boolean = True
    While keepGoing
      System.Console.WriteLine(vbCrLf + "Select command:")
      Dim type As Integer = askInt("  1) Normal  2) discount  other) exit ")
      Select Case type
      Case 1
        aGS_ = New GasStation(initial)
      Case 2
        Dim border As Integer = askInt("borderline amount? ")
        aGS_ = New DiscountGasStation(initial, border)
      Case Else
        keepGoing = False
      End Select
      If keepGoing Then
        dealCustomer()
      End If
    End While
  End Sub

  Shared Sub Main()
    Dim simulation As GasStationSimulation = New GasStationSimulation()
    simulation.simulate()
  End Sub

End Class

3.9.5 実行結果

Refuelableインターフェースを使うガソリンスタンドシミュレーションの実行結果は、次のようになります。

  For all gas stations:
    initial amount? 250
    unit price? 108

  Select command:
    1) Normal 2) Discount other)exit 1
  No.     0(Car)  1(Car)  2(Tank) 3(Car)  4(Tank)
  amount: 43      5       21      33      30
  price:  4644    540     2268    3564    3240
  total: 132 [liter] / 14256 [yen]

  Select command:
    1) Normal 2) Discount other)exit 2
  borderline amount? 25
  No.     0(Car)  1(Car)  2(Car)  3(Car)  4(Tank)
  amount: 7       46      5       49      6
  price:  756     4738    540     5047    648
  total: 113 [liter] / 11729 [yen]

  Select command:
    1) Normal 2) Discount other)exit 3

prev next