工作中常用的設計模式--介面卡模式

lpe234 發表於 2022-11-24
一般做業務開發,不太容易有大量使用設計模式的場景。這裡總結一下在業務開發中使用較為頻繁的設計模式。當然語言為Java,基於Spring框架。

1 介面卡模式(Adapter Pattern)

已存在的介面、服務,跟我們所需、目的介面不相容時,我們需要透過一定的方法將二者進行相容適配。一個常見的例子,家用電源(國標)220V,而手機標準輸入一般為5V,此時我們便需要一個介面卡來將220V轉換為5V使用。

介面卡模式一般有3個角色:

  • Target: 目標介面
  • Adaptee: 需要進行適配的類(受改造者)
  • Adapter: 介面卡(將Adaptee轉為Target)

這個出現的場景其實挺多,但實際完全按照介面卡模式編寫程式碼的場景可能並不多。簡單業務場景,直接就將適配、相容程式碼混雜在業務程式碼中了。並沒有將其摘出來處理。大部分業務程式碼,可能後續並不會再做擴充套件之類,過度設計反而會降低可讀性並增加程式碼的複雜性。

介面卡模式一般分為類介面卡物件介面卡
類介面卡的話,使用繼承方式實現:class Adapter extends Adaptee implements Target;而物件介面卡的話,則使用組合方式實現。這種情況更靈活一些。畢竟大家都推薦多用組合少用繼承。

在學生髮生約課完課等事件時,我們需要將部分資料同步到外部CRM系統中。課程的話,按班級型別分為:1v1,小班課、大班課。不同的班級型別課程資料有所不同。事件上報時,並不是全量資料,有些資料需要消費者按需查詢。如課程課程編號、名稱、預約上課時間等。

不同班級型別的課程由三個不同的FeignClient(Adaptee)提供服務,而我們想要的就是查詢課程相關資訊(Target)。

為了模擬服務提供者,我們Mock如下服務。

@Data
@Builder
public class OneClass {
    // 課程編號
    private String lessonNo;
    // 課程名稱
    private String lessonName;

    // 其他資訊
    private String one;
}

@Data
@Builder
public class SmallClass {
    // 課程編號
    private String lessonNo;
    // 課程名稱
    private String lessonName;

    // 其他資訊
    private String small;
}

@Data
@Builder
public class BigClass {
    // 課程編號
    private String lessonNo;
    // 課程名稱
    private String lessonName;

    // 其他資訊
    private String big;
}

public interface RemoteClassClient {

    default OneClass getOne() {
        return OneClass.builder().lessonNo("one").lessonName("1V1").build();
    }

    default SmallClass getSmall() {
        return SmallClass.builder().lessonNo("small").lessonName("小班課").build();
    }

    default BigClass getBig() {
        return BigClass.builder().lessonNo("big").lessonName("大班課").build();
    }
}

public class RemoteClassClientImpl implements RemoteClassClient {
}

該服務統一由RemoteClassClient對外提供各個班級型別的查詢服務。

ClassService (Target目標介面、及目標物件)
/**
 * 課程資訊
 */
@Data
@Builder
public class ClassInfoBO {
    // 課程型別 1:1v1 2:small 3:big
    private String type;

    // 班級ID
    private String classId;
    // 課程編號
    private String lessonNo;
    // 課程名稱
    private String lessonName;
}

/**
 * 目標介面
 */
public interface ClassService {

    boolean match(String classType);

    ClassInfoBO getClassInfo(String classId);
}

下面我們就需要幾個介面卡來完成適配。

1.1 物件介面卡

OneClassAdapter
/**
 * 1v1介面卡
 */
@Component
@RequiredArgsConstructor
public class OneClassAdapter implements ClassService {
    private static final String TYPE = "1";

    private final RemoteClassClient classClient;

    @Override
    public boolean match(String classType) {
        return TYPE.equals(classType);
    }

    @Override
    public ClassInfoBO getClassInfo(String classId) {
        final OneClass one = classClient.getOne();
        return ClassInfoBO.builder()
                .type("1")
                .classId(classId)
                .lessonNo(one.getLessonNo()).lessonName(one.getLessonName())
                .build();
    }
}
SmallClassAdapter
/**
 * 小班課介面卡
 */
@Component
@RequiredArgsConstructor
public class SmallClassAdapter implements ClassService {
    private static final String TYPE = "2";

    private final RemoteClassClient classClient;

    @Override
    public boolean match(String classType) {
        return TYPE.equals(classType);
    }

    @Override
    public ClassInfoBO getClassInfo(String classId) {
        final SmallClass small = classClient.getSmall();
        return ClassInfoBO.builder()
                .type("2")
                .classId(classId)
                .lessonNo(small.getLessonNo()).lessonName(small.getLessonName())
                .build();
    }
}
BigClassAdapter
/**
 * 大班課介面卡
 */
@Component
@RequiredArgsConstructor
public class BigClassAdapter implements ClassService {
    private static final String TYPE = "3";

    private final RemoteClassClient classClient;

    @Override
    public boolean match(String classType) {
        return TYPE.equals(classType);
    }

    @Override
    public ClassInfoBO getClassInfo(String classId) {
        final BigClass big = classClient.getBig();
        return ClassInfoBO.builder()
                .type("3")
                .classId(classId)
                .lessonNo(big.getLessonNo()).lessonName(big.getLessonName())
                .build();
    }
}

至此,介面卡完成。可以根據具體場景選擇不同的介面卡,去適配當前的場景。再來個介面卡工廠類。

ClassAdapterFactory
/**
 * 課程資訊介面卡工廠
 */
@Service
@RequiredArgsConstructor
public class ClassAdapterFactory {
    private final List<ClassService> classServiceList;

    ClassService getAdapter(String classType) {
        return classServiceList.stream()
                .filter(cs -> cs.match(classType)).findFirst()
                .orElse(null);
    }
}

1.2 類介面卡

僅以其一舉例。這種介面卡不如物件介面卡靈活。

/**
 * 小班課介面卡(類介面卡)
 */
@Component
public class SmallClassAdapter2 extends RemoteClassClientImpl implements ClassService {
    private static final String TYPE = "2";

    @Override
    public boolean match(String classType) {
        return TYPE.equals(classType);
    }

    @Override
    public ClassInfoBO getClassInfo(String classId) {
        final SmallClass small = super.getSmall();
        return ClassInfoBO.builder()
                .type("2")
                .classId(classId)
                .lessonNo(small.getLessonNo()).lessonName(small.getLessonName())
                .build();
    }
}

可以看到,我們透過繼承的方式,使SmallClassAdapter2具備了RemoteClassClientImpl查詢課程資訊的能力。跟SmallClassAdapter也沒有啥太大區別。

1.3 單測

@SpringBootTest
class ClassServiceTest {

    @Autowired
    private ClassAdapterFactory adapterFactory;

    @Test
    void testOne() {
        String classType = "1";
        String classId = "11111111";
        Optional.ofNullable(adapterFactory.getAdapter(classType)).ifPresent(ad -> {
            final ClassInfoBO classInfo = ad.getClassInfo(classId);
            assertEquals("one", classInfo.getLessonNo());
        });
    }

    @Test
    void testSmall() {
        String classType = "2";
        String classId = "22222222";
        Optional.ofNullable(adapterFactory.getAdapter(classType)).ifPresent(ad -> {
            final ClassInfoBO classInfo = ad.getClassInfo(classId);
            assertEquals("small", classInfo.getLessonNo());
        });
    }

    @Test
    void testBig() {
        String classType = "3";
        String classId = "33333333";
        Optional.ofNullable(adapterFactory.getAdapter(classType)).ifPresent(ad -> {
            final ClassInfoBO classInfo = ad.getClassInfo(classId);
            assertEquals("big", classInfo.getLessonNo());
        });
    }
}

2 思考

介面卡模式能帶來什麼?

  1. 感覺最主要的還是帶了一種解決方案(😅當然設計模式本來就是如此)。其次是對某些場景提供了一種規範,加入沒有這個設計模式,我們也能有解決方案,但具體實現可能千奇百怪(😅當然設計模式本來就是從千奇百怪中提煉出來的)。
  2. 設計模式不是銀彈。避免濫用。

跟策略模式有啥區別?

  1. 單從實現方式(套路)來說。不能說毫無區別,簡直就是一模一樣。
  2. 細想一下,Adaptee在策略模式中,並不是必然存在的。它的目的是不同策略的具體實現和呼叫分開。而介面卡模式的重點在於Adaptee -> Target的適配。

封面圖來源: https://refactoring.guru/desi...