デザインパターンとは
デザインパターン(設計パターン)とは、過去のソフトウェア設計者が発見し編み出した設計ノウハウに名前をつけ、再利用しやすいように特定の規約に従ってカタログ化したものです。
デザインパターンを利用するメリットは、再利用性の高い柔軟な設計ができるという点です。設計者の直感や経験などに依存していた設計が、デザインパターンを導入することで、初心者でも先人たちが詰め込んだ「知恵」を利用して設計をすることが可能になります。
もう一つのメリットとして、技術者どうしの意思疎通が容易になることが挙げられます。デザインパターンを習得している技術者どうしであれば、パターン名で設計の概要の合意を取ることが可能になります。このように、デザインパターンを学習しておくことで開発者どうしの意思疎通がスムーズになるのです。
以下は、Flutterにおける主要なデザインパターンです。
- Model-View-Controller (MVC)
- Model-View-ViewModel (MVVM)
- Provider(プロバイダー) パターン
- Bloc(ブロック) パターン
- Singleton(シングルトン) パターン
- Factory(ファクトリー) パターン
- Builder(ビルダー) パターン
- Composite(コンポジット) パターン
これらのパターンを駆使することで、開発者はアプリケーションの品質を向上させ、開発をスムーズに進めることができ、プロジェクトの規模やメンバーのスキルに応じた柔軟な設計が行えるのです。
今回は Singletonパターン に対象を絞って解説をしていきます。
Singletonパターン
Singleton (シングルトン)とは、あるクラスのインスタンスがアプリケーション全体で、
たった一つしか存在しないことを保証するためのデザインパターンです。
例えば、アプリケーション全体で共通の設定やログ管理、データベース接続など、
状態を共有する必要がある場合に使用されます。
1. 定義
Singletonパターンでは、クラスがインスタンス化される際に、
そのクラスの唯一のインスタンスを返すように設計されます。
特徴
- インスタンスが一度しか作成されない。
- 作成されたインスタンスは、どこからでもアクセスできる。
- クラス自体がその唯一のインスタンスへのアクセスを管理する。
2. コンポーネント
-
Singletonクラス: 唯一のインスタンスを生成し、管理するクラス。
-
privateコンストラクタ: 外部からインスタンスを生成できないようにするために使用されるコンストラクタ。クラスのコンストラクタをプライベートにすることで、外部から直接インスタンス化できないようにします。
-
staticメソッド: クラスの唯一のインスタンスを取得するために使用される。
3. メリット
-
インスタンスの一貫性: インスタンスが一つしか存在しないため、データの整合性を保つことができます。
-
リソースの節約: インスタンスが複数作成されないため、メモリやその他のリソースの使用を最小限に抑えることができます。
-
グローバルアクセス: アプリケーション全体で同じインスタンスにアクセスできます。
-
オーバーヘッドの削減: 複数のインスタンス生成によるメモリ消費を抑えることができます。
-
状態の共有: アプリケーション全体で状態を共有できます。
4. サンプルコード
class Singleton {
// クラス内で一つのインスタンスを保持
static final Singleton _instance = Singleton._internal();
// プライベートコンストラクタ
Singleton._internal();
// インスタンスを取得するためのファクトリコンストラクタ
factory Singleton() {
return _instance;
}
// サンプルメソッド
void someMethod() {
print("This is a method from the Singleton instance.");
}
}
void main() {
// Singletonインスタンスを取得
var s1 = Singleton();
var s2 = Singleton();
// インスタンスが同一であることを確認
print(s1 == s2); // true
// Singletonのメソッドを呼び出し
s1.someMethod();
}
- 静的インスタンス (_instance):
static final Singleton _instance = Singleton._internal();
Singletonクラスの唯一のインスタンスを作成しています。 _instance
は final
で定義しているので、一度作成されたら変更されることはありません。
static キーワード は、この変数がクラスそのものに属することを意味します。
つまり、 _instance
は クラスレベルのフィールド であり、クラスのインスタンス(オブジェクト)ごとではなく、クラス自体に対して一つだけ存在します。
シングルトンパターンでは、クラス全体で一つだけのインスタンスを保持したいので、このインスタンスをクラスに結びつけるために static
を使います。
staticを付与しない場合
もしこの static
を外した場合、以下のようなことが起こります
-
コンパイルエラー:
final Singleton _instance = Singleton._internal();
とすると、
この行自体でコンパイルエラーが発生します。なぜなら、インスタンス変数の初期化にはインスタンスメソッド(この場合は_internal()
)を使用できないからです。 -
インスタンスレベルのフィールドになる:
static
を付与しない場合、_instance
はクラスのインスタンスごとに生成されます。つまり、Singleton
クラスの新しいインスタンスを作成するたびに、新しい_instance
変数が生成され、異なるインスタンスが保持されます。 -
シングルトンの目的が達成されない:
シングルトンパターンは、クラスのインスタンスを一つだけに制限するためのものです。static
を付けないと、各インスタンスごとに異なる_instance
が作成されるため、クラスのインスタンスが複数存在する可能性が出てきます。これは、シングルトンパターンの本来の目的である「一つのインスタンスのみを共有する」という目的が達成されなくなります。 -
プライベートコンストラクタ (_internal):
Singleton._internal();
_internal() は、コンストラクタをプライベートアクセスにしています。 _internal() という名前は慣例的なものであって、必ずしもこの名前である必要はありません。極端な話 _() などでも構いません。
このコンストラクタはプライベートなので、クラスの外部から直接呼び出すことはできません。これにより、クラスの外部で新しいインスタンスを作成することが防止されます。
- ファクトリコンストラクタ:
factory Singleton()
ファクトリコンストラクタは、クラスのインスタンスを返すために使用されます。この場合、すでに作成された静的インスタンス(_instance
)を返すようにしています。これにより、呼び出しごとに新しいインスタンスを作成するのではなく、常に同じインスタンス(既に存在する _instance
)が返されます。
コンストラクタについて
クラスのインスタンスが生成される際に、自動的に呼び出される初期化処理のことを指します。
factoryキーワードについて
コンストラクタが呼ばれたオブジェクトを常に作成するのではなく、キャッシュがあればキャッシュを使うことができるようになります。このキーワードがコンストラクタの前に記載されていない場合、コンストラクタで値をreturnすることはできません。
- サンプルメソッド (someMethod):
void someMethod() {...}
このメソッドは、Singletonインスタンスから呼び出されるサンプルのメソッドです。単純にメッセージをコンソールに表示します。
- インスタンスの取得 (s1とs2):
var s1 = Singleton();
var s2 = Singleton();
s1とs2はどちらもSingletonクラスのインスタンスを取得しています。しかし、ファクトリコンストラクタを通じて取得されるため、実際には同じインスタンス(_instance
)が返されます。
- インスタンスの比較 (s1 == s2):
print(s1 == s2); // true
s1とs2が同一のインスタンスであることを確認するためのコードです。ここでは、trueが出力され、同じインスタンスであることが証明されます。
- メソッドの呼び出し (someMethod):
s1.someMethod();
s1からsomeMethodメソッドを呼び出しています。これにより、"This is a method from the Singleton instance."というメッセージがコンソールに表示されます。
5. 普通のクラス vs シングルトンクラス
ではシングルトンクラスを理解するために、普通のクラスとSingletonクラスの違いを
確認してみましょう。今回は例として、ログを確認する際に、
使用するLoggerクラスを元に説明していきます。
普通のクラス Logger
まず、Singletonパターンを使用しない場合のログクラスを見てみましょう。
Loggerクラスは、複数のインスタンスが作成でき、それぞれが独自のログを
保持します。
class Logger {
Logger();
List<String> logs = [];
void addLog(String log) {
logs.add(log);
print('Log added: $log');
}
List<String> getLogs() {
return logs;
}
}
このLoggerクラスを使って、ログを追加するコードは以下の通りです。
void main() {
final logger1 = Logger();
logger1.addLog('User logged in');
logger1.addLog('User viewed profile');
print(logger1.getLogs()); // [User logged in, User viewed profile]
final logger2 = Logger();
logger2.addLog('User logged out');
print(logger2.getLogs()); // [User logged out]
}
上記のコードでは、 logger1
と logger2
はそれぞれ別々のLoggerインスタンスを
持ちます。そのため、 logger1
が保持するログと logger2
が保持するログは異なり、
ログの管理が分散されています。これは、異なるインスタンスが独立してログを
保持しているためです。
シングルトンクラス SingletonLogger
次に、Singletonパターンを使用したSingletonLoggerクラスを見てみましょう。
このクラスでは、アプリケーション全体で唯一のインスタンスが使用され、
すべてのログが一元管理されます。
class SingletonLogger {
static final SingletonLogger _instance = SingletonLogger._internal();
List<String> logs = [];
factory SingletonLogger() {
return _instance;
}
SingletonLogger._internal();
void addLog(String log) {
logs.add(log);
print('Log added: $log');
}
List<String> getLogs() {
return logs;
}
}
このSingletonLoggerクラスを使って、ログを追加するコードは以下の通りです。
void main() {
final logger1 = SingletonLogger();
logger1.addLog('User logged in');
logger1.addLog('User viewed profile');
print(logger1.getLogs()); // [User logged in, User viewed profile]
final logger2 = SingletonLogger();
logger2.addLog('User logged out');
print(logger2.getLogs()); // [User logged in, User viewed profile, User logged out]
}
SingletonLoggerクラスでは、logger1
と logger2
が同じインスタンスを
共有しているため、ログが一つのリストに集約され、
ログの一貫性が保たれ、管理が容易になります。
Singletonパターンを使用することで、リソースや状態の一貫性を
保ちながら効率的に管理することができます。
6. Tips
一般的には②が利用されることが多いですが、
シングルトンの書き方にはいろんなパターンが存在します。
①「staticのクラス変数」と「privateコンストラクタ」
class Singleton {
// instanceはprivateではない
static final Singleton instance = Singleton._internal();
Singleton._internal();
}
void main() {
Singleton singleton = Singleton.instance;
}
②「staticのprivateクラス変数」と「factoryコンストラクタ」と「privateコンストラクタ」
class Singleton {
static final Singleton _instance = Singleton._internal();
factory Singleton() {
return _instance;
}
Singleton._internal();
}
void main() {
Singleton singleton = Singleton();
}
class Singleton {
static Singleton? _instance; //null safetyのprivateクラス変数
factory Singleton() {
//_instanceがnullであればSingleton._internal()でインスタンス化したものをreturn
//_instanceがnullでなければ既にキャッシュしている_instanceをreturn
return _instance ??= Singleton._internal();
//1回目は_instanceがnullなのでSingleton._internal()がreturnされる
//2回目以降は、_instanceがnullでないので_instanceがreturnされる
}
Singleton._internal();
}
void main() {
Singleton singleton = Singleton();
}
③「staticのprivateクラス変数」と「privateコンストラクタ」と「getter」
class Singleton {
static final Singleton _instance = Singleton._internal();
Singleton._internal();
static Singleton get instance => _instance;
}
void main() {
Singleton singleton = Singleton.instance;
}
まとめ
Singletonパターンは、アプリケーション全体で一つのインスタンスしか
存在しないクラスを作成するためのデザインパターンです。
これにより、リソースの節約や状態の一貫性を保つことができます。
Dartでは、プライベートコンストラクタと静的プロパティを使って
Singletonパターンを実装します。
このパターンを適切に利用することで、特定のクラスのインスタンスを
効率的に管理できるようになるので、これを機にぜひ活用してみてください!
参考資料
告知
最後にお知らせとなりますが、イーディーエーでは一緒に働くエンジニアを
募集しております。詳しくは採用情報ページをご確認ください。
みなさまからのご応募をお待ちしております。