需求一:上游系统会下发10+个文件,文件名称有固定的前缀和后缀,中间有一部分是按规律可变的。每个文件对应的不同表的数据,每个表的字段个数、字段长度都是独立的。
比如文件xxx.xxx.202509171.001.gz、xxx.xxx.202509171.002.gz
20250917是日期,每天下发文件的日期,1 是标识全量文件-0标识增量文件,目前只需要按照标识为1的来,001表示文件内容标识,表示文件内是table-A的数据。同理,002表示table-B的数据。
实现:
pubic abstract class FileHandleAbstract<T>{
public abstract String getFileName();
public abstract T trans(String[] arr);
protected abstract Integer batchInsert(List<T> list);
public void execute() {
// sftp读取远程服务器文件代码
String filePath = "本地文件保存路径" + getFileName();
int insertDataCount = 0;
boolean success = false;
try (GZIPInputStream gzipInputStream = new GZIPInputStream(new FileInputStream(filePath))) {
BufferedReader bf = new BufferedReader(new InputStreamReader(gzipInputStream));
List<String[]> list = new ArrayList<>();
int insertBatchNum = 0;
// 逐行读取文件的数据
String line = "";
String temp;
while ((temp = bf.readLine()) != null) {
if (temp.contains("dip-control")) {
break;
}
// 由于文件的行末换行符 不是 \n ,需要规范成 \n,所以bf.readLine()方法在读取到换行时,不能作为读取一行结束
temp = temp + "\n";
line += temp;
if (!temp.endsWith("行分隔符")) {
continue;
}
/**
* 对单行数据进行分隔 数据分隔符 \n SOH \n
* 由于分隔符从 "\n" 换成了 "\nSOHSOH\n",在解析切割的时候,行末会多余一行
* 故需舍弃读出来的一列:通过匹配 SOH SOH \n ,再换掉
*/
line = line.replace("行分隔符", "");
String[] fields = line.split("行分隔符", -1);
if (fields.length != getTableColumnCount()) {
log.error("{} 文件的实际列数和对应表的列数对应不上,实际文件列数:{},table Column:{}", fileName, fields.length, getTableColumnCount());
return;
}
list.add(fields);
if (list.size() == 5000) {
batchAdd(list);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void batchAdd(List<String[]> list) {
List<T> insertList = new ArrayList<>();
for (String[] fields : list) {
T trans = trans(fields);
insertList.add(trans);
}
batchInsert(insertList);
}
}
采取模板方法模式,定义一个抽象父类 FileHandleAbstract,定义一个public
的方法execute
,主要代码逻辑:
public abstract T trans(String[] arr);
,子类继承FileHandleAbstract
。protected abstract Integer batchInsert(List<T> list);
,子类继承FileHandleAbstract
。需求二:上游系统推送的日间更新数据到kafka,子系统通过订阅kafka的消息获取数据。子系统按照约定好的topic的key订阅获取message,消息头中包含键值对Table-Name:表名
。子系统可以根据表名来区分推送的消息。
在处理kafka接收上游数据时,采用 策略模式+简单工厂方法模式,在工厂中根据表名动态创建实现类,在子类中实现具体消息处理逻辑。
这两种行为型设计模式都旨在封装和复用算法,但它们的侧重点和实现机制截然不同。理解它们的区别对于在正确场景下选择正确的模式至关重要。
特性 | 策略模式 (Strategy) | 模板方法模式 (Template Method) |
---|---|---|
核心思想 | “换芯” - 定义算法家族,使之可相互替换。 | “定框” - 定义算法骨架,允许子类重写特定步骤。 |
实现方式 | 对象组合 (Has-A) | 类继承 (Is-A) |
关注点 | 灵活地替换整个算法 | 稳定地复用算法结构 |
控制权 | 客户端拥有控制权,主动选择策略。 | 父类拥有控制权,子类被动填充细节。 |
代码复用 | 通过组合不同的策略对象来复用行为。 | 通过继承在父类中复用算法骨架。 |
灵活性 | 高。运行时动态改变算法。 | 相对较低。编译时通过创建子类来改变行为。 |
开闭原则 | 对扩展开放(新增策略类),对修改关闭(无需改上下文)。 | 对扩展开放(重写钩子方法),对修改关闭(不修改模板方法)。 |
意图:定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。策略模式使得算法可以独立于使用它的客户而变化。
结构:
PaymentStrategy
)CreditCardPayment
, PayPalPayment
)ConcreteStrategy
对象来配置。上下文是策略的使用者。(例如:ShoppingCart
)Java代码示例:
java
// 策略接口
public interface PaymentStrategy {
void pay(int amount);
}
// 具体策略A
public class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
public CreditCardPayment(String cardNumber) { this.cardNumber = cardNumber; }
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using Credit Card: " + cardNumber);
}
}
// 具体策略B
public class PayPalPayment implements PaymentStrategy {
private String email;
public PayPalPayment(String email) { this.email = email; }
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using PayPal: " + email);
}
}
// 上下文
public class ShoppingCart {
private PaymentStrategy paymentStrategy;
private int totalAmount = 0;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void addItem(int price) { totalAmount += price; }
public void checkout() {
paymentStrategy.pay(totalAmount); // 委托给策略对象
totalAmount = 0;
}
}
// 客户端使用
public class Client {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
cart.addItem(100);
cart.addItem(200);
// 运行时动态选择策略
cart.setPaymentStrategy(new CreditCardPayment("1234-5678-9012"));
cart.checkout();
// 可以轻松切换策略
cart.addItem(50);
cart.setPaymentStrategy(new PayPalPayment("user@example.com"));
cart.checkout();
}
}
应用场景:
意图:定义一个操作中的算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤。
结构:
Java代码示例:
java
// 抽象类 - 定义算法骨架
public abstract class Game {
// 模板方法 (通常声明为final,防止子类重写算法结构)
public final void play() {
initialize(); // 步骤1
startPlay(); // 步骤2
endPlay(); // 步骤3
}
// 基本方法 - 已实现(也可声明为Hook方法,空实现)
protected void initialize() {
System.out.println("Game Initialized! Default setup.");
}
// 抽象方法 - 必须由子类实现
protected abstract void startPlay();
protected abstract void endPlay();
}
// 具体子类A
public class Cricket extends Game {
@Override
protected void startPlay() {
System.out.println("Cricket Game Started. Enjoy the match!");
}
@Override
protected void endPlay() {
System.out.println("Cricket Game Finished!");
}
}
// 具体子类B
public class Football extends Game {
@Override
protected void initialize() { // 可选择重写已有实现的方法
System.out.println("Football Game Initialized! Players are ready.");
}
@Override
protected void startPlay() {
System.out.println("Football Game Started. Kick off!");
}
@Override
protected void endPlay() {
System.out.println("Football Game Finished!");
}
}
// 客户端使用
public class Client {
public static void main(String[] args) {
Game game = new Cricket();
game.play(); // 调用模板方法,执行定义好的流程
System.out.println();
game = new Football();
game.play();
}
}
输出:
text
Game Initialized! Default setup.
Cricket Game Started. Enjoy the match!
Cricket Game Finished!
Football Game Initialized! Players are ready.
Football Game Started. Kick off!
Football Game Finished!
应用场景:
JdbcTemplate
)。方面 | 策略模式 | 模板方法模式 |
---|---|---|
关系类型 | 组合关系 (Has-A)。上下文拥有一个策略。 | 继承关系 (Is-A)。具体子类是一个抽象父类。 |
运行时行为 | 可以动态改变。setStrategy() 方法允许在运行时切换整个算法。 | 编译时确定。通过new ConcreteClass() 确定行为,行为在对象创建后通常不变。 |
代码复用 | 通过委托复用策略对象的行为。 | 通过继承在父类中复用算法骨架代码。 |
算法完整性 | 每个策略完整实现整个算法。 | 父类和子类协同工作,共同完成一个完整的算法。 |
设计原则 | 优先使用组合而非继承。更符合开闭原则(新增策略无需修改上下文)。 | 合理使用继承,将不变部分封装起来,扩展可变部分。 |
JdbcTemplate
中的execute()
方法定义了流程(获取连接、执行语句、处理异常、释放连接),你只需要实现PreparedStatementCallback
这个“步骤”。框架生命周期(初始化、启动、销毁)、数据解析流程(打开文件、读取头、解析数据、关闭文件)。总结一句话:策略模式让你换掉整个“发动机”,而模板方法模式让你只换掉发动机里的“几个活塞”。