IORI online School

JavaScript、html、css の無料学習サイト

【JavaScript 上級講座】オブジェクト指向とクラス設計

[JavaScript 上級講座]オブジェクト指向とクラス設計

本講座では、プログラムを製造しながら学習を進める。 まず、通常の非オブジェクト指向で作成したプログラムを見る。 そのプログラムを批評した後、 プログラムをオブジェクト指向のプログラムへ製造しなおす。 非オブジェクト指向オブジェクト指向の差異に注目してほしい。

本講座は、オブジェクト指向について一通り学習済みの方を対象とする。 「オブジェクト指向を学習したが、それをどうのように プログラムに活かすのか分からない」とお考えの方に 読んでもらいたい。

学習中、所々で格言めいた事を記述する。 では、本講座で製造するプログラムを確認する。

※注意)本講座では、「コンストラクタ関数」の事を「クラス」と呼ぶ。 しかし、本講座で製造するクラスは実際のclassとは異なる。 実際は、コンストラクタ関数ではなく、class構文を利用してクラスを製造する。

背景色が灰色の部分はCanvasと呼ばれる。 Canvasは図形などを表示するタグである。 Canvasの上に「四角」「丸」 「ストップ」 の3つのリンクがある。 「四角」リンクをクリックすると、 四角い図形が左から右方向へ動く(クリックすると、実際に動きます)。 「ストップ」リンクをクリックすると、図形が停止する。 「丸」リンクをクリックすると、丸い図形が左から右へ移動する。

このプログラムを製造しながら、オブジェクト指向を学習する。 まずプログラムのポイントを述べる。

  • 現在動いている図形が四角か丸か確認する必要がある。
  • 四角い図形が動いている場合は、四角い図形を描画する
  • 丸い図形が動いている場合は、丸い図形を描画する

現在動いている図形が、四角か丸かで描画する処理が異なる。 この部分の実装がポイントになる。

オブジェクト指向で記述したプログラム

オブジェクト指向で記述したプログラムを掲載する。

sample.htmlを掲載する。

001<!DOCTYPE html><head>
002<script type="text/javascript" src="logic.js" >
003</script>
004</head>
005<body>
006
007<!-- コントロール -->
008<div>
009  <a href="javascript:startRect()">四角</a>
010  <a href="javascript:startCircle()">丸</a>
011  <a href="javascript:stopTimer()">ストップ</a>
012</div>
013
014<!-- Canvas -->
015<canvas id="iori-draw-canvas" width="500px" height="150px"
016 style="background:#aaa">
017</canvas>
018
019<!-- タイマーのステータス -->
020<p id="iori-p-status"></p>
021
022</body>
023</html>

2行目でscriptタグを利用して、logic.jsファイルを読み込む。 logic.jsファイルについては後述する。 9行目で四角リンクを作成し、クリックした時にstartRect()関数を実行する。 startRect()関数が実行すると、四角い図形が左から右へ移動する。 10行目で丸リンクを作成し、クリックした時にstartCircle()関数を実行する。 startCircle()関数が実行すると、丸い図形が左から右へ移動する。 11行目でストップリンクを作成し、クリックした時にstopTimer()関数を実行する。 stopTimer()関数が実行すると、動いていた図形が停止する。 15行目でCanvasタグを設置する。 幅「500px」高さ「150px」とした。20行目に、pタグを設置する。 pタグに、状態を表示する。 図形が動いている場合は「実行中」、図形が停止している場合は「停止」と表示する。

では、logic.jsを見る。

001let context;
002let timerStatus = "" ;
003let timer = null;
004let itemType = "rect" ;
005let currentX = 50 ;
006
007window.onload = function() {
008  //Canvasオブジェクトを取得
009  const canvas = tag ( "iori-draw-canvas" );
010  
011  //Contextオブジェクトを取得
012  context = canvas.getContext ( "2d" );
013  
014  //タイマーの状態を表示するpタグ
015  timerStatus = tag ( "iori-p-status" );
016}
017function tag ( id ) {
018  return document.getElementById (id);
019}
020function stopTimer () {
021  clearInterval ( timer ) ;
022  timer = null;
023  timerStatus.textContent = "停止" ;
024  currentX = 50 ;
025}
026function startRect () {
027  if ( timer === null ) {
028    itemType = "rect" ;
029    timer = setInterval ( draw , 500 ) ;
030  }
031}
032function startCircle() {
033  
034  if ( timer === null ) {
035    itemType = "circle" ;
036    timer = setInterval ( draw , 500 ) ;
037  }
038}
039function draw () {
040  
041  if ( timer !== null ) {
042    timerStatus.textContent = "実行中:" + timer;
043    
044    //描画処理
045    context.clearRect ( 0 , 0 , 500 , 150 ) ;
046    
047    //x座標を加算
048    currentX += 20 ;
049    
050    //種別により移動距離を分岐
051    if ( itemType === "rect" ) {
052      
053      context.fillRect ( currentX , 50 , 20 , 20 ) ;
054      
055    } else if ( itemType === "circle" ){
056      
057      context.beginPath();
058      context.arc (
059          currentX , 50 , 20 ,
060          0, Math.PI*2 ) ;
061      context.fill();
062    }
063  }
064}

分割して解説する。まず、グローバル変数を見る。

001let context;
002let timerStatus = "" ;
003let timer = null;
004let itemType = "rect" ;
005let currentX = 50 ;

グローバル変数である。1行目でcontextを宣言する。 これは、Canvasから取得するContextオブジェクトである。 描画処理をする時に利用する。 2行目で変数timerStatusを宣言する。 図形が動いているかどうか表示するpタグオブジェクトを代入する。 3行目はタイマーのIDである。 プログラムでタイマーを利用する。 タイマーのIDを変数timerに代入して、 タイマーを停止する時に利用する。 4行目は図形の種別である。 今動いている図形が、四角い図形か丸い図形か記憶するために利用する。 初期値として文字列「rect」を代入する。 5行目で変数currentXを宣言する。 変数currentXは、現在位置の座標を保存する変数である。 こんかい、図形が左から右へ移動する。 つまり、時間と共に図形を描くx座標を加算していく。 現在の図形のx座標を変数curentXに代入しておいて、 時間とともに、currentXを加算する。 図形を描画する時に、currentXの値をx座標として利用する。 つぎに、処理を解説する。

007window.onload = function() {
008  //Canvasオブジェクトを取得
009  const canvas = tag ( "iori-draw-canvas" );
010  
011  //Contextオブジェクトを取得
012  context = canvas.getContext ( "2d" );
013  
014  //タイマーの状態を表示するpタグ
015  timerStatus = tag ( "iori-p-status" );
016}
017function tag ( id ) {
018  return document.getElementById (id);
019}

7行目で、ブラウザ読み込み完了時の処理を記述する。 ここでグローバル変数に値を代入する。 9行目で、Canvasオブジェクトを取得する。 tag()関数は17行目で定義している。 tag()関数を実行すると、 document.getElementById()関数が実行する。 document.getElementById()の名前が長いので、 tag()関数を作った。意味は無い。 12行目からContextオブジェクトを取得して、 グローバル変数contextへ代入する。15行目で タイマーの状態を表示するpタグのオブジェクトを取得する。 図形が動いているかどうか(タイマーが動いているかどうか)、 状態を表示する。 以上が、初期設定である。

つぎに、「ストップ」リンクをクリックした時の処理を見る。

020function stopTimer () {
021  clearInterval ( timer ) ;
022  timer = null;
023  timerStatus.textContent = "停止" ;
024  currentX = 50 ;
025}

「ストップ」リンクをクリックすると、stopTimer()関数が実行する。 stopTimer()関数で、図形を停止する処理をする。 つまり、タイマーを止める。 まず21行目でclearInterval()関数を実行して、タイマーを停止する。 22行目でグローバル変数timerにnullを代入する。 変数timerはタイマーのIDを代入する変数である。 タイマーを停止するので、変数timerにnullを代入する。 23行目でグローバル変数timerStatusに「停止」を表示する。 グローバル変数timerStatusは、 タイマーの状態を表示する「pタグのオブジェクト」である。 タイマーを停止するので、「停止」と表示する。 24行目で、 変数currentXに50を代入する。 currentXは図形のx座標を意味する、グローバル変数である。 タイマーを停止したので、座標をデフォルト値に戻したい。 こんかい、x座標のデフォルト値を50にした。 特に意味は無い。図形が左から右へ移動する時、 まず左から50pxの位置に図形が表示して、そこから右方向へ移動する。 解説は以上である。つぎに、startRect()関数とstartCircle()関数を見る。

026function startRect () {
027  if ( timer === null ) {
028    itemType = "rect" ;
029    timer = setInterval ( draw , 500 ) ;
030  }
031}
032function startCircle() {
033  
034  if ( timer === null ) {
035    itemType = "circle" ;
036    timer = setInterval ( draw , 500 ) ;
037  }
038}

「四角」リンクをクリックすると、startRect()関数が実行する。 27行目でグローバル変数timerのnullチェックを行なう。 timerがnullの場合は「タイマーが動いていない」と判断して、 図形を動かす処理をする。 timerに値が入っている場合、「タイマーが動いている」と判断して、 何もしない。タイマーが動いている時に、 「四角」リンクをクリックしても何もしない。 if文の処理を解説する。 28行目でグローバル変数itemTypeに「rect」を代入する。 グローバル変数itemTypeは、動かす図形を設定する変数である。 こんかい、四角リンクをクリックしたので、四角い図形を動かしたい。 そのため、 グローバル変数itemTypeに「rect」と代入する。 この変数は、描画処理で利用する。 29行目でdraw()関数を実行する。このdraw()関数で 描画処理をする。後述する。

つぎに、startCircle()関数を見る。 「丸」リンクをクリックするとstartCircle()関数が実行する。 35行目で、グローバル変数itemTypeに「circle」を設定する。 それ以外は、startRect()関数と同じ処理である。 では、描画処理をするdraw()関数を見る。

039function draw () {
040  
041  if ( timer !== null ) {
042    timerStatus.textContent = "実行中:" + timer;
043    
044    //描画処理
045    context.clearRect ( 0 , 0 , 500 , 150 ) ;
046    
047    //x座標を加算
048    currentX += 20 ;
049    
050    //種別により移動距離を分岐
051    if ( itemType === "rect" ) {
052      
053      context.fillRect ( currentX , 50 , 20 , 20 ) ;
054      
055    } else if ( itemType === "circle" ){
056      
057      context.beginPath();
058      context.arc (
059          currentX , 50 , 20 ,
060          0, Math.PI*2 ) ;
061      context.fill();
062    }
063  }

タイマーを動かして描画処理をする関数である。 解説する。41行目でグローバル変数timerのnullチェックを行なう。 タイマーが動いている場合、描画処理をする。 タイマーが動いていない場合は、描画処理をしない。 if文に入る。 42行目でタイマーの状態を表示するtimerStatusオブジェクトに、 文字列「実行中」を設定する。変数timerを文字列結合するが、 変数timerはタイマーIDが入っている。 特に意味は無い。 45行目でCanvasをクリアにする(Canvasを初期化する)。 Canvasをクリアにしないと、移動前の図形と移動後の図形が表示する事になる。 この部分をコメントアウトして動かすと、ひどい事になる。 48行目で移動処理をする。 図形を表示する場合、x座標にグローバル変数currentXを利用する。 つまり、変数currentXの値を加算すると、 図形は右方向へ移動する。 51行目から最後までが、if文である。 ここが学習のポイントである。 if文である。解説する。 51行目で、グローバル変数itemTypeと文字列「rect」を比較する。 trueの場合、53行目が実行して、四角い図形を表示する。 グローバル変数itemTypeは、今動いている図形が保存されている。 値が「rect」の場合、現在動いている図形は四角なので、 53行目で四角を描画する処理をする。 55行目のelse-if文でグローバル変数itemTypeの値と文字列「circle」を比較する。 グローバル変数itemTypeの値が「circle」の場合、 動いている図形は円なので、円を描画する。 57行目から61行目で円を描画する。 実装はどうでもよろしい。

解説は以上である。これが非オブジェクト指向で書かれたプログラムある。

気になる点は、当然draw()関数のif文である。

プログラミング 拡張性と柔軟性が重要

仕様を満たすプログラムを記述する事は、それほど難しくない。 それよりも、「将来、仕様が変更された時、または 機能を追加する時、容易に対応できるプログラム」を製造する事が、 何より重要である。

プログラミングとif文 if文を疑う

if文がある時、そのif文は正当なものか、常に疑う必要がある。 どのようにif文を疑うのだろうか?

if文を疑う if文をclassで置き換える事はできないのか、疑う

if文をclassで代替できないか検討する。必ず検討する。 if文とclassは全く異なるものだが、 代替できる可能性が高い。 そして、わずかな可能性があれば、必ず代替する。 もう一度今回のプログラムを見る。

051    if ( itemType === "rect" ) {
052      
053      context.fillRect ( currentX , 50 , 20 , 20 ) ;
054      
055    } else if ( itemType === "circle" ){
056      
057      context.beginPath();
058      context.arc (
059          currentX , 50 , 20 ,
060          0, Math.PI*2 ) ;
061      context.fill();
062    }

draw()関数の中で分岐処理をしている。 ここで「拡張性」について考える。 今後Canvasに「四角」や「丸」以外に、 「画像」を描画するかもしれない。 あるいは、様々な図形を追加するかもしれない。 それはありうる事だ。 いま、描画する図形をif文で分岐している。 描画する図形の種類が増えると、このif文が増える。 これは「拡張性に乏しい」と言える。 また、ある図形は動くスピードが速く、 別の画像はスピードが遅い、という機能を追加するかもしれない。 その場合は、このプログラムでは(エレガントに)対応できない。

もう少しイメージを持ってもらうため、会話形式にする。

四角と丸だけでは、つまらない。画像を動かすように機能を追加してほしい。

(画像を追加するには、if文で分岐処理を増やすのか。 大変だな)無理です。

えっ?画像を追加するだけだよ?簡単だよね?

(画像を追加するには、if文で分岐処理を増やすのか。 他にも変更するプログラムがあるかもしれない。 プログラム全体を見直す必要があるな。)無理です。

(だめだ・・・)

こうなる。プログラムを知らない人は、 「画像を追加するだけ」という感覚である。 しかし、良いプログラムを書いていないと、 プログラム全体を見直す必要がある。

ここではっきり認識してほしい事は、 「画像を追加するだけ」という感覚がオブジェクト指向的な 考え方である。

仕様を満たすためにプログラムを書くのではなく、 プログラムの機能を「モノ」のように見立てて、 その「モノ」を組み上げていく。

では、物を製造する。

オブジェクト指向とクラス設計

まず、「モノ」を探す。

クラス名
四角 Item
ItemCircle

Canvasに描画するものを「モノ」とみなす。 現在、四角と丸の「モノ」がある。 今後「モノ」は増えるかもしれない。 例えば画像を追加する場合は、 ItemImageクラスを作るのが良いだろう。 このようにプログラムの仕様の中から「モノ」を見つける (色々異論反論はあるだろうが、学習用の解説をしている)。

では、「モノ」に機能を付ける。

関数 解説 実装
move() 移動する x座標を加算する
draw() 描画処理 context.fillRect(x,y,size,size) ;

四角図形の機能はmove()とdraw()とした。 move()関数を実行すると、描画する座標xを加算する。 これで、右方向へ移動する。 Itemクラスに対してdraw()関数を実行すると、描画処理を実行する。 Itemクラスは通常抽象クラスとして製造するが、 学習用として、このように実装する。

では、完成品を解説する。 sample.htmlから見る。

001<!DOCTYPE html><head>
002<script type="text/javascript" src="clz.js" >
003</script>
004<script type="text/javascript" src="logic.js" >
005</script>
006</head>
007<body>
008
009<!-- コントロール -->
010<div>
011  <a href="javascript:startRect()">四角</a>
012  <a href="javascript:startCircle()">丸</a>
013  <a href="javascript:stopTimer()">ストップ</a>
014</div>
015
016<!-- Canvas -->
017<canvas id="iori-draw-canvas" width="500px" height="150px"
018 style="background:#aaa">
019</canvas>
020
021<!-- タイマーのステータス -->
022<p id="iori-p-status"></p>
023
024</body>
025</html>

2行目でscriptタグを利用して、clz.jsファイルを読み込む。 4行目でlogic.jsファイルを読み込む。 他の部分は、前回と同じである。

clz.jsを見る。

001'use strict'
002const Item = function ( x , y , size ){
003  this.x = x ;
004  this.y = y ;
005  this.size = size ;
006}
007//移動
008Item.prototype.move = function () {
009  this.x += 20 ;
010}
011//描画
012Item.prototype.draw = function ( context ) {
013  context.fillRect(
014    this.x ,this.y ,this.size ,this.size) ;
015}
016//ItemCircleを作成
017var ItemCircle = function ( x , y , size ){
018  Item.call ( this , x , y ,size ) ;
019}
020//プロトタイプチェーンを構築
021Object.setPrototypeOf (
022    ItemCircle.prototype ,
023    Item.prototype ) ;
024    
025//オーバーライド
026ItemCircle.prototype.draw = function ( context ) {
027  context.beginPath();
028  context.arc ( this.x , this.y ,
029      this.size, 0, Math.PI*2 ) ;
030  context.fill();
031}

ItemクラスとItemCircleクラスを定義したファイルである。 ItemクラスとItemCircleクラスはプロトタイプチェーンで 繋がっている。つまり、Itemクラスの関数はItemCircleも持っている事になる。 Itemクラスから解説する。

002const Item = function ( x , y , size ){
003  this.x = x ;
004  this.y = y ;
005  this.size = size ;
006}
007//移動
008Item.prototype.move = function () {
009  this.x += 20 ;
010}
011//描画
012Item.prototype.draw = function ( context ) {
013  context.fillRect(
014    this.x ,this.y ,this.size ,this.size) ;
015}

Itemコンストラクタ関数である。 2行目の引数にx、y、sizeを受け取り、それぞれオブジェクトのプロパティに代入する。 x、y、sizeは「描画するx座標」「描画するy座標」「図形のサイズ」を意味する。

8行目でmove()関数(メソッド)を定義する。 タイマーが繰り返すごとに、move()関数を実行する。 9行目で、x座標を加算する。これで、右方向へ移動する。 12行目でdraw()関数(メソッド)を定義する。 引数にContextオブジェクトを受け取る。 13行目で、四角を描画する。 解説は以上である。実際は、Itemクラスは抽象クラスで定義する。

016//ItemCircleを作成
017var ItemCircle = function ( x , y , size ){
018  Item.call ( this , x , y ,size ) ;
019}
020//プロトタイプチェーンを構築
021Object.setPrototypeOf (
022    ItemCircle.prototype ,
023    Item.prototype ) ;
024    
025//オーバーライド
026ItemCircle.prototype.draw = function ( context ) {
027  context.beginPath();
028  context.arc ( this.x , this.y ,
029      this.size, 0, Math.PI*2 ) ;
030  context.fill();
031}

17行目でItemCircleコンストラクタ関数を定義する。 ItemCircleコンストラクタ関数は円の図形を表現するクラスである。 17行目の引数で、x座標、y座標、図形のサイズを受け取り、 18行目で生成したオブジェクトに対して、Item関数を実行する。 Item関数を実行すると生成するオブジェクトのプロパティに値が入る。 21行目でItemコンストラクタ関数とItemCircleコンストラクタ関数に プロトタイプチェーンを構築する。 26行目でdraw()関数(メソッド)を定義する。 いわゆるオーバーライドの形になる。 ItemCircleコンストラクタ関数は円を表現する。 draw()関数の中で円を描く処理をする。 27行目から30行目で円を描画する。 実装方法はどうでも宜しい。

解説は以上である。

では確認する。ItemCircleコンストラクタ関数から 製造したオブジェクトをItemCircleオブジェクトとする。 ItemCircleオブジェクトに対してdraw()関数を実行すると、 26行目が実行する。 一方、ItemCircleオブジェクトに対して、move()関数を実行すると、 ItemCircleオブジェクトはmove()関数を持っていないので、 Item.prototype.move()関数が実行する。 これが、プロトタイプチェーンである。

では、この2つのクラスをどのように利用するのだろうか? logic.jsファイルを見る。

001let context;
002let item = null;
003let timerStatus = "" ;
004let timer = null ;
005
006window.onload = function() {
007  //Canvasオブジェクトを取得
008  const canvas = tag ( "iori-draw-canvas" );
009  
010  //Contextオブジェクトを取得
011  context = canvas.getContext ( "2d" );
012  
013  //タイマーの状態を表示するpタグ
014  timerStatus = tag ( "iori-p-status" );
015}
016function tag ( id ) {
017  return document.getElementById (id);
018}
019function stopTimer () {
020  clearInterval ( timer ) ;
021  timer = null;
022  timerStatus.textContent = "停止" ;
023}
024function startRect () {
025  if ( timer === null) {
026    item = new Item ( 50 , 50 , 20 ) ;
027    timer = setInterval ( draw , 500 ) ;
028  }
029}
030function startCircle() {
031  
032  if ( timer === null) {
033    item = new ItemCircle ( 50 , 50 , 20 ) ;
034    timer = setInterval ( draw , 500 ) ;
035  }
036}
037function draw ( ) {
038  
039  if ( timer !== null) {
040    timerStatus.textContent = "実行中"+timer;
041    
042    context.clearRect ( 0 , 0 , 500 , 150 ) ;
043    
044    //移動
045    item.move ();
046    
047    //描画
048    item.draw ( context );
049  }
050}

学習用としては悪くないプログラムである。グローバル変数から解説する。

001let context;
002let item = null;
003let timerStatus = "" ;
004let timer = null ;

グローバル変数を解説する。1行目でcontext変数を宣言する。 context変数に、Canvasから取得したContextオブジェクトを代入する。 2行目が新しい。item変数を宣言する。 item変数に「移動中のクラス(Itemコンストラクタ関数または ItemCircleコンストラクタ関数のオブジェクト)」を代入する。 非オブジェクト指向で作ったプログラムでは、 itemType変数を宣言して、そこに「rect」または「circle」という 文字列(フラグ)を代入して、どの図形を描画するかコントロールしていた。 こんかいは、コンストラクタ関数から製造したオブジェクトを利用する。 3行目で変数timerStatusを宣言する。 timerStatusには、pタグのオブジェクトを代入する。 4行目で変数timerを宣言する。タイマーのIDを代入して、 「ストップ」リンクがクリックされてタイマーをとめる時に利用する。 つぎに、関数を見る。

006window.onload = function() {
007  //Canvasオブジェクトを取得
008  const canvas = tag ( "iori-draw-canvas" );
009  
010  //Contextオブジェクトを取得
011  context = canvas.getContext ( "2d" );
012  
013  //タイマーの状態を表示するpタグ
014  timerStatus = tag ( "iori-p-status" );
015}
016function tag ( id ) {
017  return document.getElementById (id);
018}

ブラウザの読み込み完了後に実行する関数である。 前回と同じである。 8行目でCanvasオブジェクトを取得して、 11行目でContextオブジェクトを取得する。 Contextオブジェクトはグローバル変数contextに代入する。 14行目でpタグのオブジェクトを取得して、 グローバル変数timerStatusへ代入する。 解説は以上である。つぎに、 コントロールの関数を見る。

019function stopTimer () {
020  clearInterval ( timer ) ;
021  timer = null;
022  timerStatus.textContent = "停止" ;
023}
024function startRect () {
025  if ( timer === null) {
026    item = new Item ( 50 , 50 , 20 ) ;
027    timer = setInterval ( draw , 500 ) ;
028  }
029}
030function startCircle() {
031  
032  if ( timer === null) {
033    item = new ItemCircle ( 50 , 50 , 20 ) ;
034    timer = setInterval ( draw , 500 ) ;
035  }
036}

ブラウザ上で「ストップ」リンクをクリックすると、stopTimer()関数が 実行する。20行目でタイマーを停止して、 21行目でタイマーIDをnullにする。 22行目で、タイマーのステータスを表示するpタグに、 文字列「停止」を表示する。解説は以上である。

ブラウザ上で「四角」リンクをクリックすると、startRect()関数が 実行する。 25行目でグローバル変数timerのnullチェックをする。 timerの値が入っている場合、タイマーは動いているので何もしない。 timerの値がnullの場合、タイマーは動いていないので、 円を動かす。 26行目でItemコンストラクタ関数をnewして、 生成したオブジェクトをグローバル変数itemへ代入する。 グローバル変数itemは 「動いている(動かす)図形」を表現する。 こんかい、四角を動かすので、グローバル変数itemに Itemコンストラクタ関数のオブジェクトを代入する。 27行目でタイマーを実行する。

ブラウザ上で「丸」リンクをクリックすると、startCircle()関数が 実行する。 32行目でグローバル変数timerのnullチェックを行なう。 nullで無い場合、if文に入る。 33行目でItemCircleコンストラクタ関数のオブジェクトを生成し、 グローバル変数itemへ代入する。 グローバル変数itemは 「動いている図形」である。ここに、ItemCircleオブジェクトを代入する。 34行目でタイマーを動かす。 解説は以上である。つぎに、draw()関数を見る。

037function draw ( ) {
038  
039  if ( timer !== null) {
040    timerStatus.textContent = "実行中"+timer;
041    
042    context.clearRect ( 0 , 0 , 500 , 150 ) ;
043    
044    //移動
045    item.move ();
046    
047    //描画
048    item.draw ( context );
049  }
050}

シンプルで宜しい。draw()関数はタイマーが実行するごとに 呼ばれる関数である。 39行目でタイマーが動いているか確認する。 タイマーが動いている場合は、if文に入る。 40行目でpタグに文字列「実行中」を表示する。 タイマーは動いているので「実行中」と表示する。 42行目でCanvasをクリアにする。 42行目が無いと、移動前の図形が描画されたまま、 移動後の図形が描画される。 45行目でグローバル変数itemが参照するオブジェクトに対して、 move()関数を実行する。 グローバル変数itemが参照するオブジェクトは、 ItemオブジェクトかItemCircleオブジェクトのどちらかである。 変数itemがどちらを参照しているか分からないが、 どちらのオブジェクトもmove()関数を持っている (ItemCircleオブジェクトはプロトタイプチェーンを辿り、 move()関数を実行できる)。 そのため、このプログラムは問題なく実行する。 clz.jsファイルを見る。

007//移動
008Item.prototype.move = function () {
009  this.x += 20 ;
010}

clz.jsファイルの、Itemコンストラクタ関数のmove()関数である。 ItemCircleオブジェクトもプロトタイプチェーンを辿り、 move()関数を実行できる。 logic.jsのdraw()関数の中でmove()関数を実行すると、 この関数が実行する。オブジェクトのxプロパティに 20を加算する。xプロパティは、図形のx座標を意味する。 このプログラムが実行すると、図形が右方向へ移動する事になる。 logic.jsへ戻る。

037function draw ( ) {
038  
039  if ( timer !== null) {
040    timerStatus.textContent = "実行中"+timer;
041    
042    context.clearRect ( 0 , 0 , 500 , 150 ) ;
043    
044    //移動
045    item.move ();
046    
047    //描画
048    item.draw ( context );
049  }
050}

45行目でグローバル変数itemが参照するオブジェクトに対して、 move()関数を実行した後、48行目でdraw()関数を実行する。 draw()関数の引数にcontextを渡す。 グローバル変数itemは、 Itemオブジェクトまたは、ItemCircleオブジェクトのどちらかを 参照する。まず、Itemコンストラクタ関数のdraw()関数を見る。

011//描画
012Item.prototype.draw = function ( context ) {
013  context.fillRect(
014    this.x ,this.y ,this.size ,this.size) ;
015}

Itemオブジェクトのdraw()関数を実行すると、 13行目で四角形を描画する。問題ない。 つぎに、ItemCircleコンストラクタ関数を見る。

025//オーバーライド
026ItemCircle.prototype.draw = function ( context ) {
027  context.beginPath();
028  context.arc ( this.x , this.y ,
029      this.size, 0, Math.PI*2 ) ;
030  context.fill();
031}

logic.jsファイルのグローバル変数itemが ItemCircleオブジェクトを参照する時、 draw()関数を実行すると、26行目が動く。 26行目のdraw()関数が動くと、 問題なく、引数のContextオブジェクトに円を描画する。 解説は以上である。

オブジェクト指向プログラムと非オブジェクト指向プログラムの比較

プログラムの解説は終了した。 オブジェクト指向プログラムでは、if文が取り除かれていた。 なぜ、if文が消えたのだろうか? もう一度logic.jsのdraw()関数を比較する。

047    //x座標を加算
048    currentX += 20 ;
049    
050    //種別により移動距離を分岐
051    if ( itemType === "rect" ) {
052      
053      context.fillRect ( currentX , 50 , 20 , 20 ) ;
054      
055    } else if ( itemType === "circle" ){
056      
057      context.beginPath();
058      context.arc (
059          currentX , 50 , 20 ,
060          0, Math.PI*2 ) ;
061      context.fill();
062    }

こちらが非オブジェクト指向のプログラムである。 51行目のif文で、移動中の図形により、処理を分岐する。 図形の種類が増えると、ここのプログラムは肥大化する。

042    context.clearRect ( 0 , 0 , 500 , 150 ) ;
043    
044    //移動
045    item.move ();
046    
047    //描画
048    item.draw ( context );

オブジェクト指向のプログラムである。 グローバル変数itemが参照するオブジェクトに対して、 move()関数とdraw()関数を実行する。 if文がない。if文がない理由を解説する。 非オブジェクト指向プログラムでは、 「移動中の図形の種類」により処理を分岐した。 一方オブジェクト指向プログラムでは、 「移動中の図形の種類」は分からないが、 とにかくmove()関数とdraw()関数を実行する。 「四角ならこの処理」「丸ならこの処理」という 分岐処理は、オブジェクト指向プログラムでは記述していない。

解説は以上である。

オブジェクト指向プログラムの責任分担と拡張性と柔軟性

オブジェクト指向の責任分担と拡張性を解説する。

四角と丸だけでは、つまらない。画像を動かすように機能を追加してほしい。

(画像を追加するには、画像クラスを作成して、 グローバル変数itemに画像オブジェクトを代入すれば、 自動的に画像が動くだろう。)簡単です。

よろしく!!(さすがだ)

こうなる。オブジェクト指向プログラムでは、 それぞれのプログラムの責任が明確になるので、 新たな機能を追加する時、 全てのプログラムを見直す必要は無い。

画像を移動させるには、まず画像コンストラクタ関数を作成して、 Itemコンストラクタ関数とプロトタイプチェーンを構築する。 そして、draw()関数の中に画像を描画する処理を記述する。 それだけである。 あとは、HTMLファイルにstartImage()関数を呼び出すリンクを作って、 startImage()関数の中でグローバル変数itemに 画像コンストラクタ関数のオブジェクトを代入する。 それだけである。

つぎに柔軟性を解説する。

丸の図形は四角よりも速く移動してほしいな。

(ItemCircleコンストラクタ関数に move()関数を追加して、 xに大きな数字を加算しよう)簡単です。

よろしく!!(さすがだ)

こうなる。丸の図形の移動速度を速める場合、 既存のプログラムを少しだけ変更する。 まず、既存のプログラムを見る。

007//移動
008Item.prototype.move = function () {
009  this.x += 20 ;
010}
011//描画
012Item.prototype.draw = function ( context ) {
013  context.fillRect(
014    this.x ,this.y ,this.size ,this.size) ;
015}
016//ItemCircleを作成
017var ItemCircle = function ( x , y , size ){
018  Item.call ( this , x , y ,size ) ;
019}
020//プロトタイプチェーンを構築
021Object.setPrototypeOf (
022    ItemCircle.prototype ,
023    Item.prototype ) ;

「丸の図形」を変更するので、ItemCircleコンストラクタ関数を見る。 ItemCircleコンストラクタ関数にmove()関数は無い。 Itemコンストラクタ関数にはmove()関数がある。 しかし、このmove()関数よりも速く動くようにプログラムを変更したい。 ItemCircleコンストラクタ関数にmove()関数を追加する。

001'use strict'
002const Item = function ( x , y , size ){
003  this.x = x ;
004  this.y = y ;
005  this.size = size ;
006}
007//移動
008Item.prototype.move = function () {
009  this.x += 20 ;
010}
011//描画
012Item.prototype.draw = function ( context ) {
013  context.fillRect(
014    this.x ,this.y ,this.size ,this.size) ;
015}
016//ItemCircleを作成
017var ItemCircle = function ( x , y , size ){
018  Item.call ( this , x , y ,size ) ;
019}
020//プロトタイプチェーンを構築
021Object.setPrototypeOf (
022    ItemCircle.prototype ,
023    Item.prototype ) ;
024    
025//オーバーライド
026ItemCircle.prototype.draw = function ( context ) {
027  context.beginPath();
028  context.arc ( this.x , this.y ,
029      this.size, 0, Math.PI*2 ) ;
030  context.fill();
031}
032ItemCircle.prototype.move = function () {
033  this.x += 50 ;
034}

32行目にmove()関数を追加した。これだけである。 ItemCircleオブジェクトのmove()関数を実行すると、 xプロパティの値に50を加算する。 四角を表現するItemコンストラクタ関数のmove()関数は、 10行目で定義されている。 こちらはxプロパティの値に20を加算する。 つまり、ItemオブジェクトよりもItemCircleオブジェクトの方が、 移動距離が大きくなる。つまりは移動速度が速くなる。

オブジェクト指向プログラムは 拡張性、柔軟性に優れていると言わざるを得ない。

まとめ

プログラミング 拡張性と柔軟性が重要

プログラミングとif文 if文を疑う

if文を疑う if文をclassで置き換える事はできないのか、疑う