がるの健忘録

エンジニアでゲーマーで講師で占い師なおいちゃんのブログです。

Singletonをcloneさせたくないエトセトラ

たまにはちょっとお馬鹿い考察を(笑


Singletonで顕著なのですが、インスタンスを「1つにしたい」ってケースで。
まぁ勿論規約で縛るとかってのもよいのですが、縛っても踏み越えて乗り越えてくるケースってのはあるので。
ある程度くらいまでは「不可能な状況」を作るのも、程度問題ですが、大事だと思うのです。


んと…つまり。
プロパティは「アクセサ経由でアクセスさせたいんなら、プロパティ自体はprivateないしせめてprotectedにしとけよ」と。
そんな感じです。


さて。
まずとりあえずざっくりしたSingletonのクラスを。

class hoge {
  private function __construct() {
  }
  static public function get_instance() {
    static $obj = null;
    if (null === $obj) {
      $obj = new static();
    }
    return $obj;
  }
}
//
$obj = hoge::get_instance();
var_dump($obj);
$obj2 = $obj;
var_dump($obj2);
//
$obj3 = clone $obj;
var_dump($obj3);


$obj3が、cloneによって「二つ目のインスタンス」作れてしまいます。
勿論規約的に「Singletonのインスタンスをcloneすんな」って書いておけばよいのですが、書いたってやるヤツはやらかします。
ここはしっかりロックしてみましょう。

class hoge {
  private function __construct() {
  }
  static public function get_instance() {
    static $obj = null;
    if (null === $obj) {
      $obj = new static();
    }
    return $obj;
  }
  // clone禁止
  private function __clone() {
    throw new Exception('させねぇよ!');
  }
}
//
$obj = hoge::get_instance();
var_dump($obj);
$obj2 = $obj;
var_dump($obj2);
//
$obj3 = clone $obj;
var_dump($obj3);

これでcloneできません。
これで仕舞い…かと思ったのですが、ヤな事を思いついてみたので書いてみます。

class hoge {
  private function __construct() {
  }
  static public function get_instance() {
    static $obj = null;
    if (null === $obj) {
      $obj = new static();
    }
    return $obj;
  }
  // clone禁止
  private function __clone() {
    throw new Exception('させねぇよ!');
  }
}
//
$obj = hoge::get_instance();
var_dump($obj);
$obj2 = $obj;
var_dump($obj2);
//
//$obj3 = clone $obj;
//var_dump($obj3);
$obj4 = unserialize(serialize($obj));
var_dump($obj4);

可能ですねぇ。
馬鹿をなめちゃいけませんこーゆー「無駄で不必要で有害な工夫」だけは、しっかりやってきます。
とりあえす、シリアライズできなきゃどうにかなるでしょう。
ただ、以下の書き方は「誤り」になります。

class hoge {
  private function __construct() {
  }
  static public function get_instance() {
    static $obj = null;
    if (null === $obj) {
      $obj = new static();
    }
    return $obj;
  }
  // clone禁止
  private function __clone() {
    throw new Exception('させねぇよ!');
  }
  // serialize禁止
  private function __sleep() {
    throw new Exception('させねぇよ!');
  }
}
//
$obj = hoge::get_instance();
var_dump($obj);
$obj2 = $obj;
var_dump($obj2);
//
//$obj3 = clone $obj;
//var_dump($obj3);
$obj4 = unserialize(serialize($obj));
var_dump($obj4);

Warning: Invalid callback hoge::__sleep, cannot access private method hoge::__sleep() in ..................

単純に「callbackが上手く呼べなかったから警告出しとくよ〜」で終わってしまいます。
なので、ここは「メソッドを潰す」のではなくて「受け入れた上で締め上げる」作戦でいきましょう。

class hoge {
  private function __construct() {
  }
  static public function get_instance() {
    static $obj = null;
    if (null === $obj) {
      $obj = new static();
    }
    return $obj;
  }
  // clone禁止
  private function __clone() {
    throw new Exception('させねぇよ!');
  }
  // serialize禁止
  public function __sleep() {
    throw new Exception('させねぇよ!');
  }
}
//
$obj = hoge::get_instance();
var_dump($obj);
$obj2 = $obj;
var_dump($obj2);
//
//$obj3 = clone $obj;
//var_dump($obj3);
$obj4 = unserialize(serialize($obj));
var_dump($obj4);

これでよいか…と思いきや、もう一つ方法があります困った事に。
コードの最後にこれをいれると、やっぱり「インスタンスのcopy」が可能になります。

$obj5 = unserialize('O:4:"hoge":0:{}');
var_dump($obj5);

「自力でserializeの文字列組み立てるなよ」とか思うのですが、やるヤツはやらかします。
なので、 __wakeup() マジックメソッドも、しっかりと締め上げておきましょう。


かくして、締めまくったのが、以下になります。

class hoge {
  private function __construct() {
  }
  static public function get_instance() {
    static $obj = null;
    if (null === $obj) {
      $obj = new static();
    }
    return $obj;
  }
  // clone禁止
  private function __clone() {
    throw new Exception('させねぇよ!');
  }
  // serialize禁止
  public function __sleep() {
    throw new Exception('させねぇよ!');
  }
  // unserialize禁止
  public function __wakeup() {
    throw new Exception('させねぇよ!');
  }
}
//
$obj = hoge::get_instance();
var_dump($obj);
$obj2 = $obj;
var_dump($obj2);
//
//$obj3 = clone $obj;
//var_dump($obj3);
//$obj4 = unserialize(serialize($obj));
//var_dump($obj4);
//$obj5 = unserialize('O:4:"hoge":0:{}');
//var_dump($obj5);


これが「本当に役に立つ」シーンは稀でしょうし、可能ならンなシーンに遭遇したくないだろうなぁ、というのは恐らく全世界の人類共通の願いではあるのですが。
たまにはこんな思考実験も面白いンじゃないかなぁ? と思いました。


「privateメソッドを無理くりcallする」以外の方法で、抜けそうな方法をご連絡いただければアップデートしたいなぁ、と思うので。
興味のある方は、遊び心で参加いただければ幸いでございます。