← Назад к вопросам

Что такое DRY?

1.2 Junior🔥 161 комментариев
#Архитектура Flutter#ООП и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

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 не абсолютный закон — иногда небольшой дубль лучше, чем неправильная абстракция. Баланс — вот ключ.