JavaScriptで学ぶオブジェクト指向
本講座ではJavaScriptを利用して、オブジェクト指向を学習する。 オブジェクト指向はプログラミング界隈の一般的な用語であり、 JavaScript固有の用語ではない。 また本講座はJavaScriptのオブジェクト指向を学習するのではなく、 プログラミング一般で言われるオブジェクト指向を学習する。
学習順序を解説する。 オブジェクト指向を解説する前に、 プログラムの実例をご覧頂く。 最初に非オブジェクト指向で作ったプログラムを見る。 これを批評した後、オブジェクト指向プログラミングで 作り変える。実際のプログラムを見て、イメージを持った後、 オブジェクト指向の概念を解説する。
非オブジェクト指向のプログラム
[メッセージ]
- message A
- message B
- message C
- message D
製造するプログラムを解説する。 文字列を4つ画面に表示するプログラムである。 会員サイトのメッセージ機能をイメージしてほしい。 会員サイトにログインすると、 メッセージボックスにメッセージが表示される。 そのメッセージの文字列を画面に表示するプログラムを作る。 まず、sample.htmlを見る。
002<head><meta charset="UTF-8">
003<script type="text/javascript" src="app.js" >
004</script>
005</head>
006<body>
007
008 <p>[メッセージ]</p>
009 <ul id="messages"></ul>
010
011</body>
012</html>
sample.htmlファイルである。 本講座では、このファイルを変更せずに終始利用する。 プログラムを解説する。 3行目で、app.jsファイルを読み込む。 app.jsファイルで、画面に4つの文字列を表示する。 9行目にulタグを設置する。 HTMLでは文字列を表示していないが、 JavaScriptを利用して、このulタグにメッセージを表示する。 ulタグに、文字列を表示したliタグを追加する形で、 4つの文字列を画面に表示する。 では、app.jsを見る。
002//表示するメッセージの文字列
003const messages = [
004 " message A" ," message B" ,
005 " message C" ," message D" ,
006];
007//ブラウザ読み込み完了時に実行
008window.onload = () => {
009
010 const ul =
011 document.getElementById("messages");
012
013 //メッセージを表示
014 messages.forEach ( ( message ) => {
015
016 //liタグを作成
017 const li = document.createElement("li");
018
019 //文字列設定
020 li.textContent = message ;
021
022 //ulタグへ追加
023 ul.append ( li ) ;
024 });
025}
3行目から6行目がメッセージの内容である。 メッセージを配列で持ち、それぞれのメッセージを ulタグに追加する。 今回、文字列「mesage A」「mesage B」 「mesage C」「mesage D」が画面に表示する。 では、プログラムを詳解する。 8行目で、ブラウザの読み込み完了時に実行する 関数を登録する。
011 document.getElementById("messages");
012
013 //メッセージを表示
014 messages.forEach ( ( message ) => {
015
016 //liタグを作成
017 const li = document.createElement("li");
018
019 //文字列設定
020 li.textContent = message ;
021
022 //ulタグへ追加
023 ul.append ( li ) ;
024 });
ブラウザ読み込み完了時に実行するプログラムである。 メッセージを画面に表示する処理をする。 10行目から解説する。HTMLからidの値がmessagesの オブジェクトを取得して変数ulへ代入する。 idの値がmessagesのタグはulタグである。 ulタグのオブジェクトが変数ulに代入する。 14行目でforEach()関数を利用して、 messages配列の要素を全て取り出す。 messages配列の要素は、メッセージボックスに表示する文字列である。 文字列が、14行目の変数messageに入る。 17行目でliタグを作成して、 20行目で、liタグに文字列を代入する。 これで、messages配列に入った文字列がliタグに表示する。 23行目でliタグをulタグに追加する。 解説は以上である。 画面の表示を見る。
[メッセージ]
- message A
- message B
- message C
- message D
4つの文字列が表示する。プログラムの解説は以上である。 さて突然ではあるが、 このプログラムは良いプログラムだろうか?
良いプログラム 拡張性と柔軟性がある
プログラムを製造する時、目の前のプログラムを作る事に 専念しがちである。 しかし、それは誤っている。 作るべきプログラムは、当然作る。 さらに、プログラムの機能追加、 改修に対応できなくてはならない。 プログラムを作って終わり、というわけにはいかない。 当然その後、機能追加や改修作業がある。 速やかに機能追加や改修作業ができる プログラムが、良いプログラムである。
オブジェクト指向プログラミング プログラムに拡張性と柔軟性を持たせるための プログラミング技法
オブジェクト指向プログラミングは、プログラムに拡張性と柔軟性を与える。
では、話を元へ戻す。 現在製造したプログラムは非オブジェクト指向で作っている。 これに機能を追加する。
[メッセージ]
- message A
- message B[重要]
- message C
- message D[重要]
各メッセージに、重要度を設定する。 重要度の高いメッセージは、目立つように表示する。 今回、「message B」と「message D」の重要度を 仮に「10」とする。そして、重要度が「10」 のメッセージは、メッセージの後ろに「重要」と表記する。 では、app.jsファイルを見る(sample.htmlは変更しない)。
002//メッセージ
003const messages = [
004 [ "message A" , "0" ] ,
005 [ "message B" , "10" ] ,
006 [ "message C" , "0" ] ,
007 [ "message D" , "10" ] ,
008];
009//ブラウザ読み込み完了時に実行
010window.onload = () => {
011
012 const ul =
013 document.getElementById ("messages");
014
015 //メッセージを表示
016 messages.forEach ( ( value ) => {
017
018 //liタグを作成
019 const li = document.createElement("li");
020
021 //文字列設定
022 if ( value [ 1 ] === "10" ){
023 li.textContent = value[0] + "[重要]";
024 } else {
025 li.textContent = value[0] ;
026 }
027
028 //ulタグへ追加
029 ul.append ( li ) ;
030 });
031}
まず、4行目から7行目でメッセージを作成する。
003const messages = [
004 [ "message A" , "0" ] ,
005 [ "message B" , "10" ] ,
006 [ "message C" , "0" ] ,
007 [ "message D" , "10" ] ,
008];
前回のプログラムでは、メッセージは文字列であった。 こんかい、メッセージを配列で持つ。 各メッセージの配列[0]に文字列を代入して、 配列[1]に重要度を代入する。 「message B」と「message D」の重要度を「10」にして、 「message A」と「message C」の重要度は「0」にした。 では、続きを見る。
013 document.getElementById ("messages");
014
015 //メッセージを表示
016 messages.forEach ( ( value ) => {
017
018 //liタグを作成
019 const li = document.createElement("li");
020
021 //文字列設定
022 if ( value [ 1 ] === "10" ){
023 li.textContent = value[0] + "[重要]";
024 } else {
025 li.textContent = value[0] ;
026 }
027
028 //ulタグへ追加
029 ul.append ( li ) ;
030 });
ブラウザの読み込みが完了すると、このプログラムが実行する。 12行目でulタグオブジェクトを取得した後、 16行目で配列のforEach()関数を実行する。 16行目の引数valueにメッセージが代入される。 19行目でliタグを作成する。 22行目が問題のプログラムである。 こんかい、重要度により表示する文字列が異なる。 その分岐処理を22行目のif文で実行する。 22行目で変数valueのインデックス「1」を確認する。 変数valueはメッセージであり、インデックス「1」に メッセージの重要度が入っている。 インデックス「1」の値が「10」の場合、 23行目が実行する。 変数valueのインデックス「0」にメッセージの文字列が代入されている。 メッセージと文字列[重要]が文字列結合して、liタグに表示する。 変数valueのインデックス「1」の値が「10」以外の場合、 elseブロックが実行する。 elseブロックで、メッセージをliタグに表示する。 解説は以上である。画面をもう一度見る。
[メッセージ]
- message A
- message B[重要]
- message C
- message D[重要]
重要度が高い 「message B」と「message D」に、文字列[重要]が表示する。 今回のプログラムは、 「if文により、表示する方法を分岐したプログラム」と言える。
このプログラムは非オブジェクト指向と言える。 問題の所在は明らかである。if文である。 たとえば、今後重要度の種別が増えた時、 if文にどんどん条件文を追加する必要がある。
あるいは、今後メッセージを表示する画面を増やすと仮定する。 メッセージを表示する画面を増やすたびに、 このif文の分岐処理を記述する必要がある。 if文が悪いとは思わない。 多かれ少なかれif文を記述する必要がある。 しかし、不必要に同じようなif文を至る所で記述する事は、 メンテナンス性が低いと言える。 このプログラムをオブジェクト指向プログラムへ変更する。
オブジェクト指向プログラミング
オブジェクト指向プログラミングでは、 クラスを作成する。
クラス設計 機能によりクラスを分割する
オブジェクト指向というと、「モノ」というイメージがある。 しかし実際のプログラムでは「モノ」は存在しない。 存在するものは、「機能」である。
アプリケーションを製造する前に、 通常、アプリケーションで必要となる機能を列挙する。 そして、その機能を担当するクラスを製造する。 つまり、機能ごとにクラスを製造する。これが普通である。 では、今回のプログラムで必要となるクラスは何か? こんかいは、Messageクラスを作成する。 Messageクラスはメッセージを管理するクラスである。 では、オブジェクト指向でプログラムを書く。
[メッセージ]
- message A
- message B
- message C
- message D
はじめに製造する画面は、文字列が4つ表示する画面である。 では、プログラムを見る。
002//メッセージクラス
003class Message {
004 constructor ( title ) {
005 this._title = title ;
006 }
007 getLabel () {
008 return this._title ;
009 }
010}
011//メッセージ
012const messages = [
013 new Message ( "message A" ) ,
014 new Message ( "message B" ) ,
015 new Message ( "message C" ) ,
016 new Message ( "message D" ) ,
017];
018//ブラウザ読み込み完了時に実行
019window.onload = () => {
020
021 const ul =
022 document.getElementById("messages");
023
024 //メッセージを表示
025 messages.forEach ( ( value ) => {
026
027 //liタグを作成
028 const li = document.createElement("li");
029
030 //文字列設定
031 li.textContent = value.getLabel ();
032
033 //ulタグへ追加
034 ul.append ( li ) ;
035 });
036}
長くもなく、短くも無いプログラムである。 では、順番に解説する。
003class Message {
004 constructor ( title ) {
005 this._title = title ;
006 }
007 getLabel () {
008 return this._title ;
009 }
010}
Messageクラスを定義する。 4行目のコンストラクタで、メッセージの文字列を受け取る。 7行目でgetLabel()メソッドを定義する。 getLabel()メソッドを実行すると、 画面に表示するメッセージを戻す。 解説を続ける。
004 constructor ( title ) {
005 this._title = title ;
006 }
007 getLabel () {
008 return this._title ;
009 }
010}
011//メッセージ
012const messages = [
013 new Message ( "message A" ) ,
014 new Message ( "message B" ) ,
015 new Message ( "message C" ) ,
016 new Message ( "message D" ) ,
017];
12行目でメッセージ配列を作成する。 配列の要素は、Messageインスタンスとする。 Messageクラスをインスタンス化する時、 画面に表示する文字列を渡す。 この文字列は、Messageクラスのコンストラクタの処理により、 _titleフィールドに代入される(5行目の処理)。 もし、7行目のgetLabel()メソッドを実行すると、 ここで渡した文字列が、戻り値として戻る。
022 document.getElementById("messages");
023
024 //メッセージを表示
025 messages.forEach ( ( value ) => {
026
027 //liタグを作成
028 const li = document.createElement("li");
029
030 //文字列設定
031 li.textContent = value.getLabel ();
032
033 //ulタグへ追加
034 ul.append ( li ) ;
035 });
ブラウザ読み込み完了時に実行するプログラムである。 21行目でulタグオブジェクトを取得する。 25行目でメッセージ配列のforEach()関数を実行する。 28行目でliタグを生成して、 変数liへ代入する。 31行目で、変数valueが参照するMessageインスタンスに対して、 getLabel()関数を実行する。
004 constructor ( title ) {
005 this._title = title ;
006 }
007 getLabel () {
008 return this._title ;
009 }
010}
getLabel()関数は、コンストラクタで渡された文字列を戻す。 例えば、文字列「message A」や「message B」などである。
022 document.getElementById("messages");
023
024 //メッセージを表示
025 messages.forEach ( ( value ) => {
026
027 //liタグを作成
028 const li = document.createElement("li");
029
030 //文字列設定
031 li.textContent = value.getLabel ();
032
033 //ulタグへ追加
034 ul.append ( li ) ;
035 });
31行目でMessageインスタンスのgetLabel()メソッドを実行して、 戻り値の文字列を、liタグに表示する。
[メッセージ]
- message A
- message B
- message C
- message D
画面には、このように表示する。
オブジェクト指向プログラムを拡張する
プログラムを拡張する。
[メッセージ]
- message A
- message B[重要]
- message C
- message D[重要]
「message B」と「message D」に文字列[重要]を表示する。 非オブジェクト指向プログラムの場合、 if文を利用して表示するメッセージを変更していた。 オブジェクト指向プログラムだと、どうだろうか?
app.jsを見る。
002//メッセージクラス
003class Message {
004 constructor ( title ) {
005 this._title = title ;
006 }
007 getLabel () {
008 return this._title ;
009 }
010}
011class Message10 extends Message {
012 constructor ( title ) {
013 super ( title ) ;
014 }
015 getLabel () {
016 return this._title + "[重要]";
017 }
018}
019//メッセージ
020const messages = [
021 new Message ( "message A" ) ,
022 new Message10 ( "message B" ) ,
023 new Message ( "message C" ) ,
024 new Message10 ( "message D" ) ,
025];
026//ブラウザ読み込み完了時に実行
027window.onload = () => {
028
029 const ul =
030 document.getElementById("messages");
031
032 //メッセージを表示
033 messages.forEach ( ( value ) => {
034
035 //liタグを作成
036 const li = document.createElement("li");
037
038 //文字列設定
039 li.textContent = value.getLabel ();
040
041 //ulタグへ追加
042 ul.append ( li ) ;
043 });
044}
if文が消えた。クラスから見る。
004 constructor ( title ) {
005 this._title = title ;
006 }
007 getLabel () {
008 return this._title ;
009 }
010}
011class Message10 extends Message {
012 constructor ( title ) {
013 super ( title ) ;
014 }
015 getLabel () {
016 return this._title + "[重要]";
017 }
018}
Messageクラスに変更はない。 今回、Message10クラスを新規作成した。 Message10クラスは重要度が「10」のメッセージを表現したクラスである。 まず、11行目でMessageクラスを継承する。 13行目で、親クラスのコンストラクタを実行する。 15行目でgetLabel()メソッドを定義する。 getLabel()メソッドを実行すると、 コンストラクタで与えられた文字列に、 文字列[重要]を結合して戻す。 MessageクラスとMessage10クラスのgetLabel()メソッドは、 メソッド名は同じだが、実行結果は異なる。
021 new Message ( "message A" ) ,
022 new Message10 ( "message B" ) ,
023 new Message ( "message C" ) ,
024 new Message10 ( "message D" ) ,
025];
メッセージ配列である。 「message A」と「message C」はMessageのインスタンスを利用する。 「message B」と「message D」はMessage10のインスタンスを利用する。 なぜなら、「message B」と「message D」は重要度が高いからである。 これで、メッセージ配列の作成が完了した。 では、メッセージ配列を利用する。
030 document.getElementById("messages");
031
032 //メッセージを表示
033 messages.forEach ( ( value ) => {
034
035 //liタグを作成
036 const li = document.createElement("li");
037
038 //文字列設定
039 li.textContent = value.getLabel ();
040
041 //ulタグへ追加
042 ul.append ( li ) ;
043 });
ブラウザ読み込み完了時に実行するプログラムである。 プログラムに変更はない。 29行目でulタグオブジェクトを取得する。 33行目でメッセージ配列のforEach()関数を実行する。 36行目でliタグを作成する。 39行目でgetLabel()メソッドを実行する。 いま、変数valueにメッセージ配列の要素が入っている。
021 new Message ( "message A" ) ,
022 new Message10 ( "message B" ) ,
023 new Message ( "message C" ) ,
024 new Message10 ( "message D" ) ,
025];
メッセージ配列には、MessageインスタンスとMessage10インスタンスが 入っている。
033 messages.forEach ( ( value ) => {
034
035 //liタグを作成
036 const li = document.createElement("li");
037
038 //文字列設定
039 li.textContent = value.getLabel ();
040
041 //ulタグへ追加
042 ul.append ( li ) ;
043 });
33行目の変数valueにメッセージ配列の要素が入る。 変数valueの値は、MessageインスタンスかMessage10インスタンスである。 39行目で変数valueが参照するインスタンスに対して、 getLabel()メソッドを実行する。
004 constructor ( title ) {
005 this._title = title ;
006 }
007 getLabel () {
008 return this._title ;
009 }
010}
011class Message10 extends Message {
012 constructor ( title ) {
013 super ( title ) ;
014 }
015 getLabel () {
016 return this._title + "[重要]";
017 }
018}
変数valueが参照するインスタンスがMessageインスタンスの場合、 getLabel()メソッドを実行すると、 コンストラクタに渡した文字列が戻る。 変数valueが参照するインスタンスがMessage10インスタンスの場合、 getLabel()メソッドを実行すると、 コンストラクタに渡した文字列に文字列[重要]が結合して戻る。
メッセージ | インスタンス | getLabel()メソッドの 戻り値 |
message A | Message | "message A" |
message B | Message10 | "message B[重要]" |
message C | Message | "message C" |
message D | Message10 | "message D[重要]" |
各メッセージとgetLabel()メソッドの戻り値をまとめた。 変数valueのインスタンスにより、getLabel()メソッドの戻り値が異なる。
033 messages.forEach ( ( value ) => {
034
035 //liタグを作成
036 const li = document.createElement("li");
037
038 //文字列設定
039 li.textContent = value.getLabel ();
040
041 //ulタグへ追加
042 ul.append ( li ) ;
043 });
39行目で、getLabel()メソッドの戻り値をliタグに表示する。
[メッセージ]
- message A
- message B[重要]
- message C
- message D[重要]
このような表示になる。
オブジェクト指向プログラムをさらに拡張
オブジェクト指向へ変更すると、 プログラムからif文が消えた。 オブジェクト指向とif文に関係性は無いが、 オブジェクト指向プログラムはif文が少ない傾向がある。 では、さらに拡張する。
[メッセージ]
- message A
- message B[重要]
- message C[確認]
- message D[重要]
重要度を増やす。 今回重要度のレベル「確認」を追加する。 「message C」の後ろに、文字列[確認]を結合する。
重要度が異なるメッセージが増えた形である。 この場合、どのように対応するだろうか? if文だろうか?いいえ、違います。 メッセージの種類が増えたので、Messageクラスを増やす。
002//メッセージクラス
003class Message {
004 constructor ( title ) {
005 this._title = title ;
006 }
007 getLabel () {
008 return this._title ;
009 }
010}
011class Message5 extends Message {
012 constructor ( title ) {
013 super ( title ) ;
014 }
015 getLabel () {
016 return this._title + "[確認]";
017 }
018}
019class Message10 extends Message {
020 constructor ( title ) {
021 super ( title ) ;
022 }
023 getLabel () {
024 return this._title + "[重要]";
025 }
026}
027//メッセージ
028const messages = [
029 new Message ( "message A" ) ,
030 new Message10 ( "message B" ) ,
031 new Message5 ( "message C" ) ,
032 new Message10 ( "message D" ) ,
033];
034//ブラウザ読み込み完了時に実行
035window.onload = () => {
036
037 const ul =
038 document.getElementById("messages");
039
040 //メッセージを表示
041 messages.forEach ( ( value ) => {
042
043 //liタグを作成
044 const li = document.createElement("li");
045
046 //文字列設定
047 li.textContent = value.getLabel ();
048
049 //ulタグへ追加
050 ul.append ( li ) ;
051 });
052}
ブラウザ読み込み完了時のプログラムに変更はない。 では、クラスを確認する。
004 constructor ( title ) {
005 this._title = title ;
006 }
007 getLabel () {
008 return this._title ;
009 }
010}
011class Message5 extends Message {
012 constructor ( title ) {
013 super ( title ) ;
014 }
015 getLabel () {
016 return this._title + "[確認]";
017 }
018}
019class Message10 extends Message {
020 constructor ( title ) {
021 super ( title ) ;
022 }
023 getLabel () {
024 return this._title + "[重要]";
025 }
026}
重要度に従ってMessageクラスを3つ作成した。 今回追加したクラスはMessage5である。 11行目でMessageクラスを継承して、Message5クラスを定義する。 15行目でgetLabel()メソッドを再定義する。 getLabel()メソッドを実行すると、 コンストラクタで受け取った文字列に、文字列[確認]を結合して戻す。 メッセージ配列を確認する。
029 new Message ( "message A" ) ,
030 new Message10 ( "message B" ) ,
031 new Message5 ( "message C" ) ,
032 new Message10 ( "message D" ) ,
033];
メッセージ配列である。 31行目で文字列「message C」を引数に渡して、Message5インスタンスを 配列に代入する。
038 document.getElementById("messages");
039
040 //メッセージを表示
041 messages.forEach ( ( value ) => {
042
043 //liタグを作成
044 const li = document.createElement("li");
045
046 //文字列設定
047 li.textContent = value.getLabel ();
048
049 //ulタグへ追加
050 ul.append ( li ) ;
051 });
ブラウザ読み込み完了時に実行するプログラムである。 変更はない。 47行目の変数valueには、メッセージ配列の要素が入る。 変数valueが参照するインスタンスに対して、getLabel()メソッドを実行すると、 参照するインスタンスにより、戻り値が異なる。
メッセージ | インスタンス | getLabel()メソッドの 戻り値 |
message A | Message | "message A" |
message B | Message10 | "message B[重要]" |
message C | Message5 | "message C[確認]" |
message D | Message10 | "message D[重要]" |
[メッセージ]
- message A
- message B[重要]
- message C[確認]
- message D[重要]
このような表示になる。
解説は以上である。