【C#】List<T>(Tはユーザ定義型)でパラメータを受ける実装を考える(考えるだけ)
はじめに
List<T>
(Tはユーザ定義型)でパラメータを受ける実装を考える
その際にどうすれば処理が散らからないようになるのか?とか何がベストプラクティスなのか?とか考える
考えるだけ
VS起動しないでコンパイラ通さず書いたコードなので通らなかったらごめんなさい(暇な時に実際つくる)
長いあらすじ・問題提起
例えばこういうパラメータクラスと、それをパラメータとして受ける(≒モデルバインドする)コントローラ及びメソッドがあるとする
public class Param { List<Person> PersonList {get; set;} } public class Person { public string Name {get; set;} public int Age {get; set;} }
public class SampleController : Controller { [Route("/Sample")] public IActionResult Index(Param param) => Content("hoge"); }
ここでいうList<Person> PersonList
がユーザ定義型のリストになる
そうしたコントローラに例えばこういうリクエストをすると
# 分かりやすく改行 /sample ?personList[0].Name=john&personList[0].Age=10 &personList[1].Name=tom&personList[1].Age=9 # name は省略 &tom&personList[1].Age=0
PersonListはこんな感じのオブジェクトとしてバインドされる
// param.PersonList = new List<Person>() { new Person { Name = "john", Age = 10 }, new Person { Name = "tom", Age = 9 }, new Person { Name = "", Age = 9 }, }
次にこのList<Person>
の中には「必ず1人は名前ありかつ10歳以上のPerson
が含まれていないといけない!」というValidationルールを設ける
※例えば体験型施設の申し込みとか、そういうものに応募する時みたいなイメージ
(そうするとPersonという命名がイケてないけど、今回は許して)
そんな条件はアプリケーションサービスの中で弾け!という話かもだけど、今回はモデルバインド時のValidationで対応しよう
例えばこのWebアプリケーションがガッツリ体験型施設申し込みサービス、みたいな感じだったらList<Person>
という括りを強固に保有し、そこにValidationをかけることにも違和感は少ない(と思うよね!)
public class Param : IValidatableObject { List<Person> PersonList {get; set;} public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { ValidatePerson().ToList().ForEach(x => yield return x); } private IEnumerable<ValidationResult> ValidatePerson() { // error! null or empty if (PersonList == null || !PersonList.Any()) { yield return new ValidationResult("", new[] { nameof(PersonList) }); } // rule: at least one person has name and over the age of 10 is required var namedOver10AgePerson = GetNamedPersons().Where(person => person.Age >= 10); if (!namedOver10AgePerson.Any()) { yield return new ValidationResult("", new[] { nameof(PersonList) }); } } // something additional method // public: it could be used in other classes public IEnumerable<Person> GetNamedPersons() => this.PersonList.Where(person => !string.IsNullOrEmpty(person.Name)); }
こんな感じにValidationとか色々なメソッドが増えるとParamクラスがどんどん肥大化していってしまう
大抵の場合はこのまま実装すると思うんだけど、Param
クラスの中に他のフィールドも増えだすと散らかり具合が大変なことになってくる
#region
で臭いものに蓋をせずに問題に向き合おう
対応策1 Listの拡張クラスをつくる
これを防ぐためには方法が2通りあるかな。と思う
まず1つ目の方法は諸々の処理をList<Person>
の拡張クラスに逃がすこと
public class Person { public string Name {get; set;} public int Age {get; set;} } public static class PersonListExtensions { public static IEnumerable<ValidationResult> ValidatePerson(this IEnumerable<Person> value) { // error! null or empty if (value == null || !value.Any()) { yield return new ValidationResult("", new[] { "PersonList" }); } // rule: at least one person has name and over the age of 10 is required var namedOver10AgePerson = value.GetNamed().Where(person => person.Age >= 10); if (!namedOver10AgePerson.Any()) { yield return new ValidationResult("", new[] { "PersonList" }); } } public static IEnumerable<Person> GetNamed(this IEnumerable<Person> value) => value.Where(person => !string.IsNullOrEmpty(person.Name)); }
やっていることは簡単かな
でもList<Person>
という括りがちょっと弱いかも
List<Person>
に名前がついていないから、このリストが即ち何を表したオブジェクトなのかが伝わりにくい?
対応策2 Listを有する値オブジェクトをつくる
2つ目の方法はList<Person>
をまるっと一つのクラスのようにすること
DDD文脈だと値オブジェクトとか言われてる気がする
public class Param { Persons PersonList {get; set;} } public class Persons { List<Person> Values {get; set;} // Maybe I should use Complete Constructor Pattern } public class Person { public string Name {get; set;} public int Age {get; set;} }
で、Persons
に色々処理をもたせる
public class Persons : IValidatableObject { List<Person> Values {get; set;} public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { // error! null or empty if (this.Values == null || !this.Values.Any()) { yield return new ValidationResult("", new[] { nameof(Persons) }); } // rule: at least one person has name and over the age of 10 is required var namedOver10AgePerson = GetNamed().Where(person => person.Age >= 10); if (!namedOver10AgePerson.Any()) { yield return new ValidationResult("", new[] { nameof(Persons) }); } } public IEnumerable<Person> GetNamed() => this.Values.Where(person => !string.IsNullOrEmpty(person.Name)); }
実際はPersons
ってよりもっといい名前をつけよう
こっちだとより収まりが良い 気がする
気がするけど論理的に説明できない
クラスというかドメインに責務がまとまっていて、
責務というかそうした振る舞いを定義するという点ではDDD的にきれいだと思う
しかし一番の問題は自前でカスタムモデルバインダを定義しないとパラメータをPersons
にモデルバインドできないこと!!
この実装の話はまた別途
まとめ
どっちがいいんでしょうね
List<Person>
を値オブジェクトとするパターンはその括り(リスト)に名前をつけられることに強い意味があるように思う
実際運用していくとメリット・デメリットがはっきりしてくるんでしょうけど、まだこうした実装に水をあげ花を咲かせたことはない