Как реализовать принцип СУХОГО при использовании ключевого слова using?


23

Рассмотрим эти методы:

public List<Employee> GetAllEmployees()
{
    using (Entities entities = new Entities())
    {
        return entities.Employees.ToList();
    }
}

public List<Job> GetAllJobs()
{
    using (Entities entities = new Entities())
    {
        return entities.Jobs.ToList();
    }
}

public List<Task> GetAllTasksOfTheJob(Job job)
{
    using (Entities entities = new Entities())
    {
        return entities.Tasks.Where(t => t.JobId == job.Id).ToList();
    }
}

Использование блока такое же и повторялось здесь 3 раза (конечно, более 100 раз в реальном приложении). Как можно реализовать принцип DRY (не повторять себя) для usingблока? Считается ли это нарушением принципа СУХОЙ вообще?

Обновление: я не говорю о том, что было реализовано внутри usingблока. То, что я на самом деле имею в виду, это using (Entities entities = new Entities()). Эта строка повторяется 100 и более раз.


2
это C #? Ответ на ваш вопрос может зависеть от языка
Дэвид

Да, Дэвид, извини, что я не упомянул свой язык. Как это может повлиять на ответ?
Саид Нимати

некоторые языки могут иметь особый синтаксис, который может помочь вам немного повлиять на ваш код. Я не знаю C #, но в Ruby, я думаю, вы могли бы использовать блоки для разложения на часть использования.
Дэвид

Оператор using фактически обеспечивает поддержку языка C # для применения принципа DRY, чтобы избежать повторяющегося кодирования при управлении удалением ресурсов с помощью шаблона проектирования Dispose . Это не значит, что мы не можем найти способы сделать вещи более сухими! Лично я считаю DRY рекурсивным процессом.
Джон Тоблер

Ответы:


24

Одна идея заключалась бы в том, чтобы обернуть его функцией, которая принимает Func.

Что-то вроде этого

public K UsingT<T,K>(Func<T,K> f) where T:IDisposable,new()
{
    using (T t = new T())
    {
        return f(t);
    }
}

Тогда ваш код выше становится

public List<Employee> GetAllEmployees()
{
    return UsingT<Entities,List<Employee>>(e=>e.Employees.ToList());
}

public List<Job> GetAllJobs()
{
    return UsingT<Entities,List<Job>>(e=>e.Jobs.ToList());
}

public List<Task> GetAllTasksOfTheJob(Job job)
{
    return UsingT<Entities,List<Task>>(e=>e.Tasks.Where(t => t.JobId == job.Id).ToList());
}

Я также сделал параметр Entitiesтипа, потому что я предполагаю, что у вас есть более одного типа, с которым вы делаете это. Если это не так, вы можете удалить его и просто использовать тип param для возвращаемого типа.

Честно говоря, этот код вообще не помогает читабельности. По моему опыту, все больше Jr коллеги тоже испытывают трудности с этим.

Обновление Некоторые дополнительные варианты помощников, которые вы могли бы рассмотреть

//forget the Entities type param
public T UsingEntities<T>(Func<Entities,T> f)
{
    using (Entities e = new Entities())
    {
        return f(e);
    }
}
//forget the Entities type param, and return an IList
public IList<T> ListFromEntities<T>(Func<Entities,IEnumerable<T>> f)
{
    using (Entities e = new Entities())
    {
        return f(e).ToList();
    }
}
//doing the .ToList() forces the results to enumerate before `e` gets disposed.

1
+1 Хорошее решение, хотя оно не решает актуальную проблему (не входит в исходный вопрос), то есть много раз Entities.
back2dos

@Doc Браун: Я думаю, что я победил вас с именем функции. Я IEnumerableисключил эту функцию на случай, если какие-либо IEnumerableсвойства T не будут возвращены вызывающим объектом, но вы правы, он немного его очистит. Возможно, IEnumerableбыло бы неплохо иметь помощника и для Single, и для результатов. Тем не менее, я все еще думаю, что это замедляет распознавание того, что делает код, особенно для тех, кто не привык использовать много обобщений и лямбд (например, ваших коллег, которые НЕ работают на SO :))
Brook

+1 Я думаю, что такой подход хорош. И читабельность может быть улучшена. Например, поместите «ToList» WithEntities, используйте Func<T,IEnumerable<K>>вместо него Func<T,K>и дайте «WithEntities» более подходящее имя (например, SelectEntities). И я не думаю, что «сущности» должны быть здесь общим параметром.
Док Браун

1
Чтобы это работало, ограничения должны быть такими where T : IDisposable, new(), как того usingтребует IDisposableработа.
Энтони Пеграм

1
@ Энтони Пеграм: исправлено, спасибо! (это то, что я получаю за кодирование C # в окне браузера)
Brook

23

Для меня это все равно, что беспокоиться о том, что вы должны несколько раз просматривать одну и ту же коллекцию: это просто то, что вам нужно сделать. Любая попытка абстрагировать его дальше сделает код гораздо менее читабельным.


Отличная аналогия @Ben. +1
Саид Нимати

3
Я не согласен, извините. Прочитайте комментарии ОП об объеме транзакции и подумайте о том, что вам нужно делать, когда вы написали 500-кратный код такого рода, а затем заметьте, что вам придется менять одну и ту же вещь 500 раз. Этот вид повторения кода может быть в порядке, только если у вас <10 ​​из этих функций.
Док Браун

Да, и не стоит забывать, что если вы для каждой коллекции более 10 раз в очень похожей манере с похожим кодом внутри своего для каждого, вы должны определенно подумать над некоторым обобщением.
Док Браун

1
Для меня это выглядит так, будто вы делаете трехслойный лайнер ... вы все еще повторяетесь.
Джим Вольф

Это зависит от контекста, если, к примеру, foreachслишком большая коллекция или логика в foreachцикле занимает много времени, например. Девиз, который я пришел принять: не зацикливайся, но всегда помни о своем подходе
Coops

9

Похоже, вы путаете принцип «Один раз и только один раз» с принципом СУХОЙ. Принцип СУХОЙ гласит:

Каждая часть знаний должна иметь одно, однозначное, авторитетное представление в системе.

Однако принцип «Один раз и только один раз» немного отличается.

Принцип [СУХОЙ] похож на OnceAndOnlyOnce, но с другой целью. С OnceAndOnlyOnce вам предлагается провести рефакторинг для устранения дублированного кода и функциональности. С DRY вы пытаетесь идентифицировать единый окончательный источник всех знаний, используемых в вашей системе, а затем использовать этот источник для создания соответствующих экземпляров этих знаний (код, документация, тесты и т. Д.).

Принцип СУХОГО обычно используется в контексте реальной логики, а не столько избыточен при использовании операторов:

Сохранять структуру программы DRY сложнее и ниже. Это бизнес-правила, операторы if, математические формулы и метаданные, которые должны появляться только в одном месте. Вещи WET - HTML-страницы, избыточные тестовые данные, запятые и разделители {} - легко игнорируются, поэтому их СУШКА менее важна.

Источник


7

Я не вижу использования usingздесь:

Как насчет:

public List<Employee> GetAllEmployees() {
    return (new Entities()).Employees.ToList();
}
public List<Job> GetAllJobs() {
    return (new Entities()).Jobs.ToList();
}
public List<Task> GetAllTasksOfTheJob(Job job) {
    return (new Entities()).Tasks.Where(t => t.JobId == job.Id).ToList();
}

Или даже лучше, так как я не думаю, что вам нужно каждый раз создавать новый объект.

private Entities entities = new Entities();//not sure C# allows for that kind of initialization, but you can do it in the constructor if needed

public List<Employee> GetAllEmployees() {
    return entities.Employees.ToList();
}
public List<Job> GetAllJobs() {
    return entities.Jobs.ToList();
}
public List<Task> GetAllTasksOfTheJob(Job job) {
    return entities.Tasks.Where(t => t.JobId == job.Id).ToList();
}

Что касается нарушения DRY: DRY не применяется на этом уровне. На самом деле ни один принцип на самом деле не действует, кроме принципа читабельности. Попытка применить DRY на этом уровне на самом деле является лишь архитектурной микрооптимизацией, которая, как и всякая микрооптимизация, является просто избавлением от велосипеда и не решает никаких проблем, но даже рискует внедрять новые.
Исходя из собственного опыта, я знаю, что если вы попытаетесь уменьшить избыточность кода на этом уровне, вы окажете негативное влияние на качество кода, запутывая то, что было действительно ясным и простым.

Редактировать:
ОК. Таким образом, проблема на самом деле не в использовании выражения, а в зависимости от объекта, который вы создаете каждый раз. Я бы предложил ввести конструктор:

private delegate Entities query();//this should be injected from the outside (upon construction for example)
public List<Employee> GetAllEmployees() {
    using (var entities = query()) {//AFAIK C# can infer the type here
        return entities.Employees.ToList();
    }
}
//... and so on

2
Но @ back2dos, есть много мест, где мы используем using (CustomTransaction transaction = new CustomTransaction())блок кода в нашем коде, чтобы определить объем транзакции. Это нельзя связать в один объект, и в каждом месте, где вы хотите использовать транзакцию, вы должны написать блок. А что если вы захотите изменить тип этой транзакции CustomTransactionна BuiltInTransactionболее чем 500 методов? Мне кажется, что это повторяющаяся задача и пример нарушения принципа СУХОЙ.
Саид Нимати

3
«Использование» здесь закрывает контекст данных, поэтому отложенная загрузка невозможна вне этих методов.
Стивен Стрига

@Saeed: это когда вы смотрите на внедрение зависимости. Но это, кажется, сильно отличается от случая, как указано в вопросе.
CVn

@Saeed: сообщение обновлено.
back2dos

@WeekendWarrior Является ли using(в этом контексте) еще более неизвестным «удобным синтаксисом»? Почему это так приятно использовать =)
Coops

4

Не только использование является дублирующим кодом (кстати, он является дублирующим кодом и фактически сравнивается с оператором try..catch..finally), но и списком toList. Я бы изменил ваш код следующим образом:

 public List<T> GetAll(Func<Entities, IEnumerable<T>> getter) {
    using (Entities entities = new Entities())
    {
        return getter().ToList();
    }
 }

public List<Employee> GetAllEmployees()
{
    return GetAll(e => e.Employees);
}

public List<Job> GetAllJobs()
{
    return GetAll(e => e.Jobs);
}

public List<Task> GetAllTasksOfTheJob(Job job)
{
    return GetAll(e => e.Tasks.Where(t => t.JobId == job.Id));
}

3

Поскольку здесь нет никакой бизнес-логики, кроме последней. Это не очень СУХОЙ, на мой взгляд.

У последнего нет СУХОГО в блоке using, но я предполагаю, что предложение where должно измениться, где бы оно ни использовалось.

Это типичная работа для генераторов кода. Напишите и покройте генератор кода и дайте ему сгенерировать для каждого типа.


Нет @arunmur. Здесь было недоразумение. Под СУХОЙ я имел в виду using (Entities entities = new Entities())блок. Я имею в виду, что эта строка кода повторяется 100 раз и повторяется все больше и больше.
Саид Нимати

DRY в первую очередь происходит из-за того, что вам нужно много раз писать тестовые случаи, а ошибка в одном месте означает, что вы должны исправить это в 100 местах. Использование (Entities ...) слишком простой код, чтобы его можно было нарушить. Это не должно быть разделено или помещено в другой класс. Если вы все еще настаиваете на упрощении. Я бы предложил функцию обратного вызова Yeild от ruby.
Арунмур

1
@arnumur - использование не всегда слишком просто сломать. У меня часто есть немного логики, которая определяет, какие опции использовать в контексте данных. Вполне возможно, что неверная строка соединения может быть передана.
Морган Херлокер

1
@ironcode - в таких ситуациях имеет смысл передать его функции. Но в этих примерах его довольно сложно сломать. Даже если это не помогает, исправление часто находится в определении самого класса Entities, который должен быть покрыт отдельным тестовым примером.
Арунмур

+1 @arunmur - согласен. Я обычно делаю это сам. Я пишу тесты для этой функции, но, кажется, немного сложно написать тест для вашего оператора использования.
Морган Херлокер

2

Поскольку вы создаете и уничтожаете один и тот же одноразовый объект снова и снова, ваш класс сам по себе является хорошим кандидатом для реализации шаблона IDisposable.

class ThisClass : IDisposable
{
    protected virtual Entities Context { get; set; }

    protected virtual void Dispose( bool disposing )
    {
        if ( disposing && Context != null )
            Context.Dispose();
    }

    public void Dispose()
    {
        Dispose( true );
    }

    public ThisClass()
    {
        Context = new Entities();
    }

    public List<Employee> GetAllEmployees()
    {
        return Context.Employees.ToList();
    }

    public List<Job> GetAllJobs()
    {
        return Context.Jobs.ToList();
    }

    public List<Task> GetAllTasksOfTheJob(Job job)
    {
        return Context.Tasks.Where(t => t.JobId == job.Id).ToList();
    }
}

Это оставляет вам только необходимость использования при создании экземпляра вашего класса. Если вы не хотите, чтобы класс отвечал за удаление объектов, вы можете заставить методы принимать зависимость в качестве аргумента:

public static List<Employee> GetAllEmployees( Entities entities )
{
    return entities.Employees.ToList();
}

public static List<Job> GetAllJobs( Entities entities )
{
    return entities.Jobs.ToList();
}

public static List<Task> GetAllTasksOfTheJob( Entities entities, Job job )
{
    return entities.Tasks.Where(t => t.JobId == job.Id).ToList();
}

1

Мой любимый кусочек непостижимой магии!

public class Blah
{
  IEnumerable<T> Wrap(Func<Entities, IEnumerable<T>> act)
  {
    using(var entities = new Entities()) { return act(entities); }
  }

  public List<Employee> GetAllEmployees()
  {
    return Wrap(e => e.Employees.ToList());
  }

  public List<Job> GetAllJobs()
  {
    return Wrap(e => e.Jobs.ToList());
  }

  public List<Task> GetAllTasksOfTheJob(Job job)
  {
    return Wrap(e => e.Tasks.Where(x ....).ToList());
  }
}

Wrapсуществует только для того, чтобы абстрагироваться от этой магии или от того, что вам нужно Я не уверен, что рекомендовал бы это все время, но возможно использовать. «Лучшей» идеей было бы использовать DI-контейнер, такой как StructureMap, и просто поместить класс Entities в контекст запроса, внедрить его в контроллер и затем позволить ему позаботиться о жизненном цикле без необходимости в вашем контроллере.


Параметры типа для Func <IEnumerable <T>, Entities> находятся в неправильном порядке. (см. мой ответ, который имеет в основном то же самое)
Брук

Ну, достаточно легко исправить. Я никогда не помню, каков правильный порядок. Я использую Funcдостаточно, я должен.
Трэвис
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.