Dark mode switch icon Light mode switch icon

Избегайте отрицательных условий

• 2 минут чтобы прочитать

Разберём на примерах известное правило избегания отрицаний в условиях. Какие бывают отрицания и чем они опасны.

Начнём с первоисточника

Как у Дяди Боба

G29: Избегайте отрицательных условий Отрицательные условия немного сложнее для понимания, чем положительные. Таким образом, по возможности старайтесь формулировать положительные условия. Например запись

if (buffer.shouldCompact())

предпочтительнее записи

if (!buffer.shouldNotCompact())

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

Что ж, давайте разбираться.

Операторы отрицания

Рассмотрим синтетический пример метода проверки авторизации пользователя и соответствующий вариант его использования:

class AnonymousAuthenticator:
  def is_authenticated(self, request: Request) -> bool:
    return True

class PublicController:
  def index_action(self, request: Request) -> str:
    authenticator = AnonymousAuthenticator()

    if not authenticator.is_authenticated(request):
      return "authentication required"

    # ...
    # do some business logic
    # ...

    return "Successfuly authenticated"

Классический ход - если пользователь не авторизован, то сразу же вернём ответ. Если предположить что Роберт Мартин имел ввиду избегать операторы отрицания, то какие у нас есть варианты?

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

Отрицания в языке

Если бы в естественных языках отрицание выражалось только частицами “не” и “нет”, то это были бы очень скудные по выразительности языки.

Рассмотрим каким образом отрицания могут реализовываться в языках программаривания:

Как ни странно, самый безобидный из этих вариантов - именно первый. Рассмотрим пример:

class EditorAccessCheck
{
  const ENABLED = 'enabled';

  public function isNotAllowed(User $user): bool
  {
    return !$user->isEditor();
  }
}

class NewsController
{
  public function editAction(Request $request, EditorAccessCheck $accessCheck): Response
  {
    if ($accessCheck->isNotAllowed()) {
      return AccessDeniedResponse();
    }

    return Response("Welcome to editor's club!");
  }
}

Использованием метода читается довольно просто и понятно, несмотря на отриание в названии. Заметим что условие, которое проверяется внутри самого метода также содержит отрицание через оператор !, однако и оно не мешает воспринимать код. Проблемы начнутся когда кто-нибудь решит воспользоваться EditorAccessCheck в ином контексте:


class EditorActionSubscriber extends EventSubscriber
{
  public function __construct(
    private EditorAccessCheck $accessCheck
  ) {}

  public function supports(): bool
  {
    return !$this->accessCheck->isNotAllowed();
  }

  public function onEvent(UserActionEvent $event): void
  {
    // React to $event
  }
}

Здесь появляется проблема: двойное отрицание. Как избавиться от двойного отрицания в условиях? Изменить условие так, чтобы можно было убрать одно из них. Вот только убрать таким образом можно лишь оператор отрицания, потому что для изменения названия метода придётся пройтись по всей кодовой базе и не просто изменить название вызываемого метода, но и изменить всю логику этих вызовов, чтобы сохранить работоспособность приложения.

Неявное двойное отрицание

Все довольно просто когда мы смотрим на переменные с частицами No и Not, однако то же самое касается и вариантов с неявным отрицанием:

if (!$disabled) {
  call_when_enabled();
}

или

if not inactive:
  call_when_active()

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

Вывод

Сам по себе оператор отрицания не несёт в себе ничего плохого и не может рассматриваться как запах кода. Проблемы с пониманием кода возникают при двойном отрицании, которое можно легко исправить если отрицание не заложено — явно или неявно — в имена переменных и методов.

Some shit

Рассмотрим примеры из реальных проектов. Например, отрицание проверки отрицательного флага

if (!$widget->isFlagEnabled(ProductFlag::DISABLED)) {
    // do something
}

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

    public function isActive(): bool
    {
        return !$this->isFlagEnabled(ProductFlag::DISABLED);
    }

Хотя кажется правильней было бы ориентироваться на константу ProductFlag::ENABLED


Ещё один пример в коде, который пока нельзя назвать проблемой, но уже явно попахивает:

abstract class WidgetFlag
{
    public const DISABLED = 0x0001;

    public static function getList(): array
    {
        return [
            self::DISABLED => 'Выключено',
        ];
    }
}

Что мы видим:


Иногда плохие примеры получаются из-за проблем самого языка. Например в некоторых языках различные типы могут неявно приводиться к булеву типу, причем с различными побочными эффектами. И бывает так что для борьбы с этим выбирают не самые удачные решения:

function isAdmin(user) {
  return ('admin' in user) ? !!user.admin : false
}

Прилагательное вместо глагола inactive -> deactivate / setInactive:

public function inactive(): void
{
    $this->isActive = false;
}

Вместо заключения

<input type="checkbox" disabled="disabled" />

Мы вероятно уже привыкли и это уже не исправить, но это отвратительно: двойное отрицание которое всё же отрицание

Впервые опубликовано от Эдуард Сухарев