31. Отрефакторить чтение документов разных форматов
Условие задачи:
Дан DocumentService, который выбирает способ чтения документа в зависимости от его типа: PDF, DOCX или XLSX. Для чтения используется самописный DocumentReader.
Нужно провести code review: определить сильные и слабые стороны реализации и предложить вариант, который будет проще тестировать и расширять при появлении новых форматов документов.
Код:
@Component
public class DocumentService {
public InputStream readDocument(String type) {
DocumentReader reader = new DocumentReader();
if (type.equals(""PDF"")) {
return reader.readPdf();
} else if (type.equals(""DOCX"")) {
return reader.readDocx();
} else if (type.equals(""XLSX"")) {
return reader.readXlsx();
} else {
return null;
}
}
}
Спойлеры к решению
Подсказки
type == null?💡
DocumentReader создаётся через new, поэтому его сложно подменить в тестах.💡 Возврат
null не объясняет причину отсутствия результата.💡 Подумай, сколько мест придётся менять при добавлении нового формата.
Решение
Проблемы:
DocumentReaderсоздаётся черезnew, поэтому сервис жёстко связан с конкретной реализацией;зависимость невозможно нормально подменить в unit-тесте;
при
type == nullвызовtype.equals(...)приведёт кNullPointerException;тип документа задаётся строкой, поэтому возможны опечатки и некорректные значения;
для неподдерживаемого типа возвращается
null;цепочка
if/elseбудет разрастаться при добавлении новых форматов;DocumentServiceзнает конкретные методы чтения каждого формата, поэтому при расширении приходится изменять сам сервис.
Что здесь нормально
Для трёх форматов код простой и легко читается. Также есть единая точка входа для чтения документа. Проблемы появляются прежде всего при тестировании и дальнейшем расширении списка форматов.
Как лучше исправить
Минимальный вариант — внедрить DocumentReader, использовать enum и явно обрабатывать неподдерживаемый тип:
public enum DocumentType {
PDF,
DOCX,
XLSX
}
@Component
@RequiredArgsConstructor
public class DocumentService {
private final DocumentReader reader;
public InputStream readDocument(DocumentType type) {
if (type == null) {
throw new IllegalArgumentException(
"Document type must not be null"
);
}
return switch (type) {
case PDF -> reader.readPdf();
case DOCX -> reader.readDocx();
case XLSX -> reader.readXlsx();
};
}
}
Это уже устраняет new, строковые значения и null-результат.
Если форматов будет много и они будут регулярно добавляться, лучше перейти к Strategy.
public interface DocumentReader {
DocumentType getType();
InputStream read();
}
Отдельная реализация для каждого формата:
@Component
public class PdfDocumentReader implements DocumentReader {
@Override
public DocumentType getType() {
return DocumentType.PDF;
}
@Override
public InputStream read() {
// чтение PDF
return ...;
}
}
Сервис тогда только выбирает подходящую стратегию:
@Component
public class DocumentService {
private final Map<DocumentType, DocumentReader> readers;
public DocumentService(
List<DocumentReader> readers
) {
this.readers = readers.stream()
.collect(Collectors.toMap(
DocumentReader::getType,
Function.identity()
));
}
public InputStream readDocument(DocumentType type) {
DocumentReader reader = readers.get(type);
if (reader == null) {
throw new IllegalArgumentException(
"Unsupported document type: " + type
);
}
return reader.read();
}
}
Теперь при добавлении нового формата достаточно создать новую реализацию DocumentReader: код DocumentService менять не нужно.
Для небольшого фиксированного набора форматов достаточно варианта с enum и switch. Strategy имеет смысл, когда форматов становится много или логика их обработки развивается независимо.