GASの「起動時間の最大値を超えました」の壁を超えてみる!!!

 

新玉ねぎっておいしいですよね。甘くって。
ただ、生で食べる時は、まあまあ薄くきらないと苦くって悲しくなりました。

今回は、みんな大好きGASの
「最大起動時間の壁を超える!」
の巻です。

 

GASの起動時間の最大値は?

リファレンスより、

「Consumer(多分無課金の人)のScript runtime」は 6minのようですね。

 

つまり、

一回の処理が6分を超えると、
「起動時間の最大値を超えました」
と表示され処理が途中で終わってしまいます。
(そもそも、6分以上の処理させんなよって感じなんですが、、、)

ググったら、回避方法を書いてくれてる方がおり、とても助かりました。
おかげで、わたしのバカ長い処理も何回かに分けて、
最後まで実行完了することができるようになりました。

参考:http://direction-note.com/gas-beyond-execution-time/

やり方

処理が5分を超える場合は、
トリガーを設定して、1分後に処理を続きからするようにする。

 

①for文のカウントなどを保持し、正しく続きから処理を再開するために、
GASのスクリプトプロパティでデータを保持させます。

スクリプトのプロパティとは?

ファイル > プロジェクトのプロパティ > スクリプトのプロパティ
にあり、キーバリューでデータを保持できます。

スクリプトのプロパティ

↑ キー:nowRow、バリュー:10のデータを保持させている。
この画面の「行を追加」ボタンよりデータを追加/削除することが可能。

 

GASにて追加することも可能

var properties = PropertiesService.getScriptProperties();

//スクリプトプロパティの登録
properties.setProperty([key], [value]);

//スクリプトプロパティの取得
var [value] = properties.getProperty([key]);

 

 

 

②最大処理時間を超える前の5分で処理を中断し、
1分後にトリガーを設定する。

var properties = PropertiesService.getScriptProperties();

/* メイン */
function mainSort() {  
  ...
  setOutputSheet(taskMaster, dataMaster, DayDate);
}

/* 5分以上かかる可能性のある処理 */
function setOutputSheet(taskMaster, dataMaster, DayDate){
  var startTime = new Date();
  //トリガーIDを保存するときに使用するkey
  var triggerKey = "trigger";  
  ...

  //開始時刻(startTime)と現時点の処理時点の時間を比較する 
  var diff = parseInt((new Date() - startTime) / (1000 * 60));
  if(diff >= 5){
  ...
    //トリガー(1分後)を登録する
    setTrigger(triggerKey, "mainSort");
    return;
  }
  
  //全て実行終えたらトリガー不要なためを削除する
  deleteTrigger(triggerKey);
  ... 
}


/**
 *指定したkeyに保存されているトリガーIDを使って、トリガーを削除する
 *
 * @param string triggerKey
 * @return void
 */
function deleteTrigger(triggerKey) {
  var triggerId = PropertiesService.getScriptProperties().getProperty(triggerKey);
  if(!triggerId) return;
  
  ScriptApp.getProjectTriggers().filter(function(trigger){
    return trigger.getUniqueId() == triggerId;
  })
  .forEach(function(trigger) {
      ScriptApp.deleteTrigger(trigger);
  });
  PropertiesService.getScriptProperties().deleteProperty(triggerKey);
}

/**
 * トリガーを発行する
 *
 * @param string triggerKey
 * @param string funcName
 * @return void
 */
function setTrigger(triggerKey, funcName){
  //既に同名で保存しているトリガーがあったら削除
  deleteTrigger(triggerKey);
  
  //1分後にトリガーを登録する
  var date = new Date();
  date.setMinutes(date.getMinutes() + 1);
  var triggerId = ScriptApp.newTrigger(funcName).timeBased().at(date).create().getUniqueId();
  Logger.log('setTrigger function_name "%s".', funcName);
  
  //あとでトリガーを削除するために「スクリプトのプロパティ」にトリガーIDを保存しておく
  PropertiesService.getScriptProperties().setProperty(triggerKey, triggerId);
}

 

作ってみたやつ

①②を組み合わせると、最大起動時間の壁を超えれるということです!
作ってみた全体はこんな感じです。
※このコード自体の処理は5分超えなかったのでsleep()とかして試してましたっw

 

こんな感じの横並びを、、、

縦にソートします。

 

プレーヤーが日々追加されることを想定して、
一年の日数 × 最終的なプレーヤー人数分で縦に並ぶように作ってます。

 

コード全体(テキトーなので参考程度で! )

//★メイン
function mainSort() {
  
  //プレーヤーマスター配列を作成
  var playerMaster = getPlayerMaster();
  var playerCount = Object.keys(playerMaster).length
  
  //日付の配列を取得
  var dataMaster = getDataMaster();
  var dataCount = Object.keys(dataMaster).length
  
  //日別結果データの取得
  var DayDate = getDayDate(playerCount, dataCount);
  
  //出力用シート(outputSheet)に日別結果の縦版の値を出力する。
  setOutputSheet(playerMaster, dataMaster, DayDate);
}

/**
 * 「No」「プレーヤー」を配列で取得する
 * 
 * @return arrShopMaster = {
 *   1: 'リュウ',
 *   2: 'ケン'
 * };
 */
function getPlayerMaster() {
  var ABcolumnAll = inputSheet.getRange('A2:B101').getValues();
  var LastRow = ABcolumnAll.filter(String).length;
  var arrPlayerMaster = [];
  
  //「-」を除く
  for(var i = 0; i < LastRow; i++){
    if(!ABcolumnAll[i][1].match(/\-/)){
      arrPlayerMaster[ABcolumnAll[i][0]] = ABcolumnAll[i][1]; 
    }  
  }

  return arrPlayerMaster;
}

/**
 * 日付を配列で取得する
 * 
 * @return array arrDataMaster
 */
function getDataMaster() {
  var rowAll = inputSheet.getRange('C1:ND1').getValues();
  var arrDataMaster = [];
  
  for(var i = 0; i < yearSum; i++){
    arrDataMaster[i] = rowAll[0][i];  
  }

  return arrDataMaster;
}

/**
 * 日付をキーにその日の結果を格納する。
 *
 * @param intger playerCount
 * @param intger dataCount
 * @return array arrRet
 */
function getDayDate(playerCount, dataCount)
{
  var col = inputSheet.getRange('C2:NQ'+(playerCount+1)).getValues();

  var arrRet = [];
  for(var i = 0; i < playerCount; i++){
    arrRet[i] = [];
    for(var j = 0; j < dataCount; j++){
      arrRet[i][j] = col[i][j];
    }
  }
  return arrRet;
}

/**
 * 5分以上かかる可能性のある処理
 *
 * @return void
 */
function setOutputSheet(playerMaster, dataMaster, DayDate){
  //処理の開始時刻を取得
  var startTime = new Date();
  //sleep(300000);
  
  //途中経過保存用の変数
  var properties = PropertiesService.getScriptProperties();
  var rowKey = "nowRow";   //何行目まで処理したかを保存するときに使用するkey
  var dateKey = "nowDate";  //yearSumが何行目まで処理したかを保存するkey
  var playerCountKey = "nowPlayerCount"; //lastRowが何行目まで処理したかを保存するkey
  var triggerKey = "trigger";  //トリガーIDを保存するときに使用するkey
  
  var nowRow = parseInt(properties.getProperty(rowKey));
  if(!nowRow) nowRow = 1;  //初回処理の時
  
  var lastRow = Object.keys(DayDate).length;

  var now_i = 0;
  var nowDate = parseInt(properties.getProperty(dateKey));
  if(nowDate){ 
    var now_i = nowDate;
    properties.deleteProperty(dateKey);
  }
  for(var i = now_i; i < yearSum; i++){
    var date = dataMaster[i];
    
    var now_j = 0;
    var nowPlayerCount = parseInt(properties.getProperty(playerCountKey));
    if(nowPlayerCount){ 
      var now_j = nowPlayerCount;
      properties.deleteProperty(playerCountKey);
    }
    for(var j = now_j; j < lastRow; j++) { 
        //開始時刻(startTime)と現時点の処理時点の時間を比較する
        var diff = parseInt((new Date() - startTime) / (1000 * 60));     
        if(diff >= 5){
        //何行まで処理したかなどを「スクリプトのプリロパティ」に保存する
        properties.setProperty(rowKey, nowRow);
        properties.setProperty(dateKey, i);
        properties.setProperty(playerCountKey, j);
        //トリガー(1分後)を登録する
        setTrigger(triggerKey, "mainSort");
        return;
      }

      outputSheet.getRange(nowRow, 1).setValue(date);
      outputSheet.getRange(nowRow, 2).setValue(playerMaster[j+1]);
      outputSheet.getRange(nowRow, 3).setValue(DayDate[j][i]);
      nowRow++;
    }
  }
  
  //全て実行終えたらトリガーや何行目まで実行したかなどのデータは
  //不要なためを削除する
  deleteTrigger(triggerKey);
  properties.deleteProperty(rowKey);
  properties.deleteProperty(dateKey);
  properties.deleteProperty(playerCountKey);
}


/**
 * 指定したkeyに保存されているトリガーIDを使って、トリガーを削除する
 *
 * @param string triggerKey
 * @return void
 */
function deleteTrigger(triggerKey) {
  var triggerId = PropertiesService.getScriptProperties().getProperty(triggerKey);
  if(!triggerId) return;
  
  ScriptApp.getProjectTriggers().filter(function(trigger){
    return trigger.getUniqueId() == triggerId;
  })
  .forEach(function(trigger) {
      ScriptApp.deleteTrigger(trigger);
  });
  PropertiesService.getScriptProperties().deleteProperty(triggerKey);
}

/**
 * トリガーを発行。トリガーを発行した箇所から
 *
 * @param string triggerKey
 * @param string funcName
 * @return void
 */
function setTrigger(triggerKey, funcName){
  //既に同じ名前で保存しているトリガーがあったら削除
  deleteTrigger(triggerKey);
  
  var date = new Date();
  date.setMinutes(date.getMinutes() + 1); //1分後に再実行
  
  var triggerId = ScriptApp.newTrigger(funcName).timeBased().at(date).create().getUniqueId();
  Logger.log('setTrigger function_name "%s".', funcName);
  
  //あとでトリガーを削除するために「スクリプトのプロパティ」にトリガーIDを保存しておく
  PropertiesService.getScriptProperties().setProperty(triggerKey, triggerId);
}

 

 

Facadeパターン

 

どうも!四年ぶりくらいに風邪ひいて病院いきました。
病院の看護婦さんが母のように優しくてホッとしました。

 

\今回はFacadeパターンです!/

 

前回の最小知識の原則~PrincipleOfLeastKnowledge

でも出しましたが、Facadeパターンについてまとめます。

 

Facadeパターンとは…

サブシステムの一連のインターフェースに対する、
統合されたインターフェースを提供する。
ファサードは、サブシステムをより使いやすくする
高水準インターフェースを定義する。

 

クラス図

・Facade   …Subsystemをより使いやすく統合したインターフェース

・Client     …ファサードのおかげで作業が簡単になった幸福なクライアン

・Subsystem   …より複雑なサブシステムたち

 

具体的な例

コードはこちら!

 

いろんな電気機器(アンプ/DVDプレーヤー/ポップコーンマシーン)の一連オペレーションを統括したインターフェースをもつ、ホームシアター[Facade]を実装します。

 

・HomeTheaterFacade.php[Facade]
…Subsystemの中で、ホームシアターに必要な
一連のオペレーションを統一したインターフェース

・main.php[Client]
…ホームシアターを使うクライアント

・Subsystem/[Subsystem]
…アンプやDVDプレーヤ、ポップコーンマシーン、
スクリーン、ライトなどの電気機器クラス

 

映画を見るための電気機器の一連のオペレーションを
Facadeパターンで作って、実行した例です。

main.php

$homeTheater = new HomeTheaterFacade(
    $amp,
    $tuner,
    $dvd,
    $cd,
    $projector,
    $screen,
    $light,
    $pop
);
$homeTheater->watchMovie('アナ雪');
$ php src/Facade/main.php
▶︎ 映画を見る準備をします!
ボップコーンマシーンをONにします
ボップコーンを作ります
シアターライトを10にします
スクリーンを下げてセットします
プロジェクターをONにします
プロジェクターをワイドスクリーンモードにします
アンプをONにします
アンプにDVDプレーヤをセットします
アンプにサラウンドサウンドをセットします
アンプにボリュームを5にします
DVDプレーヤーをONにします
DVD「アナ雪」をスタートします

 

Facadeにいろいろ統括系は任せてるので…

Subsystem側は自分の役目に専念できる。
Client側に使いやすいインターフェースをFacadeが提供できるので、
Clientも使いやすい!

 

依存性の高いコードになってしまうこともある!

最小知識の原則~PrincipleOfLeastKnowledgeにも書いたように、
複数のオブジェクトのメソッドを呼び出すことが多くなるので、
ある程度の決められた範囲内のメソッドのみを呼び出さないと、
依存性の高いコードになってしまいます。

なので、以下に気をつけて実装するとマルっ!
1. そのオブジェクト自身のメソッド
2. メソッドの引数として渡されたオブジェクトのメソッド
3. メソッドが作成またはインスタンス化したオブジェクトのメソッド
4. そのオブジェクトのコンポーネント(インスタンス変数で参照されるオブジェクト)[HAS-A関係]のメソッド

 

最小知識の原則~PrincipleOfLeastKnowledge~

「最小知識の原則」についての説明は、
Facadeパターンの章で出てきました。

Facadeパターンは、
サブシステムの一連のインターフェースに対する
統合されたインターフェースを提供するものです。

したがって、Facadeパターンは
複数のオブジェクトのメソッドを呼び出すことが想定されます。
しかし、ある程度の決められた範囲内のメソッドのみを呼び出さないと、
依存性の高いコードになってしまいます。
それを防ぐための原則が
「最小知識の原則」となります。

 

最小知識の原則

「直接の友達とだけ話すこと」
オブジェクト間のやりとりを最小限にするべき。

→多数のクラスが互いに結合し、
システムのある部分に対する変更が他部分に
連鎖してしまうような設計を回避する。

※デメテルの法則 (Law of Demeter, LoD) ともいう。

 

 

以下に属するメソッドだけを呼び出すべき

1. そのオブジェクト自身のメソッド
2. メソッドの引数として渡されたオブジェクトのメソッド
3. メソッドが作成またはインスタンス化したオブジェクトのメソッド
4. そのオブジェクトのコンポーネント(インスタンス変数で参照されるオブジェクト)[HAS-A関係]のメソッド

 

<?php

class Car
{
    //4.このクラスのコンポーネント(HAS-A)の
    //メソッドである、$engine.start()はOK!
    public $engin;

    public function __construct(){}

    //2.メソッドの引数として渡されたオブジェクト$keyの
    //メソッドである$key.turns()はOK!
    public function start(Key $key): void
    {
             //3.メソッドが作成したオブジェクト$doorsの
        //メソッドである$door.lock()はOK
        $doors = new Doors();  
        
        $authorized = $key.turns();
        
        if($authorized){
            $engine.start();
             
               //1.オブジェクト自身のメソッド
                    //である、updateDashboardDisplay()はOK!
            this->updateDashboardDisplay(); 
       
            $door.lock();
        }

    }

    public function updateDashboardDisplay()
    {
        //
    }

}

デメリット

この原則を意識しすぎると、ラッパークラスの記述が多くなり、
複雑になってしまうこともある。

なので、適当に活用する!

Adapterパターン

 

今年から、花粉症になったみたいです・・・。
鼻がムズムズして頭も痛くなるし大変ですね、なめてました。

 

 

今回は、
Adapterパターン
について書きます!

 

Adapterパターン

クラスのインターフェースを、
クライントの期待する別のインターフェースに変換する。

アダプタは互換性がないインターフェースのために、
そのままでは連携ができないクラス(Adaptee)を
(Clientと)連携できるようにさせます。

ラップするインターフェースを変換します。

 

 

▶︎ Adapter
Targetインターフェースを実装する。

 

▶︎ Target <<interface>>
ClientはこのTargetインターフェースを使っている感覚。
実際には、Adapterを通して、Adapteeを使っているが、
Adapteeを使っている認識は特にない。

 

▶︎ Adaptee
全てのリクエストはAdapeeに委譲される。
Adapteeの任意のサブクラスにもAdapterは適用できる
(コンポジションのため)

 

▶︎ Client
①Targetインターフェースを使って、
Adapterのメソッドを呼び出し、
Adapterにリクエストをする。
②AdapterがAdapteeインターフェースを使って
Adapteeのメソッドに変換して呼び出す
③Clientはリクエスト結果を受け取るが、
Targetインターフェースに沿っただけなので
変換をしてくれたAdapterの存在は知らない。

 

PHPで書いたみたサンプルコード

 

オブジェクトアダプタ/クラスアダプタ

実は、Adapterパターンには、
– オブジェクトアダプタ
– クラスアダプタ
の2つがある。

 

オブジェクトアダプタ

コンポジションを使うので、
Adapteeとそのサブ(具象)クラスにも適用可能、多様性あり。

 

 

クラスアダプタ

(多重)継承を使うので、
1つの特定のアダプティに特化したアダプタが実装可能。

オーバライドができる。

 

 

 

いいところ

・Adapterパターンを使わずに、互換性対応のために、
Client側のインターフェースに対する全ての実装を変更する場合、
多くのコード変更やそれによる影響調査などコストが高い

(例)TurkeyインターフェースをDockインターフェースへの互換性対応のために、
Turkeyインターフェースを変更する場合

– Turket.php

interface Turkey
{
-    public function gobble(): void;
+    public function quack(): void;
     public function fly(): void;
}

 

– WildTurkey.php(Turkeyインターフェースを実装する具象クラス)
他にも具象クラスがあれば、
全ての具象クラスのgobbleメソッドの修正が必要となる。

class WildTurkey implements Turkey
{
-    public function gobble(): void
+    public function quack(): void
     {
         echo "ゴロゴロ\n";
     }
 }

 


↓ Adapterを使うと…

1つのクラス内にすべての変更をカプセル化したクラスを提供できる。
Client/Adaptee/TargetInterfaceのコード変更は不要。

 

 

双方向アダプタを作ることも可能

実際に作ってみた!しかし、テキトーすぎて見せたくない。。。

時間あったらちゃんとさせます。

 

– DockAndTurkeyAdapter.php

class DockAndTurkeyAdapter implements DockAndTurkey
{
    /* @var Turkey $turkey */
    public $turkey;

    /* @var Dock $dock */
    public $dock;

    public function __construct($turkeyOrdock)
    {
        if($turkeyOrdock instanceof Turkey)  $this->turkey = $turkeyOrdock;
        if($turkeyOrdock instanceof Dock)  $this->dock = $turkeyOrdock;
    }

    public function quack(): void
    {
        $this->turkey->gobble();
    }

    public function gobble(): void
    {
        $this->dock->quack();
    }

    public function fly(): void
    {
        if($this->turkey !== null){
            for($i = 0; $i < 5; $i++){
                $this->turkey->fly();
            }
        } else {
            $this->dock->fly();           
        }
    }
}

– DockAndTurkey.php
DockインターフェースとTurkeyインターフェースの拡張インターフェース

use HFD\Adapter\Dock\Dock;
use HFD\Adapter\Turkey\Turkey;

interface DockAndTurkey extends Dock,Turkey
{
}

 

interfaceのextendsって初めて使いました。
こうやって使うんですね\(^^)/

 

 

 

 

Commandパターン

 

どうも。
最近、蒙古タンメンにハマりすぎて困っています。
麻婆麺が辛すぎて病みつきになります。

 

 

Commandパターン
を見ていきます。

Commandパターンの定義

リクエスト(命令)をオブジェクトとしてカプセル化し、
その結果、他のオブジェクトを異なるリクエスト、キュー
またはログリクエストなどでパラメータ化でき、
アンドゥ可能な操作もサポートする。
= リクエストをオブジェクトでカプセル化する。
= クライアントはいろんなリクエストを持つことができる。(→使える)

 

クラス図

縦長。。。

 

・Command
…全てのコマンド(オブジェクト)のためのインターフェースを定義する。

 

・ConcreteCommand
…Receiverとaction()の結びつきを(execute()に)定義する。
Invokerがexecute()することで
ConcreteCommandへリクエストを行い、
ConcreteCommandがReceiverの
1つ以上のaction()を呼び出すことでそれを実行する。

 

・Invoker
…コマンド(オブジェクト)を保持し、
ある時点でのコマンドのexecute()を呼び出すことで
コマンド(オブジェクト)にリクエストを実行するよう依頼する。
様々なコマンド(オブジェクト)をパラメタ化して保持できるが、
具体的なコマンド(オブジェクト)の内容は知らない。
(Commandインターフェースを実装している何かだと知っている程度)

 

・Receiver
…制御されるもの(制御対象)

・Client

 

具体的に…

この章でのは
「複数の電化製品のon/offを制御できるリモコン」
でした。

 

PHPで書いたみたコード

 

・Command
…制御したい命令を定義するインターフェース

・LightOnCommand / LightOffCommand [ConcreteCommand]
…具体的に制御対象であるライトの
制御(点けたり消したりする)方法がここにある

・RemoteControl [Invoker]
…制御対象(Receiver)をsetCommandで設定したり、
実際に制御するコントローラー
複数の制御対象をセットできる。

・Light [Receiver]
…制御対象となるライト

・RemoteLoader [Client]

 

いいところ

Invoker(RemoteControl)自体が
制御するReceiver(Light)を知っている場合、
Receiverが増えるたびに、if文で追記する形になってしまう。

RemoteControl.php

  if($receiver == 'Light') {
   $receiver.on();

  } else if($receiver == 'Door') {

   $receiver.open();

- }
+ //制御対象「Sprinkler」の追加
+ } else if($receiver == 'Sprinkler') {
+
+  $receiver.waterOn();
+
+ }

 


↓ Commandパターンを使うと…

 

Invoker(RemoteControl)は
具体的なコマンドオブジェクトの内容を知らずして
(インターフェスは知ってる)、
コマンドオブジェクトに具体的な処理(制御)を依頼できるので、
Invoker(RemoteControl)とReceiver(Light)を分離することができる。
= 各々の役割分担ができる。

RemoteControl.php
※Receiver(LightやDoorなど)について全く出てこない。

use HFD\Command\Command\Command;
use HFD\Command\Command\NoCommand;

class RemoteControl
{
    /**
     * @var Command[]
     */
    public $onCommands;

    /**
     * @var Command[]
     */
    public $offCommands;


    public function __construct(int $commandCnt)
    {
        for ($i = 0; $i < $commandCnt; $i++) { $this->onCommands[] = new NoCommand();
            $this->offCommands[] = new NoCommand();
        }
    }

    public function setCommand(int $slotNum, Command $onCommand, Command $offCommand): void
    {
        $this->onCommands[$slotNum] = $onCommand;
        $this->offCommands[$slotNum] = $offCommand;
    }

    public function onButtonWasPushed(int $slotNum): void
    {
        $this->onCommands[$slotNum]->execute();
        $this->undoCommand = $this->onCommands[$slotNum];
    }

    public function offButtonWasPushed(int $slotNum): void
    {
        $this->offCommands[$slotNum]->execute();
        $this->undoCommand = $this->offCommands[$slotNum];
    }
}

 
制御対象が増える可能性がある時の
拡張性を考えた時に良さそうなパターンですね!
 

 

 

 

 

Factoryパターン〜AbstractFactory〜

Head FirstのFactoryパターンの
例え話が「ピザ屋」のお話だったので、
すんごいピザが食べたくなり、ピザホール一人で食べました。
やっぱりマルゲリータですよね。
んで、追いオリーブオイル。

 

Factoryパターン一族

– Simple Factory[正確にはFactoryパターンではない]
– Factory Method
– Abstract Factory

今回は最後のAbstract Factoryについてです!

 Abstract Factory

オブジェクトを作成をカプセル化し、依存度を下げる。(Factory Methodと同じ)
FactoryMethodと異なり、オブジェクトコンポジションを使います。

具象クラスを指定することなく、
一連の関連オブジェクトや依存オブジェクトを作成するための
インターフェースを提供する。

→ 使い手は、抽象インターフェースを使って、
実際に作成される具体的な製品を知ることなく、
一連の製品を作成できる。

いいところ:
使い手は、具体的な製品の詳細から完全に分離されます。
→ 役割分担できて、個々のクラスの責任が小さくなる

 

クラス図はこんな感じ

 

 

Head Firstでは、
– 工場:ピザの食材を作る食材工場
– 製品:ピザの食材
を例としていました。

こんな感じ(PHPStormのクラス図作れるの教えてもらったので、早速)

PizzaIngredientFactoryクラスで、
関連する一連の食材(Dough, Sauce, Cheese, …)オブジェクトを
まとめてグループ化して作れるのがいいところです。

 

AbstractFactoryのサンプルコード

 

▶︎ PizzaIngredientFactory  ←抽象的な食材工場

interface PizzaIngredientFactory
{
    //実装クラスにピザの食材(製品)の作成方法は任す。
    public function createDough(): DoughInterface;
    public function createSauce(): SauceInterface;
    public function createCheese(): CheeseInterface;
       ...
}

 

▶︎ NYPizzaIngredientFactory (PizzaIngredientFactoryの実装クラス)

↑具象な食材工場

class NYPizzaIngredientFactory implements PizzaIngredientFactory
{
    //具体的なピザの食材(オブジェクト)を作成する。
    public function createDough(): DoughInterface
    {
        return new ThinCrustDough();
    }

    public function createSauce(): SauceInterface
    {
        return new MarinaraSauce();
    }
...

 

▶︎ DoughInterface ←抽象的な食材

interface DoughInterface
{
    public function getName(): string;
}

 

▶︎ ThinCrustDough (DoughInterfaceの実装クラス)

↑具象な食材

class ThinCrustDough implements DoughInterface
{
    private $name;

    public function __construct()
    {
        $this->name = "うっすい生地";
    }

    public function getName(): string
    {
        return $this->name;
    }
}

 

 

ただし、食材が追加された時
Product(MeetInterface, Bacon)の追加だけでなく、
PizzaIngredientFactoryと
PizzaIngredientFactoryを実装する全てのクラスを変更する必要があります。
→影響範囲が大きめです。

例: 肉(食材)を追加する場合

– PizzaIngredientFactory

interface PizzaIngredientFactory
{
    //実装クラスにピザの食材(製品)の作成方法は任す。
    public function createDough(): DoughInterface;
    public function createSauce(): SauceInterface;
    public function createCheese(): CheeseInterface;
   ...
+   public function createMeet(): CheeseInterface;
}

– NYPizzaIngredientFactory

class NYPizzaIngredientFactory implements PizzaIngredientFactory
{
    ...
+   public function createMeet()
+   {
+       //MeetInterfaceの実装クラスをインスタンス化する
+       return new Bacon();
+   }
}

 

 

 

ある程度、固定された一連のオブジェクトがあって、
それらを作成する必要がある時に使えそうですね。

 

 

Factoryパターン〜FactoryMethod〜

そこに、Factoryパターン一族

– Simple Factory[正確にはFactoryパターンではない]
– Factory Method
– Abstract Factory
の章があったので、
ぜひアウトプットしたく書きます!

今回はFactory Methodについてです!

Factory Method

オブジェクトを作成をカプセル化し、依存度を下げるために使います。
(Simple Factory、Abstract Factoryと同じ)

オブジェクト作成のためのインターフェースを
抽象クラス(スーパークラス)に定義しますが、
どのクラスをインスタンス化するかについてはサブクラスに決定させます

→抽象クラス(親クラス)は、
サブクラスの作成したオブジェクトを利用するだけで
具体的なオブジェクトが何なのかを知らない。

AbstractFactoryと違う部分
– 継承を使うので、サブクラス(子クラス)が
作成するオブジェクトの具体的な型を実装する。

 

クラス図でみるとこんな感じ

 

factoryMethodはサブクラスに任せるために、
Factoryクラス(スーパークラス)は抽象クラスになる。

クラス図からもわかるように、
Factoryクラス(スーパークラス)は
Prodoctオブジェクトを作成する具体的な方法は全く知りませんが、
factoryMethodを使えばProdoctオブジェクトを作成できることを知っています。
なので、anOperationメソッド内でfactoryMethodを使用することが多いです。

 

 

HeadFirstでは、
地域スタイルでピザの作り方が異なる
フランチャイズでの例がありました。

FactoryMethodのサンプルコード

 

クラス図はこんな感じ。

 

▶︎ PizzaStore(スーパークラス)
Pizzaオブジェクトを作成する具体的な方法は全く知りませんが、
createPizzaメソッドを使えばPizzaオブジェクトを作成できることを知っています。
→orderPizzaメソッド内でcreatePizzaを使用して実装している。

▶︎ NYPizzaStore(PizzaStoreのサブクラス)
具象Pizza(NYスタイル)オブジェクトを作成する

▶︎ChicagoPizzaStore(PizzaStoreのサブクラス)
具象Pizza(Chicagoスタイル)オブジェクトを作成する

▶︎ Pizza
抽象Pizzaクラス。
PizzaStoreのサブクラスにより、具象Pizzaオブジェクトが作成される。

└ NYCheesePizza 具象Pizzaクラス
└ NYClamPizza 具象Pizzaクラス
└ ChicagoCheesePizza 具象Pizzaクラス
└ ChicagoClamPizza 具象Pizzaクラス

 

メリット1:
地域ごとにピザメニューの作り方が異なっても大丈夫

地域ごとの具象PizzaStoreクラス、具象Pizzaクラスがある。
そのため、同じチーズピザでも、
NY店はゴーダチーズ、Chicago店はモッツアレラチーズにしたり
切り方はサブクラスでオーバライドするなど
多種多様に対応することができる。

 

メリット2:
ChicagoにあるけどNY出身者が多い地区のピザ屋にも対応可能

こんなクラスを作ればいい。
– ChicagoPizzaStore_ManyNewYorker.php

class ChicagoPizzaStore_ManyNewYorker extends PizzaStore
{
    public function createPizza(string $type): object
    {
        $pizza = null;
        if ($type == "チーズ(NY風)") {
            $pizza = new NYCheesePizza();
        } else if ($type == "チーズ(Chicago風)") {
            $pizza = new ChicagoCheesePizza();
        } else if ($type == "野菜(NY風)") {
            $pizza = new NYVeggiePizza();
        } else if ($type == "野菜(Chicago風)") {
            $pizza = new ChicagoVeggiePizza();
        }

        return $pizza;
    }
}

 
ピザ食べたくなりました。
やっぱりマルゲリータですよね。

 
 

Factoryパターン〜Simple Factory〜

この前、domicoのライブが宮崎であるというレアな事象ありました。
行かないわけにはいかないので、行ってきました。
めっちゃ、カッコ良いかったです。

 

早速ですが、私「増補改訂版Java言語で学ぶデザインパターン入門
を読んでますが、並行して楽しく読めると噂のこれも読んでいます。

そこに、Factoryパターン一族

– Simple Factory
– Factory Method
– Abstract Factory

の章があったので、
ぜひアウトプットしたく書きます!

今回はSimpleFactoryについてです!

Simple Factory

正確にはFactoryパターンではない!ですが便利なやつです。

サブクラスが複数存在する場合、
if文などで、各サブクラスをインスタンス化する
→ アプリケーション部分の各部分にif文が分散してしまったり、
保守と更新が困難になったり、実装の間違いが多くなるという問題があります。

 

したがって、
アプリケーション内で、
インスタンス化する部分(変化する部分)と変化しない部分
を分けてカプセル化する
というのがSimpleFactoryです。

 

SimpleFactoryのサンプルコード

こんなな感じかと思います。

 

上記でいうと、SimplePizzaFactory = SimpleFactoryになります。

 

▶︎PizzaStore

SimplePizzaFactoryのクライアント。
ピザをインスタンス取得しますが、
「ギリシャピザやクラムピザなどの具象ピザ」については知る必要がありません。
「pizzaインターフェースを実装したピザ(具象ピザ)を取得し、
prepareメソッド,bakeメソッド,…を呼び出せる」ことを知ってるだけで良い。
ピザを取得する事に関しては、SimplePizzaFactoryに任せるだけでOK!

 

▶︎SimplePizzaFactory

SimpleFactoryの主役。
ピザをの作成方法だけを扱うクラス。
具象Pizzaクラスを参照する唯一の部分。

 

▶︎Pizza

SimplePizzaFactoryの製品であるPizzaの抽象クラス。
具象ピザはこれを実装する。

└ VeggiePizza   具象ピザ(具象製品)
└ PepperoniPizza 具象ピザ(具象製品)
└ ClamPizza    具象ピザ(具象製品)
└ CheesePizza   具象ピザ(具象製品)

新作ピザの追加、ピザメニューの削除があっても。。。

大丈夫!

SimplePizzaFactory.php

- use HFD\SimpleFactory\Pizza\PepperoniPizza;
  use HFD\SimpleFactory\Pizza\ClamPizza;
  use HFD\SimpleFactory\Pizza\VeggiePizza;
+ use HFD\SimpleFactory\Pizza\GreekPizza;

function createPizza(string $type): object
{
        $pizza = null;
        if ($type == "チーズ") {
            $pizza = new CheesePizza();
-       } else if ($type == "ペパロニ") {
-           $pizza = new PepperoniPizza();
        } else if ($type == "クラム") {
            $pizza = new ClamPizza();
        } else if ($type == "野菜") {
            $pizza = new VeggiePizza();
+       } else if($type == "ギリシャ"){
+           $pizza = new GreekPizza();
        }

GeekPizza.php(新作ピザ)

+namespace HFD\SimpleFactory\Pizza;
+
+
+class GreekPizza extends Pizza {
+
+    public function __construct() {
+        $this->name = "ギリシャピザ";
+    }
+
+}

 

ピザの作成を
SimplePizzaFactoryに任せてるので、
SimplePizzaFactoryの修正と
新作ピザクラス(具象ピザ)の追加のみで対応できる。
さらに、PizzaStoreと分けているので
PizzaStoreには影響が少ない!

 

なによりシンプルでわかりやすいですね。

次は、FactoryMethodについて書きたいと思います!

Singleton(シングルトン)を使ってみる

 

今回は、ご存知デザパタの
第5章 Singleton 〜たった一つのインスタンス〜
について自分なりにまとめたいと思います。

 

シングルトンって音がかわいいですよね、「トン」とかがw

シングルトン(Singleton)とは

何も考えず、実装をしているとなんども同じインスタンスを生成しがちです。
・指定したクラスのインスタンスが絶対1つしか存在しないことを保証したい
・インスタンスが1つしか存在しないことを、プログラム上で表現したい

そんなときに、インスタンス生成コストを下げるためにも、
Singletonパターンを使います。

 

参考:
PHPで書き換えてくれてるソース
https://github.com/HappyDays-jQuery/GoF/tree/master/src/Singleton

 

 

こんな感じ

全体コード→https://github.com/kin29/DezaPata/pull/4/files

– Singletonクラス
– コンテキスト(使う側)

Singletonクラス

<?php

namespace DP\Singleton;

class Singleton
{
    /**
     * @var Singleton
     */
    //static宣言がされているため、
    //クラスのインスタンス化の必要なしにアクセスできる。
    //ただしprivateなので同クラス内のからしかアクセスできない
    private static $singleton;

    //privateなので、同クラス内からしかインスタンス生成をできない。
    //他のクラスからnew Singletonをするとエラーになる。
    //このクラスではgetInstanceがインスタンス生成をしている。
    private function __construct()
    {
        echo "create SingletonClassInstance\n";
    }

    //すでに$singletonにインスタンスがセットされている場合は、newしない。
    public static function getInstance()
    {
        //$singletonがstatic宣言されているため、
             //$this->singletonでアクセスできない。
        if (is_null(self::$singleton)) {
            self::$singleton = new Singleton();
        }
        return self::$singleton;
    }
}

 

 

contextSingleton.php ←コンテキスト(使う側)

<?php

use DP\Singleton\Singleton;

echo "Singletonクラスを普通にnewしてみる。\n";
new Singleton(); //たぶんおこられる。

外部クラスからnewすると
Singleton::__construct()がprivateなので怒られます。

$ php contextSingleton.php

Singletonクラスを普通にnewしてみる。
PHP Fatal error: Uncaught Error: Call to private DP\Singleton\Singleton::__construct() from invalid context in /Users/XXXXX/DezaPata/contextSingleton.php:10
Stack trace:
#0 {main}
 thrown in /Users/XXXXX/DezaPata/contextSingleton.php on line 10

 

 

contextSingleton.php ←コンテキスト(使う側)

<?php

use DP\Singleton\Singleton;

echo "getInstanceしてみる(instance1)\n";
$instance1 = Singleton::getInstance();
echo "もう一度、getInstanceしてみる(instance2)\n";
$instance2 = Singleton::getInstance();
echo "2回目のgetInstanceはインスタンス生成を行わないため、constructを通らないはず\n";
echo "(instance1 === instance2)なのか確認\n";
echo ($instance1 === $instance2) . "\n";

$ php contextSingleton.php

getInstanceしてみる(instance1)
create SingletonClassInstance
もう一度、getInstanceしてみる(instance2)
2回目のgetInstanceはインスタンス生成を行わないため、constructを通らないはず
(instance1 === instance2)なのか確認
1

Singletonクラスのインスタンスを得ることができるgetInsranceメソッドを
2回実行してみる。
1回目でインスタンス生成によりコンストラクラにて初期化でいを行い、
2回目はインスタンス生成していないことがわかる。
さらに、1回目と2回目のインスタンスは同じであることもわかる。

→ Singletonクラスは1つしか存在せず、
getInstanceで毎回同じSingletonクラスのインスタンスを得ることができました!!!

 

<<追記 2019-03-12>>

ただし、このままの方法ではタッチの差で、

if (is_null(self::$singleton))

判定をした場合には
インスタンスが1つ以上作られる可能性があります。

この対策としては二つがあります。
●同期化
●初期化時点でインスタンスを生成し、
getInstanceでは常にそのインスタンスを返すようにする。

class Singleton
{
    private static $singleton = new Singleton();

    private function __construct()
    {
        echo "create SingletonClassInstance\n";
    }

    public static function getInstance()
    {
        return self::$singleton;
    }
}

 

 

static宣言

HogeClass.php

class HogeClass
{
    public static $staticProperty = "staticなプロパティです。\n";

    public static function staticMethod()
    {
        return "staticなメソッドです。\n";
    }
}

 

staticなメソッド、staticなプロパティともに
インスタンス化せずとも外部からアクセスが可能である。

echo HogeClass::$staticProperty;  //staticなプロパティです。
echo HogeClass::staticMethod();   //staticなメソッドです。

 

HogeClass内からのアクセスもself::で行う。
$this->で行うと、エラーとなる。

PHP Fatal error: Uncaught Error: Using $this when not in object context in

 

※注意!

staticなプロパティはインスタンス化されたクラスオブジェクトから
アクセスすることはできない(static なメソッドにはアクセスできます)。

$HogeObj = new HogeClass;
echo $HogeObj->staticProperty;
//PHP Notice:  Accessing static property HogeClass::$staticProperty as non static in...
echo $HogeObj->staticMethod();   //staticなメソッドです。

 

 

イテレーターを(iterator)を使ってみる

 

今回は、ご存知デザパタの
第1章 Iterator 〜1つ1つ数えあげる〜
について自分なりにまとめたいと思います。

 

イテレーター(iterator)とは

何かの集合体を、数える人
→ 何かの集合体(=aggregate)つまり数える対象が必要
→ 何かの集合体(=aggregate)がいてこその役に立つ

参考:
PHPで書き換えてくれてるソース
https://github.com/HappyDays-jQuery/GoF/tree/master/src/Iterator

 

※PHPでは組み込みで複数のインターフェースがあるので便利!
Iteratorインターフェース
IteratorAggregateインターフェース ..など

何かの集合体(aggregate)と、それを数える人(iterator)を別クラスにすることで、
役割分担ができ、疎結合になる。
→ ただし、aggregateとiteratorを別にする必要がない場合は、
両方の役割をもつIteratorAggregateを使うこともできる。

 

こんな感じ

全体コード→https://github.com/kin29/DezaPata/pull/2/files

– Bookクラス
– BookShelfクラス(=aggregate)
– BookShelfIteratorクラス(=iterator)
– コンテキスト(使う側)

BookShelf(集合体)の中にBook(要素)があり、
BookShelfIteratorが本を一つ一つ数えていく感じです。

BookShelfクラス

<?php

/**
 * 何か(=本)の集合体[Aggregate]
 */
class BookShelf
{
....
    public function getIterator(): BookShelfIterator
    {
        return new BookShelfIterator($this);
    }
}

コンテキスト(使う側)

<?php 

$bookShelf = new BookShelf();
$bookShelf->appendBook(new Book('星の王子様'));
$bookShelf->appendBook(new Book('かいけつゾロリ'));
$bookShelf->appendBook(new Book('ミッケ!'));
$bookShelfIt = $bookShelf->getIterator();

while ($bookShelfIt->hasNext())
{
    echo $bookShelfIt->next()->getName() . "\n";
}

実行すると、appendした本の名前が入れた順番に1つずつ表示されます。

$ php contextIterator.php
星の王子様
かいけつゾロリ
ミッケ!

 

イテレータのいいところ

何かの集合体(=aggregate)や数え方(=iterator)を変更しても、
イテレータを使う側には変更の必要がない。

例)
変更前:数え方→集合体へ要素を追加した順番(FIFO)
変更前:数え方→集合体へ要素を追加した逆の順番(FILO)

<?php 

namespace DP\Iterator; 

/**
 * 何か(=本)の集合体を数える人[Iterator]
 *
 * Class BookShelfIterator
 * @package DP\Iterator
 */ 

class BookShelfIterator {

    private $bookShelf;
    private $index;
  
    public function __construct(BookShelf $bookShelf)
    { 
        $this->bookShelf = $bookShelf;
-       $this->index = 0;
+       $this->index = $this->bookShelf->getLength() - 1;
    }

    public function hasNext(): bool
    {
-       return $this->index < $this->bookShelf->getLength();
+       return $this->index >= 0;
    }

    public function next(): Book
    {
        $book = $this->bookShelf->getBookAt($this->index);
-       $this->index++;
+       $this->index--;
        return $book;
    }
}

実行結果

$ php contextIterator_FILO.php
ミッケ!
かいけつゾロリ
星の王子様

 

イテレータ、便利だしスマートですね!

しかし未だ、これイテレータパターン使えるっ
って言う場面に直面したことがないので、
何か集合体を見つけては、ソースに落とし込むとかして
そういう直感を養っていこうと思います。