GoFのデザインパターンの中でも特に知名度の高い (そして間違った使われ方の多い) Singleton パターン, 皆様は有効活用してますか?
今回は自分の考える Singleton パターンの有効な使いどころを NG コード集も添えて説明したいと思います.
(※サンプルコードは Java と PHP で記載します)
概要
Singleton パターンを適用すべき条件について, 結論を簡単に書いてしまうと
- 状態を持たないこと
- ポリモーフィズムが絡む (抽象クラスまたはインタフェースを実装している) こと
の 2 点に尽きます. おそらくこれらを無意識的に実践しているプログラマーも多いのではないかと思います.
状態を持たないこと
鉄則です. メンバ変数を含んだシングルトンパターンって, それ単なるグローバル変数ですから!
WEB アプリケーションのフレームワークなどでこんなコードを見たことはありませんか?
/**
* ありがちなシングルトン。実態はただのグローバル変数群
*/
class ApplicationManager {
/**
* DBに接続してCRUD操作をする何か
*/
private DbConnector connector;
/**
* (HTTPリクエストのセッション情報から取ってきた) アクセスしているユーザーをあらわすオブジェクト
*/
private User currentUser;
/**
* デバッグ情報などをログファイルに垂れ流すやつ
*/
private Logger logger;
private static final ApplicationManager instance = new ApplicationManager();
private ApplicationManager() {
// init...
}
public static ApplicationManager getInstance() {
return instance;
}
// 以下略
}
<?php
/**
* ありがちなシングルトン。実態はただのグローバル変数群
*/
class ApplicationManager
{
/**
* DBに接続してCRUD操作をする何か
* @var DbConnector
*/
private $connector;
/**
* (HTTPリクエストのセッション情報から取ってきた) アクセスしているユーザーをあらわすオブジェクト
* @var User
*/
private $currentUser;
/**
* デバッグ情報などをログファイルに垂れ流すやつ
* @var Logger
*/
private $logger;
private function __construct()
{
// init...
}
public static function getInstance()
{
static $instance = null;
if ($instance === null) {
$instance = new self();
}
return $instance;
}
// 以下略
}
サードパーティー製のフレームワークがこういう作りになっていたらどうしようもないですが, スクラッチで実装する際には出来たら避けたいパターンです.
どうしてもこの手のクラスが欲しいのであれば, シングルトン性をなくし, DI (依存性の注入) で外部から渡せるように設計を工夫しましょう.
そうすればテストもずっと容易になりますし, カスタマイズ性もアップします.
改善前
メソッドの中で ApplicationManager のクラスメソッド getInstance() を呼び出してインスタンスを取得しています.
class HomeAction {
public void executeIndex() {
ApplicationManager manager = ApplicationManager.getInstance();
String name = manager.getCurrentUser().getName();
System.out.printf("ようこそ %s さん\n", name);
}
}
class HomeAction
{
public function executeIndex()
{
$manager = ApplicationManager::getInstance();
$name = $manager->getCurrentUser()->getName();
echo "ようこそ {$name} さん", PHP_EOL;
}
}
改善後
ApplicationManager のインスタンスを引数として渡します.
class HomeAction {
public void executeIndex(ApplicationManager manager) {
String name = manager.getCurrentUser().getName();
System.out.printf("ようこそ %s さん\n", name);
}
}
class HomeAction
{
public function executeIndex(ApplicationManager $manager)
{
$name = $manager->getCurrentUser()->getName();
echo "ようこそ {$name} さん", PHP_EOL;
}
}
ポリモーフィズムが絡むこと
以下に挙げるのは, 「明らかな NG というわけではないけれど, わざわざ Singleton パターンにするだけの旨みがない」コード例です.
class TextValidator {
private static final TextValidator instance = new TextValidator();
private TextValidator() {
}
public static TextValidator getInstance() {
return instance;
}
/**
* 引数が, パスワードとして適切な形式 (アルファベット・数字・特殊記号をそれぞれ 1 文字以上含む) かどうかを調べます.
*
* @param password パスワード
* @return 妥当な形式の場合のみ true
*/
public boolean isValidPassword(String password) {
// 省略
}
/**
* 引数が, ニックネームとして適切な形式 (アルファベット, 数字, アンダースコアの組み合わせ) かどうかを調べます.
*
* @param nickname パスワード
* @return 妥当な形式の場合のみ true
*/
public boolean isValidNickname(String nickname) {
// 省略
}
}
<?php
class TextValidator
{
private function __construct()
{
}
public static function getInstance()
{
static $instance = null;
if ($instance === null) {
$instance = new self();
}
return instance;
}
/**
* 引数が, パスワードとして適切な形式 (アルファベット・数字・特殊記号をそれぞれ 1 文字以上含む) かどうかを調べます.
*
* @param string $password パスワード
* @return bool 妥当な形式の場合のみ true
*/
public function isValidPassword($password)
{
// 省略
}
/**
* 引数が, ニックネームとして適切な形式 (アルファベット, 数字, アンダースコアの組み合わせ) かどうかを調べます.
*
* @param string $nickname ニックネーム
* @return bool 妥当な形式の場合のみ true
*/
public function isValidNickname($nickname)
{
// 省略
}
}
この TextValidator クラスは, 何かのインタフェースを実装したり別のクラスを継承したりしているわけではないので, 以下のようにユーティリティクラスにしてしまって良いでしょう. (getInstance() を削除し, 各メソッドに static を付加)
class TextValidator {
private TextValidator() {
}
/**
* 引数が, パスワードとして適切な形式 (アルファベット・数字・特殊記号をそれぞれ 1 文字以上含む) かどうかを調べます.
*
* @param password パスワード
* @return 妥当な形式の場合のみ true
*/
public static boolean isValidPassword(String password) {
// 省略
}
/**
* 引数が, ニックネームとして適切な形式 (アルファベット, 数字, アンダースコアの組み合わせ) かどうかを調べます.
*
* @param nickname パスワード
* @return 妥当な形式の場合のみ true
*/
public static boolean isValidNickname(String nickname) {
// 省略
}
}
<?php
class TextValidator
{
private function __construct()
{
}
/**
* 引数が, パスワードとして適切な形式 (アルファベット・数字・特殊記号をそれぞれ 1 文字以上含む) かどうかを調べます.
*
* @param string $password パスワード
* @return bool 妥当な形式の場合のみ true
*/
public static function isValidPassword($password)
{
// 省略
}
/**
* 引数が, ニックネームとして適切な形式 (アルファベット, 数字, アンダースコアの組み合わせ) かどうかを調べます.
*
* @param string $nickname ニックネーム
* @return bool 妥当な形式の場合のみ true
*/
public static function isValidNickname($nickname)
{
// 省略
}
}
Singleton パターンを活用した典型例
interface Being {
public String sayHello();
}
class Human implements Being {
private String name;
public Human(String name) {
this.name = name;
}
public String sayHello() {
return String.format("Hello, my name is %s.", this.name);
}
}
class God implements Being {
private static final God instance = new God();
public static God getInstance() {
return instance;
}
private God() {
}
public String sayHello() {
return "I am the God.";
}
}
<?php
interface Being
{
/**
* @return string
*/
public function sayHello();
}
class Human implements Being
{
/**
* @var string
*/
private $name;
/**
* @param string $name
*/
public function __construct($name)
{
$this->name = $name;
}
/**
* @return string
*/
public function sayHello()
{
return "Hello, my name is {$this->name}.";
}
}
class God implements Being
{
private function __construct()
{
}
/**
* @return God
*/
public static function getInstance()
{
static $instance = null;
if ($instance === null) {
$instance = new self();
}
return $instance;
}
/**
* @return string
*/
public function sayHello()
{
return "I am the God.";
}
}
サンプルコードでは, Being (※「存在」, 「生命」などの意味) インタフェースを実装した Human クラスと God クラスがあります.
Human クラスは name
という状態を持っているので, new Human("John")
とか new Human("Tom")
のように様々な種類のインスタンスを作ることが出来ます.
一方 God クラスは状態を持たないので, 複数のインスタンスを生成するのは意味がありません. このようなケースでは Singleton パターンを適用して「このクラスは状態を持たないよ」という意図を明確にしましょう.
実戦で Singleton パターンが使われるケースとして, 例えば Strategy パターン (ロジックを別のクラスに移譲して, 一部処理を動的に切り替えられるようにする) を活用したアプリケーションが挙げられます.
Strategy パターンの詳しい説明は, 機会があったらまた詳しく説明したいと思います. 今日はこのへんで.