2021年6月18日

プライベートおよびプロテクトされたプロパティとメソッド

オブジェクト指向プログラミングの最も重要な原則の1つに、内部インターフェースと外部インターフェースの区別があります。

これは、「Hello World」アプリよりも複雑なものを開発する際には必須のプラクティスです。

これを理解するために、開発から離れて現実世界に目を向けましょう。

通常、私たちが使っているデバイスは非常に複雑です。しかし、内部インターフェースと外部インターフェースを区別することで、問題なくそれらを使用することができます。

現実世界の例

例えば、コーヒーメーカー。外見はシンプルです。ボタン、ディスプレイ、いくつかの穴…そしてもちろん、結果として素晴らしいコーヒー! :)

しかし内部は…(修理マニュアルからの画像)

多くの詳細があります。しかし、何も知らなくても使用できます。

コーヒーメーカーは非常に信頼性が高いですね。何年も使用できますし、何か問題が発生した場合のみ修理に出せばいいのです。

コーヒーメーカーの信頼性とシンプルさの秘密は、すべての詳細が適切に調整され、隠されていることです。

コーヒーメーカーから保護カバーを取り外すと、使用がはるかに複雑になり(どこを押せばいいのか?)、危険になります(感電する可能性があります)。

見てわかるように、プログラミングにおけるオブジェクトはコーヒーメーカーのようなものです。

しかし、内部の詳細を隠すために、保護カバーではなく、言語の特別な構文と慣習を使用します。

内部インターフェースと外部インターフェース

オブジェクト指向プログラミングでは、プロパティとメソッドは2つのグループに分けられます。

  • 内部インターフェース – クラスの他のメソッドからはアクセスできますが、外部からはアクセスできないメソッドとプロパティ。
  • 外部インターフェース – クラスの外からもアクセスできるメソッドとプロパティ。

コーヒーメーカーの例で言えば、内部に隠されているもの(ボイラーチューブ、発熱体など)が内部インターフェースです。

内部インターフェースはオブジェクトが動作するために使用され、その詳細は相互に使用されます。例えば、ボイラーチューブは発熱体に接続されています。

しかし、外部から見ると、コーヒーメーカーは保護カバーで覆われているため、誰もそれらにアクセスできません。詳細は隠されており、アクセスできません。外部インターフェースを介してその機能を使用できます。

そのため、オブジェクトを使用するために必要なのは、その外部インターフェースを知ることだけです。内部の動作方法を完全に知らなくても構いませんし、それが素晴らしいことです。

これは一般的な紹介でした。

JavaScriptでは、オブジェクトフィールド(プロパティとメソッド)には2つの種類があります。

  • パブリック:どこからでもアクセスできます。これらは外部インターフェースを構成します。これまで、私たちはパブリックプロパティとメソッドのみを使用していました。
  • プライベート:クラス内からのみアクセスできます。これらは内部インターフェース用です。

他の多くの言語には、「プロテクトされた」フィールドもあります。クラス内とそのクラスを拡張したものからのみアクセスできます(プライベートに似ていますが、継承クラスからのアクセスが追加されます)。これらも内部インターフェースに役立ちます。継承クラスにアクセスさせたいことが多いので、プライベートのものよりも広範囲に使用されています。

プロテクトされたフィールドは、言語レベルではJavaScriptで実装されていませんが、実際には非常に便利なので、エミュレートされます。

今度は、これらのすべての種類のプロパティを使用して、JavaScriptでコーヒーメーカーを作成します。コーヒーメーカーには多くの詳細がありますが、シンプルさを保つためにモデル化しません(ただし、できます)。

「waterAmount」の保護

最初に簡単なコーヒーメーカークラスを作成しましょう。

class CoffeeMachine {
  waterAmount = 0; // the amount of water inside

  constructor(power) {
    this.power = power;
    alert( `Created a coffee-machine, power: ${power}` );
  }

}

// create the coffee machine
let coffeeMachine = new CoffeeMachine(100);

// add water
coffeeMachine.waterAmount = 200;

現在、プロパティwaterAmountpowerはパブリックです。外部から簡単にそれらの値を取得/設定できます。

より細かく制御するために、waterAmountプロパティをプロテクトに変更しましょう。例えば、誰にも0未満に設定させたくありません。

プロテクトされたプロパティは、通常アンダースコア_で接頭辞を付けられます。

これは言語レベルでは強制されていませんが、プログラマの間では、そのようなプロパティとメソッドは外部からアクセスしないでくださいというよく知られた慣習があります。

そのため、プロパティは_waterAmountと呼ばれます。

class CoffeeMachine {
  _waterAmount = 0;

  set waterAmount(value) {
    if (value < 0) {
      value = 0;
    }
    this._waterAmount = value;
  }

  get waterAmount() {
    return this._waterAmount;
  }

  constructor(power) {
    this._power = power;
  }

}

// create the coffee machine
let coffeeMachine = new CoffeeMachine(100);

// add water
coffeeMachine.waterAmount = -10; // _waterAmount will become 0, not -10

これでアクセスが制御されるため、水量を0未満に設定することが不可能になります。

読み取り専用「power」

powerプロパティについては、読み取り専用にしましょう。プロパティは作成時にのみ設定され、その後変更されない場合があります。

コーヒーメーカーの場合、まさにそうです。電力は変わりません。

そのためには、セッターではなくゲッターを作成するだけです。

class CoffeeMachine {
  // ...

  constructor(power) {
    this._power = power;
  }

  get power() {
    return this._power;
  }

}

// create the coffee machine
let coffeeMachine = new CoffeeMachine(100);

alert(`Power is: ${coffeeMachine.power}W`); // Power is: 100W

coffeeMachine.power = 25; // Error (no setter)
ゲッター/セッター関数

ここでゲッター/セッター構文を使用しました。

しかし、ほとんどの場合、次のようにget.../set...関数が優先されます。

class CoffeeMachine {
  _waterAmount = 0;

  setWaterAmount(value) {
    if (value < 0) value = 0;
    this._waterAmount = value;
  }

  getWaterAmount() {
    return this._waterAmount;
  }
}

new CoffeeMachine().setWaterAmount(100);

少し長くなりますが、関数はより柔軟です。複数の引数を受け入れることができます(現状では必要ありませんが)。

一方、ゲッター/セッター構文は短いため、最終的には厳格なルールはなく、どちらを使用するかはあなた次第です。

プロテクトされたフィールドは継承されます

class MegaMachine extends CoffeeMachineを継承する場合、新しいクラスのメソッドからthis._waterAmountまたはthis._powerにアクセスすることを妨げるものは何もありません。

そのため、プロテクトされたフィールドは自然に継承可能です。後で説明するプライベートなものとは異なります。

プライベート「#waterLimit」

最近の追加
これは言語への最近の追加です。JavaScriptエンジンではサポートされていないか、部分的にしかサポートされておらず、ポリフィルが必要です。

プライベートプロパティとメソッドを言語レベルでサポートする、ほぼ標準化されたJavaScriptの提案があります。

プライベートは#で始める必要があります。クラス内からのみアクセスできます。

例えば、プライベート#waterLimitプロパティと水量チェックプライベートメソッド#fixWaterAmountがあります。

class CoffeeMachine {
  #waterLimit = 200;

  #fixWaterAmount(value) {
    if (value < 0) return 0;
    if (value > this.#waterLimit) return this.#waterLimit;
  }

  setWaterAmount(value) {
    this.#waterLimit = this.#fixWaterAmount(value);
  }

}

let coffeeMachine = new CoffeeMachine();

// can't access privates from outside of the class
coffeeMachine.#fixWaterAmount(123); // Error
coffeeMachine.#waterLimit = 1000; // Error

言語レベルでは、#はフィールドがプライベートであることを示す特別な記号です。外部または継承クラスからはアクセスできません。

プライベートフィールドはパブリックフィールドと競合しません。同時にプライベート#waterAmountフィールドとパブリックwaterAmountフィールドを持つことができます。

例えば、waterAmount#waterAmountのアクセッサにしましょう。

class CoffeeMachine {

  #waterAmount = 0;

  get waterAmount() {
    return this.#waterAmount;
  }

  set waterAmount(value) {
    if (value < 0) value = 0;
    this.#waterAmount = value;
  }
}

let machine = new CoffeeMachine();

machine.waterAmount = 100;
alert(machine.#waterAmount); // Error

プロテクトされたものとは異なり、プライベートフィールドは言語自体によって強制されます。それは良いことです。

しかし、CoffeeMachineを継承する場合、#waterAmountには直接アクセスできません。waterAmountゲッター/セッターに頼る必要があります。

class MegaCoffeeMachine extends CoffeeMachine {
  method() {
    alert( this.#waterAmount ); // Error: can only access from CoffeeMachine
  }
}

多くのシナリオでは、そのような制限は厳しすぎます。CoffeeMachineを拡張する場合、その内部にアクセスする正当な理由がある場合があります。言語構文ではサポートされていませんが、そのためプロテクトされたフィールドの方が頻繁に使用されます。

プライベートフィールドはthis[name]として利用できません

プライベートフィールドは特殊です。

ご存じのように、通常はthis[name]を使用してフィールドにアクセスできます。

class User {
  ...
  sayHi() {
    let fieldName = "name";
    alert(`Hello, ${this[fieldName]}`);
  }
}

プライベートフィールドでは、それは不可能です。this['#name']は機能しません。これはプライバシーを確保するための構文上の制限です。

要約

OOPの観点から、内部インターフェースと外部インターフェースの区別はカプセル化と呼ばれます。

これにより、次の利点が得られます。

ユーザーの保護、つまりユーザーが自分で足を撃つことを防ぎます。

コーヒーメーカーを使用する開発者のチームがいると想像してください。「Best CoffeeMachine」社によって作られ、正常に動作しますが、保護カバーが取り外されています。そのため、内部インターフェースが公開されています。

すべての開発者は礼儀正しく、コーヒーメーカーを意図したとおりに使用しています。しかし、そのうちの1人であるジョンは、自分が最も賢いと判断し、コーヒーメーカーの内部をいくつか調整しました。そのため、コーヒーメーカーは2日後に故障しました。

それは確かにジョンのせいではありませんが、保護カバーを取り外し、ジョンに操作を許可した人のせいでしょう。

プログラミングでも同じです。クラスのユーザーが外部から変更する意図のないものを変更した場合、結果は予測できません。

サポート可能

プログラミングにおける状況は現実世界のコーヒーメーカーよりも複雑です。なぜなら、一度だけ購入するわけではないからです。コードは常に開発と改善を繰り返しています。

内部インターフェースを厳密に区別すれば、クラスの開発者は、ユーザーに通知しなくても、その内部プロパティとメソッドを自由に変更できます。

クラスの開発者であれば、外部コードが依存していないため、プライベートメソッドを安全に名前変更したり、パラメーターを変更したり、削除したりできることがわかります。

ユーザーにとって、新しいバージョンがリリースされた場合、内部的には全面的な見直しが行われるかもしれませんが、外部インターフェースが同じであれば、アップグレードは簡単です。

複雑性の隠蔽

人々は、少なくとも外見上はシンプルなものを好んで使います。内部は別の話です。

プログラマーも例外ではありません。

実装の詳細が隠されていて、シンプルでよく文書化された外部インターフェースが利用できるのは常に便利です。

内部インターフェースを隠すには、プロテクトされたプロパティまたはプライベートプロパティを使用します。

  • 保護されたフィールドは_で始まります。これは広く知られた慣例であり、言語レベルでは強制されません。プログラマーは、_で始まるフィールドには、そのクラスとそのクラスを継承するクラスからのみアクセスする必要があります。
  • プライベートフィールドは#で始まります。JavaScriptは、クラス内からのみアクセスできるようにします。

現時点では、プライベートフィールドはブラウザ間で十分にサポートされていませんが、ポリフィルできます。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善点に関するご提案がありましたら、コメントする代わりにGitHub issueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 数語のコードを挿入するには<code>タグを使用し、複数行の場合は<pre>タグで囲み、10行を超える場合はサンドボックス(plnkrjsbincodepen…)を使用してください。