Немного об именовании локальных переменных
Есть задача: есть список элементов на экране, которые нужно раскрасить в разные цвета в зависимости от их внутреннего содержимого. Плюс некоторые элементы должны быть особого цвета для обозначения, что они выделены пользователем.
Чтобы это сделать, программист написал функцию, которая для каждого элемента сопоставляет нужный цвет. Она берет 2 параметра: список элементов и цвет для обозначения выделенных элементов. И выглядит это так:
Посмотрите на переменную
Лучше назвать эту переменную как
* Переменная сразу описывает, какой смысл она несет в себе. Не важно, откуда она получает значение и какое оно.
* Вторая строчка становится чище от дополнительной логики: ведь именно на ней мы говорим, что цвет выбранных элементов должен быть
* Вызов функции выглядит очень читаемо: теперь мы понимаем, что за цвет мы передаем в функцию и что он обозначает.
Пишите код легче и понятнее, и мир станет лучше.
#простокод
Есть задача: есть список элементов на экране, которые нужно раскрасить в разные цвета в зависимости от их внутреннего содержимого. Плюс некоторые элементы должны быть особого цвета для обозначения, что они выделены пользователем.
Чтобы это сделать, программист написал функцию, которая для каждого элемента сопоставляет нужный цвет. Она берет 2 параметра: список элементов и цвет для обозначения выделенных элементов. И выглядит это так:
Color accentColor = Theme.of(context).accentColor;
Map<Item, Color> itemsWithColors = _mapItemsWithColors(widget.items, accentColor);
Посмотрите на переменную
accentColor: почему она так называется? Какой смысл в том, что это именно accent цвет? Ведь мы можем захотеть взять из темы другой цвет.Лучше назвать эту переменную как
selectedColor. Таким образом мы делаем сразу несколько полезных вещей:* Переменная сразу описывает, какой смысл она несет в себе. Не важно, откуда она получает значение и какое оно.
* Вторая строчка становится чище от дополнительной логики: ведь именно на ней мы говорим, что цвет выбранных элементов должен быть
accentColor. Но задача обладать такой логикой должна находится на плечах переменной.* Вызов функции выглядит очень читаемо: теперь мы понимаем, что за цвет мы передаем в функцию и что он обозначает.
Color selectedColor = Theme.of(context).accentColor;
Map<Item, Color> itemsWithColors = _mapItemsWithColors(widget.items, selectedColor);
Пишите код легче и понятнее, и мир станет лучше.
#простокод
Цепочки против деревьев
Вот есть операция. На вход получаем какие-то данные, их нужно подготовить к отправке на сервер, отправить, получить результат, распарсить и вернуть какие-то новые объекты. Обычное дело.
И есть такой вариант, когда программист разбивает операцию следующим образом:
И получается, что выполнение некоторой задачи разнесено по куче методов, иногда по разным классам и файлам. Из этого вытекает первый минус: не знакомому с этим местом программисту сложно построить итоговый алгоритм выполнения всей операции.
Второй минус: не понятна ответственность функций. Каждая функция возвращает некий итоговый вариант, о качестве которого она понятия не имеет, так как конечное решение делегировано следующей функции.
Также в такой системе сложно отслеживать ошибки, сложно разделять работу между классами, сложно протестировать код, сложнее разбить задачу на подзадачи.
В общем, такие цепочки писать не стоит. А лучше стоит разделять вфункции в деревообразную структуру, где первая функция наглядно показывает общий алгоритм работы:
Мы можем вытащить сам запрос к серверу в
Что-то не ясно, не согласны? Задавайте вопросы, пишите свое мнение, мне интересно.
Вот есть операция. На вход получаем какие-то данные, их нужно подготовить к отправке на сервер, отправить, получить результат, распарсить и вернуть какие-то новые объекты. Обычное дело.
И есть такой вариант, когда программист разбивает операцию следующим образом:
Future<bool> doOperation(param1, param2) async {
final chekRes = _doChecks(param1);
if (chekRes) return false;
return await _doOperationA(param1, param2);
}
Future<bool> _doOperationA(param1, param2) async {
final endpoint = _getEndpoint(param1);
final headers = _getHeaders(param2);
final res = await _doOperationB(endPoint, headers);
return _isResGood(res);
}
Future<Smth> _doOperationB(url, headers) async {
final res = await http.get(url, headers: headers);
return _convertRes();
}
И получается, что выполнение некоторой задачи разнесено по куче методов, иногда по разным классам и файлам. Из этого вытекает первый минус: не знакомому с этим местом программисту сложно построить итоговый алгоритм выполнения всей операции.
Второй минус: не понятна ответственность функций. Каждая функция возвращает некий итоговый вариант, о качестве которого она понятия не имеет, так как конечное решение делегировано следующей функции.
Также в такой системе сложно отслеживать ошибки, сложно разделять работу между классами, сложно протестировать код, сложнее разбить задачу на подзадачи.
В общем, такие цепочки писать не стоит. А лучше стоит разделять вфункции в деревообразную структуру, где первая функция наглядно показывает общий алгоритм работы:
Future<bool> doOperation(param1, param2) async {
final chekRes = _doChecks(param1);
if (chekRes) return false;
final endpoint = _getEndpoint(param1);
final headers = _getHeaders(param2);
final res = _doCall(endpoint, headers);
final data = _convertRes();
return _isResGood(data);
}
Мы можем вытащить сам запрос к серверу в
_doCall, и это не будет проблемой: ведь мы не вешаем на плечи этой функции ответственность за следующий шаг.Что-то не ясно, не согласны? Задавайте вопросы, пишите свое мнение, мне интересно.
Не используйте toString для преобразования типов
Если вы хотите на экране отобразить какой-то текст на основе данных, не надо описывать это в методах
Почему? Вот несколько причин.
Ответственность
Преобразование данных в строки — это ответственность презентатора, а вовсе не модели данных. В разных случаях по разным причинам мы можем захотеть отобразить одни и те же данные по разному.
Число можно округлить до 3 значащих цифр, или до целых, или до двух знаков после точки. Если число
В итоге, модель не должна знать, кто и как хочет преобразовывать ее в строку, это не ее дело.
Поиск использования
Поиск использования метода
Как надо делать
Напишите отдельные функции или классы для конвертации данных в нужный вид и используйте их в презентаторе.
В крайнем случае, если вы твердо намерены оставить этот метод в классе модели, дайте ему уникальное имя, опишите в doc comment, почему именно такой вариант и кто его использует.
Когда использовать toString
Для отладки. Когда хочешь написать что-нибудь в консоль функцией
В своих классах я обычно возвращаю строку типа
Если вы хотите на экране отобразить какой-то текст на основе данных, не надо описывать это в методах
toString. Это касается классов данных, моделей, вью-моделей и вообще практически всего остального.Почему? Вот несколько причин.
Ответственность
Преобразование данных в строки — это ответственность презентатора, а вовсе не модели данных. В разных случаях по разным причинам мы можем захотеть отобразить одни и те же данные по разному.
Число можно округлить до 3 значащих цифр, или до целых, или до двух знаков после точки. Если число
NaN, мы можем захотеть написать "..." или "In error". Или в одних случаях нам нужно добавить единицы измерения, а в других — не нужно.В итоге, модель не должна знать, кто и как хочет преобразовывать ее в строку, это не ее дело.
Поиск использования
Поиск использования метода
toString вам не поможет, это базовый метод класса Object. Так что вы не сможете легко найти, кто и почему использует именно такой вариант конвертации в строку.Как надо делать
Напишите отдельные функции или классы для конвертации данных в нужный вид и используйте их в презентаторе.
В крайнем случае, если вы твердо намерены оставить этот метод в классе модели, дайте ему уникальное имя, опишите в doc comment, почему именно такой вариант и кто его использует.
Когда использовать toString
Для отладки. Когда хочешь написать что-нибудь в консоль функцией
print. И делать такой метод только когда возникает такая необходимость. Заранее делать — смысла нет.В своих классах я обычно возвращаю строку типа
"MyClass(val: $val)".Assert или throw в конструкторе?
Я создаю экземпляр класса и передаю в конструктор параметры. И класс предъявляет некоторые требования для их значений. Строка не должна быть пустой, а провайдер не должен быть null.
Да, у меня все еще Flutter < 2.0 на основных проектах. И проверки на
Вопрос в том, каким образом делать проверки:
Первый случай
Первый случай — класс презентатора (ну BLoC, если хотите). В него (по фен-шую) передаются dependency через конструктор. В этом случае хватит
Почему? Такого рода классы не зависят от внешних данных. Они всегда создаются в одинаковых условиях. Для проверки достаточно в отладочном режиме перетыкать все экраны приложения.
Потому что параметры презентаторов — это бизнес-логики, дата-провайдеры, фабрики и подобное. И все эти классы не зависят от данных, получаемых из вне.
Но если такой класс нуждается в передаче данных типа модель и вы используете старый флаттер — на этот случай лучше кидать исключение.
Второй случай
Класс модели создается на основе данных: примитивных значений типа строк, чисел, булевых и т.д. Прежде чем собирать модель из данных, их нужно тщательно проверить.
Я делаю это в конструкторе модели, считаю, это лучшее место.
Так как данные поступают из внешних источников, они могут быть какими угодно. Ошибки нужно обрабатывать и на продакшене обязательно. Поэтому здесь мы используем
Я создаю экземпляр класса и передаю в конструктор параметры. И класс предъявляет некоторые требования для их значений. Строка не должна быть пустой, а провайдер не должен быть null.
Да, у меня все еще Flutter < 2.0 на основных проектах. И проверки на
null — самое частое, что мне приходится делать.Вопрос в том, каким образом делать проверки:
assert или throw? И у меня есть ответ.Первый случай
Первый случай — класс презентатора (ну BLoC, если хотите). В него (по фен-шую) передаются dependency через конструктор. В этом случае хватит
assert.Почему? Такого рода классы не зависят от внешних данных. Они всегда создаются в одинаковых условиях. Для проверки достаточно в отладочном режиме перетыкать все экраны приложения.
Потому что параметры презентаторов — это бизнес-логики, дата-провайдеры, фабрики и подобное. И все эти классы не зависят от данных, получаемых из вне.
Но если такой класс нуждается в передаче данных типа модель и вы используете старый флаттер — на этот случай лучше кидать исключение.
Второй случай
Класс модели создается на основе данных: примитивных значений типа строк, чисел, булевых и т.д. Прежде чем собирать модель из данных, их нужно тщательно проверить.
Я делаю это в конструкторе модели, считаю, это лучшее место.
Так как данные поступают из внешних источников, они могут быть какими угодно. Ошибки нужно обрабатывать и на продакшене обязательно. Поэтому здесь мы используем
throw.Просто код pinned «Введение В этом канале хочу описать ряд мыслей, которые сформировались по ходу работы. В основном они про архитектуру front-end приложений. Также, я работаю сейчас на языке Dart, в проектах использую Flutter. Поэтому часть постов будет более актуальна именно…»
3 уровня безопасности моделей
Сегодня расскажу про классы данных: модели и вью-модели.
За свою долгую практику я выработал три уровня безопасности таких классов:
1. иммутабельность,
2. исключения в конструкторе: отсекаем неправильные состояния,
3. разные интерфейсы для разных состояний.
Объясню каждый уровень.
Иммутабельность
В первую очередь классы должны быть неизменяемы, тогда в рантайме не будет неожиданных ошибок, не нужно будет проектировать параллельные асинхронные процессы, когда в одном месте программы изменили модель и ui, а в другом месте этого не произошло.
Например, удалили из списка пару задач, а на экране счетчик не обновился, и показывает уже не актуальное количество. Или в выпадающем списке все еще лежат штуки, которые уже удалили.
Не надо будет строить кучу стримов про обновление каждого поля данных или кучу проверок, что обновилось, а что — нет. Стримов и ошибок будет гораздо меньше.
Когда такие ошибки видны на одном экране, их, конечно, легко находить. Если они разнесены по приложению, или вообще не отображаются визуально — проблема может вылезти уже на релизе.
Что делаем: все поля
Что получили: состояния не меняются, проще код, меньше ошибок совместного обращения к данным.
Сегодня расскажу про классы данных: модели и вью-модели.
За свою долгую практику я выработал три уровня безопасности таких классов:
1. иммутабельность,
2. исключения в конструкторе: отсекаем неправильные состояния,
3. разные интерфейсы для разных состояний.
Объясню каждый уровень.
Иммутабельность
В первую очередь классы должны быть неизменяемы, тогда в рантайме не будет неожиданных ошибок, не нужно будет проектировать параллельные асинхронные процессы, когда в одном месте программы изменили модель и ui, а в другом месте этого не произошло.
Например, удалили из списка пару задач, а на экране счетчик не обновился, и показывает уже не актуальное количество. Или в выпадающем списке все еще лежат штуки, которые уже удалили.
Не надо будет строить кучу стримов про обновление каждого поля данных или кучу проверок, что обновилось, а что — нет. Стримов и ошибок будет гораздо меньше.
Когда такие ошибки видны на одном экране, их, конечно, легко находить. Если они разнесены по приложению, или вообще не отображаются визуально — проблема может вылезти уже на релизе.
Что делаем: все поля
final, избегаем List, Map и прочие изменяемые типы, указываем @immutable. И никакого наследования или расширения (кроме редких и действительно нужных случаев).Что получили: состояния не меняются, проще код, меньше ошибок совместного обращения к данным.
3 уровня безопасности моделей. Конструкторы
Вторым шагом в тело конструктора вносим все проверки на валидность данных. Если данные не верны — создать модель должно быть невозможно.
Если модель может содержать неправильные или неполные данные, то в каждом месте использования нам придется писать проверки, придумывать сценарии обработки провала таких проверок.
Я считаю, что эти проверки должен взять на себя тот, кто создает такие модели. Во первых, потому, что таких мест меньше. Во-вторых, все пользователи данных будут уверены в их правильности.
Все это приводит к тому, что проверок в коде нужно писать в несколько раз меньше, как и количество потенциальных ошибок.
А самое главное: решение об обработки неправильности данных будет брать на себя правильный класс: data-source, репозиторий или app-логика. а вовсе не кнопка или экран профиля пользователя.
Что делаем: все-все-все проверки данных описываем в конструкторе и кидаем исключения по любому поводу. Можно даже unit-тесты написать.
Если класс может описывать несколько состояний объекта (состояние задачи в такс-менеджере, например), и для каждого состояния свой набор правил, то создаем именные конструкторы на каждое состояние.
Что получили: сделали так, что неправильные состояния моделей сделали невозможными. Еще меньше проверок и ошибок при обращении к данным.
Такое разделение конструкторами помогает на этапе создания данных, чем помогают провайдерам и логикам. Но совсем не помогают пользователям данных. Об этом — третий пункт.
Вторым шагом в тело конструктора вносим все проверки на валидность данных. Если данные не верны — создать модель должно быть невозможно.
Если модель может содержать неправильные или неполные данные, то в каждом месте использования нам придется писать проверки, придумывать сценарии обработки провала таких проверок.
Я считаю, что эти проверки должен взять на себя тот, кто создает такие модели. Во первых, потому, что таких мест меньше. Во-вторых, все пользователи данных будут уверены в их правильности.
Все это приводит к тому, что проверок в коде нужно писать в несколько раз меньше, как и количество потенциальных ошибок.
А самое главное: решение об обработки неправильности данных будет брать на себя правильный класс: data-source, репозиторий или app-логика. а вовсе не кнопка или экран профиля пользователя.
Что делаем: все-все-все проверки данных описываем в конструкторе и кидаем исключения по любому поводу. Можно даже unit-тесты написать.
Если класс может описывать несколько состояний объекта (состояние задачи в такс-менеджере, например), и для каждого состояния свой набор правил, то создаем именные конструкторы на каждое состояние.
Что получили: сделали так, что неправильные состояния моделей сделали невозможными. Еще меньше проверок и ошибок при обращении к данным.
Такое разделение конструкторами помогает на этапе создания данных, чем помогают провайдерам и логикам. Но совсем не помогают пользователям данных. Об этом — третий пункт.
3 уровня безопасности моделей. Интерфейсы
*На уровне интерфейса убираем state errors и обращения к null*
Бывает, что сущность, которую представляет модель, имеет разные состояния.
У экрана со списком задач тоже несколько состояний. View model такого экрана будет иметь следующие состояния:
- "данные грузятся" — никаких данных больше не нужно,
- "ошибка загрузки данных" — текст ошибки, чтобы показать ее пользователю,
- "данные загружены" — список задач для отображения.
Если описать состояния одним классом, будет такое:
В виджете придется делать проверки на текущее состояние и пользоваться тем или иным набором данных. Виджету придется понимать логику этой модели.
Например, на то, что данные загружены, указывают два факта:
Еще мне не нравится, что если мы захотим изменить набор состояний, перераспределить поля между ними, придется менять как презентатор, так и виджет экрана.
Смысл этого уровня в том, чтобы разбить эти состояния на отдельные наборы данных, не объединенные общим интерфейсом.
Исправить такие проблемы можно в духе пакета freezed. Я стараюсь избегать любых дополнений в классах моделей и в бизнес-логике. Но вот эта очень помогает получить безопасный код и сохранить время работы.
Freezed помогают сделать для каждого состояния свой интерфейс. Условно говоря, в результате такого подхода на каждое состояние будут три калбека, каждый со своим набором полей:
Таким образом, мы снова значительно улучшаем код:
- виджету не нужно знать логику состояний,
- виджет всегда сразу получает все необходимые данные,
- мы никогда не забудем обновить ui после изменения логики состояний.
Что делаем: используем freezed для view моделей.
Что получаем: простой и надежный код, а все знание о логике состояний модели сосредоточена на уровне презентаторов и app-логики.
*На уровне интерфейса убираем state errors и обращения к null*
Бывает, что сущность, которую представляет модель, имеет разные состояния.
У экрана со списком задач тоже несколько состояний. View model такого экрана будет иметь следующие состояния:
- "данные грузятся" — никаких данных больше не нужно,
- "ошибка загрузки данных" — текст ошибки, чтобы показать ее пользователю,
- "данные загружены" — список задач для отображения.
Если описать состояния одним классом, будет такое:
class TasksState {
final bool isLoading;
final String errorMessage;
final List<SomeClass> data;
}
В виджете придется делать проверки на текущее состояние и пользоваться тем или иным набором данных. Виджету придется понимать логику этой модели.
Например, на то, что данные загружены, указывают два факта:
isLoading == false и data ≠ null, и наверное не пустое. Не очевидно, как надо проверять факт загруженности данных.Еще мне не нравится, что если мы захотим изменить набор состояний, перераспределить поля между ними, придется менять как презентатор, так и виджет экрана.
Смысл этого уровня в том, чтобы разбить эти состояния на отдельные наборы данных, не объединенные общим интерфейсом.
Исправить такие проблемы можно в духе пакета freezed. Я стараюсь избегать любых дополнений в классах моделей и в бизнес-логике. Но вот эта очень помогает получить безопасный код и сохранить время работы.
Freezed помогают сделать для каждого состояния свой интерфейс. Условно говоря, в результате такого подхода на каждое состояние будут три калбека, каждый со своим набором полей:
final TasksState data = snapshot.data;
data.when(
loading: () {},
loaded: (data) {},
error: (errorMessage) {},
);
Таким образом, мы снова значительно улучшаем код:
- виджету не нужно знать логику состояний,
- виджет всегда сразу получает все необходимые данные,
- мы никогда не забудем обновить ui после изменения логики состояний.
Что делаем: используем freezed для view моделей.
Что получаем: простой и надежный код, а все знание о логике состояний модели сосредоточена на уровне презентаторов и app-логики.
3 уровня безопасности моделей. Итоги
Таким образом, мы самой организацией кода технически делаем невозможными большое количество ошибок.
Уменьшаем количество проверок во всех местах использования таких классов данных. Особенно это касается самой стремной, неудобной и часто меняющейся части кода: презентаторы и UI.
Прозрачный и понятный интерфейс предлагает более простую обработку данных и подбор подходящего отображения.
Не надо будет писать код на скорую руку, игнорируя внутреннего перфекциониста, который кричит: "а что если это null?! А если эти две штуки будут иметь неконсистентные данные?".
Таким образом, мы самой организацией кода технически делаем невозможными большое количество ошибок.
Уменьшаем количество проверок во всех местах использования таких классов данных. Особенно это касается самой стремной, неудобной и часто меняющейся части кода: презентаторы и UI.
Прозрачный и понятный интерфейс предлагает более простую обработку данных и подбор подходящего отображения.
Не надо будет писать код на скорую руку, игнорируя внутреннего перфекциониста, который кричит: "а что если это null?! А если эти две штуки будут иметь неконсистентные данные?".
Как и когда использовать union типы
Юнион типы очч классные ребята, помогают избегать ошибок, делать код чище и прямее. Но и с ними можно легко напортачить и влезть в дебри говнокода, если не соблюдать вот эти простые правила.
Используй union типы только в двух местах:
1. тип возвращаемого значения функции или метода,
2. тип в коллекции.
Избегай использовать union для указания типа поля класса. Скорее всего, значение нужно заранее привести к какому-то более общему или конкретному типу.
Юнион типы очч классные ребята, помогают избегать ошибок, делать код чище и прямее. Но и с ними можно легко напортачить и влезть в дебри говнокода, если не соблюдать вот эти простые правила.
Используй union типы только в двух местах:
1. тип возвращаемого значения функции или метода,
2. тип в коллекции.
Избегай использовать union для указания типа поля класса. Скорее всего, значение нужно заранее привести к какому-то более общему или конкретному типу.
МАПЫ ВМЕСТО МОДЕЛЕЙ — ПЛОХО
Но это не точно
Я делал дашборд, и у меня в нем были виджеты, с набором пропертей. Еще есть редактор этого дашборда, где есть панель пропертей виджета.
Спецификации, параметры я хранил как мапы <String, Smth>. И буквально все остальные модели были организованы через мапы.
Чем это плохо
Геморно менять имя поля. Да, можно выделить константу на имя поля, но не решает других проблем.
Нет валидации полноты модели. Мап — это мап, ему все равно, что ты там задал.
Много мест для изменений, которые легко пропустить. Плохо для добавления новых полей в виджеты.
Что делать
Я пока разбираюсь, как тут лучше. Пока есть идея все сделать иммутабельными классами с полями.
Вся идея с мапами идет от желания получить общий интерфейс при работе с моделями виджетов разных типов. Сейчас есть union-type (freezed). Значит, в нужных местах сделаем юнион.
Вопрос только в том, не надо ли будет создавать миллион похожих юнион типов, и не надо ли будет где еще какие проблемы третьим способом решать. Например, как получить список названий полей и их значений для отображения в панели пропертей?
Но это не точно
Я делал дашборд, и у меня в нем были виджеты, с набором пропертей. Еще есть редактор этого дашборда, где есть панель пропертей виджета.
Спецификации, параметры я хранил как мапы <String, Smth>. И буквально все остальные модели были организованы через мапы.
Чем это плохо
Геморно менять имя поля. Да, можно выделить константу на имя поля, но не решает других проблем.
Нет валидации полноты модели. Мап — это мап, ему все равно, что ты там задал.
Много мест для изменений, которые легко пропустить. Плохо для добавления новых полей в виджеты.
Что делать
Я пока разбираюсь, как тут лучше. Пока есть идея все сделать иммутабельными классами с полями.
Вся идея с мапами идет от желания получить общий интерфейс при работе с моделями виджетов разных типов. Сейчас есть union-type (freezed). Значит, в нужных местах сделаем юнион.
Вопрос только в том, не надо ли будет создавать миллион похожих юнион типов, и не надо ли будет где еще какие проблемы третьим способом решать. Например, как получить список названий полей и их значений для отображения в панели пропертей?
Кстати, если кому-то не нравится кодо-генерация, вместо freezed можно попробовать использовать sealed_unions. Выглядит неплохо на первый взгляд.
Я, конечно, не новостной канал, но с новым flutter 3.3 и dart 2.18 появляются проблемы в их оптимизации async операции.
У меня в каком-нибудь презентере (типа блок) в конструкторе создается запрос на загрузку доп. данных с сервера (инфа о пользователе, например), а после этого я закидываю в поток первое состояние экрана с ожиданием загрузки.
В новом дарте может выйти так, что данные о пользователе уже загружены и закешированны, и я покажу сразу нормальный рабочий экран, а следующей строчкой я добавляю уже не актуальное состояние предзагрузки, и пользователь никогда не увидит рабочу форму.
Упс, не удобненько выйдет. Надо внимательно все переписывать и тестировать теперь после апдейта.
У меня в каком-нибудь презентере (типа блок) в конструкторе создается запрос на загрузку доп. данных с сервера (инфа о пользователе, например), а после этого я закидываю в поток первое состояние экрана с ожиданием загрузки.
В новом дарте может выйти так, что данные о пользователе уже загружены и закешированны, и я покажу сразу нормальный рабочий экран, а следующей строчкой я добавляю уже не актуальное состояние предзагрузки, и пользователь никогда не увидит рабочу форму.
Упс, не удобненько выйдет. Надо внимательно все переписывать и тестировать теперь после апдейта.
Антипаттерн “Класс с инициализацией”
Пишем класс типа:
Вот так делать плохо.
У нас уже существует объект, поля которого не инициализированы. Если во время
Надо переделать код, вот к чему придем:
- Мы разделим создание объекта и загрузку данных, это хорошо.
- Если объект существует, значит он валидный. И это очень хорошо.
- Объекту не придется хранить доп. поля для инициализации (
- Мы избежим очень косячных типов полей
Новый код будет выглядеть так:
Все стало проще, понятнее и независимее.
Заметка, опять же, по мотивам очень злого бага на проде.
Пишем класс типа:
class AntiClass {
final String uri;
late final String param1;
late final String param2;
AntiClass(this.uri);
Future<void> init() async {
final result = await LoadSomth();
param1 = result.param1;
param2 = result.param2;
}
}
/// somewhere:
final obj = AntiClass(uri);
await obj.init();
Вот так делать плохо.
У нас уже существует объект, поля которого не инициализированы. Если во время
init была ошибка, то объект так и остается существовать в памяти, и его могут ошибочно использовать дальше. То есть, никто нам не скажет, правильное ли у объекта состояние.Надо переделать код, вот к чему придем:
- Мы разделим создание объекта и загрузку данных, это хорошо.
- Если объект существует, значит он валидный. И это очень хорошо.
- Объекту не придется хранить доп. поля для инициализации (
uri).- Мы избежим очень косячных типов полей
late final, что тоже хорошо.Новый код будет выглядеть так:
Future<GoodClass> loadObj() async {
final result = await LoadSomth();
return GoodClass(
param1: result.param1,
param2: result.param2,
);
}
class GoodClass {
final String param1;
final String param2;
GoodClass({@required this.param1, @required this.param2});
}
/// somewhere:
final obj = await loadObj(uri);
Все стало проще, понятнее и независимее.
Заметка, опять же, по мотивам очень злого бага на проде.
Частичное переиспользование enum
Переиспользование кода часто приводит к проблемам в коде. Вот сегодняшний герой из этой лиги.
Представьте ситуацию: мы получаем с сервера данные о качестве воздуха, значения могут быть нескольких типов.
Для обозначения типа мы сделали enum
Я часто наблюдаю ситуацию, когда для обозначения текущей вкладки используют тот же enum
Получается, что где-то в виджете экрана есть место, где отбрасываются два ненужных значения этого enum, а ошибки игнорируются. И частенько таких мест больше одного!
Так, конечно, делать плохо. Будет сложно рефакторить, будет слишком много сцепок между совершенно независящими друг от друга слоями приложения.
Чего будет стоит добавление новой вкладки для показа температуры и влажности вместе! А если наоборот, в типы загружаемых данных добавят еще три новых значения?
В таких случаях надо обязательно иметь два разных набора значений. количество и назначение вкладок ну точно не должны зависеть от типов данных.
И, конечно, с самого начала при написании приложения или нового экрана такие “переиспользования” следует избегать.
#антипаттерн #переиспользование_кода
Переиспользование кода часто приводит к проблемам в коде. Вот сегодняшний герой из этой лиги.
Представьте ситуацию: мы получаем с сервера данные о качестве воздуха, значения могут быть нескольких типов.
Для обозначения типа мы сделали enum
ValueType с четырьмя значениями: влажность, температура, co2 и pm2.5. Где-то в приложении есть экран с двумя вкладками: co2, pm25.Я часто наблюдаю ситуацию, когда для обозначения текущей вкладки используют тот же enum
ValueType.Получается, что где-то в виджете экрана есть место, где отбрасываются два ненужных значения этого enum, а ошибки игнорируются. И частенько таких мест больше одного!
Так, конечно, делать плохо. Будет сложно рефакторить, будет слишком много сцепок между совершенно независящими друг от друга слоями приложения.
Чего будет стоит добавление новой вкладки для показа температуры и влажности вместе! А если наоборот, в типы загружаемых данных добавят еще три новых значения?
В таких случаях надо обязательно иметь два разных набора значений. количество и назначение вкладок ну точно не должны зависеть от типов данных.
И, конечно, с самого начала при написании приложения или нового экрана такие “переиспользования” следует избегать.
#антипаттерн #переиспользование_кода
Человек пишет про хороший код на примере создания экрана инвенторя в игре на Unity. Первым делом он предлагает создать абстрактный базовый класс BaseItem для описания разных предметов.
У меня сразу ладонь летит в лоб. Думаю, парень, с чего ты взял, что тебе нужна древовидная иерархия предметов по одному полю тип? Чем этот тип такой волшебный? Ты уверен, что тебе надо создать 100500 классов для всех типов предметов вместо одного класса с полем itemType?
Наследование нужно применять там, где нужно иметь разные варианты поведения. Но предметы в интвентаре ведут себя одинаково! А все их различия можно описать полями в едином классе.
Это, наверное, пришло из учебников по ООП, где выстраивали цепочки "животное" - "собака" - "лайка". И почему-то наследование стало дефолтным способом разработки чего угодно.
К чему это я? К тому, что мне мой опыт говорит, чтоб я бежал как можно дальше от наследования. Так много от него проблем было.
Update. Извините, погорячился
https://habr.com/ru/post/700272/
У меня сразу ладонь летит в лоб. Думаю, парень, с чего ты взял, что тебе нужна древовидная иерархия предметов по одному полю тип? Чем этот тип такой волшебный? Ты уверен, что тебе надо создать 100500 классов для всех типов предметов вместо одного класса с полем itemType?
Наследование нужно применять там, где нужно иметь разные варианты поведения. Но предметы в интвентаре ведут себя одинаково! А все их различия можно описать полями в едином классе.
Это, наверное, пришло из учебников по ООП, где выстраивали цепочки "животное" - "собака" - "лайка". И почему-то наследование стало дефолтным способом разработки чего угодно.
К чему это я? К тому, что мне мой опыт говорит, чтоб я бежал как можно дальше от наследования. Так много от него проблем было.
Update. Извините, погорячился
https://habr.com/ru/post/700272/
Хабр
Почему тяжело писать про хороший код?
Всем привет. Меня зовут Гриша Дядиченко, и я технический продюсер. Почему так сложно писать про хороший код? Меня периодически спрашивают, почему я так мало пишу про архитектуру. В то же время я даже...
👍3
Простая переделка Singleton
Увидел такое решение забавное, которое позволит заменить ненавистный синглтон на другую похожую конструкцию без прокидывания зависимостей.
Обычно выглядит так:
А можно переделать это на такой вариант:
Стало ли лучше?
Да. Теперь мы можем вместо создания настоящего экземпляра MySingleton создать mockSingleton, использовать его для тестов, или для смены версии базы данных, и подобное.
Кроме того, нам почти не приходится переписывать много кода и реорганизовывать его по новому.
Стало ли хуже?
Немного да. Теперь надо во-первых, хранить как-то этот id. А во вторых, мы добавили теоретическую возможность того, что экземпляр класса еще не создан или не задан для переданного id.
Что в итоге
Думаю, так можно пробовать делать для распутывания немаленького приложения, уводя его от плохого паттерна Синглтон. Это решение напоминает service locator.
Если проект замучен синглтонами, это — хорошее движение в сторону лучшего кода.
Увидел такое решение забавное, которое позволит заменить ненавистный синглтон на другую похожую конструкцию без прокидывания зависимостей.
Обычно выглядит так:
class MySingleton {
static final MySingleton instance = MySingleton._();
MySingleton._();
}
class SomeClass {
void someMethod() {
MySingleton.instance.doSmth();
}
}
А можно переделать это на такой вариант:
class MySingleton {
// some mistery here so far
}
void main() {
final singleton = MySingleton.create();
MySingleton.put('my_id', singleton);
//...
}
class SomeClass {
void someMethod() {
MySingleton.get('my_id').doSmth();
}
}
Стало ли лучше?
Да. Теперь мы можем вместо создания настоящего экземпляра MySingleton создать mockSingleton, использовать его для тестов, или для смены версии базы данных, и подобное.
Кроме того, нам почти не приходится переписывать много кода и реорганизовывать его по новому.
Стало ли хуже?
Немного да. Теперь надо во-первых, хранить как-то этот id. А во вторых, мы добавили теоретическую возможность того, что экземпляр класса еще не создан или не задан для переданного id.
Что в итоге
Думаю, так можно пробовать делать для распутывания немаленького приложения, уводя его от плохого паттерна Синглтон. Это решение напоминает service locator.
Если проект замучен синглтонами, это — хорошее движение в сторону лучшего кода.
👍1
Media is too big
VIEW IN TELEGRAM
Привет! Решил записать в формате видео, напишите ,как вам, дослушали или бросили, интересно было, или мне нужно лучше готовить текст. Удобно ли в таком формате?
Тема видео: показываю простые базовые элементы архитектуры, чтобы мы лучше понимали друг друга. Чтобы я какие-то более сложные мысли по архитектуре мог описать и был понят.
Это эксперимент. Сам удивился, что получилось так длинно. Да, пишите в комменты, чо как вам, понятно, доступно или совсем наоборот. Или даже мысли по теме ролика. 👍
Тема видео: показываю простые базовые элементы архитектуры, чтобы мы лучше понимали друг друга. Чтобы я какие-то более сложные мысли по архитектуре мог описать и был понят.
Это эксперимент. Сам удивился, что получилось так длинно. Да, пишите в комменты, чо как вам, понятно, доступно или совсем наоборот. Или даже мысли по теме ролика. 👍
👍2