Является ли большое логическое выражение более читабельным, чем то же выражение, разбитое на методы предикатов? [закрыто]


63

Что легче понять: большой логический оператор (довольно сложный) или тот же оператор, разбитый на методы предикатов (много дополнительного кода для чтения)?

Вариант 1, большое логическое выражение:

    private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
    {

        return propVal.PropertyId == context.Definition.Id
            && !repo.ParentId.HasValue || repo.ParentId == propVal.ParentId
            && ((propVal.SecondaryFilter.HasValue && context.SecondaryFilter.HasValue && propVal.SecondaryFilter.Value == context.SecondaryFilter) || (!context.SecondaryFilter.HasValue && !propVal.SecondaryFilter.HasValue));
    }

Вариант 2. Условия разбиты на методы предикатов:

    private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
    {
        return MatchesDefinitionId(context, propVal)
            && MatchesParentId(propVal)
            && (MatchedSecondaryFilter(context, propVal) || HasNoSecondaryFilter(context, propVal));
    }

    private static bool HasNoSecondaryFilter(CurrentSearchContext context, TValToMatch propVal)
    {
        return (!context.No.HasValue && !propVal.SecondaryFilter.HasValue);
    }

    private static bool MatchedSecondaryFilter(CurrentSearchContext context, TValToMatch propVal)
    {
        return (propVal.SecondaryFilter.HasValue && context.No.HasValue && propVal.SecondaryFilter.Value == context.No);
    }

    private bool MatchesParentId(TValToMatch propVal)
    {
        return (!repo.ParentId.HasValue || repo.ParentId == propVal.ParentId);
    }

    private static bool MatchesDefinitionId(CurrentSearchContext context, TValToMatch propVal)
    {
        return propVal.PropertyId == context.Definition.Id;
    }

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


13
Вариант 2 аналогичен тому, что рекомендует Мартин Фаулер в своей книге по рефакторингу. Кроме того, имена ваших методов служат целью всех случайных выражений, а содержимое методов - это просто детали реализации, которые могут со временем меняться.
программист

2
Это действительно то же самое выражение? «Или» имеет меньший приоритет, чем «И». В любом случае, второй говорит о вашем намерении, другой (первый) - технический.
упаковщик

3
Что говорит @thepacker Тот факт, что первый шаг привел к тому, что вы совершили ошибку, является довольно хорошим признаком того, что первый способ нелегко понять для очень важного сектора вашей целевой аудитории. Себя!
Стив Джессоп

3
Вариант 3: мне не нравится ни один. Второй смехотворно многословен, первый не эквивалентен второму. Скобки помогают.
Дэвид Хаммен

3
Это может быть педантичным, но у вас нет каких - либо if заявлений в любом блоке кода. Ваш вопрос о булевых выражениях .
Кайл Стрэнд,

Ответы:


88

Что легче понять

Последний подход. Это не только легче для понимания, но и для написания, тестирования, рефакторинга и расширения. Каждое необходимое условие может быть безопасно отделено и обработано по-своему.

это проблематично, потому что вы должны прочитать все методы, чтобы понять код

Это не проблема, если методы названы правильно. На самом деле это будет легче понять, так как имя метода будет описывать цель условия.
Для зрителя if MatchesDefinitionId()более объяснительным, чемif (propVal.PropertyId == context.Definition.Id)

[Лично, первый подход ранит мои глаза.]


12
Если имена методов хороши, то это также легче понять.
BЈовић

И, пожалуйста, сделайте их (имена методов) значимыми и краткими. 20+ имен методов chars болят мои глаза. MatchesDefinitionId()является пограничным.
Mindwin

2
@Mindwin. Если дело доходит до выбора между сохранением «коротких» имен методов и сохранением их значимости, я говорю, что каждый раз принимайте последнее. Коротко это хорошо, но не за счет читабельности.
Ajedi32

@ Ajedi32, не нужно писать эссе о том, что метод делает с именем метода, или иметь грамматически обоснованные названия методов. Если придерживаться стандартов аббревиатур (для всей рабочей группы или организации), проблем с короткими именами и удобочитаемостью не будет.
Mindwin

Используйте закон Ципфа: сделайте вещи более многословными, чтобы препятствовать их использованию.
hoosierEE

44

Если это единственное место, где будут использоваться эти предикатные функции, вы также можете использовать boolвместо них локальные переменные:

private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
{
    bool matchesDefinitionId = (propVal.PropertyId == context.Definition.Id);
    bool matchesParentId = (!repo.ParentId.HasValue || repo.ParentId == propVal.ParentId);
    bool matchesSecondaryFilter = (propVal.SecondaryFilter.HasValue && context.No.HasValue && propVal.SecondaryFilter.Value == context.No);
    bool hasNoSecondaryFilter = (!context.No.HasValue && !propVal.SecondaryFilter.HasValue);

    return matchesDefinitionId
        && matchesParentId
        && matchesSecondaryFilter || hasNoSecondaryFilter;
}

Они также могут быть разбиты дальше и переупорядочены, чтобы сделать их более читабельными, например, с

bool hasSecondaryFilter = propVal.SecondaryFilter.HasValue;

а затем заменить все экземпляры propVal.SecondaryFilter.HasValue. Одна вещь, которая сразу бросается в глаза, это то, что hasNoSecondaryFilterиспользует логическое И для отрицательных HasValueсвойств, в то время как matchesSecondaryFilterиспользует логическое И для неотрицанных HasValue- так что это не полная противоположность.


3
Это решение довольно хорошее, и я, конечно, написал много похожего кода. Это очень читабельно. Недостатком, по сравнению с решением, которое я опубликовал, является скорость. С помощью этого метода вы выполняете кучу условных тестов, несмотря ни на что. В моем решении, операции могут быть значительно сокращены на основе обработанных значений.
BuvinJ

5
@BuvinJ Тесты, подобные показанным здесь, должны быть довольно дешевыми, поэтому, если я не знаю, что некоторые условия являются дорогостоящими или если это не очень чувствительный к производительности код, я бы выбрал более читаемую версию.
svick

1
@svick Без сомнения, это вряд ли будет представлять проблему производительности в большинстве случаев. Тем не менее, если вы можете сократить операции без потери читабельности, то почему бы не сделать это? Я не уверен, что это намного более читабельно, чем мое решение. Это дает самодокументирующиеся «имена» тестам - и это хорошо ... Я думаю, что все сводится к конкретному случаю использования и тому, насколько понятны сами тесты.
BuvinJ

Добавление комментариев может также помочь читабельности ...
BuvinJ

@BuvinJ Что мне действительно нравится в этом решении, так это то, что, игнорируя все, кроме последней строки, я могу быстро понять, что он делает. Я думаю, что это более читабельно.
svick

42

В общем, последнее является предпочтительным.

Это делает сайт вызова более пригодным для повторного использования. Он поддерживает DRY (то есть у вас меньше мест для изменения при изменении критериев, и вы можете делать это более надежно). И очень часто эти подкритерии являются вещами, которые будут использоваться независимо друг от друга в другом месте, что позволяет вам сделать это.

О, и это делает этот материал намного проще для модульного тестирования, давая вам уверенность, что вы сделали это правильно.


1
Да, хотя ваш ответ должен также касаться исправления использования repo, которое выглядит как статическое поле / свойство, то есть глобальная переменная. Статические методы должны быть детерминированными и не использовать глобальные переменные.
Дэвид Арно,

3
@DavidArno - хотя это не так уж и здорово, кажется, что это касается вопроса. И без большего количества кода вполне вероятно, что есть веская причина для такой работы.
Теластин

1
Да, не обращайте внимания на репо. Я должен был немного
запутать

23

Если это между этими двумя вариантами, то последний лучше. Однако это не единственный выбор! Как насчет разбить одну функцию на несколько ifs? Проверьте способы выхода из функции, чтобы избежать дополнительных тестов, грубо эмулируя «короткое замыкание» в тесте с одной линией.

Это легче читать (вам может потребоваться перепроверить логику для вашего примера, но концепция верна):

private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
{
    if( propVal.PropertyId != context.Definition.Id ) return false;

    if( repo.ParentId.HasValue || repo.ParentId != propVal.ParentId ) return false;

    if( propVal.SecondaryFilter.HasValue && 
        context.SecondaryFilter.HasValue && 
        propVal.SecondaryFilter.Value == context.SecondaryFilter ) return true;

    if( !context.SecondaryFilter.HasValue && 
        !propVal.SecondaryFilter.HasValue) return true;

    return false;   
}

3
Почему я получил понижение за это в течение нескольких секунд после публикации? Пожалуйста, добавьте комментарий, когда вы понижаете! Этот ответ действует так же быстро и его легче читать. Так в чем проблема?
BuvinJ

2
@BuvinJ: Абсолютно ничего плохого в этом нет. То же, что и исходный код, за исключением того, что вам не нужно бороться с дюжиной скобок и одной строкой, которая простирается до конца экрана. Я могу прочитать этот код сверху вниз и сразу же понять его. WTF count = 0.
gnasher729

1
Возврат значения, отличного от конца функции, делает код менее читаемым и не более читаемым, IMO. Я предпочитаю единую точку выхода. Некоторые хорошие аргументы в обе стороны по этой ссылке. stackoverflow.com/questions/36707/…
Брэд Томас

5
@Brad thomas Я не могу согласиться с единственной точкой выхода. Обычно это приводит к глубоким вложенным скобкам. Возвращение заканчивает путь, поэтому для меня намного легче читать.
Борхаб

1
@BradThomas Я полностью согласен с Борхабом. Именно поэтому я избегаю глубоких вложений, поэтому чаще использую этот стиль, чем разбиваю длинные условные выражения. Я использую, чтобы найти себя, пишущий код с тоннами вложений. Затем я начал искать способы едва ли углубиться в одну или две вложенности, и в результате мой код стал НАМНОГО проще для чтения и поддержки. Если вы можете найти способ выйти из своей функции, сделайте это как можно скорее! Если вы можете найти способ избежать глубоких вложений и длинных условных выражений, сделайте это!
BuvinJ

10

Мне больше нравится вариант 2, но я бы предложил одно структурное изменение. Объедините две проверки в последней строке условия в один вызов.

private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
{
    return MatchesDefinitionId(context, propVal)
        && MatchesParentId(propVal)
        && MatchesSecondaryFilterIfPresent(context, propVal);
}

private static bool MatchesSecondaryFilterIfPresent(CurrentSearchContext context, 
                                                    TValToMatch propVal)
{
    return MatchedSecondaryFilter(context, propVal) 
               || HasNoSecondaryFilter(context, propVal);
}

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

Я не уверен, что MatchesSecondaryFilterIfPresent()это лучшее название для комбинации; но нет ничего лучше, сразу приходит на ум.


Очень хорошо, пытаться объяснить, что делается внутри методов, на самом деле лучше, чем просто реструктурировать вызовы.
Клар

2

Хотя в C # код не очень объектно-ориентированный. Он использует статические методы и выглядит как статические поля (например repo). Обычно считается, что статика делает ваш код трудным для рефакторинга и тестирования, в то же время затрудняя его повторное использование, и, на ваш вопрос: такое статическое использование менее читабельно и легко поддерживается, чем объектно-ориентированная конструкция.

Вы должны преобразовать этот код в более объектно-ориентированную форму. Когда вы это сделаете, вы обнаружите, что есть разумные места для размещения кода, который выполняет сравнение объектов, полей и т. Д. Вполне вероятно, что вы могли бы тогда попросить объекты сравнить себя, что уменьшило бы ваш большой оператор if до простой запрос для сравнения (например if ( a.compareTo (b) ) { }, который может включать все сравнения полей)

C # имеет богатый набор интерфейсов и системных утилит для сравнения объектов и их полей. Помимо очевидного .Equalsспособа, для начала, посмотрите на IEqualityComparer, IEquatableи коммунальные услуги , как System.Collections.Generic.EqualityComparer.Default.


0

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


0

Я бы сказал, что они примерно одинаковы, ЕСЛИ вы добавите несколько пробелов для удобства чтения и некоторые комментарии, чтобы помочь читателю в более неясных частях.

Помните: хороший комментарий говорит читателю, о чем вы думали, когда писали код.

С такими изменениями, которые я предложил, я бы, вероятно, пошел с прежним подходом, так как он менее загроможден и рассеян. Вызовы подпрограмм похожи на сноски: они предоставляют полезную информацию, но нарушают процесс чтения. Если бы предикаты были более сложными, то я бы разбил их на отдельные методы, чтобы концепции, которые они воплощают, можно было собрать в понятные куски.


Заслуживает +1. Хорошая пища для размышлений, хотя и не популярное мнение, основанное на других ответах. Спасибо :)
Виллем

1
@ Виллем Нет, это не заслуживает +1. Два подхода не совпадают. Дополнительные комментарии глупы и не нужны.
BЈовић

2
Хороший код НИКОГДА не зависит от комментариев, чтобы быть понятным. На самом деле комментарии являются худшим беспорядком, который может иметь код. Код должен говорить сам за себя. Кроме того, два подхода, которые OP хочет оценить, никогда не могут быть «примерно одинаковыми», независимо от того, сколько пробелов добавляется.
чудо

Лучше иметь понятное имя функции, чем читать комментарий. Как указано в книге «Чистый код», комментарий - это неспособность выразить код броска. Зачем объяснять, что вы делаете, если функция могла бы изложить это гораздо яснее.
Борхаб

0

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

Теперь давайте посмотрим на ваш первый вариант и признаем, что ни ваши отступы и переносы строк не были столь полезны, ни условно структурированная структура:

private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal) {
    return propVal.PropertyId == context.Definition.Id && !repo.ParentId.HasValue
        || repo.ParentId == propVal.ParentId
        && propVal.SecondaryFilter.HasValue == context.SecondaryFilter.HasValue
        && (!propVal.SecondaryFilter.HasValue || propVal.SecondaryFilter.Value == context.SecondaryFilter.Value);
}

0

Первый абсолютно ужасен. Вы использовали || для двух вещей на одной линии; это либо ошибка в вашем коде, либо намерение запутать ваш код.

    return (   (   propVal.PropertyId == context.Definition.Id
                && !repo.ParentId.HasValue)
            || (   repo.ParentId == propVal.ParentId
                && (   (   propVal.SecondaryFilter.HasValue
                        && context.SecondaryFilter.HasValue 
                        && propVal.SecondaryFilter.Value == context.SecondaryFilter)
                    || (   !context.SecondaryFilter.HasValue
                        && !propVal.SecondaryFilter.HasValue))));

Это, по крайней мере, наполовину прилично отформатированный (если форматирование сложное, это потому, что условие if сложное), и у вас есть, по крайней мере, шанс выяснить, есть ли здесь что-то бессмысленное. По сравнению с вашим отформатированным мусором, если что-нибудь еще лучше. Но вы, похоже, способны делать только крайности: либо полный беспорядок оператора if, либо четыре совершенно бессмысленных метода.

Обратите внимание, что (cond1 && cond2) || (! cond1 && cond3) можно записать как

cond1 ? cond2 : cond3

что уменьшило бы беспорядок. Я бы написал

if (propVal.PropertyId == context.Definition.Id && !repo.ParentId.HasValue) {
    return true;
} else if (repo.ParentId != propVal.ParentId) {
    return false;
} else if (propVal.SecondaryFilter.HasValue) {
    return (   context.SecondaryFilter.HasValue
            && propVal.SecondaryFilter.Value == context.SecondaryFilter); 
} else {
    return !context.SecondaryFilter.HasValue;
}

-4

Мне не нравится ни одно из этих решений, их сложно рассуждать и трудно читать. Абстракция к меньшим методам только ради меньших методов не всегда решает проблему.

В идеале, я думаю, что вы должны метапрограммически сравнивать свойства, чтобы у вас не было определения нового метода или ветвления каждый раз, когда вы хотите сравнить новый набор свойств.

Я не уверен насчет c #, но в javascript нечто подобное было бы НАМНОГО лучше и могло бы по крайней мере заменить MatchesDefinitionId и MatchesParentId

function compareContextProp(obj, property, value){
  if(obj[property])
    return obj[property] == value
  return false
}

1
Не должно быть проблемой реализовать что-то подобное в C #.
Снуп

Я не вижу, чтобы булеву комбинацию из ~ 5 вызовов compareContextProp(propVal, "PropertyId", context.Definition.Id)было бы легче прочитать, чем булеву комбинацию из ~ 5 сравнений формы propVal.PropertyId == context.Definition.Id. Он значительно длиннее и добавляет дополнительный слой, не скрывая сложности с сайтом вызовов. (если это имеет значение, я не понизил)
Ixrec
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.