オブジェクト指向でエンタープライズ視聴覚室を作る

 オブジェクト指向の説明にアニマルクラスやら自動車クラスは飽きたのでエンタープライズ視聴覚室を作る。

部屋Interfaceを作る

 部屋だから入室できないといけない。

public interface Room{
  public void join();
}

 でも、何かの理由(鍵がかかっているなど)で入室できないこともあるので、例外を実装する。

public interface Room{
  public void join() throws RoomException;
}

 入れるのは人間だけ。

public interface Room{
  public void join(Human human) throws RoomException;
}

 ホラー映画じゃないので、もちろん部屋から出られる必要がある。

public interface Room{
  public void join(Human human) throws RoomException;
  public void leave(Human human) throws RoomException;
}

 完成。

RoomExceptionを作る

 部屋で何か問題が起こったときは、理由を通知しなければならない。

public class RoomException extends Exception{}

 部屋で問題が起こることは当然想定されるので、RuntimeExceptionではなく、Exceptionを継承する。

鍵をかけられるInterfaceを作る

 視聴覚室には機材などの資産があるので鍵を掛けられる必要がある。逆に、鍵を掛ける必要のない部屋も考えられるので、Roomインターフェースに鍵を掛けられる機能を付けてはならない。

  • 鍵がかかってるかどうかを判定できる
  • 施錠することができる
  • 開錠することができる
public interface Lockable{
  public void lock(Key key);
  public void unlock(Key key);
  public boolean isLocked();
}

鍵穴インターフェースを作る

 鍵が鍵穴と合うかどうか確認できるメソッドを持っておく。

public interface Keyhole{
  public boolean isValidKey(Key key);
}

鍵インターフェースを作る

 鍵の形を取得できるメソッドを持っておく。

public interface Key{
  public String getKeyString();
}

部屋用の鍵穴抽象クラスを作る

public abstract class RoomKeyhole implements Keyhole{
  @Override
  public boolean isValidKey(Key key){
    if(getKeyString() == null){
      throw new KeyStringNotDefinedException();
    }
    return getKeyString().equals(key.getKeyString());
  }

  protected abstract String getKeyString();
}

鍵定義されてないですよ例外を作る

 鍵が定義されていないことは通常ありえないので、RuntimeExceptionでよい。実装者が気をつけていれば防げることはRuntimeExceptionで実装する。

public class KeyStringNotDefinedException extends RuntimeException{}

視聴覚室用の鍵穴クラスを作る

 getKeyString()をオーバーライドすることで、鍵の値を持たせる。Object.hashCode()みたいなもの。

public class AudioVisualRoomKeyhole extends RoomKeyhole{
  @Override
  protected String getKeyString(){
    return "AudioVisualRoomKey";
  }
}

視聴覚室用の鍵クラスを作る

 こちらも同様だが、protectedでなくpublicにしておく必要がある。鍵穴の中の形は見えないが、鍵の形は外から見える。という違いがある。

public class AudioVisualRoomKey implements Key{
  @Override
  public String getKeyString(){
    return "AudioVisualRoomKey";
  }
}

施錠可能な部屋の抽象クラスを作る

 鍵をかけられる部屋を作る。鍵の実装をここで集約してしまうので、finalを付けておく。

public abstract class LockableRoom implements Room, Lockable{

  protected abstract List<Human> getHumans();
  protected abstract Keyhole getKeyhole();
  private boolean locked = false;

  @Override
  public void join(Human human) throws RoomLockedException{
    if(isLocked()){
      throw new RoomLockedException();
    }
    getHumans().add(human);
  }

  @Override
  public void leave(Human human) throws RoomLockedException{
    if(isLocked()){
      throw new RoomLockedException();
    }
    getHumans().remove(human);
  };

  @Override
  public final boolean isLocked(){
    return locked;
  }

  @Override
  public final void lock(Key key){
    if(getKeyhole().isValidKey(key)){
      locked = true;
    }
  }

  @Override
  public final void unlock(Key key){
    if(getKeyhole().isValidKey(key)){
      locked = false;
    }
  }
}

鍵かかってますよ例外を作る

 部屋に入れなかった理由を明確にする。このようにExceptionに名前を付けることで、名前から例外の内容を推測できるようになる。

public class RoomLockedException extends RoomException{}

視聴覚室を作る

public class AudioVisualRoom extends LockableRoom{

  private Keyhole keyhole = null;
  private List<Human> humans = null;

  public AudioVisualRoom(){
    keyhole = new AudioVisualRoomKeyhole();
    humans = new ArrayList<>();
  }

  @Override
  protected List<Human> getHumans(){
    return humans;
  }

  @Override
  protected Keyhole getKeyhole(){
    return keyhole;
  }

  // 収容された人数を返す
  public int count(){
    return humans.size();
  }
}

ヒトインターフェースを作る

public interface Human{}

 このような、「これを実装するクラスは人間ですよ」ということを示すためだけの空のインターフェースをマーカーインターフェースという(Java特有)

 ビルトインではSerializable、Cloneable、RandamAccessなど。

学校利用者クラスを作る

public abstract class SchoolUser implements Human{

  // 部屋に入れること以外を知らなくていいので、引数にはRoomインターフェースを宣言する
  public void joinTo(Room room){
    try{
      room.join(this);
    }catch(RoomException e){
      e.printStackTrace();
    }
  }

  public void leaveFrom(Room room){
    try{
      room.leave(this);
    }catch(RoomException e){
      e.printStackTrace();
    }
  }
}

学校管理者クラスを作る

 管理者は部屋に出入りできる、SchoolUserの権限拡張版である。鍵の開錠、施錠が可能。

public abstract class SchoolAdministrator extends SchoolUser{

  private Key key = null;

  public void pickKey(Key key){
    this.key = key;
  }

  // 施錠・開錠できること以外は知らなくていいので、引数にはLockableインターフェースを宣言する
  public void lock(Lockable lockable){
    lockable.lock(key);
  }

  public void unlock(Lockable lockable){
    lockable.unlock(key);
  }
}

生徒クラスを作る

 生徒は利用者だから教室に出入りすることしかできない。

public class Student extends SchoolUser{}

先生クラスを作る

 先生は管理者だから鍵の開閉も可能。

public class Teacher extends SchoolAdministrator {}

テスト

 一連の流れを追ってみる。

public class Main{
  public static void main(String[] args){
    // 視聴覚室を作る
    AudioVisualRoom audioVisualRoom = new AudioVisualRoom();

    // 生徒を作る
    Student student = new Student();

    // 先生を作る
    Teacher teacher = new Teacher();

    // 先生が鍵を取る
    teacher.pickKey(new AudioVisualRoomKey());

    // 先生が鍵を開ける
    teacher.unlock(audioVisualRoom);

    // 生徒が部屋に入る
    student.joinTo(audioVisualRoom);

    // 先生も部屋に入る
    teacher.joinTo(audioVisualRoom);

    System.out.println(audioVisualRoom.count()); // -> 2

    // 生徒が部屋から出る
    student.leaveFrom(audioVisualRoom);

    // 先生も部屋から出る
    teacher.leaveFrom(audioVisualRoom);

    System.out.println(audioVisualRoom.count()); // -> 0

    // 先生が鍵を掛ける
    teacher.lock(audioVisualRoom);

    // 生徒が部屋に入ろうとするが鍵が開かないので入れない
    student.joinTo(audioVisualRoom);  // -> RoomLockedException

    System.out.println(audioVisualRoom.count()); // -> 0
  }
}

余談

 ここで、Javaに触れたことがある方は、「なぜRoomではなくAudioVisualRoomとして型を固定しているのか」と気になって仕方ないはず。

 Javaに限らず、型は出来るだけ抽象クラスやインターフェースを宣言したほうがよい。たとえば、以下の構文がその最たる例である。

List<String> list = new ArrayList<>();

 この構文が、ArrayListでなくListインターフェースで型を宣言している一番の理由は、「ArrayListである理由がないから」である。

 つまり、「ArrayListでなければならない確たる理由がある」というのなら、ArrayListで宣言するほかない。以下は、「ArrayList固有のメソッドを使用したい場合」の例。

ArrayList<String> arrayList = new ArrayList<>(10000);

for(int i = 0; i < 100; i++){
  arrayList.add("test");
}

// 初期値に満たなかったので内部のArrayのサイズをtrimする
arrayList.trimToSize();

 今回の例だと、生徒と先生は視聴覚室に入りたいわけで、何の部屋でも良いというわけではない、ということ。

まとめ

 なにこれ。