当实现一个第三方的接口时应该怎么做?
最近在做一个CDC数据同步的需求,需要将消费者接口改为另外一种接口来实现数据同步。工作中我们会遇到很多这类问题,很多时候可能由于时间紧就放弃草草了事,也可能是没有意识到这样的需求还会发生变化,但世事无常。我在此分享一下我的解决方案,欢迎留言与我探讨。
拿到需求的第一直觉可能是下面这样:
第一直觉:
//mq回调接口
public interface MessageListener<T> {
void onMessage(T t);
}
public class MessageConsumer implements MessageListener<MessageRecord> {
private static final Set<String> FOCUS_OPTIONS = Sets.newHashSet("update");
//自动注入
private BusinessService1 service1;
private BusinessService2 service2;
@Override
public void onMessage(MessageRecord message) {
String table = message.getTable();
String type = message.getType();
// 验证、幂等...
if (!FOCUS_OPTIONS.contains(type)) {
log.debug("not supported message type : {}", message);
return;
}
try{
switch (table) {
case "table_1":
//数据转换
//doTable1Business
case "table_2":
//doTable2Business
}
catch(...){
...
}
}
private doTable1Business(Table1DTO dto){
//...验证等
service1.handle(..);
}
private doTable2Business(Table2DTO dto){
//...验证等
service2.handle(..);
}
}
看到这样的代码你是否似曾相识呢。实际上这只是一个初级版本,需要我们去优化,很多时候我们就到此为止了,接下来我会一步步分析代码中存在的问题。首先是优点和缺点:
优点:逻辑简单只有一个类
缺点:可维护性不好,如果后续生变更不得不修改这个类,可能因为修改doTable1Business方法的时误修改了doTable2Business方法;
扩展性不好:当要添加新的表、新的类型时不得不继续增加这个类的代码,后续如果改用其他方式同步时我们不得不对这个类再做代码修改;
可测试性不好:不能对这个类做单元测试,耦合太严重。
代码高度耦合:MessageConsumer依赖BusinessService1和BusinessService2后续还可能会增加,后续修改BusinessService1、BusinessService2等时可能会误伤MessageConsumer。
优化思路:
1.识别变化部分抽象接口
2.识别不变部分提升到父类
3.将业务逻辑和第三方接口组合
优化1:
public interface DataChangeEventHandler<R> {
enum DataChangeEventType {
/**
* 插入
*/
INSERT,
/**
* 更新
*/
UPDATE,
/**
* 删除
*/
DELETE,
/**
* 未知
*/
UNKNOWN
}
void handle(DataChangeContext<R> dataChangeContext);
R convertData(String data);
String taleName();
/**
* 幂等+保证消费的顺序型
*/
String uniqueKey();
}
public class DataChangeContext<T> {
private DataChangeEventType dataChangeEventType;
private String tableName;
private String databaseName;
private T beforeChangeData;
private T afterChangeData;
}
//模板类
@Slf4j
public abstract class AbstractDataChangeHandler<T> implements DataChangeEventHandler<T> {
@Override
public void handle(DataChangeContext<T> dataChangeContext) {
if (!shouldSkip(dataChangeContext)) {
doHandle(dataChangeContext);
} else {
//..
}
}
private void doHandle(DataChangeContext<T> dataChangeContext) {
T oldData = dataChangeContext.getBeforeChangeData();
T newData = dataChangeContext.getAfterChangeData();
switch (dataChangeContext.getDataChangeEventType()) {
case INSERT:
doInsertEvent(dataChangeContext.getAfterChangeData());
break;
case UPDATE:
doUpdateEvent(oldData, newData);
break;
case DELETE:
doDeleteEvent(newData);
break;
default:
log.warn("table={},type={}", taleName(), dataChangeContext.getDataChangeEventType());
}
}
protected boolean shouldSkip(DataChangeContext<T> dataChangeContext) {
return false;
}
protected void doInsertEvent(T data) {
//not do anything
}
protected void doUpdateEvent(T oldData, T newData) {
//not do anything
}
protected void doDeleteEvent(T data) {
//not do anything
}
//入口类MessageListener
public class DefaultDataChangeHandler implements MessageListener {
private LockService lockService;
//自动注入
private List<DataChangeEventHandler> eventHandlers;
@Override
public void onMessage(MessageRecord message) throws Exception {
DataChangeEventHandler handler = getHandler(message.getTable());
LockService lock = lockService.lock(getKey(message, handler), 10, 5);
printChangeDataContent(message, getColumnValue(message, handler));
DataChangeContext context = new DataChangeContext<>();
try {
context.setDatabaseName(message.getSchema());
context.setTableName(message.getTable());
Optional.ofNullable(message.getBeforeColumn()).map(JSON::toJSONString).map(handler::convertData).ifPresent(context::setBeforeChangeData);
Optional.ofNullable(wares.getAfterColumn()).map(JSON::toJSONString).map(handler::convertData).ifPresent(context::setAfterChangeData);
if (Type.UPDATE.desc().equalsIgnoreCase(message.getType())) {
context.setDataChangeEventType(DataChangeEventType.UPDATE);
} else if (Type.INSERT.desc().equalsIgnoreCase(message.getType())) {
context.setDataChangeEventType(DataChangeEventType.INSERT);
} else if (Type.DELETE.desc().equalsIgnoreCase(message.getType())) {
context.setDataChangeEventType(DataChangeEventType.DELETE);
} else {
context.setDataChangeEventType(DataChangeEventType.UNKNOWN);
}
} catch (Exception e) {
log.error("execute error {}", e.getMessage());
throw e;
} finally {
lock.unlock();
}
}
private String getKey(Message message , DataChangeEventHandler handler) {
return this.getClass().getName() + ":" + getColumnValue(message, handler);
}
private String getColumnValue(Message message, DataChangeEventHandler handler) {
String column = Optional.ofNullable(handler.uniqueKey()).orElseThrow(() -> new RuntimeException("must define a uniqueKey"));
String uniqueKey = message.getStringValueFromAfterColumn(column);
if (StringUtils.isEmpty(uniqueKey)) {
log.error("the uniqueKey {} is {}", column, uniqueKey);
}
return uniqueKey;
}
private DataChangeEventHandler getHandler(String tableName) {
DataChangeEventHandler result = null;
if (CollectionUtils.isEmpty(eventHandlers)) {
log.error("not config any DataChangeEventHandler");
return null;
}
for (DataChangeEventHandler h : eventHandlers) {
if (StringUtils.isEmpty(h.taleName())) {
throw new RuntimeException("must set table name");
}
if (h.taleName().equals(tableName)) {
result = h;
break;
}
}
if (Objects.isNull(result)) {
throw new RuntimeException("not find any handler for table :" + tableName);
}
return result;
}
private void printChangeDataContent(Message message, String uniqueValue) { }
}
//重构后的 BusinessServiceDataChangeHandler
public class Table1DataChangeHandler extends AbstractDataChangeHandler<Table1DTO> {
//自动注入
private BusinessService1 service1;
@Override
protected void doUpdateEvent(Table1DTO oldData, Table1DTO newData) {
service1.handle(newData);
}
@Override
public Table1DTO convertData(String data) {
//转换
}
@Override
public String taleName() {
return "table_1";
}
@Override
public String uniqueKey() {
return "order_no";
}
}
按照刚刚说的3个步骤抽象了DataChangeEventHandler接口,也就是说这部分对于我们来说是可变的。第二个步骤是提取不可变部分,那么整个流程中,加锁、转换、处理等流程的结构是不会发生变化的我们可以抽取实现一个父类,其中表名称、数据等是不会发生变化的我们可以抽象为一个上下文类用做数据传递。第三个步骤将我们的设计和第三方接口结合,这里使用的是spring的自动注入,如果第三方接口发生变化我们也只需要修改这部分逻辑,而原有的逻辑并不会发生任何变化。
我们没有特意地去使用任何设计模式,这里还是用到了桥接模式和模板模式,桥接模式是结构型模式,主要用来做类结构的组装,而模板模式是行为型模式,侧重的是提供了什么样的功能。与我们的目的不谋而合。
当对接第三方接口例如消息队列、短信发送等可能变化的接口时,可以使用上面的通用模板。
DefaultDataChangeHandler 为什么不继承AbstractDataChangeHandler实现MessageListener使用适配器模式呢?欢迎留言