先日、とあるプロジェクトに参画していて、タイトルにあるようなコーディング規約( 原則という言葉すらありませんでした)が提案されたのですが、全力で阻止しましたw
この記事はリテラル(以降、この記事では直定数と記述します)に対する考察です。

さて、一見、問題無さそうに見えるこのルールですが、私には 守ることが出来ません

何故かと言うと うまく定数名を付けられない直定数が有るからです。

ここまでの内容でピンと来た方には、これ以降の内容は釈迦に説法だと思います。

そうでは無い方は一緒に考えてみて頂けると幸いです。

画面の中央にウィンドウを表示してみよう

突然の謎のタイトルですが、これを行うためのロジックの中にうまく名前を付けられない直定数が登場するので、しぱしお付き合いください。

さて、前提となる要件ですが、

  • ウィンドウの表示位置は、左上の頂点の座標を設定することで変えられます。
  • わかっている情報は画面のサイズとウィンドウのサイズです。
  • 座標やサイズの単位は画素数とします。端数処理はしません。ほぼ中央ならOKとします。

となります。

以下のような状況をイメージしていただけると良いと思います。

画面の中央にウィンドウを表示する
求めたい座標を算出するのに以下のようなロジックを考えました。

var positionLeft = (screenWidth - windowWidth) / 2;
var positionTop = (screenHeight - windowHeight) / 2;

さて、上記コードに登場する直定数の 2ですが、皆さんこれに素敵な名前を付けてみてください。

なぜうまく名前を付けられないのか

良い名前が付けられましたか?
付けることが出来た方はぜひコメントでお知らせください。

私には出来ませんでした。

なぜ出来ないのかと考えてみると、この2マジックナンバーでは無いからなのだと思います。

では、何なのでしょうか?

直定数ということで 2に注目しがちですが、 実はこれは / 2なのです。半分にするという演算操作なのです。

int DivideHalf(int target) => target / 2;

var positionLeft = DivideHalf(screenWidth - windowWidth);
var positionTop = DivideHalf(screenHeight - windowHeight);

直定数を定数化するというよりも、操作として抽象化するアプローチの方が正解なのでは?と思います。
まぁ、実際単に2で割るだけの関数を定義することはほぼ無いでしょう。

int CalcCenteringPosition(int parentSize, int targetSize) => (parentSize - targetSize) / 2;

var positionLeft = CalcCenteringPosition(screenWidth, windowWidth);
var positionTop = CalcCenteringPosition(screenHeight, windowHeight);

それでもCalcCenteringPositionの中に直定数として 2が残ってます。
こういう直定数は数学の公式の中にも沢山出てきますよね。私は数学は苦手なのですが...

苦手なので、これくらいしか例示出来ませんw (数学というか算数だろ!

演算操作の一部として登場する直定数、これは定数化出来ない場合があると思うのですが皆さんはどう思いますか?

ではマジックナンバーとの違いは何でしょう?

結局、説明が必要な謎の数値なのかどうか というあたりになるのではないでしょうか?
半分にするという文脈において / 22は自明。説明不要だということでしょう。
* 0.5でも同様です。

配列のインデックスがゼロオリジンである場合の要素数を求める際の最大インデックス + 11も自明なので定数化しませんよね。ちょっと唐突ですが...w

コード中には 文脈において自明な直定数というものがれっきとして存在するということになるでしょう。

データとしての直定数

とある変換処理の例です。

string ConvertToaruName(string toaruCode)
{
  if (toaruCode == "a") return "あいうえお";
  else if (toaruCode == "b") return "かきくけこ";
  else if (toaruCode == "c") return "さしすせそ";
  else if (toaruCode == "d") return "たちつてと";
  else return "ん?";
}

ひどい例だ...(ぉぃ)
辞書にしろよとかいう意見は聞こえないフリして話を進めます。

さて、直定数だらけですね。それぞれに素敵な名前を...(以下、略)。

コード例は示しませんが、これらに名前を付けることは可能でしょう。おそらく。
でも、皆さんなら名前を付けて定数化しますか?

この関数内に出てくる直定数それぞれは単なるデータであるし、 その文脈の中ではその直定数であること自体がロジックの一部となっていると思いませんか?
こういうケース、私は定数化はしません(したくない~

まとめ

定数化、 そもそもこれってどういうことでしょうね。

  • マジックナンバーに名前を付ける ... 抽象化
  • 同じものが2カ所以上登場する場合にタイプセーフにする

これ以外の目的で定数化するケースってどんな場合が考えられるでしょうか?
この記事を書く前に色々考えたのですが、ちょっと思いつきませんでした。
私が従事してきたビジネス業務系以外の分野では、別の目的(例えばパフォーマンスのためーとか、バイナリサイズを抑えるためーとか)が有るかもしれません。
こういう用途で定数化する場合も有るんだよというご意見が有りましたら、ぜひコメントで教えてくださいませませ。

番外編 定数化は出来ても共通化はしにくいものがある

またまた突然ですが、日付をとある書式でフォーマットします。

var timestamp = DateTime.Now.ToString("yyyy/MM/dd HH:mm:dd");

このyyyy/MM/dd HH:mm:dd、書式指定文字列というやつですが、これを定数化してみましょう。

const string TIMESTAMP_FORMAT = "yyyy/MM/dd HH:mm:dd";
:
:
var timestamp = DateTime.Now.ToString(TIMESTAMP_FORMAT);

お、名前が付けられました。これは大丈夫そう。

色々な場面で使いそうだし、共通化しておきましょう!共通化するライブラリは別アセンブリとして提供します。バージョニング問題(定数)もちゃんと考慮に入れて!

// 別アセンブリとして共通化
public static DateTimeFormats
{
  public readonly string TimeStamp = "yyyy/MM/dd HH:mm:dd";
}

// 利用する側のコード
var timestamp = DateTime.Now.ToString(DateTimeFormats.TimeStamp);

日付の部分だけ使いたいんだよねぇ。フォーマットを追加してもらえる?
お安い御用です。

// 別アセンブリとして共通化
public static DateTimeFormats
{
  public readonly string TimeStamp = "yyyy/MM/dd HH:mm:dd";
  public readonly string Date = "yyyy/MM/dd";
}

// 利用する側のコード
var today = DateTime.Today.ToString(DateTimeFormats.Date);

ファイル名の一部として使いたいんだ。スラッシュで区切らなくていい。
了解しました。

// 別アセンブリとして共通化
public static DateTimeFormats
{
  public readonly string TimeStamp = "yyyy/MM/dd HH:mm:dd";
  public readonly string Date = "yyyy/MM/dd";
  public readonly string DateFix = "yyyyMMdd";
}

// 利用する側のコード
var fileName = $"ファイル名{DateTime.Today.ToString(DateTimeFormats.DateFix)}.txt";

ファイル名の一部として時分秒まで入れたいよ。
アイさー。

// 別アセンブリとして共通化
public static DateTimeFormats
{
  public readonly string TimeStamp = "yyyy/MM/dd HH:mm:dd";
  public readonly string Date = "yyyy/MM/dd";
  public readonly string DateFix = "yyyyMMdd";
  public readonly string TimeStampFix = "yyyyMMddHHmmdd";
}

// 利用する側のコード
var fileName = $"ファイル名{DateTime.Today.ToString(DateTimeFormats.TimeStampFix)}.txt";

和暦表示したい。
ふむ...。

// 別アセンブリとして共通化
public static DateTimeFormats
{
  public readonly string TimeStamp = "yyyy/MM/dd HH:mm:dd";
  public readonly string Date = "yyyy/MM/dd";
  public readonly string DateFix = "yyyyMMdd";
  public readonly string TimeStampFix = "yyyyMMddHHmmdd";
  public readonly string JapaneseDate = "ggyy年MM月dd日";
}

// 利用する側のコード
var today = DateTime.Today.ToString(DateTimeFormats.JapaneseDate);

和暦だけど年、月、日はスラッシュで。
....。

// 別アセンブリとして共通化
public static DateTimeFormats
{
  public readonly string TimeStamp = "yyyy/MM/dd HH:mm:dd";
  public readonly string Date = "yyyy/MM/dd";
  public readonly string DateFix = "yyyyMMdd";
  public readonly string TimeStampFix = "yyyyMMddHHmmdd";
  public readonly string JapaneseDate = "ggyy年MM月dd日";
  public readonly string JapaneseDateSeparateSlush = "ggyy/MM/dd日";
}

// 利用する側のコード
var today = DateTime.Today.ToString(DateTimeFormats.JapaneseDateSeparateSlush);

区切り文字をハイフンにしたい!

// 別アセンブリとして共通化
public static DateTimeFormats
{
  public readonly string TimeStamp = "yyyy/MM/dd HH:mm:dd";
  public readonly string Date = "yyyy/MM/dd";
  public readonly string DateFix = "yyyyMMdd";
  public readonly string TimeStampFix = "yyyyMMddHHmmdd";
  public readonly string JapaneseDate = "ggyy年MM月dd日";
  public readonly string JapaneseDateSeparateSlush = "ggyy/MM/dd日";
  public readonly string DateSeparateHyphen = "yyyy-MM-dd";
  public readonly string JapaneseDateSeparateHyphen = "ggyy/MM/dd日";
}

// 利用する側のコード
var today = DateTime.Today.ToString(DateTimeFormats.DateSeparateHyphen);

ハイフンで区切る方がほとんどなんだよね。名前が長いなぁ。
Dateってやつ、何で区切られるんだっけか?

...。


日付に限った話では有りませんが、書式指定文字列というのはそれ自体がロジックの一部です。
似たようなものに正規表現パターンとかもあるでしょう。
とある操作を行うコマンドを文字列などで指定するものですね。
使う側からすれば文字列が重要なのではなく、操作が重要であるのです。

本当によく使うものに限って共通化するのは有りかもしれませんが、ほどほどに。
その場合でも、書式指定文字列を定数化するのではなく、日付のフォーマット関数としてよく使うものだけを提供するのが正解な気がします。

定数を定義する際はスコープに注意しましょうね(色々な意味で)という例でした。