Концентрация Flutter-разработчиков на них зашкаливает. Делимся яркими моментами с конференции CrossConf.
Героини и авторы постов @flutterfriendly тоже там были: Катя и Анна модерировали потоки по Flutter. Роза выступала с докладом.
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
❤12🔥2
Сегодня хочу рассказать про шейдеры и показать, как их использовать во Flutter-проектах. Прежде чем мы начнем что-то подключать и запускать, давайте разберемся, что это вообще такое.
Шейдер — это программа, которая выполняется на графическом процессоре и отвечает за вычисление цвета каждого пикселя. Именно благодаря шейдерам мы можем получать освещение, тени, блики, искажения, волны, шумы, стеклянные поверхности и множество других эффектов. Для их написания используется язык GLSL. Он напоминает Си и специально создан для задач реального времени, например, в играх и анимации.
Шейдеры могут быть полезны, когда хочется настоящего размытия, а не его имитации, или при создании динамичных эффектов вроде волн, огня, дождя, стекла и других, которые делают интерфейс более уникальным.
Во Flutter можно использовать как готовые шейдеры, так и писать их самостоятельно. Сегодня мы разберем ситуацию, когда шейдер у нас уже есть. В следующем посте попробуем создать его с нуля или адаптировать готовый под наши потребности.
1️⃣ Чтобы шейдер стал частью приложения, достаточно подключить его в pubspec.yaml:
flutter:
shaders:
- shaders/my_shader.frag
Здесь важно именно расширение .frag. Flutter пока поддерживает только фрагментные шейдеры.
2️⃣ После этого загружаем шейдер:
late FragmentProgram program;
Future<void> loadMyShader() async {
program = await FragmentProgram.fromAsset('shaders/my_shader.frag');
}
Объект FragmentProgram можно использовать для создания одного или нескольких экземпляров FragmentShader.
3️⃣ Далее создаем FragmentShader и передаем нужные параметры. Например, размер canvas или время для анимации. Сам рендеринг удобно выполнять через CustomPainter:
class ShaderPainter extends CustomPainter {
final FragmentShader shader;
final double time;
ShaderPainter(FragmentShader fragmentShader, this.time)
: shader = fragmentShader;
@override
void paint(Canvas canvas, Size size) {
final paint = Paint();
shader.setFloat(0, size.width);
shader.setFloat(1, size.height);
shader.setFloat(2, time);
paint.shader = shader;
canvas.drawRect(Offset.zero & size, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}Важно помнить про несколько нюансов:
▪️Шейдеры во Flutter пока ограничены только фрагментными программами. Их размер и сложность влияют на вес сборки и производительность, поэтому лучше не переусердствовать
▪️Желательно кэшировать FragmentProgram, чтобы не загружать его повторно
▪️И, конечно, следить за количеством uniform-переменных
Кстати, Uniform-переменные — это глобальные параметры шейдера, которые задаются приложением. Они одинаковые для всех пикселей и позволяют управлять эффектом из Flutter-кода. Например, если в шейдере есть uTime, то мы можем передавать туда анимационное значение:
shader.setFloat(0, controller.value);
Контроллер меняет число каждый кадр, шейдер пересчитывает волну, и ваш UI плавно анимируется, нагружая GPU, а не CPU.
❕Ключевой момент: индекс в shader.setFloat(index, value) обязательно должен совпадать с порядком объявления uniform-переменных в GLSL-коде.
Сегодня мы прошли только базу,а дальше можно экспериментировать бесконечно. Если вам интересно углубиться, загляните в документацию.
❤️ — если хотите посты, в которых мы разберем создание своего собственного шейдера и подключение его к анимации
Please open Telegram to view this post
VIEW IN TELEGRAM
❤15👍2🔥1
This media is not supported in your browser
VIEW IN TELEGRAM
Сегодня расскажу о сравнении коллекций и почему встроенный оператор == работает не так, как ожидают многие разработчики
Базовое сравнение
Встроенный оператор == у коллекций (List, Set, Map) проверяет ссылки на объект, а не содержимое. То есть два списка с одинаковыми элементами будут не равны, если это разные объекты в памяти:
void main() {
var a = [1, 2, 3];
var b = [1, 2, 3];
print(a == b); // false, разные объекты
print(identical(a, b)); // false
}Правильное сравнение содержимого
Для сравнения по значениям используют пакет collection. Он предоставляет специализированные классы для разных типов коллекций.
ListEquality
Сравнение списков поэлементно в строгом порядке:
import 'package:collection/collection.dart';
void main() {
var a = [1, 2, 3];
var b = [1, 2, 3];
print(const ListEquality().equals(a, b)); // true
}
SetEquality
Сравнение множеств без учета порядка элементов:
import 'package:collection/collection.dart';
void main() {
var a = {1, 2, 3};
var b = {3, 2, 1};
print(const SetEquality().equals(a, b)); // true
}
MapEquality
Сравнение словарей по ключам и значениям:
import 'package:collection/collection.dart';
void main() {
var a = {"x": 1, "y": 2};
var b = {"y": 2, "x": 1};
print(const MapEquality().equals(a, b)); // true
}
Глубокое сравнение
Если коллекции вложенные (List<Map<String, Set<int>>>), то используют DeepCollectionEquality. Он рекурсивно сравнивает элементы на любом уровне вложенности:
import 'package:collection/collection.dart';
void main() {
var a = [
{"nums": {1, 2}}
];
var b = [
{"nums": {2, 1}}
];
print(const DeepCollectionEquality().equals(a, b)); // true
}
Сравнение с кастомной логикой
Можно задать свою функцию сравнения для элементов коллекций:
import 'package:collection/collection.dart';
void main() {
var a = ["Hello", "world"];
var b = ["hello", "WORLD"];
var eq = ListEquality(StringEquality(ignoreCase: true));
print(eq.equals(a, b)); // true
}
❤️ — если было полезно
Please open Telegram to view this post
VIEW IN TELEGRAM
❤24👍6🔥4
This media is not supported in your browser
VIEW IN TELEGRAM
У вас собеседование через 5 минут!
🔤 🔤 🔤 ! Испугались? Привет! Это Анна, Flutter Team Lead Friflex. Сегодня мы опустим все вопросы технических реализаций и поговорим о насущном, о том, что вселяет страх и ужас начинающим специалистам — техническом собеседовании на позицию Flutter-разработчика.
Для начала отмечу, что в зависимости от компании и от уровня кандидата собеседования могут проходить немного по-разному. Здесь будет некоторая усредненная информация, которая поможет подготовиться именно начинающим разработчикам.
1️⃣ Описание опыта
Даже если у вас в резюме уже указан ваш опыт работы или его отсутствие, будьте готовы, что вопрос о нем все равно прозвучит на собеседовании. Интервьюеру всегда важно послушать, что и как вы говорите о своем опыте. В резюме можно написать все, что угодно, но именно живой рассказ помогает понять, правда это или нет.
Здесь рекомендую перед собеседованием вспомнить, с чем вы уже имели дело. Чаще всего спрашивают о технологиях и библиотеках, с которыми вы сталкивались, о самых интересных и самых сложных задачах за последнее время. Важно отвечать достаточно развернуто: что именно давалось с трудом, как решили, что нового узнали в процессе. Не бойтесь говорить о pet-проектах, если коммерческого опыта пока нет.
2️⃣ Технические вопросы
Техническое собеседование всегда подразумевает их наличие. Здесь рекомендую заранее изучить списки вопросов под свой уровень в интернете. А также саму вакансию — почти всегда в вакансии указывается стек технологий, по которому с большей долей вероятности вы получите вопрос на собеседовании. От себя для новичков могу посоветовать изучить следующие темы:
Dart
▫️ Null-safety
▫️ Event Loop
▫️ параллельность
▫️ dart:async, Future, Stream, Isolate
▫️ модификаторы классов и переменных, конструкторы классов
▫️ коллекции и операции с ними
▫️ обработка ошибок
Flutter
▫️ Stateless- и Stateful-виджеты, жизненный цикл Stateful-виджета
▫️ Layout-виджеты, отличия Flexible/Expanded
▫️ InheritedWidget
▫️ BuildContext, Widget, Element
▫️ навигация без библиотек
▫️ Navigator, отличия императивной и декларативной (верхнеуровневой)
▫️ анимации
▫️ виджеты прокрутки (SingleChildScrollView, ListView, CustomScrollView)
▫️ тесты
Конечно, это только малая часть. Если у вас есть, чем дополнить этот список — wellcome в комментарии!
3️⃣ Моральная подготовка
Собеседование — это всегда стресс для кандидата. Честно скажу, я пока не встречала ни одного человека, который не переживал бы перед ним. И здесь важно понять, что волнение — это абсолютно нормальная реакция на этот этап. От волнения можно сбиться, запутаться, что-то забыть, и это норма!
Поверьте, те, кто проводят собеседование, прекрасно понимают, под каким психологическим давлением вы находитесь. Если вы чувствуете, что вам нужна пара минут, чтобы перевести дух, прийти в себя и хорошо подумать над вопросом — не стесняйтесь об этом сказать. Все мы люди, вас никто не осудит.
И в первую очередь настраивайте себя на позитивный исход. Даже если вы не получите заветный оффер, вы получите гораздо больше — бесценный опыт.
Давайте в комментариях соберем лучшие советы для всех, кто проходит и кто проводит собеседования👇
Для начала отмечу, что в зависимости от компании и от уровня кандидата собеседования могут проходить немного по-разному. Здесь будет некоторая усредненная информация, которая поможет подготовиться именно начинающим разработчикам.
1️⃣ Описание опыта
Даже если у вас в резюме уже указан ваш опыт работы или его отсутствие, будьте готовы, что вопрос о нем все равно прозвучит на собеседовании. Интервьюеру всегда важно послушать, что и как вы говорите о своем опыте. В резюме можно написать все, что угодно, но именно живой рассказ помогает понять, правда это или нет.
Здесь рекомендую перед собеседованием вспомнить, с чем вы уже имели дело. Чаще всего спрашивают о технологиях и библиотеках, с которыми вы сталкивались, о самых интересных и самых сложных задачах за последнее время. Важно отвечать достаточно развернуто: что именно давалось с трудом, как решили, что нового узнали в процессе. Не бойтесь говорить о pet-проектах, если коммерческого опыта пока нет.
2️⃣ Технические вопросы
Техническое собеседование всегда подразумевает их наличие. Здесь рекомендую заранее изучить списки вопросов под свой уровень в интернете. А также саму вакансию — почти всегда в вакансии указывается стек технологий, по которому с большей долей вероятности вы получите вопрос на собеседовании. От себя для новичков могу посоветовать изучить следующие темы:
Dart
▫️ Null-safety
▫️ Event Loop
▫️ параллельность
▫️ dart:async, Future, Stream, Isolate
▫️ модификаторы классов и переменных, конструкторы классов
▫️ коллекции и операции с ними
▫️ обработка ошибок
Flutter
▫️ Stateless- и Stateful-виджеты, жизненный цикл Stateful-виджета
▫️ Layout-виджеты, отличия Flexible/Expanded
▫️ InheritedWidget
▫️ BuildContext, Widget, Element
▫️ навигация без библиотек
▫️ Navigator, отличия императивной и декларативной (верхнеуровневой)
▫️ анимации
▫️ виджеты прокрутки (SingleChildScrollView, ListView, CustomScrollView)
▫️ тесты
Конечно, это только малая часть. Если у вас есть, чем дополнить этот список — wellcome в комментарии!
3️⃣ Моральная подготовка
Собеседование — это всегда стресс для кандидата. Честно скажу, я пока не встречала ни одного человека, который не переживал бы перед ним. И здесь важно понять, что волнение — это абсолютно нормальная реакция на этот этап. От волнения можно сбиться, запутаться, что-то забыть, и это норма!
Поверьте, те, кто проводят собеседование, прекрасно понимают, под каким психологическим давлением вы находитесь. Если вы чувствуете, что вам нужна пара минут, чтобы перевести дух, прийти в себя и хорошо подумать над вопросом — не стесняйтесь об этом сказать. Все мы люди, вас никто не осудит.
И в первую очередь настраивайте себя на позитивный исход. Даже если вы не получите заветный оффер, вы получите гораздо больше — бесценный опыт.
Давайте в комментариях соберем лучшие советы для всех, кто проходит и кто проводит собеседования👇
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥10❤8😁3🥰1
Что это?
Аннотации — это специальные метки (метаданные), которые можно добавлять к классам, методам, переменным и другим элементам кода. Они не изменяют сам код напрямую, но дают дополнительную информацию компилятору, инструментам или фреймворкам.
Пример:
class Person {
final String name;
const Person(this.name);
}
@deprecated // помечает метод как устаревший
void oldMethod() {
print("Этот метод больше не рекомендуется использовать");
}
@override // указывает, что метод переопределяет родительский
String toString() => "Person";📍Встроенные
@override — помечает метод, который переопределяет родительский@deprecated — помечает элемент как устаревший@pragma — специальные инструкции для VM (например, @pragma('vm:entry-point'))📍Из пакета meta
@immutable — класс неизменяемый (все поля final)@required — аннотация для обязательных параметров (устарела с введением модификатора required, который теперь указывается прямо в объявлении параметра)@protected — метод/поле должно использоваться только внутри самого класса или подклассов@visibleForTesting — элемент предназначен только для тестов@visibleForOverriding — метод/поле предназначено для переопределения@mustCallSuper — при переопределении метода нужно обязательно вызвать super@nonVirtual — метод не может быть предопределен@sealed — класс нельзя наследовать за пределами своей библиотеки@doNotStore — нельзя хранить ссылку на объект (например, BuildContext)@factory — метод/конструктор должен возвращать новый объект каждый раз📍Для генерации кода (через build_runner)
@JsonSerializable — для генерации JSON-сериализации (json_serializable)@JsonKey — настройка маппинга конкретного поля в JSON@HiveType / @HiveField — для сериализации в Hive@Freezed и связанные (@freezed) — генерация immutable-классов и union typesВ прошлом посте Аня рассказывала о собеседованиях, так что давайте проверим ваши знания про аннотации. Чур не подсматривать!🫣
Please open Telegram to view this post
VIEW IN TELEGRAM
❤5👍3🔥1👏1
❤5
Какая аннотация используется для классов, все поля которых должны быть final?
Anonymous Quiz
8%
1%
87%
4%
❤4
Для чего используется аннотация @visibleForTesting?
Anonymous Quiz
6%
Для генерации тестов автоматически
91%
Для обозначения элементов, предназначенных только для тестов
1%
Для скрытия кода от тестирования
2%
Для запуска тестов в production
❤4
Какая аннотация запрещает наследование класса за пределами его библиотеки?
Final Results
38%
5%
54%
3%
❤4
This media is not supported in your browser
VIEW IN TELEGRAM
Пока я готовлю пост про шейдеры, давайте обсудим, как можно создать свой CLI-пакет на Dart. Мы сталкиваемся с CLI каждый день, зачастую даже не задумываясь об этом. Когда вы вводите flutter doctor, чтобы проверить окружение, или запускаете firebase init, чтобы создать проект Firebase, вы взаимодействуете именно с CLI.
CLI (Command Line Interface) — это интерфейс взаимодействия с приложением через консоль: вы передаете команды и аргументы, а приложение что-то выполняет. Если сильно упростить, CLI — это возможность ввести в консоли:
mytool login
И программа выполнит какое-то действие, не открывая UI.
Создать свой CLI на Dart довольно просто. Для этого нужно выполнить команду:
dart create -t console-full my_cli_tool
cd my_cli_tool
После генерации у вас появится базовая структура проекта:
/bin
my_cli_tool.dart <-- точка входа
/lib
...
Все, что находится в папке bin/, является входной точкой нашего CLI.
Когда структура готова, можно добавить команды. Для обработки аргументов и команд идеально подходит пакет args. Кстати, про эту библиотеку вы можете подробнее прочитать в этом посте.
Представим, что у нас есть приложение, и мы хотим добавить CLI-команду для авторизации пользователя. Создадим команду LoginCommand:
class LoginCommand extends Command {
@override
final name = 'login'; // название команды
@override
final description = 'Login to service'; // описание, показывается при -h
@override
Future<void> run() async {
// ... логика авторизации
}
}Для взаимодействия с терминалом используем потоки: stdout, stderr и stdin.
✔️ stdout — вывод обычной информации в терминал (сообщения, результаты)
✔️ stderr — поток ошибок (то, что пользователю важно видеть, если что-то пошло не так)
✔️ stdin — ввод данных пользователем. Например, stdin.readLineSync() просто ждет, пока пользователь нажмет Enter, и возвращает введенный текст
stdout.write('📧 Enter your email: ');
final email = stdin.readLineSync();Для более удобной работы с интерактивным вводом (например, чтобы скрыть ввод пароля или добавить выбор стрелками) можно использовать библиотеку dcli.
mixin CliMixin {
String askStringField(
String prompt, {
String? defaultValue,
bool required = true,
bool hidden = false,
}) {
final result = ask(
prompt,
hidden: hidden,
defaultValue: defaultValue,
required: required,
validator: const NotEmptyValidator(),
);
return result;
}
String askSelectField(
String prompt,
List<String> options, {
String? defaultValue,
}) {
final selected = menu(
prompt,
options: options,
defaultOption: defaultValue,
);
return selected;
}
}После успешного логина данные можно сохранить на диск (например, в ~/.my_cli/config.json). В следующих командах CLI автоматически поймет, что пользователь уже авторизован.
И вот наступает самый приятный момент: CLI готов, его можно сделать глобальной системной командой. Для этого в файле pubspec.yaml нужно добавить секцию executables:
executables:
mytool: my_cli_tool
После этого выполняем:
dart pub global activate --source path
Теперь в любой директории можно набрать:
mytool login
и команда запустится, будто это встроенная системная утилита.
Если вы хотите пойти дальше, можно собрать бинарник под macOS, Linux или Windows:
dart compile exe bin/my_cli_tool.dart -o mytool
Таким образом, CLI становится полноценным инструментом, который можно использовать на любых платформах и с приложениями на разных языках программирования: его можно публиковать на pub.dev, подключить через Homebrew или распространять внутри команды.
❤️ — если нужно продолжение про публикацию CLI в Homebrew и сборку бинарников
Please open Telegram to view this post
VIEW IN TELEGRAM
❤18🔥4👍1
Поговорим сегодня про assert в Dart. Разберемся, что же это такое, как работает и почему он точно будет вам полезен.
Assert — это оператор в языке Dart для проверки утверждений в процессе отладки вашего кода. Он помогает разработчику проверить, что определенные условия выполняются корректно. В случае, если это не так, программа выдает ошибку
AssertionError. Синтаксис очень прост — в оператор требуется передать условие для проверки, а также сообщение, которое поможет разобраться в том, почему же была получена ошибка. При этом сообщение не является обязательным, но линтер все же рекомендует его добавлять.
assert(<условие>, '<сообщение об ошибке>');
Применять assert можно как в методах, так и в конструкторах для проверки входных параметров.
Для примера реализуем простую программу для вывода количества товаров. Конечно, количество не может быть представлено отрицательным числом. Поэтому здесь стоит добавить проверку, чтобы избежать логических ошибок.
void setProductsAmount(int amount) {
assert(amount >= 0, 'Количество товаров не может быть отрицательным!');
print('Количество товаров установлено: $amount');
}
void main() {
setProductsAmount(25); // Код выполняется без ошибок
setProductsAmount(-5); // Ошибка в debug режиме: Количество товаров не может быть отрицательным!
}
Возьмем еще один более сложный пример. Нам нужно создать класс данных карточки. Карточки могут быть двух типов — простая и с изображением. При этом карточка с изображением обязательно должна получить ссылку на него. Чтобы ограничить создание карточки с картинкой без ссылки на изображение, достаточно в конструктор добавить соответствующий assert.
enum CardType { simple, withImage }
class Card {
const Card({
required this.type,
required this.title,
required this.subtitle,
this.imageUrl,
}) : assert(
type != CardType.withImage || imageUrl != null,
'Для карточки типа withImage необходимо указать imageUrl!',
);
final CardType type;
final String title;
final String subtitle;
final String? imageUrl;
}
void main() {
try {
final card = Card(
type: CardType.withImage,
title: 'Карточка с картинкой',
subtitle: 'Это карточка с картинкой',
);
print('Карточка с картинкой успешно создана!');
} catch (error) {
print(error);
}
// Этот код выведет в консоль сообщение об ошибке - Для карточки типа withImage необходимо указать imageUrl!
}
Здесь важно учесть, что assert работают только в debug режиме, поэтому валидировать с помощью них действия пользователя или внешние данные нельзя. Их можно и нужно применять в тех случаях, в которых сам разработчик может допустить ошибку на этапе выполнения задачи.
Еще больше полезного можно найти здесь:
◾️ Assert Statements in Dart
◾️ Error handling, Asserts
❤️ — если было полезно
Please open Telegram to view this post
VIEW IN TELEGRAM
❤12🔥7🥰2
Продолжаю серию постов про создание CLI-инструментов на Dart. В прошлый раз мы разобрались, что Dart позволяет упаковать CLI в нативный бинарный файл, и пользователю не нужно устанавливать Dart SDK, чтобы пользоваться утилитой — достаточно скачать файл и запустить его.
Но этот процесс можно улучшить!🚀
Мы можем добавить установку CLI через Homebrew, чтобы пользователи могли установить ваш инструмент одной командой:
brew install mytool
Давайте разберемся, как это сделать.
Если вы хотя бы раз набирали
brew install, значит, вы уже пользовались Homebrew. Это менеджер пакетов для macOS и Linux. Но мало кто знает, что для распространения своего CLI не обязательно попадать в официальный Homebrew-core.Достаточно создать Homebrew Tap — обычный GitHub-репозиторий с формулой Homebrew. Это своего рода ваш собственный mini-магазин пакетов для brew, доступных по команде:
brew tap yourname/yourtap
brew install mytool
Tap удобен, когда:
✔️ вы делаете внутренний CLI в компании
✔️ часто обновляете инструмент
✔️ хотите распространять бинарники сразу после релиза
✔️ не хотите проходить ревью в Homebrew-core
А теперь давайте разберемся на практике. На самом деле все сводится к созданию двух репозиториев на GitHub и одной формуле на Ruby.
Шаг 1. Создайте репозиторий с вашим инструментом.
Если это Bash-скрипт — убедитесь, что он исполняемый:
#!/usr/bin/env bash
chmod +x mytool.sh
Если вы делаете CLI на Dart, собираем бинарники:
mkdir -p dist
dart compile exe bin/my_cli_tool.dart -o dist/mytool-macos
dart compile exe bin/my_cli_tool.dart -o dist/mytool-linux
Затем создайте GitHub Release и загрузите бинарники (mytool-macos, mytool-linux) как assets. Homebrew будет скачивать их оттуда.
Шаг 2. Создаем Homebrew Tap. Для этого необходимо создать отдельный репозиторий:
myusername/homebrew-mytool
Внутри создайте папку
Formula/ и файл Formula/mytool.rb:class Mytool < Formula
desc "CLI tool for automation"
homepage "https://github.com/myusername/my_cli_tool"
version "1.0.0"
if OS.mac?
url "https://github.com/myusername/my_cli_tool/releases/download/v1.0.0/mytool-macos"
sha256 "SHA256_OF_MACOS_FILE"
elsif OS.linux?
url "https://github.com/myusername/my_cli_tool/releases/download/v1.0.0/mytool-linux"
sha256 "SHA256_OF_LINUX_FILE"
end
def install
bin.install Dir["mytool-*"].first => "mytool"
end
end
SHA256 можно получить командой:
shasum -a 256 dist/mytool-macos
Этот хеш гарантирует, что Homebrew загружает именно тот файл, который вы опубликовали, и что он не был поврежден или подменен.
И на этом все! Теперь можно подключить tap и установить CLI:
brew tap myusername/mytool
brew install mytool
Важные моменты:
▪️SHA256 должен совпадать: Homebrew проверяет целостность файлов, поэтому после каждого релиза обязательно пересчитывайте SHA
▪️Выпуск обновлений: для новой версии создайте тег, пересоберите бинарники, обновите SHA и отредактируйте формулу в tap
▪️ Проверяйте локально: перед публикацией убедитесь, что установка работает:
brew install --build-from-source ./Formula/mytool.rb
▪️ Репозиторий должен начинаться с homebrew-, а имя формулы совпадать с названием CLI
▪️ Tap должен быть публичным. Homebrew не умеет устанавливать пакеты из приватных репозиториев
На этом все — ваш CLI готов к установке через brew
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥9❤2👍1
This media is not supported in your browser
VIEW IN TELEGRAM
Сегодня я начинаю серию постов о принципах SOLID — фундаментальных правилах разработки программного обеспечения, которые помогают создавать чистый, масштабируемый и поддерживаемый код.
SOLID — это аббревиатура из пяти принципов объектно-ориентированного программирования. Разберем каждую букву отдельно, начиная с S (Single Responsibility Principle)
Что такое Single Responsibility Principle
Принцип единственной ответственности гласит: каждый класс должен иметь только одну причину для изменения. Проще говоря — один класс решает одну задачу.
Применение к классам
❌Нарушение SRP
class UserManager {
// Работа с БД
User getUser(String id) => User(id: id, name: 'John');
void saveUser(User user) { /* сохранение */ }
// Валидация
bool validateEmail(String email) => email.contains('@');
// Уведомления
void sendWelcomeEmail(User user) { /* отправка */ }
// Отчеты
String generateReport(User user) => 'Отчет: ${user.name}';
}Класс выполняет четыре разные задачи: работа с БД, валидация, уведомления и отчеты. У него слишком много причин для изменения.
✅Правильное применение SRP
// Работа с базой данных
class UserRepository {
User getUser(String id) => User(id: id, name: 'John');
void saveUser(User user) { /* сохранение */ }
void deleteUser(String id) { /* удаление */ }
}
// Валидация данных
class UserValidator {
bool validateEmail(String email) {}
bool validatePassword(String password) {}
}
// Отправка уведомлений
class NotificationService {
void sendWelcomeEmail(User user) {}
}
// Генерация отчетов
class ReportGenerator {
String generateUserReport(User user) {}
}
Теперь каждый класс имеет только одну ответственность и одну причину для изменения.
Применение к методам
Принцип работает и на уровне методов — каждый метод должен
выполнять одну задачу.
❌Нарушение на уровне метода
void processOrder(Order order) {
// Валидация
if (order.items.isEmpty) throw Exception('Корзина пуста');
// Расчет
double total = 0;
for (var item in order.items) {
total += item.price * item.quantity;
}
// Сохранение
database.save(order);
// Уведомление
emailService.send(order.userEmail, 'Заказ оформлен');
}✅Правильное применение
class OrderService {
final OrderValidator validator;
final PriceCalculator calculator;
final OrderRepository repository;
final NotificationService notifications;
void processOrder(Order order) {
_validateOrder(order);
final total = _calculateTotal(order);
_saveOrder(order, total);
_notifyUser(order);
}
void _validateOrder(Order order) => validator.validate(order);
double _calculateTotal(Order order) => calculator.calculate(order);
void _saveOrder(Order order, double total) => repository.save(order);
void _notifyUser(Order order) => notifications.send(order);
}Каждый метод выполняет одну конкретную задачу.
Преимущества SRP
✔️Упрощение тестирования — каждый класс тестируется изолированно
✔️Легкость поддержки — изменения в одной части не влияют на другие
✔️Лучшая читаемость — код становится понятнее
✔️Переиспользование — специализированные классы легче использовать повторно
В следующих постах я расскажу о других принципах SOLID.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍17❤6🔥6