策略设计模式是一种行为模式,它使我们能够在运行时选择算法的行为。这种模式允许我们定义一组算法,将它们放在不同的类中,并使它们可以互换[1]。
这只是一个定义,但让我们通过了解我们试图解决的问题来更好地理解它。
问题
假设你正在开发一个名为文件解析器的功能。你需要编写一个 API,用户可以上传文件,我们的系统应该能够从中提取数据并将其持久化到数据库中。目前我们被要求支持 CSV、JSON 和 XML 文件。我们的直接解决方案可能如下所示。
@Service
public class FileParserService {
public void parse(File file, String fileType) {
if (Objects.equals(fileType, "CSV")) {
// TODO : a huge implementation to parse CSV file and persist data in db
} else if (Objects.equals(fileType, "JSON")) {
// TODO : a huge implementation to parse JSON file and persist data in db
} else if (Objects.equals(fileType, "XML")) {
// TODO : a huge implementation to parse XML file and persist data in db
} else {
throw new IllegalArgumentException("Unsupported file type");
}
}
}
从业务角度来看,现在一切都很好,但当我们将来想要支持更多文件类型时,事情就会开始变得糟糕。我们开始添加多个 else if 块,类的规模会迅速增长,最终变得难以维护。对文件解析器实现的任何更改都会影响整个类,从而增加在已经正常工作的功能中引入错误的机会。
不仅如此,还有另一个问题。假设我们现在需要额外支持 sqlite 和 parquet 文件类型。两个开发者会介入并开始在同一个庞大的类上工作。他们很可能会遇到合并冲突,这不仅对任何开发者来说都很烦人,而且解决冲突也很耗时。最重要的是,即使在冲突解决后,对整个功能正常工作的信心也会降低。
解决方案
这就是策略设计模式介入并拯救我们的地方。我们将所有文件解析器实现移动到单独的类中,称为策略。在当前类中,我们将根据文件类型动态获取适当的实现并执行策略。
以下是一个 UML 图,提供我们即将实现的设计模式的高级概述。
现在,让我们深入代码。
我们需要一个类来维护支持的不同文件类型。稍后我们将使用它来创建具有自定义名称的 Spring Bean(即策略)。
public class FileType {
public static final String CSV = "CSV";
public static final String XML = "XML";
public static final String JSON = "JSON";
}
创建一个文件解析器的接口
public interface FileParser {
void parse(File file);
}
既然我们已经创建了一个接口,让我们为不同的文件类型创建不同的实现,即策略
@Service(FileType.CSV)
public class CsvFileParser implements FileParser {
@Override
public void parse(File file) {
// TODO : impl to parse csv file
}
}
@Service(FileType.JSON)
public class JsonFileParser implements FileParser {
@Override
public void parse(File file) {
// TODO : impl to parse json file
}
}
@Service(FileType.XML)
public class XmlFileParser implements FileParser {
@Override
public void parse(File file) {
// TODO : impl to parse xml file
}
}
注意,我们已经为上述 Bean 提供了自定义名称,这将帮助我们将这三个 Bean 注入到我们需要的类中。
现在我们需要找到一种方法,在运行时根据文件类型选择上述实现之一。
让我们创建一个 FileParserFactory 类。这个类负责在给定文件类型时决定选择哪个实现。我们将利用 Spring Boot 强大的依赖注入功能在运行时获取适当的策略。(有关更多详细信息,请参阅以下代码块中的注释或 [2])
@Component
@RequiredArgsConstructor
public class FileParserFactory {
/**
* Spring boot's dependency injection feature will construct this map for us
* and include all implementations available in the map with the key as the bean name
* Logically, the map will look something like below
* {
* "CSV": CsvFileParser,
* "XML": XmlFileParser,
* "JSON": JsonFileParser
* }
*/
private final Map<String, FileParser> fileParsers;
/**
* Return's the appropriate FileParser impl given a file type
* @param fileType one of the file types mentioned in class FileType
* @return FileParser
*/
public FileParser get(String fileType) {
FileParser fileParser = fileParsers.get(fileType);
if (Objects.isNull(fileParser)) {
throw new IllegalArgumentException("Unsupported file type");
}
return fileParser;
}
}
现在,让我们对 FileParserService 进行更改。我们将使用 FileParserFactory 根据 fileType 获取适当的 FileParser 并调用 parse 方法。
@Service
@RequiredArgsConstructor
public class FileParserService {
private final FileParserFactory fileParserFactory;
public void parse(File file, String fileType) {
FileParser fileParser = fileParserFactory.get(fileType);
fileParser.parse(file);
}
}
就是这样。我们完成了!
结论
如果我们需要支持更多文件类型,我们只需创建像 SqliteFileParser 和 ParquetFileParser 这样的新类,这些类实现了 FileParser 接口。因此,多个开发者实现这些新文件解析器时将避免任何合并冲突。
现有的文件解析器保持不变,从而减少了破坏现有功能的机会。
此外,我们的代码现在符合 SOLID 原则,特别是我们喜爱的开闭原则。通过将文件解析实现封装到单独的类中,我们可以在不修改现有代码的情况下扩展系统的新解析策略。这使得我们的系统更能适应未来的需求,更易于维护。
参考资料
- https://refactoring.guru/design-patterns/strategy
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/annotation/Autowired.html
没有回复内容