От простого поиска к многопоточной архитектуре

Сейчас я занимаюсь новым pet-проектом — File Search Processor (FSP).

Это будущая консольная утилита, которая позволит искать информацию в файлах по заданной строке или шаблону, проходя по директориям и их содержимому.

С самого начала было понятно, что проект станет хорошей практикой многопоточности: при работе с большим количеством файлов последовательная обработка может стать узким местом. Но прежде чем переходить к параллельному выполнению, я решил разобраться с архитектурой.

С чего начал

Первым этапом стал простой поиск строки в одном файле. Для поисковой логики я выбрал паттерн Фабричный метод: в будущем планирую добавить поиск по шаблонам и другие варианты поиска, поэтому хотел отделить создание конкретного поискового объекта от остальной логики.

Пока реализован только "SearchLine", который выполняет обычный поиск строки. При этом я понимаю, что паттерн не должен использоваться ради самого паттерна: если в дальнейшем требования изменятся, архитектурное решение тоже может потребовать пересмотра.

Архитектура обработки файлов

Когда я перешёл к обработке директорий, стало очевидно, что здесь пересекается несколько разных задач: навигация по файловой системе, чтение данных, их обработка и координация потоков.

Чтобы не смешивать всё в одном классе, я сделал схему компонентов и начал разделять ответственность между ними. (Смотрите приложенную схему)

Основные компоненты: - DirectoryScanner — отвечает за обход директорий и поиск файлов. Он не должен знать, что происходит с содержимым найденных файлов. - FileReader — отвечает за чтение данных с диска. Его задача — предоставить данные, а не решать, зачем они нужны. - FileScanner/FileProcessor — отвечает за обработку содержимого и поисковую бизнес-логику. - FilesQueue — хранит пути к файлам и отвечает за синхронизацию доступа к очереди между потоками. - FileProcessingManager — координирует работу компонентов и управляет процессом обработки.

Где здесь SOLID?

Я не ставил цель "внедрить все пять принципов". Скорее, SOLID стал способом проверить, насколько удачно я разделил ответственность.

- Single Responsibility. Каждый компонент занимается своей задачей: обходом, чтением, обработкой, очередью или координацией. - Open/Closed. Абстракции вроде "IDirectoryScanner" и "IFileProcessor" позволяют добавлять новые реализации, не привязывая менеджер к конкретному классу. - Liskov Substitution. Если компонент реализует соответствующий интерфейс, его можно заменить другой реализацией без нарушения ожидаемого контракта. Например, в будущем это может быть другой способ сканирования директорий. - Interface Segregation. Вместо одного универсального интерфейса используются специализированные — например, для сканирования директорий и обработки файлов. Это избавляет реализации от методов, которые им не нужны. - Dependency Inversion. "FileProcessingManager" должен зависеть от абстракций, а не от конкретных реализаций. Создание объектов и передача зависимостей выполняются на верхнем уровне приложения.

Что дальше?

Следующий этап — связать компоненты в работающий конвейер обработки файлов, а затем перейти к многопоточному выполнению и измерению производительности. Мне интересно не просто реализовать утилиту, а понять, "где действительно нужны абстракции, как организовать взаимодействие потоков и какие архитектурные решения оправданы на практике".

Проект продолжается, поэтому вполне возможно, что часть текущих решений ещё изменится.

Ссылка на репозиторий проекта GitHub: https://github.com/semens901/File-Search-Processor

От простого поиска к многопоточной архитектуре | Сетка — социальная сеть от hh.ru