Чи великий виразний вираз виразніше, ніж той самий вираз, розбитий на предикативні методи? [зачинено]


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
Це справді той самий вираз? "Або" має менший пріоритет, ніж "І", інакше другий говорить про ваш намір, інший (перший) - технічний.
thepacker

3
Що говорить @thepacker Те, що зробити це першим способом призвело до того, що ви помилилися, є досить хорошою підказкою, що перший спосіб не є зрозумілим для дуже важливого сектора вашої цільової аудиторії. Сам!
Стів Джессоп

3
Варіант 3: мені не подобається жоден. Друга - смішно багатослівна, перша не рівнозначна другій. Допомагають дужки батьків.
Девід Хаммен

3
Це може бути педантичним, але у вас немає жодного if твердження в жодному блоці коду. Ваше запитання щодо булевих виразів .
Кайл Странд

Відповіді:


88

Що простіше зрозуміти

Останній підхід. Це не тільки простіше зрозуміти, але і простіше писати, перевіряти, рефактор і розширювати. Кожну необхідну умову можна безпечно розв’язати і поводити по-своєму.

це проблематично, оскільки вам потрібно прочитати всі методи, щоб зрозуміти код

Це не проблематично, якщо методи названі правильно. Насправді, було б простіше зрозуміти, як назва методу описувала б умову умови.
Для глядача if MatchesDefinitionId()більш пояснювальна, ніжif (propVal.PropertyId == context.Definition.Id)

[Особисто мені перший підхід болить мені очі.]


12
Якщо назви методів хороші, то це також легше зрозуміти.
BЈовић

І будь ласка, зробіть їх (назви методів) значущими та короткими. Імена методів 20+ символів болять мені очі. MatchesDefinitionId()є прикордонною.
Міндвін

2
@Mindwin Якщо зводиться до вибору між збереженням назв методів "короткими" та збереженням їх значущого, я кажу, приймайте останні щоразу. Короткий - це добре, але не за рахунок читабельності.
Ajedi32

@ Ajedi32 не потрібно писати есе про те, що робить метод на ім'я методу, або мати граматично звукові назви методів. Якщо дотримуватись чітких стандартів абревіатури (у робочій групі чи організації), проблем із короткими назвами та читабельністю не виникне.
Міндвін

Використовуйте закон Ціпфа: зробити речі більш багатослівним , щоб перешкоджати їх використанню.
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використовує логічний AND на заперечені HasValueвластивості, тоді як matchesSecondaryFilterвикористовує логічний AND на негативні 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 Я повністю погоджуюся з Borjab. Уникаючи глибоких гнізд, насправді, чому я використовую цей стиль частіше, ніж розбивати довгі умовні висловлювання. Я використовую для того, щоб писати код з тонами гнізд. Тоді я почав шукати способи навряд чи коли-небудь заглибити більше одного-двох гнізд, і мій код став МНОГО легше читати та підтримувати в результаті. Якщо ви можете знайти спосіб вийти зі своєї функції, зробіть це якнайшвидше! Якщо ви зможете знайти спосіб уникнути глибоких гнізд і довгих умов, зробіть це!
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()найкраща назва комбінації; але нічого кращого відразу не спадає на думку.


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

2

Хоча в C #, код не дуже об'єктно орієнтований. Він використовує статичні методи і схожі на статичні поля (наприклад repo). Як правило, вважається, що статика робить ваш код важким для рефактора і важко перевірити, перешкоджаючи повторному використанню, і, на ваше запитання: статичне використання, подібне до цього, є менш читабельним та доступним для експлуатації, ніж об'єктно-орієнтована конструкція.

Ви повинні перетворити цей код у більш об'єктно-орієнтовану форму. Коли ви це зробите, ви побачите, що є розумні місця для розміщення коду, який робить порівняння об'єктів, полів тощо. Цілком ймовірно, що ви можете потім попросити об'єкти порівняти самі, що зменшить ваш великий, якщо заява до простий запит для порівняння (наприклад if ( a.compareTo (b) ) { }, який може включати всі польові порівняння.)

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


0

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


0

Я б сказав, що вони приблизно однакові, ЯКЩО ви додасте пробіл для читабельності та деякі коментарі, щоб допомогти читачеві про більш незрозумілі частини.

Пам'ятайте: хороший коментар повідомляє читачеві, що ви думали, коли писали код.

З такими змінами, як я запропонував, я, мабуть, пішов би з колишнім підходом, оскільки він менш захаращений і розсіяний. Дзвінки підпрограми - це як виноски: вони надають корисну інформацію, але порушують потік читання. Якби предикати були складнішими, то я би розбив їх на окремі методи, щоб поняття, які вони втілюють, могли будуватися в зрозумілих шматках.


Заслуговує +1. Гарна їжа для роздумів, хоча і не популярна думка, заснована на інших відповідях. Дякую :)
willem

1
@willem Ні, це не заслуговує +1. Два підходи не однакові. Додаткові коментарі дурні та непотрібні.
BЈоviћ

2
Хороший код НІКОЛИ не залежить від коментарів, які будуть зрозумілими. Насправді коментарі - це найгірший загрозливий код. Код повинен говорити сам за себе. Крім того, два підходи, які ОП хоче оцінити, ніколи не можуть бути «приблизно однаковими», незалежно від того, скільки пробілів додається.
чудеса

Краще мати важливу назву функції, ніж читати коментар. Як зазначено в книзі "Чистий код", коментар - це невдала виразити код кидання. Навіщо пояснювати, що ви робите, коли функція могла це викласти набагато чіткіше.
Борджаб

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. Це значно довше і додає додатковий шар, не дійсно приховуючи будь-яку складність із сайту виклику. (якщо це має значення, я не спростовував)
Іксрек
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.