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

Какая архитектура была на прошлом месте работы?

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

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

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

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

Архитектура приложения на прошлом месте работы

На моём последнем месте работы мы использовали многоуровневую архитектуру, построенную на принципах Clean Architecture с элементами MVVM и Repository Pattern. Это была медицинская социальная сеть с 500K+ пользователей.

Общая структура проекта

lib/
├── core/
│   ├── constants/
│   ├── errors/
│   ├── extensions/
│   ├── network/
│   ├── utils/
│   └── themes/
├── data/
│   ├── datasources/
│   │   ├── remote/
│   │   └── local/
│   ├── models/
│   ├── repositories/
│   └── mappers/
├── domain/
│   ├── entities/
│   ├── repositories/
│   └── usecases/
├── presentation/
│   ├── controllers/
│   ├── pages/
│   ├── widgets/
│   ├── riverpod_providers/
│   └── bloc/
└── main.dart

Слои архитектуры

1. Domain Layer — чистая бизнес-логика

Этот слой не зависит от других слоёв и содержит:

  • Entities (сущности)
  • Repositories (интерфейсы)
  • Use Cases (бизнес-логика)
// Entities — модели домена
class Doctor {
  final String id;
  final String name;
  final String specialty;
  final double rating;
  final List<String> qualifications;
  
  Doctor({
    required this.id,
    required this.name,
    required this.specialty,
    required this.rating,
    required this.qualifications,
  });
}

// Repository Interface
abstract class DoctorRepository {
  Future<List<Doctor>> getDoctorsBySpecialty(String specialty);
  Future<Doctor> getDoctorById(String id);
  Future<void> bookAppointment(String doctorId, DateTime dateTime);
}

// Use Case
class GetDoctorsBySpecialtyUseCase {
  final DoctorRepository _repository;
  
  GetDoctorsBySpecialtyUseCase({required DoctorRepository repository})
    : _repository = repository;
  
  Future<List<Doctor>> call(String specialty) {
    return _repository.getDoctorsBySpecialty(specialty);
  }
}

2. Data Layer — работа с данными

Этот слой отвечает за получение и сохранение данных:

  • Remote Data Sources (API)
  • Local Data Sources (БД, SharedPreferences)
  • Models (DTO с JSON сериализацией)
  • Repositories (имплементация)
// DTO Model (Data Transfer Object)
class DoctorModel {
  final String id;
  final String name;
  final String specialty;
  final double rating;
  final List<String> qualifications;
  
  DoctorModel({
    required this.id,
    required this.name,
    required this.specialty,
    required this.rating,
    required this.qualifications,
  });
  
  // JSON Serialization
  factory DoctorModel.fromJson(Map<String, dynamic> json) {
    return DoctorModel(
      id: json['id'],
      name: json['name'],
      specialty: json['specialty'],
      rating: (json['rating'] as num).toDouble(),
      qualifications: List<String>.from(json['qualifications']),
    );
  }
  
  Map<String, dynamic> toJson() => {
    'id': id,
    'name': name,
    'specialty': specialty,
    'rating': rating,
    'qualifications': qualifications,
  };
  
  // Конвертация в Domain Entity
  Doctor toDomain() => Doctor(
    id: id,
    name: name,
    specialty: specialty,
    rating: rating,
    qualifications: qualifications,
  );
}

// Remote Data Source
abstract class DoctorRemoteDataSource {
  Future<List<DoctorModel>> getDoctorsBySpecialty(String specialty);
}

class DoctorRemoteDataSourceImpl implements DoctorRemoteDataSource {
  final ApiService _apiService;
  
  DoctorRemoteDataSourceImpl({required ApiService apiService})
    : _apiService = apiService;
  
  @override
  Future<List<DoctorModel>> getDoctorsBySpecialty(String specialty) async {
    try {
      final response = await _apiService.get('/doctors?specialty=$specialty');
      return (response as List)
        .map((json) => DoctorModel.fromJson(json))
        .toList();
    } catch (e) {
      throw ServerException(e.toString());
    }
  }
}

// Repository Implementation
class DoctorRepositoryImpl implements DoctorRepository {
  final DoctorRemoteDataSource _remoteDataSource;
  final DoctorLocalDataSource _localDataSource;
  final NetworkInfo _networkInfo;
  
  DoctorRepositoryImpl({
    required DoctorRemoteDataSource remoteDataSource,
    required DoctorLocalDataSource localDataSource,
    required NetworkInfo networkInfo,
  })
  : _remoteDataSource = remoteDataSource,
    _localDataSource = localDataSource,
    _networkInfo = networkInfo;
  
  @override
  Future<List<Doctor>> getDoctorsBySpecialty(String specialty) async {
    if (await _networkInfo.isConnected) {
      try {
        final models = await _remoteDataSource.getDoctorsBySpecialty(specialty);
        // Кэшируем результат
        await _localDataSource.cacheDoctors(models);
        return models.map((m) => m.toDomain()).toList();
      } catch (e) {
        // При ошибке берём из кэша
        final cachedModels = await _localDataSource.getCachedDoctors();
        return cachedModels.map((m) => m.toDomain()).toList();
      }
    } else {
      // Offline режим — только локальные данные
      final cachedModels = await _localDataSource.getCachedDoctors();
      return cachedModels.map((m) => m.toDomain()).toList();
    }
  }
}

3. Presentation Layer — UI и управление состоянием

Этот слой отвечает за UI и взаимодействие с пользователем. Мы использовали Riverpod для управления состоянием:

// Riverpod Provider для репозитория
final doctorRepositoryProvider = Provider<DoctorRepository>((ref) {
  final apiService = ref.watch(apiServiceProvider);
  final localDataSource = ref.watch(doctorLocalDataSourceProvider);
  final networkInfo = ref.watch(networkInfoProvider);
  
  return DoctorRepositoryImpl(
    remoteDataSource: DoctorRemoteDataSourceImpl(apiService: apiService),
    localDataSource: localDataSource,
    networkInfo: networkInfo,
  );
});

// Provider для бизнес-логики
final getDoctorsBySpecialtyProvider = FutureProvider.family<
  List<Doctor>, String
>((ref, specialty) {
  final repository = ref.watch(doctorRepositoryProvider);
  return repository.getDoctorsBySpecialty(specialty);
});

// Presentation Widget
class DoctorListScreen extends ConsumerWidget {
  final String specialty;
  
  const DoctorListScreen({required this.specialty});
  
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final doctorsAsync = ref.watch(
      getDoctorsBySpecialtyProvider(specialty)
    );
    
    return Scaffold(
      appBar: AppBar(title: Text(specialty)),
      body: doctorsAsync.when(
        data: (doctors) => ListView.builder(
          itemCount: doctors.length,
          itemBuilder: (context, index) => DoctorCard(
            doctor: doctors[index],
            onPressed: () => _onDoctorTapped(doctors[index]),
          ),
        ),
        loading: () => LoadingWidget(),
        error: (error, stack) => ErrorWidget(message: error.toString()),
      ),
    );
  }
  
  void _onDoctorTapped(Doctor doctor) {
    // Навигация
  }
}

Ключевые компоненты

Error Handling

// Custom Exception Hierarchy
abstract class AppException implements Exception {
  final String message;
  AppException(this.message);
}

class ServerException extends AppException {
  ServerException(String message) : super(message);
}

class CacheException extends AppException {
  CacheException(String message) : super(message);
}

class NetworkException extends AppException {
  NetworkException(String message) : super(message);
}

// Either тип для обработки ошибок (используём fpdart)
type DoctorResult = Either<AppException, List<Doctor>>;

Dependency Injection

// Все зависимости инициализируются в один месте
final apiServiceProvider = Provider<ApiService>((ref) {
  return ApiService(baseUrl: 'https://api.example.com');
});

final networkInfoProvider = Provider<NetworkInfo>((ref) {
  return NetworkInfoImpl();
});

final localDataSourceProvider = Provider<LocalDataSource>((ref) {
  return LocalDataSourceImpl();
});

Преимущества такой архитектуры

  1. Разделение ответственности — каждый слой имеет одну задачу
  2. Тестируемость — легко мокировать зависимости
  3. Масштабируемость — легко добавлять новые features
  4. Переиспользуемость — бизнес-логика не зависит от UI
  5. Независимость от фреймворков — domain слой чист

Вызовы, которые мы решали

1. Offline-first стратегия

  • Кэширование всех запросов в SQLite
  • Синхронизация при восстановлении сети

2. Real-time обновления

  • WebSocket для real-time данных
  • Stream-based updates с Riverpod

3. Большой объём данных

  • Pagination для списков
  • Lazy loading изображений
  • Сжатие API ответов

4. Performance

  • Code splitting на feature modules
  • Использование isolates для тяжёлых операций
  • Профилирование с DevTools

Инструменты и зависимости

dependencies:
  flutter: sdk: flutter
  riverpod: ^2.4.0
  flutter_riverpod: ^2.4.0
  http: ^1.1.0
  sqflite: ^2.2.0
  hive: ^2.2.0
  connectivity_plus: ^5.0.0
  fpdart: ^0.5.0

dev_dependencies:
  mockito: ^5.4.0
  build_runner: ^2.4.0

Вывод

Эта архитектура позволила нам:

  • Управлять кодовой базой с 50+ экранами
  • Быстро добавлять новые features
  • Легко исправлять баги благодаря изоляции слоёв
  • Обучать junior разработчиков благодаря ясной структуре
  • Достичь 85% code coverage благодаря тестируемости

На новом месте работы я готов применить эти знания и адаптировать архитектуру под требования конкретного проекта.