ValueObjectGenerator
この投稿では、C# 9.0で加わったC# Source Generator
とそれを使って開発したValueObjectGenerator
を紹介します。
コードはこちら!
https://github.com/RyotaMurohoshi/ValueObjectGenerator
背景
次のProduct
クラスは、2つのint型のプロパティProductId
とProductCategoryId
を持っています。
publicclassProduct{publicProduct(stringname,intproductId,intproductCategoryId){Name=name;ProductId=productId;ProductCategoryId=productCategoryId;}publicstringName{get;}publicintProductId{get;}publicintProductCategoryId{get;}}
この型のインスタンスの利用シーンにおいて、いくつかの場所ではProductId
が必要で、他の場所ではProductCategoryId
が必要でしょう。どちらもint型で名前が似ているので、うっかりとProductId
とProductCategoryId
を取り違えてしまうかもしれません.
この取り違え型を防ぐにはどうしたらいいでしょうか?一つの方法としては、次のようなProductId
型とCategoryId
型を作り、それらを利用することです。これらの型を利用することで、コンパイラはProductId
プロパティとProductCategoryId
プロパティの取り違えを検出し、プログラム上のミスを防ぐことができます。このようにValueObjectクラス(もしくは、Wrapperクラス)を作成し利用することで、int型やstring型のプロパティの取り違えやミスを防ぐことができます。
ProductId
型とCategoryId
型は次のような感じになります。
publicsealedclassProductId:IEquatable<ProductId>{publicintValue{get;}publicProductId(intvalue){Value=value;}publicoverrideboolEquals(objectobj)=>ReferenceEquals(this,obj)||objisProductIdother&&Equals(other);publicoverrideintGetHashCode()=>Value.GetHashCode();publicoverridestringToString()=>Value.ToString();publicstaticbooloperator==(ProductIdleft,ProductIdright)=>Equals(left,right);publicstaticbooloperator!=(ProductIdleft,ProductIdright)=>!Equals(left,right);publicboolEquals(ProductIdother){if(ReferenceEquals(null,other))returnfalse;if(ReferenceEquals(this,other))returntrue;returnValue==other.Value;}publicstaticexplicitoperatorProductId(intvalue)=>newProductId(value);publicstaticexplicitoperatorint(ProductIdvalue)=>value.Value;}publicclassCategroyId:IEquatable<CategroyId>{publicintValue{get;}publicCategroyId(intvalue){Value=value;}publicoverrideboolEquals(objectobj)=>ReferenceEquals(this,obj)||objisCategroyIdother&&Equals(other);publicoverrideintGetHashCode()=>Value.GetHashCode();publicoverridestringToString()=>Value.ToString();publicstaticbooloperator==(CategroyIdleft,CategroyIdright)=>Equals(left,right);publicstaticbooloperator!=(CategroyIdleft,CategroyIdright)=>!Equals(left,right);publicboolEquals(CategroyIdother){if(ReferenceEquals(null,other))returnfalse;if(ReferenceEquals(this,other))returntrue;returnValue==other.Value;}publicstaticexplicitoperatorCategroyId(intvalue)=>newCategroyId(value);publicstaticexplicitoperatorint(CategroyIdvalue)=>value.Value;}
このProductId
型とCategoryId
型を使った、Product
型はこんな感じ。
publicclassProduct{publicProduct(stringname,ProductIdproductId,CategroyIdproductCategoryId){Name=name;ProductId=productId;ProductCategoryId=productCategoryId;}publicstringName{get;}publicProductIdProductId{get;}publicCategroyIdProductCategoryId{get;}}
このようにValueObjectクラス(もしくは、Wrapperクラス)を作成し利用することでコンパイルエラーを防ぐことができます。よかった、よかった!けれど、これらのコードは非常に大量のボイラープレートで溢れていますね。長いですね!ProductId
型とCategoryId
型。長すぎます!これらのボイラープレートコードは、他の大切な意味のあるコードを読む際にとても邪魔です。
さて、こんなふうなボイラープレートコードには、C# 9.0から加わったC# Source Generator
が大活躍します。
C# Source Generatorはこんなの
C# Source Generator
は、ビルド時にC#のソースコードを生成する仕組みです。
- メインのプロジェクトがビルドされる前にコード生成
- コード生成するために必要な入力値はコンパイル時に必要
- 出力結果は、プロジェクトの一部となる
- IDEにおいて、生成したコードの宣言にジャンプもできる
- ILではなくC#を生成するので、デバックがすごい楽
- 既存のソースコードを上書きしたりけしたりすることはできない
このC# Source Generator
を使うことで、プログラマティカルにコード生成をすることができます。C# Source Generator
をつかうことで、ボイラープレートのコードは非常に簡単になります。
他、参考。
- Introducing C# Source Generators
- New C# Source Generator Samples
- Source Generators Cookbook
- C# 9.0の新機能 コード ジェネレーターのサポート
使い方
さてそんなC# Source Generator
を使って、私が開発した、ValueObjectGenerator
を紹介します。
次のように IntValueObject
属性をクラスに付与します。
usingValueObjectGenerator;[IntValueObject]publicpartialclassProductId{}
そうすると次のようなコードが生成されます。
publicpartialclassProductId:IEquatable<ProductId>{publicintValue{get;}publicProductId(intvalue){Value=value;}publicoverrideboolEquals(objectobj)=>ReferenceEquals(this,obj)||objisProductIdother&&Equals(other);publicoverrideintGetHashCode()=>Value.GetHashCode();publicoverridestringToString()=>Value.ToString();publicstaticbooloperator==(ProductIdleft,ProductIdright)=>Equals(left,right);publicstaticbooloperator!=(ProductIdleft,ProductIdright)=>!Equals(left,right);publicboolEquals(ProductIdother){if(ReferenceEquals(null,other))returnfalse;if(ReferenceEquals(this,other))returntrue;returnValue==other.Value;}publicstaticexplicitoperatorProductId(intvalue)=>newProductId(value);publicstaticexplicitoperatorint(ProductIdvalue)=>value.Value;}
ProductId
は、ValueObjectクラス(もしくは、Wrapperクラス)です。
次のように、ValueObjectクラス(もしくは、Wrapperクラス)を定義するためにあった、大量のボイラープレートコードはなくなりました。
[IntValueObject]publicclassProductId{}[IntValueObject]publicclassCategoryId{}publicclassProduct{publicProduct(stringname,ProductIdproductId,CategoryIdproductCategoryId){Name=name;ProductId=productId;ProductCategoryId=productCategoryId;}publicstringName{get;}publicProductIdProductId{get;}publicCategoryIdProductCategoryId{get;}}
こんなふうに、ValueObjectGenerator
を使えば、 ValueObjectクラス(もしくは、Wrapperクラス)の生成に必要な大量のボイラープレートコードを排除することが可能です。
ValueObjectGeneratorの機能
ValueObjectGeneratorの機能の紹介をします。
- サポートするValue型
- クラスと構造体のサポート
- プロパティの名前指定
サポートするValue型
ValueObjectGenerator
は、int型以外のValueObjectクラス(もしくは、Wrapperクラス)をサポートしています。
[StringValueObject]publicpartialclassProductName{}publicsealedpartialclassProductName:System.IEquatable<ProductName>{publicstringValue{get;}publicProductName(stringvalue){Value=value;}publicoverrideboolEquals(objectobj)=>ReferenceEquals(this,obj)||objisProductNameother&&Equals(other);publicoverrideintGetHashCode()=>Value.GetHashCode();publicoverridestringToString()=>Value.ToString();publicstaticbooloperator==(ProductNameleft,ProductNameright)=>Equals(left,right);publicstaticbooloperator!=(ProductNameleft,ProductNameright)=>!Equals(left,right);publicboolEquals(ProductNameother){if(ReferenceEquals(null,other))returnfalse;if(ReferenceEquals(this,other))returntrue;returnValue==other.Value;}publicstaticexplicitoperatorProductName(stringvalue)=>newProductName(value);publicstaticexplicitoperatorstring(ProductNamevalue)=>value.Value;}
次のテープルは、提供している属性とそれに対応する型を示しています。
属性 | 型 |
---|---|
StringValueObject | string |
IntValueObject | int |
LongValueObject | long |
FloatValueObject | float |
DoubleValueObject | double |
クラスと構造体のサポート
ValueObjectGenerator
は、クラスと構造体、両方の生成のサポートをします。
まずは、クラス利用例。
[IntValueObject]publicpartialclassIntClassSample{}
クラスの場合、次のようなコードが生成されます。
publicsealedpartialclassIntClassSample:System.IEquatable<IntClassSample>{publicintValue{get;}publicIntClassSample(intvalue){Value=value;}publicoverrideboolEquals(objectobj)=>ReferenceEquals(this,obj)||objisIntClassSampleother&&Equals(other);publicoverrideintGetHashCode()=>Value.GetHashCode();publicoverridestringToString()=>Value.ToString();publicstaticbooloperator==(IntClassSampleleft,IntClassSampleright)=>Equals(left,right);publicstaticbooloperator!=(IntClassSampleleft,IntClassSampleright)=>!Equals(left,right);publicboolEquals(IntClassSampleother){if(ReferenceEquals(null,other))returnfalse;if(ReferenceEquals(this,other))returntrue;returnValue==other.Value;}publicstaticexplicitoperatorIntClassSample(intvalue)=>newIntClassSample(value);publicstaticexplicitoperatorint(IntClassSamplevalue)=>value.Value;}
次に、構造体の利用例。
[IntValueObject]publicpartialstructIntStructSample{}
構造体の場合、次のようなコードが生成されます。
publicpartialstructIntStructSample:System.IEquatable<IntStructSample>{publicintValue{get;}publicIntStructSample(intvalue){Value=value;}publicoverrideboolEquals(objectobj)=>ReferenceEquals(this,obj)||objisIntStructSampleother&&Equals(other);publicoverrideintGetHashCode()=>Value.GetHashCode();publicoverridestringToString()=>Value.ToString();publicstaticbooloperator==(IntStructSampleleft,IntStructSampleright)=>Equals(left,right);publicstaticbooloperator!=(IntStructSampleleft,IntStructSampleright)=>!Equals(left,right);publicboolEquals(IntStructSampleother){returnValue==other.Value;}publicstaticexplicitoperatorIntStructSample(intvalue)=>newIntStructSample(value);publicstaticexplicitoperatorint(IntStructSamplevalue)=>value.Value;}
プロパティの名前指定
生成するValueObjectクラス(もしくは、Wrapperクラス)が持つ、プロパティの名前も指定することができます。
次のようにすることで、
[StringValueObject(PropertyName="StringValue")]publicpartialclassCustomizedPropertyName{}
次のような型が生成されます。
publicsealedpartialclassCustomizedPropertyName:System.IEquatable<CustomizedPropertyName>{publicstringStringValue{get;}publicCustomizedPropertyName(stringvalue){StringValue=value;}publicoverrideboolEquals(objectobj)=>ReferenceEquals(this,obj)||objisCustomizedPropertyNameother&&Equals(other);publicoverrideintGetHashCode()=>StringValue.GetHashCode();publicoverridestringToString()=>StringValue.ToString();publicstaticbooloperator==(CustomizedPropertyNameleft,CustomizedPropertyNameright)=>Equals(left,right);publicstaticbooloperator!=(CustomizedPropertyNameleft,CustomizedPropertyNameright)=>!Equals(left,right);publicboolEquals(CustomizedPropertyNameother){if(ReferenceEquals(null,other))returnfalse;if(ReferenceEquals(this,other))returntrue;returnStringValue==other.StringValue;}publicstaticexplicitoperatorCustomizedPropertyName(stringvalue)=>newCustomizedPropertyName(value);publicstaticexplicitoperatorstring(CustomizedPropertyNamevalue)=>value.StringValue;}
今後の改善案
やりたいなと思っているのは、
- IComparableのサポート
- JSON serializer/deserializer
- 他のValueタイプ
- 算術演算のサポート
などです。
あと以前、ufcppさんのYoutube配信に、お邪魔した時に、ufcppとゲストのxin9leさんに、このValueObjectGenerator
を紹介した時に、いろいろご指摘をいただきました。その番組の指摘のおかげで
- string interpolationを使うとパフォーマンスが悪いので、パフォーマンスもよくする
- 余分なメンバを持っていたらコンパイルエラーにするアナライザー
も入れたほうがいいことがわかりました。ufcppさん、xin9leさん、ありがとうございます。
もしかしたら、recordでいいかも?
先に紹介した、ufcppさんのYoutube配信番組で、「1要素Recordとそんなに変わらない」という指摘もいただきました。
publicrecordProductId(intValue){}
いや、もうその通りなんですよね。
ただ、Recordではできないものがあります。それは、「オーバーライドを認めないという」ものです。Recordではできないのでそれがメリットになります。と、ufcppさんとxin9leさんにご指摘いただきました!
ありがとうございます!
まとめ
この投稿では、C# 9.0で加わったC# Source Generator
とそれを使って開発したValueObjectGenerator
を紹介しました。
https://github.com/RyotaMurohoshi/ValueObjectGenerator
現在、ValueObjectGeneratorは開発・改善途中です。「便利じゃん!」「使いたい!」という方は、ぜひGitHubでStarをください!励ましになります!
ご意見、ご要望があれば、GitHubのissueか、Twitterの@RyotaMurohoshiまで!