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

Сценарий: мы пишем приложение Flutter. Мы хотим сделать собственный виджет, PlatformName . Виджет PlatformName должен быть простой оболочкой для виджета Text: text внутри виджета Text должно отображать только название платформы. PlatformName должен принимать параметр, platformName где пользователь может переопределить имя платформы по умолчанию. вот характеристики виджета:

  • Если потребитель API виджета (мы будем называть его «пользователем» виджета, не путать с «конечным пользователем» приложения) не предоставляет переопределение (просто позволяет platformName быть по умолчанию) , мы должны по умолчанию использовать переменную defaultTargetPlatform, которая знает, на какой платформе мы работаем. Вот как это выглядит:

https://gist.github.com/6ee01f9f99bb10ca71e4200e74fa2c30

  • Если пользователь предоставляет переопределение, мы должны использовать переопределение: просто отображать то, что вводит пользователь.
  • И последнее предостережение: в нашей вымышленной вселенной Flutter есть случаи, когда мы не знаем платформу. В этом случае мы должны сказать «Неизвестно». Это представлено пользователем, передающим null .

Это, пожалуй, самый интуитивно понятный API. null означает именно то, что должно означать null: отсутствие известного значения. Отсутствие аргумента означает именно то, что отсутствие аргумента должно означать: мы не заполнили поле, поэтому дайте нам поведение по умолчанию.

Здесь все имеет смысл. Но у нас есть вопиющая проблема: как, черт возьми, вы это реализуете?

Проблема с реализацией

Если вы смотрите на предыдущий пример и думаете, что реализовать его тривиально, я умоляю вас попробовать и сделать это. (Если вы поймете это с первого раза: вы намного умнее меня — с большой силой приходит и большая ответственность. Используйте свои знания с умом.)

Проблема с реализацией заключается в том, что «нулевой» случай и случай «по умолчанию» сталкиваются друг с другом. Вот как я часто вижу реализованные случаи по умолчанию:

Но проблема здесь в том, что мы теряем различие между null и значением по умолчанию. рассматривая нулевой регистр как случай по умолчанию, мы теряем функциональность, когда null означает «Мы не знаем платформу!». Пользователь будет запускать эту версию Flutter на своем Chevy Equinox 2019 — поскольку эта архитектура не обрабатывается в бизнес-логике их приложений, они хотят, чтобы она отображала "Unknown"; вместо этого срабатывает откат к defaultTargetPlatform, и приложение отображает "Linux". В этом и есть ошибка.

Мы не можем использовать здесь параметры Dart по умолчанию (что интуитивно понятно): это потому, что параметры по умолчанию не могут быть непостоянными, а defaultTargetPlatform непостоянна.

Если это сработает, у нас будет вся необходимая функциональность: если пользователь передает значение null, мы переключаем переменную-член на "Unknown". Проблема в том, что dart выдает нам ошибку: «Значение необязательного параметра по умолчанию должно быть постоянным».

Вот некоторые другие решения, которые являются неоптимальными, прежде чем я объясню свое любимое решение с наименьшим количеством компромиссов:

  • Имейте вторую переменную конструктора, которая говорит platformIsUnknown: это работает, но теперь у нас есть два источника правды, и мы увеличиваем количество случаев в 2 раза. Мы также вводим недопустимые состояния времени выполнения: platformIsUnknown == false && targetPlatform == null — это неопределенное поведение, но моделируемое на уровне типов (т. е. компилятор говорит «LGTM!»).
  • Заставлять пользователя вводить «Неизвестно» или поведение по умолчанию: это решение просто отстой. Вы поднимаете руки вверх и навязываете сложности своему пользователю!
  • Несколько конструкторов: это могло бы сработать, но, по моему опыту, это делает очень несухой код. Это работает в случае, когда есть только один параметр конфигурации, но как только вы увеличите его до большего, вы получите экспоненциально растущее количество конструкторов. Есть и другие причины, по которым следует избегать этого шаблона, и большинство из них усложняет работу конечного пользователя.

(Два) решения

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

Как мы можем обойти две истины из предыдущего абзаца? Какой из них мы можем согнуть и обойти? У нас есть что-то вроде условного выражения в предыдущем утверждении — «это не может быть его окончательным значением, поскольку финальное значение непостоянно». На самом деле нас не волнует, является ли значение по умолчанию его окончательным значением — нас заботит только то, что мы можем сказать, что это его значение по умолчанию.

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

Однако есть ли другое решение этой проблемы? есть. Если значение, по которому мы хотим различать три состояния, является более сложным объектом (рассмотрите объект конфигурации, где мы можем полностью отключить функцию, передав значение null, мы можем создать пользовательскую конфигурацию, передав пользовательскую конфигурацию, и где мы можем оставить его по умолчанию в конфигурации по умолчанию, не делая ничего из этого.) тогда мы можем фактически использовать шаблон, который я ранее отклонил: отключенный флаг.

Однако вместо того, чтобы отключать флаг на объекте-потребителе, мы можем установить его на самом объекте конфигурации.

Это повышает читабельность: вместо какого-то волшебного флага для отключения функциональности мы явно говорим Configuration.disabledConfig . Значение null выглядит красиво, но прямое указание «использовать отключенную конфигурацию» выглядит еще лучше.

Мы получаем некоторую читабельность, но теряем некоторую обнаруживаемость. Программисту может быть неясно, что для отключения конфигурации ему нужно найти какой-то фабричный конструктор. Передача нулевого значения гораздо более интуитивно понятна, но намерение нулевого значения не так ясно, как явное указание «отключить XYZ». Еще одним огромным преимуществом здесь является то, что функциональность гораздо более ограничена и контролируется разработчиком; если они добавляют дополнительные функции к объекту-потребителю, это может быть выражено через его объект конфигурации, не беспокоясь об обратной совместимости («поведение по умолчанию» остается по умолчанию, а «отключенное» поведение означает отсутствие поведения вообще). Напротив, строки — это просто строки, и, следовательно, любые дополнительные функции должны быть добавлены к объекту-потребителю (наш виджет PlatformName), а шаблон флага-стража должен повторяться. Красота в глазах смотрящего на этом.

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

  1. В первую очередь потребители API: это ваши конечные пользователи, и их счастье — это то, что поддерживает работу проектов. Пожертвовать некоторой внутренней читабельностью в долгосрочной перспективе может быть полезно, если это очистит внешний API.
  2. Во-вторых, автор кодовой базы: важно учитывать стоимость обслуживания. замороженный упрощает этот паттерн способом, который слишком сложен для человека. Для работы требуется конвейер через конструкторы фабрик, объекты-стражи и несколько копий конструкторов. Freezed может сделать это, потому что код генерируется автоматически и редко когда-либо просматривается человеком; внутренняя сложность обеспечивает огромный выигрыш во внешнем API.

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

Вот и все! Надеюсь, это поможет вам немного задуматься о том, как думают ваши пользователи, и о разработке API, которые будут служить вам всю жизнь. Если я пропустил какие-либо решения или что-то было непонятно, пожалуйста, оставьте комментарий! Я постараюсь добраться до него как можно скорее.

Повышение уровня кодирования

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу