Страницы

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

пятница, 20 декабря 2019 г.

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

#c_sharp


Задача в том, чтобы с наименьшим количеством кода определить немутируемый (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 какой-нибудь новый
синтаксис?
    


Ответы

Ответ 1



Если нужен 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-класс тоже можно генерировать автоматически.

Ответ 2



если свойств становится много, например, 20, то как-то неудобно, долго писать класс Если надо определить класс-обертку для таблицы из БД, в котором много свойств, то можно использовать T4 -- генератор кода в Visual Studio. Для этого в Visual Studio надо нажать Ctrl+Shift+A, Ctrl+E, набрать t4, и выбрать Text Template. В созданном файле TextTemplate.tt указать следующий код: <#@ template debug="false" hostspecific="false" language="C#" #> <#@ assembly name="System.Core" #> <#@ assembly name="System.Data" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Text" #> <#@ import namespace="System.Collections.Generic" #> <#@ output extension=".cs" #> <# var cs = @"Data Source=(localdb)\DB;Initial Catalog=Test;Integrated Security=True;"; var cn = new System.Data.SqlClient.SqlConnection(cs); // тут читаем схему базы дынных и создаем коллекцию имен и типов свойств // ... var ps = new [] { new [] { "p1", "int" }, new [] { "p2", "string" } // ... много других элементов }; #> namespace App { public class Wrapper { <# foreach(var p in ps) { #> public virtual <#= p[1] #> <#= p[0] #> { get; protected set; } <# } #> } } При сохранении TextTemplate.tt будет создан TextTemplate.сs namespace App { public class Wrapper { public virtual int p1 { get; protected set; } public virtual string p2 { get; protected set; } } }

Ответ 3



На всякий случай, напишу про то как можно избежать прописывание вручную многих параметров в конструктор. Если что, то критикуйте. Объект: public class RoadDevice { public virtual int Id { get; protected set; } public virtual string Name { get; protected set; } public virtual bool IsDecommissioned { get; protected set;} protected RoadDevice() { } } Билдер: //Этот объкт immutable и в перспективе достаточно большой, поэтому для него можно сделать билдер для удобства internal class RoadDeviceBuilder : RoadDevice { public RoadDeviceBuilder() : base() { } public RoadDeviceBuilder WithIdAndName(int id, string name) { this.Id = id; this.Name = name; return this; } public RoadDeviceBuilder WithIsDecomissionedFlag(bool isDecommissioned) { this.IsDecommissioned = isDecommissioned; return this; } public RoadDevice Build() { return this; } }

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

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