Як реалізувати принцип DRY при використанні ключового слова "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блоку? Чи взагалі це вважається порушенням принципу DRY?

Оновлення: я не говорю про те, що було реалізовано всередині usingблоку. Що я тут фактично маю на увазі, це using (Entities entities = new Entities()). Цей рядок повторюється 100 і більше разів.


2
це C #? Відповідь на ваше запитання може залежати від мови
Девід,

Так @David, вибач, що я не згадав про свою мову. Як це може вплинути на відповідь?
Saeed Neamati

у деяких мовах може бути особливий синтаксис, який може допомогти вам трохи визначити свій код. Я не знаю C #, але в Ruby, я думаю, ви могли б використовувати блоки для розміщення частини, яка використовує.
Девід

Оператор, що використовує, фактично забезпечує підтримку мови C # для застосування принципу DRY, щоб уникнути повторного кодування під час управління розпорядженням ресурсами за допомогою шаблону дизайну Dispose . Це не означає, що ми не можемо знайти способи зробити речі сушеними! Особисто я вважаю СУХУ як рекурсивний процес.
Джон Тоблер

Відповіді:


24

Однією з ідей було б обернути її функцією, яка виконує функції a 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парам типу, тому що я припускаю, що ти маєш більше одного типу. Якщо цього немає, ви можете вийняти його та просто використати параметр типу для типу повернення.

Якщо чесно, хоча такий тип коду зовсім не допомагає читати. На моєму досвіді, більше молодших колег теж дуже важко проводять час з цим.

Оновіть кілька додаткових варіантів помічників, які ви можете розглянути

//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 Brown: Я думаю, що я перебив тебе на нього з назвою функції. Я IEnumerableвийшов із функції, якщо були якісь IEnumerableвластивості Т, які абонент хотів повернути, але ти маєш рацію, це трохи очистить його. Можливо, мати допомогу як для одиноких, так і для IEnumerableрезультатів було б добре. Однак, я все ще думаю, що це уповільнює розпізнавання того, що робить код, особливо для тих, хто не звик користуватися великою кількістю дженериків та лямбда (наприклад, ваші колеги, які НЕ так) :)
Брук

+1 Я думаю, що цей підхід чудово. І читабельність можна покращити. Наприклад, введіть "ToList" WithEntities, використовуйте Func<T,IEnumerable<K>>замість нього Func<T,K>та дайте "WithEntities" кращу назву (наприклад, SelectEntities). І я не думаю, що "Суб'єкти" мають бути загальним параметром.
Doc Brown

1
Щоб зробити цю роботу, обмеження повинні бути такими where T : IDisposable, new(), як це usingпотрібно IDisposableдля того, щоб працювати.
Ентоні Пеграм

1
@Anthony Pegram: Виправлено, дякую! (це те, що я отримую за кодування C # у вікні браузера)
Брук

23

Мені це було б як турбуватися про пророкування про одну і ту ж колекцію кілька разів: це просто щось, що вам потрібно зробити. Будь-яка спроба її абстрактно надалі зробить код менш читабельним.


Чудова аналогія @Ben. +1
Saeed Neamati

3
Я не згоден, вибачте. Прочитайте коментарі ОП щодо обсягу транзакцій і подумайте, що вам потрібно зробити, коли ви написали 500 разів такий код коду, а потім зауважте, що вам доведеться 500 разів змінити те саме. Цей тип повторення коду може бути нормальним, лише якщо у вас є <10 таких функцій.
Док Браун

О, і не забувати, якщо ви - для кожної однієї і тієї ж колекції більше 10 разів дуже подібним чином, з подібним виглядом кодом всередині кожного для кожного, вам слід остаточно подумати про якесь узагальнення.
Док Браун

1
Для мене це просто схоже на те, що ти робиш три вкладиші в однолінійку ... ти все ще повторюєш себе.
Джим Волф

Це залежить від контексту, якщо, наприклад foreach, надмірна колекція або логіка в foreachциклі, наприклад, займає багато часу. Девіз, який я прийшов прийняти: Не одержимо, але завжди
пам’ятай

9

Здається, що ви плутаєте принцип "Раз і тільки раз" з принципом DRY. Принцип DRY визначає:

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

Однак принцип "Один і єдиний раз" дещо інший.

Принцип [DRY] схожий на OnceAndOnlyOnce, але з іншою метою. Завдяки OnceAndOnlyOnce вам пропонується зробити рефактор, щоб усунути дублюваний код та функціональні можливості. За допомогою DRY ви намагаєтеся визначити єдине, остаточне джерело кожного знання, що використовується у вашій системі, а потім використовувати це джерело для створення застосовних примірників цього знання (код, документація, тести тощо).

Принцип DRY зазвичай використовується в контексті фактичної логіки, не настільки зайвої, використовуючи заяви:

Зберігати структуру програми DRY важче та менше. Це бізнес-правила, заяви if, математичні формули та метадані, які мають з’являтися лише в одному місці. Власні речі - 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 методів? Це здається мені повторюваним завданням і порушенням прикладу принципу DRY.
Saeed Neamati

3
Якщо "використовувати" тут, закривається контекст даних, тому ліньке завантаження неможливо поза цими методами.
Стівен Стрига

@Saeed: Це коли ви дивитесь на ін'єкцію залежності. Але це, здається, сильно відрізняється від випадку, про який йдеться у питанні.
CVn

@Saeed: повідомлення оновлено.
back2dos

@WeekendWarrior Чи є using(у цьому контексті) ще більш невідомий "зручний синтаксис"? Чому це так приємно використовувати =)
Чашки

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

Оскільки тут немає жодної ділової логіки, окрім останньої. На мій погляд, це насправді НЕ СУХО.

Останній не має жодного DRY у використанні блоку, але я здогадуюсь, де пункт повинен змінитися там, де він використовувався.

Це типова робота для генераторів коду. Напишіть і вкрийте генератор коду і нехай він генерується для кожного типу.


Ні @arunmur Тут виникло непорозуміння. Під DRY я мав на увазі using (Entities entities = new Entities())блок. Я маю на увазі, цей рядок коду повторюється 100 разів і повторюється все більше і більше.
Саїд Неаматі

DRY в першу чергу пов’язаний з тим, що вам потрібно писати тестові випадки, що багато разів і помилка в одному місці означає, що вам доведеться виправити це в 100 місцях. Використання (Entities ...) занадто простий код, щоб його зламати. Його не потрібно розбивати або ставити в інший клас. Якщо ви все-таки наполягаєте на спрощенні. Я б запропонував функцію зворотного дзвінка Yeild із рубіну.
арунмур

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> знаходяться в неправильному порядку. (див. мою відповідь, яка має в основному те саме)
Брук

Ну, досить просто виправити. Я ніколи не пам’ятаю, який правильний порядок. Я використовую Funcs достатньо я повинен.
Тревіс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.