本記事は、オブジェクト指向設計で nullを返したくなるときというテーマで書きます。
オブジェクト指向設計においては、nullを返してはいけない ということを聞いたことがあります。
↓の書籍にも明記されています。この書籍は、ぼくがすごく参考にしているものの1冊です。
・nullを渡さない/nullを返さない
現場で役立つシステム設計の原則 第5章 アプリケーション機能を組み立てる P. 166
それでも、nullを返したくなるときはあります。そのときどのように対応すればよいのでしょうか?
結論として、nullを返したくなったときは、値がないことを表す型を戻り値とすることです。これを約束事とすることで、未定義値を返すことができ、コードをすっきりさせることができます。
サンプルの言語はC#です。
null を返したくなるとき
例えば、
ファーストクラスコレクションクラスにて、条件と一致する値を取得する
なる処理をつくる場合、一致するものがないときnullを返したくなります。
以下の例で考えてみます。IDと名称を持った、地図記号(MapSymbol) 一覧クラスで、IDと一致する地図記号を取得するメソッドを実装します。
.NET Framework 4.8.1 を使用しています。
namespace NullReturnNG
{
/// <summary>
/// 地図記号
/// </summary>
class MapSymbol
{
/// <summary>ID</summary>
private int id;
/// <summary>名称</summary>
public string name { get; }
public MapSymbol(int id, string name)
{
this.id = id;
this.name = name;
}
public bool EqualId( int id )
{
return this.id == id;
}
}
}
using System.Collections.Generic;
using System.Linq;
namespace NullReturnNG
{
/// <summary>
/// 地図記号一覧
/// </summary>
class MapSymbols
{
private List<MapSymbol> list;
public MapSymbols(List<MapSymbol> list)
{
this.list = list;
}
/// <summary>
/// IDと一致する地図記号の名称を返す
/// </summary>
/// <remarks>存在しない場合はnullを返す</remarks>
/// <param name="id"></param>
/// <returns>地図記号の名称</returns>
public string Search(int id)
{
MapSymbol mapSymbol = list.Where(obj => obj.EqualId(id)).FirstOrDefault();
if( mapSymbol == null)
{
return null;
}
return mapSymbol.name;
}
}
}
idと一致する地図記号がないときは、nullを返したくなります。
でも、存在しないときはnullを返すのが分かりやすいと思います。存在しない=nullとするのは同じプロジェクトメンバーにも伝わりやすいです。
空文字だったり、None 文字を返すというのもありますが、余計なルールを追加することになり、あまりやりたくありません。他のクラスのメソッドで、別のルールが混在する可能性もあります。
例外を発生させるという対策もありますが、メソッド内で例外を発生させたくないこともあります。
Null許容型を使用する
1つ目の対策として、Null許容型を使用することが考えられます。基になる型に対し、未定義の値を表したいときに使います。
Null許容参照型はC#8.0 以降で使用できます。
.NET 5.0以降。.NET Core3.0 以降。.NET Standard 2.1以降。.NET Frameworkは非対応。
Null許容型を使用して、先ほどのSearchメソッドの戻り値をstring 型→string Null許容型に変更します。
Null許容参照型を使用するため、環境を.NET Framework 4.81 → .NET 6.0 に変更します。
namespace NullReturnNG2
{
/// <summary>
/// 地図記号一覧
/// </summary>
internal class MapSymbols
{
private List<MapSymbol> list;
public MapSymbols(List<MapSymbol> list)
{
this.list = list;
}
/// <summary>
/// IDと一致する地図記号の優先度を返す
/// </summary>
/// <param name="id"></param>
/// <returns>地図記号の名称</returns>
public string? Search(int id)
{
string? name = list.Where(obj => obj.EqualId(id)).FirstOrDefault()?.name;
return name;
}
}
}
戻り値をNull許容型( string? ) とすることで、戻り値はnullの可能性があるということを表明します。
逆に言えば、戻り値がstring型ならnullの可能性がないです。
戻り値の型に値がないことを表すことができます。
「存在しない場合はnullを返す」というコメントも消すことができました。 string?はこのコメントの内容を含んでいます。
契約による設計ということで、約束事として、プロジェクトメンバーと共有できていれば問題ないはずです。
C#言語の例ですが、多言語でも同じような方法ができるかもしれません。
Option 型を使用する
もう1つの対策として、Option型を使用する方法をご紹介します。Null許容型を使う方法と似ています。
C#8.0 が使用できないときに使えます。
Option型は、値がないことを表すことができるものです。
↓のOption型を使って、先ほどのSearch関数を実装します。
環境を.NET Framework 4.81 に戻します。
using System.Collections.Generic;
using System.Linq;
namespace NullReturnNG
{
/// <summary>
/// 地図記号一覧
/// </summary>
class MapSymbols
{
private List<MapSymbol> list;
public MapSymbols(List<MapSymbol> list)
{
this.list = list;
}
/// <summary>
/// IDと一致する地図記号の優先度を返す
/// </summary>
/// <remarks>存在しない場合はnullを返す</remarks>
/// <param name="id"></param>
/// <returns>地図記号の名称</returns>
public IOption<string> Search(int id)
{
MapSymbol mapSymbol = list.Where(obj => obj.EqualId(id)).FirstOrDefault();
if( mapSymbol == null)
{
return new None<string>();
}
return new Some<string>(mapSymbol.name);
}
}
}
戻り値をIOption<string> 型とすることで、Null許容型と同じく、stringの値がないかもしれないということを表すことができます。
まとめ
オブジェクト指向設計で nullを返したくなるときというテーマで書きます。
結論として、nullを返したくなったときは、値がないことを表す型を戻り値とすることです。これを約束事とすることで、未定義値を返すことができ、コードをすっきりさせることができます。
本記事の続きは↓。
コメント