Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
DRY (Don't Repeat Yourself)
DRY — один из фундаментальных принципов программирования: не повторяй один и тот же код несколько раз. Вместо этого извлекай повторяющуюся логику в функции, компоненты или константы.
Определение
DRY закон: Каждая часть знания (бизнес-логика, значение, структура) должна иметь единственное, непротиворечивое представление в системе.
Это значит:
- Одна функция вместо скопипастенного кода
- Одна константа вместо одного значения в разных местах
- Один компонент вместо дублирующихся виджетов
Почему это важно
Проблема: копипаста кода
// Плохо — DRY violation
FlatButton(
child: Text('Sign In'),
onPressed: () => _handleSignIn(),
color: Color(0xFF6200EE),
textColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
)
// ... 10 строк дальше
FlatButton(
child: Text('Sign Up'),
onPressed: () => _handleSignUp(),
color: Color(0xFF6200EE),
textColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
)
// ... 10 строк дальше
FlatButton(
child: Text('Continue'),
onPressed: () => _handleContinue(),
color: Color(0xFF6200EE),
textColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
)
Если нужно изменить цвет, нужно менять в трёх местах. Вероятность ошибки — 100%.
Решение: извлечение в компонент
// Хорошо — DRY compliant
class PrimaryButton extends StatelessWidget {
final String label;
final VoidCallback onPressed;
const PrimaryButton({
required this.label,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return FlatButton(
child: Text(label),
onPressed: onPressed,
color: Color(0xFF6200EE),
textColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
);
}
}
// Использование
PrimaryButton(label: 'Sign In', onPressed: _handleSignIn),
PrimaryButton(label: 'Sign Up', onPressed: _handleSignUp),
PrimaryButton(label: 'Continue', onPressed: _handleContinue),
Теперь одна версия истины: PrimaryButton. Меняешь стиль один раз — везде обновляется.
Примеры нарушений DRY и их исправления
1. Повторяющиеся значения
// Плохо
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: Color(0xFF6200EE),
accentColor: Color(0xFF03DAC6),
fontFamily: 'Roboto',
scaffoldBackgroundColor: Color(0xFFFAFAFA),
),
);
}
}
// Потом в другом файле
class MyButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Color(0xFF6200EE), // тот же цвет!
child: Text('...',
style: TextStyle(fontFamily: 'Roboto'),
),
);
}
}
Хорошо:
// constants.dart
const Color primaryColor = Color(0xFF6200EE);
const Color accentColor = Color(0xFF03DAC6);
const String fontFamily = 'Roboto';
// Использование везде
color: primaryColor,
fontFamily: fontFamily,
2. Повторяющаяся бизнес-логика
// Плохо
class OrderService {
double calculateTotal(List<Product> products) {
double total = 0;
for (var product in products) {
total += product.price * product.quantity;
}
return total;
}
}
class ReportService {
double getOrderValue(Order order) {
double total = 0;
for (var product in order.products) {
total += product.price * product.quantity;
}
return total;
}
}
Хорошо:
class PriceCalculator {
double calculateTotal(List<Product> products) {
return products.fold(
0.0,
(sum, product) => sum + (product.price * product.quantity),
);
}
}
class OrderService {
final PriceCalculator _calculator;
double calculateTotal(List<Product> products) {
return _calculator.calculateTotal(products);
}
}
class ReportService {
final PriceCalculator _calculator;
double getOrderValue(Order order) {
return _calculator.calculateTotal(order.products);
}
}
3. Повторяющиеся стили в UI
// Плохо — одна и та же сетка spacing'а скопирована
Widget build(BuildContext context) {
return Column(
children: [
SizedBox(height: 16),
Text('Title'),
SizedBox(height: 16),
Text('Subtitle'),
SizedBox(height: 16),
ElevatedButton(onPressed: () {}, child: Text('Action')),
],
);
}
Хорошо:
const double spacing = 16.0;
Widget build(BuildContext context) {
return Column(
children: [
SizedBox(height: spacing),
Text('Title'),
SizedBox(height: spacing),
Text('Subtitle'),
SizedBox(height: spacing),
ElevatedButton(onPressed: () {}, child: Text('Action')),
],
);
}
4. Повторяющиеся тесты
// Плохо
test('sum 2 + 2', () {
final calculator = Calculator();
final result = calculator.sum(2, 2);
expect(result, 4);
});
test('sum 5 + 3', () {
final calculator = Calculator();
final result = calculator.sum(5, 3);
expect(result, 8);
});
test('sum 10 + 20', () {
final calculator = Calculator();
final result = calculator.sum(10, 20);
expect(result, 30);
});
Хорошо:
void main() {
late Calculator calculator;
setUp(() {
calculator = Calculator();
});
void testSum(int a, int b, int expected) {
expect(calculator.sum(a, b), expected);
}
group('Calculator.sum', () {
test('2 + 2 = 4', () => testSum(2, 2, 4));
test('5 + 3 = 8', () => testSum(5, 3, 8));
test('10 + 20 = 30', () => testSum(10, 20, 30));
});
}
DRY vs YAGNI (You Ain't Gonna Need It)
Зачастую есть конфликт между DRY и YAGNI:
Ситуация: код выглядит похожим, но используется в разных контекстах.
// Два похожих виджета, но они развиваются независимо
// Стоит ли их объединять?
class UserProfileCard extends StatelessWidget {
// ...
}
class CompanyProfileCard extends StatelessWidget {
// ...
}
Решение:
- Если они развиваются вместе — выноси в один компонент
- Если независимо — оставь как есть
- Rule of 3: повтори один раз, второй раз прощается, третий раз выноси
Практические инструменты для DRY
1. Функции и методы
String formatDate(DateTime date) {
return DateFormat('dd.MM.yyyy').format(date);
}
// Использование везде
Text(formatDate(user.createdAt)),
2. Компоненты
class PrimaryButton extends StatelessWidget {
// ...
}
3. Константы и конфиги
const appColors = AppColors(
primary: Color(0xFF6200EE),
accent: Color(0xFF03DAC6),
);
4. Миксины (mixins)
mixin ValidationMixin {
bool isValidEmail(String email) {
return email.contains('@');
}
}
class AuthProvider with ValidationMixin {
// ...
}
5. Расширения (extensions)
extension StringExt on String {
bool get isValidEmail => contains('@');
}
// Использование
if (email.isValidEmail) { }
Мнемоника
DRY это про:
- Dont — не
- Repeat — повторяй
- Yourself — себя
Не повторяй себя — извлекай в функции/компоненты/константы.
Вывод
DRY принцип снижает:
- Ошибки: одно место для изменений
- Время разработки: не нужно обновлять код в 5 местах
- Сложность кода: меньше дублирования = понятнее
- Техдолг: не накапливаются копии старого кода
Но DRY не абсолютный закон — иногда небольшой дубль лучше, чем неправильная абстракция. Баланс — вот ключ.