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

Приведи пример использования принципа DRY

2.2 Middle🔥 111 комментариев
#Архитектура Flutter#ООП и паттерны

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

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

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

Пример использования принципа DRY в Flutter

DRY (Don't Repeat Yourself) — один из самых важных принципов чистого кода. Суть: не повторяй код, вынеси в функции, компоненты, константы. Покажу множество примеров нарушения и как это исправить.

Пример 1: Вынос стилей в переменные

❌ Плохо: Дублирование стилей

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Кнопка 1
        Container(
          width: 120,
          height: 50,
          decoration: BoxDecoration(
            color: Colors.blue,
            borderRadius: BorderRadius.circular(8),
          ),
          child: Center(
            child: Text(
              'Button 1',
              style: TextStyle(
                color: Colors.white,
                fontSize: 16,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        ),
        // Кнопка 2 (скопирована, дублируется!)
        Container(
          width: 120,
          height: 50,
          decoration: BoxDecoration(
            color: Colors.blue,
            borderRadius: BorderRadius.circular(8),
          ),
          child: Center(
            child: Text(
              'Button 2',
              style: TextStyle(
                color: Colors.white,
                fontSize: 16,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        ),
        // Кнопка 3 (скопирована ещё раз!)
        Container(
          width: 120,
          height: 50,
          decoration: BoxDecoration(
            color: Colors.blue,
            borderRadius: BorderRadius.circular(8),
          ),
          child: Center(
            child: Text(
              'Button 3',
              style: TextStyle(
                color: Colors.white,
                fontSize: 16,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        ),
      ],
    );
  }
}

✅ Хорошо: Вынесение стилей

class MyApp extends StatelessWidget {
  // Константы стилей
  static const double _buttonWidth = 120;
  static const double _buttonHeight = 50;
  static const double _borderRadius = 8;
  
  static final _buttonDecoration = BoxDecoration(
    color: Colors.blue,
    borderRadius: BorderRadius.circular(_borderRadius),
  );
  
  static final _buttonTextStyle = TextStyle(
    color: Colors.white,
    fontSize: 16,
    fontWeight: FontWeight.bold,
  );
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        _buildButton('Button 1'),
        _buildButton('Button 2'),
        _buildButton('Button 3'),
      ],
    );
  }
  
  Widget _buildButton(String label) {
    return Container(
      width: _buttonWidth,
      height: _buttonHeight,
      decoration: _buttonDecoration,
      child: Center(
        child: Text(
          label,
          style: _buttonTextStyle,
        ),
      ),
    );
  }
}

Пример 2: Вынос компонентов

❌ Плохо: Повторяющийся ListTile

class UserListPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [
        // Item 1
        Container(
          padding: EdgeInsets.all(16),
          child: Row(
            children: [
              CircleAvatar(
                backgroundImage: NetworkImage('url1'),
              ),
              SizedBox(width: 16),
              Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('John Doe', style: TextStyle(fontWeight: FontWeight.bold)),
                  Text('john@example.com', style: TextStyle(color: Colors.grey)),
                ],
              ),
            ],
          ),
        ),
        // Item 2 (дублировано)
        Container(
          padding: EdgeInsets.all(16),
          child: Row(
            children: [
              CircleAvatar(
                backgroundImage: NetworkImage('url2'),
              ),
              SizedBox(width: 16),
              Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('Jane Smith', style: TextStyle(fontWeight: FontWeight.bold)),
                  Text('jane@example.com', style: TextStyle(color: Colors.grey)),
                ],
              ),
            ],
          ),
        ),
      ],
    );
  }
}

✅ Хорошо: Компонент UserTile

class UserTile extends StatelessWidget {
  final String name;
  final String email;
  final String avatarUrl;
  
  const UserTile({
    required this.name,
    required this.email,
    required this.avatarUrl,
  });
  
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(16),
      child: Row(
        children: [
          CircleAvatar(backgroundImage: NetworkImage(avatarUrl)),
          SizedBox(width: 16),
          Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(name, style: TextStyle(fontWeight: FontWeight.bold)),
              Text(email, style: TextStyle(color: Colors.grey)),
            ],
          ),
        ],
      ),
    );
  }
}

class UserListPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [
        UserTile(
          name: 'John Doe',
          email: 'john@example.com',
          avatarUrl: 'url1',
        ),
        UserTile(
          name: 'Jane Smith',
          email: 'jane@example.com',
          avatarUrl: 'url2',
        ),
      ],
    );
  }
}

Пример 3: Валидация (повторяющаяся логика)

❌ Плохо: Повторение валидации

class RegistrationForm extends StatefulWidget {
  @override
  State<RegistrationForm> createState() => _RegistrationFormState();
}

class _RegistrationFormState extends State<RegistrationForm> {
  String _email = '';
  String _password = '';
  String _phone = '';
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          onChanged: (value) => _email = value,
          decoration: InputDecoration(
            errorText: _email.isEmpty || !_email.contains('@')
              ? 'Invalid email'
              : null,
          ),
        ),
        TextField(
          onChanged: (value) => _password = value,
          decoration: InputDecoration(
            errorText: _password.isEmpty || _password.length < 8
              ? 'Password must be 8+ chars'
              : null,
          ),
        ),
        TextField(
          onChanged: (value) => _phone = value,
          decoration: InputDecoration(
            errorText: _phone.isEmpty || _phone.length < 10
              ? 'Invalid phone'
              : null,
          ),
        ),
      ],
    );
  }
}

✅ Хорошо: Утилиты валидации

class ValidationUtils {
  static String? validateEmail(String? value) {
    if (value?.isEmpty ?? true) return 'Email required';
    if (!value!.contains('@')) return 'Invalid email';
    return null;
  }
  
  static String? validatePassword(String? value) {
    if (value?.isEmpty ?? true) return 'Password required';
    if (value!.length < 8) return 'Min 8 characters';
    return null;
  }
  
  static String? validatePhone(String? value) {
    if (value?.isEmpty ?? true) return 'Phone required';
    if (value!.length < 10) return 'Invalid phone';
    return null;
  }
}

class RegistrationForm extends StatefulWidget {
  @override
  State<RegistrationForm> createState() => _RegistrationFormState();
}

class _RegistrationFormState extends State<RegistrationForm> {
  String _email = '';
  String _password = '';
  String _phone = '';
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          onChanged: (value) => _email = value,
          decoration: InputDecoration(
            errorText: ValidationUtils.validateEmail(_email),
          ),
        ),
        TextField(
          onChanged: (value) => _password = value,
          decoration: InputDecoration(
            errorText: ValidationUtils.validatePassword(_password),
          ),
        ),
        TextField(
          onChanged: (value) => _phone = value,
          decoration: InputDecoration(
            errorText: ValidationUtils.validatePhone(_phone),
          ),
        ),
      ],
    );
  }
}

Пример 4: API запросы (повторяющиеся)

❌ Плохо: Каждый раз пишешь http.get

class UserRepository {
  Future<List<User>> getUsers() async {
    final response = await http.get(
      Uri.parse('https://api.example.com/users'),
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer token_123',
      },
    );
    
    if (response.statusCode != 200) {
      throw Exception('Failed to load users');
    }
    
    final list = jsonDecode(response.body) as List;
    return list.map((item) => User.fromJson(item)).toList();
  }
  
  Future<User?> getUserById(int id) async {
    final response = await http.get(
      Uri.parse('https://api.example.com/users/$id'),
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer token_123',
      },
    );
    
    if (response.statusCode != 200) {
      throw Exception('Failed to load user');
    }
    
    return User.fromJson(jsonDecode(response.body));
  }
}

✅ Хорошо: ApiClient обёртка

class ApiClient {
  static const String _baseUrl = 'https://api.example.com';
  static const String _token = 'Bearer token_123';
  
  static final _headers = {
    'Content-Type': 'application/json',
    'Authorization': _token,
  };
  
  Future<dynamic> get(String endpoint) async {
    final response = await http.get(
      Uri.parse('$_baseUrl$endpoint'),
      headers: _headers,
    );
    
    _checkResponse(response);
    return jsonDecode(response.body);
  }
  
  Future<dynamic> post(String endpoint, Map<String, dynamic> body) async {
    final response = await http.post(
      Uri.parse('$_baseUrl$endpoint'),
      headers: _headers,
      body: jsonEncode(body),
    );
    
    _checkResponse(response);
    return jsonDecode(response.body);
  }
  
  void _checkResponse(Response response) {
    if (response.statusCode != 200) {
      throw Exception('API Error: ${response.statusCode}');
    }
  }
}

class UserRepository {
  final ApiClient _api;
  
  UserRepository(this._api);
  
  Future<List<User>> getUsers() async {
    final data = await _api.get('/users') as List;
    return data.map((item) => User.fromJson(item)).toList();
  }
  
  Future<User?> getUserById(int id) async {
    final data = await _api.get('/users/$id');
    return User.fromJson(data);
  }
}

Пример 5: Форматирование (повторяющиеся функции)

❌ Плохо: Везде форматируешь дату

class OrderHistory extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [
        ListTile(
          title: Text('Order #1'),
          subtitle: Text(
            DateTime.parse('2024-03-26T10:30:00').toLocal().toString(),
          ),
        ),
        ListTile(
          title: Text('Order #2'),
          subtitle: Text(
            DateTime.parse('2024-03-25T15:45:00').toLocal().toString(),
          ),
        ),
      ],
    );
  }
}

✅ Хорошо: Утилиты форматирования

class DateFormatter {
  static String formatDate(DateTime date) {
    return '${date.day}/${date.month}/${date.year}';
  }
  
  static String formatDateTime(DateTime date) {
    return '${formatDate(date)} ${date.hour}:${date.minute}';
  }
  
  static String formatRelative(DateTime date) {
    final diff = DateTime.now().difference(date);
    if (diff.inSeconds < 60) return 'just now';
    if (diff.inMinutes < 60) return '${diff.inMinutes}m ago';
    if (diff.inHours < 24) return '${diff.inHours}h ago';
    return '${diff.inDays}d ago';
  }
}

class OrderHistory extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [
        ListTile(
          title: Text('Order #1'),
          subtitle: Text(DateFormatter.formatRelative(
            DateTime.parse('2024-03-26T10:30:00'),
          )),
        ),
        ListTile(
          title: Text('Order #2'),
          subtitle: Text(DateFormatter.formatRelative(
            DateTime.parse('2024-03-25T15:45:00'),
          )),
        ),
      ],
    );
  }
}

Пример 6: Цвета (постоянное дублирование)

❌ Плохо: Хардкодные цвета везде

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primaryColor: Color(0xFF2196F3),
        scaffoldBackgroundColor: Color(0xFFFAFAFA),
        textTheme: TextTheme(
          headlineLarge: TextStyle(color: Color(0xFF212121)),
          bodyMedium: TextStyle(color: Color(0xFF757575)),
        ),
      ),
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Color(0xFF2196F3),  # Повтор!
        title: Text('Home'),
      ),
      body: Column(
        children: [
          Text(
            'Welcome',
            style: TextStyle(color: Color(0xFF212121)),  # Повтор!
          ),
          Text(
            'Subtitle',
            style: TextStyle(color: Color(0xFF757575)),  # Повтор!
          ),
        ],
      ),
    );
  }
}

✅ Хорошо: Констант для цветов

class AppColors {
  static const Color primary = Color(0xFF2196F3);
  static const Color background = Color(0xFFFAFAFA);
  static const Color textPrimary = Color(0xFF212121);
  static const Color textSecondary = Color(0xFF757575);
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primaryColor: AppColors.primary,
        scaffoldBackgroundColor: AppColors.background,
        textTheme: TextTheme(
          headlineLarge: TextStyle(color: AppColors.textPrimary),
          bodyMedium: TextStyle(color: AppColors.textSecondary),
        ),
      ),
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: AppColors.primary,
        title: Text('Home'),
      ),
      body: Column(
        children: [
          Text(
            'Welcome',
            style: TextStyle(color: AppColors.textPrimary),
          ),
          Text(
            'Subtitle',
            style: TextStyle(color: AppColors.textSecondary),
          ),
        ],
      ),
    );
  }
}

Резюме DRY

Как применить DRY:

  1. Вынеси стили в переменные/константы

    • const _buttonHeight = 50
    • final _buttonDecoration = BoxDecoration(...)
  2. Создай компоненты для повторяющихся UI

    • UserTile вместо дублирования ListTile кода
    • CustomButton вместо повтора Container'ов
  3. Создай утилиты для повторяющейся логики

    • ValidationUtils.validateEmail()
    • DateFormatter.formatDate()
  4. Создай wrapper'ы для API/Database

    • ApiClient для http запросов
    • Repository для доступа к данным
  5. Используй константы для значений

    • Цвета в AppColors
    • Строки в AppStrings
    • Размеры в AppDimensions

Преимущества:

  • ✅ Меньше кода
  • ✅ Легче менять
  • ✅ Меньше ошибок
  • ✅ Проще читать
  • ✅ Легче тестировать