注意
申し訳ありません。
この記事の例題のA君はクロージャの使い方を間違えています。
この記事を読むときは十分にご注意ください。
詳しくは@links_2_3_4さんのコメントをご参照ください。
(@links_2_3_4さん、コメントをありがとうございます。)
はじめに
「グローバル変数で定義されてて、どこで変更されとるんか全くわからんっっっっ!!!」
誰しもが経験する地獄だと思います。
この記事ではクロージャを使うことで、そんな悪名高いグローバル変数を使わずに保守性の高いコードを書く例を紹介します。
例ではC#を使用しますが、クロージャはJavaScriptやPythonなど数多くの言語で取り入れられているので、C#以外の方はそれぞれの言語に応じた書き方で試してみてください。
対象者
この記事は以下のような方を対象としています。
- グローバル変数にうんざりしている人
- 保守が用意なコードを書きたいと思っている人
- クロージャってどういうときに使えばいいの?と思っている人
それでは頑張っていきましょう。
そもそもクロージャって何?
そもそもクロージャって何?という人もいるかと思うので、実際のコードでクロージャを使ってみます。
using System;
public class Hello
{
public static void Main()
{
var counter = MakeCounter();
Console.WriteLine(counter.Invoke()); // 1
Console.WriteLine(counter.Invoke()); // 2
Console.WriteLine(counter.Invoke()); // 3
}
private static Func<int> MakeCounter()
{
var num = 0;
Func<int> inc = () => {
num += 1;
return num;
};
return inc;
}
}
MakeCounter
メソッドは外部で定義されたローカル変数num
をインクリメントするメソッドを返します。
その結果、メソッドを呼び出すたびにnum
の値が維持されたままインクリメントすることができます。
このようにメソッドの中で作られるメソッドが外の変数を参照している場合、外で定義した変数を閉じ込めて使用していることからクロージャと呼んだりします。
今回の例でいうと、MakeCounter
で定義したnum
を参照するinc
がクロージャとなります。
クロージャを使ってグローバル変数を駆逐する
クロージャについて説明したところで、実際にクロージャを使ってグローバル変数を駆逐してみます。
例を出すだけなら他にクロージャを紹介しているサイトと変わらないので、ここでは実際の現場でも起こりそうな自体を想定し、クロージャを適用してみようと思います。
(この例題で登場するグローバル変数は実際にはメンバ変数ですが、クラス内のどこからでも参照できるということで、便宜上グローバル変数と呼ぶこととします。)
例題
ある日、システム利用者からA君宛にこんな依頼が届きました。
「画面が開くのに時間がかかるんだけど、もう少し早く開けませんか?」
A君が早速、調査対象の画面を調査してみると、画面に表示するフルネームを取得するのに、以下のクラスのGetFullName
メソッドが利用されており、その中で呼ばれるDBアクセスの部分で時間がかかることが分かりました。
public class UserUtil
{
private int _userId;
public UserUtil(int userId)
{
_userId = userId;
}
// 指定したユーザのフルネームを取得する
// 今回の例題ではこのメソッドが使われている
public string GetFullName()
{
var firstName = GetFirstName();
var lastName = GetLastName();
return $"{lastName} {firstName}";
}
// 指定したユーザの名前を取得する
public string GetFirstName()
{
return Find()["first_name"].ToString();
}
// 指定したユーザの名字を取得する
public string GetLastName()
{
return Find()["last_name"].ToString();
}
// usersテーブルから指定されたユーザIDに紐づくレコードを取得する
private Dictionary<string, object> Find()
{
var connection = new DBConnection();
connection.Open();
// SQLを作成し、DBにアクセスする
// ここで時間がかかる!!
Dictionary<string, object> record = connection.FindBySql(
"SELECT * FROM users WHERE id = ?"
, _userId);
connection.Close();
return record;
}
}
GetFullName
メソッドを呼ぶとDBアクセスが2回起きてしまうため、余計に時間がかかっていると判断したA君は、DBアクセスを1回で済ませられるように1度目に取得したユーザ情報をキャッシュしておこうと考えました。
グローバル変数を使う
A君はグローバル変数を利用すれば、キャッシュを実現できそうだと考え、以下のように修正ました。
public class UserUtil
{
// キャッシュ用のグローバル変数を用意する
private Dictionary<string, object> _cache = null;
// 指定したユーザのフルネームを取得する
// 今回の例題ではこのメソッドが使われている
public string GetFullName()
{
var firstName = GetFirstName();
var lastName = GetLastName();
return $"{lastName} {firstName}";
}
// 指定したユーザの名前を取得する
public string GetFirstName()
{
return Find()["first_name"].ToString();
}
// 指定したユーザの名字を取得する
public string GetLastName()
{
return Find()["last_name"].ToString();
}
// usersテーブルから指定されたユーザIDに紐づくレコードを取得する
private Dictionary<string, object> Find()
{
if(_cache != null)
{
// キャッシュにすでにユーザ情報があればDBアクセスせずにキャッシュを返す
return _cache;
}
var connection = new DBConnection();
connection.Open();
// SQLを作成し、DBにアクセスする
// ここで時間がかかる!!
Dictionary<string, object> record = connection.FindBySql(
"SELECT * FROM users WHERE id = ?"
, _userId);
connection.Close();
// キャッシュしておく
_cache = record;
return record;
}
}
一度DBアクセスして取得したデータをグローバル変数に保持しておき、そのデータを再利用するという方針です。
A君は画面が開くのが早くなったことを確認し、コードレビューをしてもらうように上司に連絡しました。
クロージャを使う
上司からのコードレビューのコメントが返ってきました。
「_cache
変数がグローバルだと誤った使い方で使われてしまうかもしれません。クロージャにしてみてはどうでしょうか?」
確かに、このクラスのことを知っているA君は_cache
変数の使い方を知っていますが、知らない人がこのクラスに機能の追加や修正をした際に、想定外の使い方で_cache
変数を利用してしまう可能性はありそうです。
A君は上司のコメントにあったクロージャを調べて実装し直してみることにしました。
public class UserUtil
{
private int _userId;
public UserUtil(int userId)
{
_userId = userId;
}
// 指定したユーザのフルネームを取得する
// 今回の例題ではこのメソッドが使われている
public string GetFullName()
{
var firstName = GetFirstName();
var lastName = GetLastName();
return $"{lastName} {firstName}";
}
// 指定したユーザの名前を取得する
public string GetFirstName()
{
return Find()["first_name"].ToString();
}
// 指定したユーザの名字を取得する
public string GetLastName()
{
return Find()["last_name"].ToString();
}
// キャッシュするためにFindUsingCacheメソッドで作られるクロージャに処理を移動した
private Dictionary<string, object> Find()
{
return FindUsingCache().Invoke();
}
// usersテーブルから指定されたユーザIDに紐づくレコードを取得する
// キャッシュ対応版
private Func<Dictionary<string, object>> FindUsingCache()
{
// キャッシュ用の変数を用意する
Dictionary<string, object> _cache = null;
// クロージャを作る
Func<Dictionary<string, object>> f = () => {
if(_cache != null)
{
// キャッシュにすでにユーザ情報があればキャッシュを返す
return _cache;
}
var connection = new DBConnection();
connection.Open();
// SQLを作成し、DBにアクセスする
// ここで時間がかかる!!
Dictionary<string, object> record = connection.FindBySql(
"SELECT * FROM users WHERE id = ?"
, _userId);
connection.Close();
// キャッシュに入れておく
_cache = record;
return record;
};
// クロージャを返す
return f;
}
}
Find
メソッドの内容をFindUsingCache
メソッドで作るクロージャに移動し、Find
メソッドはクロージャを呼び出すだけに変更しました。
グローバル変数として定義していた_cache
変数がクロージャを使うことにより、FindUsingCache
メソッドでしか参照することのできないローカル変数に変更されていることがわかると思います。
クロージャに変えたことにより、_cache
変数が想定外の使い方をされる可能性がなくなりました。
クロージャを使うことのメリット
以上の例題を通じて、改めてクロージャを使うことのメリットをまとめてみます。
- グローバル変数をローカル変数に移すことができる
- ローカル変数にすることにより、変数が変更される場所が限定される
- そのため、修正や機能追加の際に考慮する箇所が限定され、保守性が高まる
まとめ
今回はクロージャを使うことでグローバル変数を使わずに、保守性を高くする様子を例題と共に見ていきました。
確かにグローバル変数を使うことでも例題と同じようなことはできます。
しかし、そのグローバル変数が1ヶ月後、半年後、1年後、チームを苦しめているかもしれません。
この記事が保守性の高いコードを書く手助けになれば嬉しいです。
それではまた。
TomoProg