11-2 privatepublic

前節で全てのコンポーネントを変数や関数・手続きにしました。現時点でユニットの冒頭部は次のようになっているはずです。

type
  TFormInvader = class(TForm)
    procedure Draw; virtual;
    procedure Erase; virtual;
    procedure GoHorizontal; virtual;
    procedure GoDown; virtual;
    procedure Move; virtual;
    procedure Construct; virtual;
    procedure Destruct; virtual;
    procedure HitCheck; virtual;

  private
    { Private 宣言 }
  public
    { Public 宣言 }
    X: Integer;
    Y: Integer;
    Speed: Integer;
    Alive: Boolean;
  end;

でも、この中にある「private」(私的) と「public」(公的) はどういう意味なのでしょうか?

【練習問題】

次の2つの場合で、「procedure」で始まる全てのモジュールの宣言を移動させて実行させ、何が起こるか確認してみましょう。

(1) private の下へ移動させる場合

type
  TFormInvader = class(TForm)
  private
    { Private 宣言 }
    procedure Draw; virtual;
    procedure Erase; virtual;
    procedure GoHorizontal; virtual;
    procedure GoDown; virtual;
    procedure Move; virtual;
    procedure Construct; virtual;
    procedure Destruct; virtual;
    procedure HitCheck; virtual;
  public
    { Public 宣言 }
    X: Integer;
    Y: Integer;
    Speed: Integer;
    Alive: Boolean;
  end;

(2) public の下へ移動させる場合

type
  TFormInvader = class(TForm)
  private
    { Private 宣言 }
  public
    { Public 宣言 }
    X: Integer;
    Y: Integer;
    Speed: Integer;
    Alive: Boolean;
    procedure Draw; virtual;
    procedure Erase; virtual;
    procedure GoHorizontal; virtual;
    procedure GoDown; virtual;
    procedure Move; virtual;
    procedure Construct; virtual;
    procedure Destruct; virtual;
    procedure HitCheck; virtual;
  end;

(1) の場合、 control ユニットで「未定義の識別子」というエラーが発生してしまいますが、(2) の場合は問題なく実行できます。

実は、private 部 (private の行以下) に書かれた変数や関数・手続きには他のユニットからアクセスできないのです。一方、public 部に書かれた変数やモジュールへは他のユニットからもアクセスできます。 privatepublic の両者を用意しているのは、「必要最小限のものだけを外部に公開し、その他のものは外部から触れないようにしよう」という考えを実現するためです。この考えをオブジェクト指向の言葉で「情報隠蔽(いんぺい)」と言います。

【練習問題】

例えば、変数 X は外部からアクセスされていないので、private 部に移動することができます。プログラムを次のように変更し、実行してみてください。

type
  TFormInvader = class(TForm)
  private
    { Private 宣言 }
    X: Integer;
  public
    { Public 宣言 }
    Y: Integer;
    Speed: Integer;
    Alive: Boolean;
    procedure Draw; virtual;
    procedure Erase; virtual;
    procedure GoHorizontal; virtual;
    procedure GoDown; virtual;
    procedure Move; virtual;
    procedure Construct; virtual;
    procedure Destruct; virtual;
    procedure HitCheck; virtual;
  end;

逆に、手続き Move は control ユニットから呼び出されているので、private 部に移動するとエラーが発生します。プログラムを次のように変更して実行し、これを確認してください。

type
  TFormInvader = class(TForm)
  private
    { Private 宣言 }
    X: Integer;
    procedure Move; virtual;
  public
    { Public 宣言 }
    Y: Integer;
    Speed: Integer;
    Alive: Boolean;
    procedure Draw; virtual;
    procedure Erase; virtual;
    procedure GoHorizontal; virtual;
    procedure GoDown; virtual;
    procedure Construct; virtual;
    procedure Destruct; virtual;
    procedure HitCheck; virtual;
  end;

【基礎課題 11-4】

変数 X 以外の変数1つ1つについて、private 部に動かせるかどうかを試し、少しでも多くの変数を隠蔽して (private部に移動させて) 下さい。

一般にオブジェクト指向言語では、変数は private部におきます。外部 (他のユニット) から参照する必要のあるものについてのみ public 部に置く、という情報隠蔽の原則があるからです。一方、手続き・関数は、全て private 部におくとそのフォームは役に立たなくなります (外部から操作できなくなります)。 そのため、 private 部に置けない場合がよくあります。この点については次節 11-3 で詳しくふれます。

【練習問題】

全ての関数・手続きを public 部に戻してください。

さて、ここまでの説明を読んで、なぜ情報隠蔽なんて面倒くさいことを考える必要があるの? と疑問に思ったかもしれません。これは、(プログラミング上の) 思わぬミスやトラブルを防ぐ“防犯上”の観点から捉えると分かりやすいと思います。これを、一軒家の住宅の例で考えてみましょう。この場合、変数は金庫 (あるいはその他の財産)、手続き・関数は居住者、そして外部のオブジェクトは配達員やセールスマンなどに当たります。

配達員 (外部オブジェクト) が小包を持ってきても、受け取りに必要な印鑑 (変数) を配達員が自分で探すことはありません (そんなことができたら、防犯上大問題です)。 印鑑 (変数) の出し入れは、必ず居住者 (手続き・関数) を介して行います。

この事情はプログラムにおいても同様です。フォーム (より一般にはクラスと言います。11-5参照) の public 部に変数を置いてしまうと、その変数は誰でも (どの外部オブジェクトからでも) アクセスできます。ですから、もしその変数におかしな値が入ってプログラムが思ったように動かなくなった場合、プログラムの誤りを直す(デバッグと言います) には、全てのオブジェクトを疑わねばならなくなります。一方、private部に変数を置いた場合、その変数にアクセスできるのはフォーム(クラス) 内の手続き・関数だけですから、デバッグの対象が限定されます。

さて、【基礎課題 11-4】で試して分かったと思いますが、このプログラムはこのままでは変数 Speedprivate 部へ隠すことができません。このような場合は、変数 Speed にアクセスするための関数 (あるいは手続き) を用意して、外部から変数 Speed へのアクセスは全てその関数を通す様にします。そうすることで、変数 Speedprivate 部へ隠すことができます。それを以下の練習問題で行いましょう。

【練習問題】

変数 Speed にアクセスするための関数・手続き

を用意します。まずは、プログラムの冒頭部に次のように2つの関数・手続きを書き加えてください。

type
  TFormInvader = class(TForm)
  private
    { Private 宣言 }
    X: Integer;
    Y: Integer;
    Alive: Boolean;
  public
    { Public 宣言 }
    Speed: Integer;
    procedure Draw; virtual;
    procedure Erase; virtual;
    procedure SetSpeed(S: Integer); virtual;
    function GetSpeed: Integer; virtual;
    procedure GoHorizontal; virtual;
    procedure GoDown; virtual;
    procedure Move; virtual;
    procedure Construct; virtual;
    procedure Destruct; virtual;
    procedure HitCheck; virtual;

  end;

GetSpeed には引数がありません。このような引数のない関数も使用可能です。

そして、他の関数・手続きと同様に、その処理の中身を書き込みます。(書き込む場所はプログラムの末尾、end.の直前で構いません。)

// 速度書き込み
procedure TFormInvader.SetSpeed(S: Integer);
begin
  Speed := S;
end;

// 速度呼び出し
function TFormInvader.GetSpeed: Integer;
begin
  GetSpeed := Speed;
end;

これで条件が整ったので、変数 Speedprivate 部へ隠します。

type
  TFormInvader = class(TForm)
  private
    { Private 宣言 }
    X: Integer;
    Y: Integer;
    Speed: Integer;
    Alive: Boolean;
  public
    { Public 宣言 }
    procedure Draw; virtual;
    procedure Erase; virtual;
    procedure SetSpeed(S: Integer); virtual;
    function GetSpeed: Integer; virtual;
    procedure GoHorizontal; virtual;
    procedure GoDown; virtual;
    procedure Move; virtual;
    procedure Construct; virtual;
    procedure Destruct; virtual;
    procedure HitCheck; virtual;
  end;

実行してみましょう。今まで変数 Speed に直接アクセスしていた invader_boss ユニットの手続き RandomMove でエラーが発生するので、次のように変更します。

procedure TFormInvaderBoss.RandomMove;
var
  NewSpeed: Integer;
begin
  NewSpeed := GetSpeed + Random(7) - 3;
  SetSpeed(NewSpeed);
  Move;
end;

これで、変数 Speedprivate 部へ隠したにも関わらず今まで通りに動くようになりました。(最後に出てくる変数 NewSpeed を使わなくてもプログラムが作れます。興味がある学生は挑戦してみてください。)

コラム アクセス用の関数・手続きは無意味じゃないのか?

今までの作業を見て、鋭い方は

いくら変数 Speedprivate 部へ隠したって、どうせ public 部にある2つの関数・手続きで自由に内容を変更できるのだから、全然隠蔽されていないじゃないか。本質的に何も変わっていないなら、今まで通り変数 Speedpublic 部に置く方が余計なプログラミングをしなくていい。

と感じたかも知れません。確かに、このままでは今まで通り変数 Speedpublic 部に置いてある状態と変わりません。ですが、例えば「変数 Speed の値は 30 を超えないようにする」ということを実現しようとしたときには、こういうアクセス用関数・手続きが威力を発揮するのです (次のようにします)。

// 速度書き込み
procedure TFormInvader.SetSpeed(S: Integer);
begin
  Speed := S;
  if Speed > 30
    then
      begin
        Speed := 30;
      end
  ;
end;

こうすることによって、外部からどんなに大きな値が書き込まれようとしても 30 を超えることは有り得なくなります。これは、変数 Speed の値についてインベーダーフォーム自身が責任を持っている、と言い換えることもできます。

一方、今まで通り変数 Speedpublic 部にあると、外部から書き込もうとする側が「値が 30 を超えていないかどうか」を確認しなくてはなりません。つまり、責任は外部にあることになります。

この違いは、施錠できる部屋の内側と外側のどちらに物を置くか、ということと似ています。部屋の中で1万円札がなくなったとしても、部屋を片付ければいつかは見つかるでしょう。それとも、1週間前に豪遊してその1万円札を使ってしまったことをあなたが忘れているだけかもしれません。いずれにしても、なくなったのはあなたの責任です。一方、部屋の外に置いといた1万円札がなくなったとしたら、その理由を調べることは大変困難です。誰が持ち去ったのか、犯人を特定できません。これと同様に、private 部にある変数の値がおかしくなってもその原因を突き止めることは容易ですが、 public 部にある変数の場合は困難です。だから、原則として全ての変数は private 部に置くのです。

ここまでのプログラムをダウンロード(D)