Страницы

Поиск по вопросам

четверг, 1 ноября 2018 г.

Как удобно создавать Immutable Value Object с большим количеством свойств в C#

Задача в том, чтобы с наименьшим количеством кода определить немутируемый (immutable) ValueObject с большим количеством свойств.
Использую NHibernate как ORM, поэтому свойства должны быть virtual и public/protected. Этот объект мэпится к таблице из БД.
Этот объект не должен меняться, но надо, чтобы его можно было создать.
Если свойств не много, то проблем нет, например так:
public class ElectricDevice { public virtual int Id { get; protected set; } public virtual string Name { get; protected set; } public virtual bool IsDecommissioned { get; protected set;}
public ElectricDevice(int id, string name, bool isDecommissioned) { Id = id; Name = name; IsDecommissioned = isDecommissioned; } }
Но если свойств становится много, например, 20, то как-то неудобно, долго писать класс (таких классов к тому же должно быть много). Так же не красиво создавать такой объект через конструктор. Есть какие-нибудь идеи? Может быть в C#6 какой-нибудь новый синтаксис?


Ответ

Если нужен fluent-интерфейс, у меня получилось вот такое решение, основанное на ответе @Stack:
Заводим атрибут, чтобы отмечать классы, к которым нужно строить Builder
namespace BuildCodegen { class CreateBuilderAttribute : Attribute { } }
Помечаем этим атрибутом наш класс:
[CreateBuilder] public class ElectricDevice { public virtual int Id { get; protected set; } public virtual string Name { get; protected set; } public virtual bool IsDecommissioned { get; protected set; }
public ElectricDevice(int id, string name, bool isDecommissioned) { Id = id; Name = name; IsDecommissioned = isDecommissioned; } } Заводим в нашем проекте TextTemplate (в соседнем ответе расписано, как именно это делается), называем его Builders.tt
Для доступа к существующему коду используем CodeModel, а не рефлексию, т. к. у рефлексии есть известные проблемы (несвоевременное обновление, блокировка сборок в памяти).
Итак, помещаем в Builders.tt следующий код:
<# /* hostspecific = true, т. к. мы используем CodeModel Visual Studio */ #> <#@ template debug="false" hostspecific="true" language="C#" #> <#@ assembly name="System.Core" #> <#@ assembly name="System.Data" #> <#@ assembly name="EnvDte" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Text" #> <#@ import namespace="System.Collections" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="EnvDTE" #> <#@ output extension=".cs" #> <# // задаём вручную имя атрибута (как сделать лучше?) var attributeFullName = "BuildCodegen.CreateBuilderAttribute"; var visualStudio = (EnvDTE.DTE)((this.Host as IServiceProvider) .GetService(typeof(EnvDTE.DTE))); var project = (EnvDTE.Project)visualStudio.Solution .FindProjectItem(this.Host.TemplateFile).ContainingProject; var codeModel = project.CodeModel; // получили модель кода: var classes = codeModel.CodeElements.Cast().SelectMany(GetClasses); var classesThatNeedBuilder = classes.Where(c => c.Attributes.Cast() .Any(attr => attr.FullName == attributeFullName)); foreach (var ccl in classesThatNeedBuilder) { var namespaceName = ccl.Namespace.FullName; var className = ccl.Name; var builderName = className + "Builder"; var properties = ccl.Children .OfType() .Select(p => new PropertyDescriptor(p)) .ToList(); // Каждый builder кладём в то же пространство имён, что и // производимый им класс. (Это легко переделать, разумеется.) // Также мы предполагаем, что у производимого класса есть // конструктор, принимающий все атрибуты, с именами, соответствующими // именам свойств, но со строчной буквы (если это не так, нужно // поискать конструктор и имена параметров через CodeModel) #>
namespace <#= namespaceName #> { public class <#= builderName #> { public <#= className #> Build() { return new <#= className #>(<#= string.Join(", ", properties.Select(p => p.LowerName + ": " + p.LowerName)) #>); } <# foreach (var prop in properties) { // для каждого из свойств определяем несущее поле и // fluent-метод его установки #>
<#= prop.Type #> <#= prop.LowerName #>;
public <#= builderName #> With<#= prop.UpperName #>(<#= prop.Type #> <#= prop.LowerName #>) { this.<#= prop.LowerName #> = <#= prop.LowerName #>; return this; } <# } #> } }
<# } #>
<#+ // вспомогательный метод: получаем рекурсивно список классов // мы смотрим только внутри пространств имён, но не внутри других классов // (исключительно из-за лени, ну и нужно для вложенного класса придумать, // куда же класть builder) IEnumerable GetClasses(CodeElement elt) { CodeClass ccl = elt as CodeClass; if (ccl != null) return new[] { ccl }; CodeNamespace cns = elt as CodeNamespace; if (cns != null) return cns.Members.Cast().SelectMany(GetClasses); return Enumerable.Empty(); }
// ну и мелкий класс-обёртка для свойства class PropertyDescriptor { public readonly string Type; public readonly string UpperName; public readonly string LowerName;
public PropertyDescriptor(CodeProperty property) { this.Type = property.Type.AsString; var name = property.Name; this.UpperName = char.ToUpper(name[0]) + name.Substring(1); this.LowerName = char.ToLower(name[0]) + name.Substring(1); } } #> Получаем такой автоматически сгенерированный класс:
namespace BuildCodegen { public class ElectricDeviceBuilder { public ElectricDevice Build() { return new ElectricDevice(id: id, name: name, isDecommissioned: isDecommissioned); }
int id;
public ElectricDeviceBuilder WithId(int id) { this.id = id; return this; }
string name;
public ElectricDeviceBuilder WithName(string name) { this.name = name; return this; }
bool isDecommissioned;
public ElectricDeviceBuilder WithIsDecommissioned(bool isDecommissioned) { this.isDecommissioned = isDecommissioned; return this; } } }
Кстати, сам Entity-класс тоже можно генерировать автоматически.

Комментариев нет:

Отправить комментарий