preloader
學習

運用 Adapter 模式,替換現有產品的前端中心化狀態管理方式

運用 Adapter 模式,替換現有產品的前端中心化狀態管理方式

我規劃如何替換我現在服務的小型新創公司,它其中的一個產品如何繼續順利調整結構,讓開發者能夠有更簡潔容易的開發體驗,因為現在仍使用 overkill 的 redux observable 做狀態管理,產生很多樣版程式碼,但很少需要中途取消 request (redux-observable 的長處)的需求。這個產品是做雲端檔案管理,你可以想像成是 Google 雲端硬碟,又客製化與擴充成特定領域的作業需求。我希望新的狀態管理函式庫是 zustand,所以文章內容用它舉例。

我想到的方法是設計模式(Design pattern)裡的 Adapter 模式,因為產品需要持續正常運作,且依照頁面、功能與元件逐步上線,所以想到 proxy-state 的升級和重寫方式。

架構的使用情境:

Component → Hook → StoreManager → Adapter → Redux/Zustand

這是一篇 Adapter 模式在 SOLID 原則中的實作與架構解析,以下用 AI 工具產生內容,已經過我潤飾。

Adapter 模式的設計模式分類

結構型設計模式 (Structural Design Pattern)

Adapter 模式屬於 結構型設計模式,主要用於:

  • 介面轉換:讓不相容的介面能夠協同工作
  • 程式碼重複使用:無需修改現有程式碼就能使用新的實作
  • 系統整合:整合不同的系統或庫
// 經典的 Adapter 模式範例
interface Target {
  request(): string;
}

class Adaptee {
  specificRequest(): string {
    return "Special behavior";
  }
}

class Adapter implements Target {
  private adaptee: Adaptee;

  constructor(adaptee: Adaptee) {
    this.adaptee = adaptee;
  }

  request(): string {
    return `Adapter: ${this.adaptee.specificRequest()}`;
  }
}

SOLID 原則

1. SRP (Single Responsibility Principle) - 單一職責原則

// ✅ 每個 Adapter 只負責一個轉換職責
class ReduxObservableAdapter implements StoreAdapter<UserState> {
  // 只負責:Redux Observable ↔ 統一介面的轉換
  constructor(private store: Store<UserState>) {}
  
  getState(): UserState { /* 只做狀態獲取轉換 */ }
  setState(partial: Partial<UserState>): void { /* 只做狀態轉換 */ }
  subscribe(listener: Function): Function { /* 只做訂閱轉換 */ }
}

class ZustandAdapter implements StoreAdapter<UserState> {
  // 只負責:Zustand ↔ 統一介面的轉換
  constructor(private store: any) {}
  
  getState(): UserState { /* 只做狀態獲取轉換 */ }
  setState(partial: Partial<UserState>): void { /* 只做狀態轉換 */ }
  subscribe(listener: Function): Function { /* 只做訂閱轉換 */ }
}

2. OCP (Open/Closed Principle) - 開放封閉原則

// ✅ 對擴展開放,對修改封閉
interface StoreAdapter<T> {
  getState(): T;
  setState(partial: Partial<T>): void;
  subscribe(listener: (state: T) => void): () => void;
}

// 可以新增新的 Adapter 而不修改現有程式碼
class JotaiAdapter<T> implements StoreAdapter<T> {
  // 新增 Jotai 支援,不影響現有的 Redux/Zustand Adapter
}

class ValtioAdapter<T> implements StoreAdapter<T> {
  // 新增 Valtio 支援,不影響現有程式碼
}

// 使用方的程式碼完全不需要修改
function useStore<T>(storeKey: string): [T, (partial: Partial<T>) => void] {
  const store = storeManager.getStore<T>(storeKey); // 不變
  // ...使用邏輯完全相同
}

3. LSP (Liskov Substitution Principle) - 里氏替換原則

// ✅ 所有 Adapter 都可以互相替換而不影響功能
function testStoreAdapter(adapter: StoreAdapter<UserState>) {
  // 無論是 ReduxAdapter 還是 ZustandAdapter,行為都一致
  const initialState = adapter.getState();
  
  adapter.setState({ name: 'John' });
  expect(adapter.getState().name).toBe('John');
  
  let callbackInvoked = false;
  const unsubscribe = adapter.subscribe(() => {
    callbackInvoked = true;
  });
  
  adapter.setState({ age: 25 });
  expect(callbackInvoked).toBe(true);
  
  unsubscribe();
}

// 所有實作都能通過相同的測試
testStoreAdapter(new ReduxObservableAdapter(reduxStore));
testStoreAdapter(new ZustandAdapter(zustandStore));
testStoreAdapter(new JotaiAdapter(jotaiStore));

4. ISP (Interface Segregation Principle) - 介面隔離原則

分成讀取、寫入和訂閱狀態的介面,元件和 hook 可以依照自己的使用情境,取得自己需要用的函式,getState(), setState()subscribe()

// ✅ 客戶端只依賴它需要的介面
interface StateReader<T> {
  getState(): T;
}

interface StateWriter<T> {
  setState(partial: Partial<T>): void;
}

interface StateSubscriber<T> {
  subscribe(listener: (state: T) => void): () => void;
}

// 組合成完整介面
interface StoreAdapter<T> extends StateReader<T>, StateWriter<T>, StateSubscriber<T> {}

// 客戶端可以只使用需要的部分
function ReadOnlyComponent<T>(reader: StateReader<T>) {
  // 只能讀取狀態,符合元件需求
  const state = reader.getState();
  return <div>{JSON.stringify(state)}</div>;
}

function SubscriberComponent<T>(subscriber: StateSubscriber<T>) {
  // 只需要訂閱功能
  useEffect(() => {
    return subscriber.subscribe((state) => {
      console.log('State changed:', state);
    });
  }, [subscriber]);
  
  return <div>Listening...</div>;
}

5. DIP (Dependency Inversion Principle) - 依賴反轉原則

// ✅ 高層模組不依賴低層模組,都依賴抽象
class StoreManager {
  private adapters = new Map<string, StoreAdapter<any>>(); // 依賴抽象介面
  
  // 不依賴具體的 Redux 或 Zustand,只依賴 StoreAdapter 介面
  registerStore<T>(key: string, adapter: StoreAdapter<T>): void {
    this.adapters.set(key, adapter);
  }
  
  getStore<T>(key: string): StoreAdapter<T> | undefined {
    return this.adapters.get(key);
  }
}

// Hook 層也只依賴抽象
function useStore<T>(storeKey: string): [T, (partial: Partial<T>) => void] {
  const store = storeManager.getStore<T>(storeKey); // 依賴抽象介面
  
  if (!store) {
    throw new Error(`Store ${storeKey} not found`);
  }
  
  // 不關心底層是 Redux 還是 Zustand
  const [state, setState] = useState<T>(store.getState());
  
  useEffect(() => {
    return store.subscribe((newState) => {
      setState(newState);
    });
  }, [store]);
  
  const updateState = useCallback((partial: Partial<T>) => {
    store.setState(partial);
  }, [store]);
  
  return [state, updateState];
}

架構層次解析

Component → Hook → StoreManager → Adapter → Redux/Zustand

詳細說明每一層:

第一層:Component (使用者)

function UserProfile() {
  // 只關心業務邏輯,不知道底層實作
  const [userState, updateUser] = useUserStore();
  
  const handleLogin = () => {
    updateUser({ isLoading: true });
    // ...業務邏輯
  };
  
  return (
    <div>
      {userState.isLoading ? 'Loading...' : userState.user?.name}
    </div>
  );
}

第二層:Hook (統一介面)

function useUserStore() {
  // 提供統一的 API,隱藏底層複雜性
  return useStore<UserState>('user');
}

function useStore<T>(storeKey: string): [T, (partial: Partial<T>) => void] {
  // 通用的狀態管理邏輯
  const store = storeManager.getStore<T>(storeKey);
  
  const [state, setState] = useState<T>(store.getState());
  
  useEffect(() => {
    return store.subscribe(setState);
  }, [store]);
  
  const updateState = useCallback((partial: Partial<T>) => {
    store.setState(partial);
  }, [store]);
  
  return [state, updateState];
}

第三層:StoreManager (註冊中心)

class StoreManager {
  private adapters = new Map<string, StoreAdapter<any>>();
  
  // 管理不同的 store instance
  registerStore<T>(key: string, adapter: StoreAdapter<T>): void {
    this.adapters.set(key, adapter);
  }
  
  // 提供統一的獲取方式
  getStore<T>(key: string): StoreAdapter<T> {
    return this.adapters.get(key);
  }
}

第四層:Adapter (轉換層)

// 將不同的狀態管理庫轉換成統一介面
class ReduxObservableAdapter implements StoreAdapter<UserState> {
  constructor(private reduxStore: Store<UserState>) {}
  
  getState(): UserState {
    return this.reduxStore.getState();
  }
  
  setState(partial: Partial<UserState>): void {
    this.reduxStore.dispatch({
      type: 'UPDATE_USER_STATE',
      payload: partial
    });
  }
  
  subscribe(listener: (state: UserState) => void): () => void {
    return this.reduxStore.subscribe(() => {
      listener(this.reduxStore.getState());
    });
  }
}

class ZustandAdapter implements StoreAdapter<UserState> {
  constructor(private zustandStore: any) {}
  
  getState(): UserState {
    return this.zustandStore.getState();
  }
  
  setState(partial: Partial<UserState>): void {
    this.zustandStore.setState(partial);
  }
  
  subscribe(listener: (state: UserState) => void): () => void {
    return this.zustandStore.subscribe(listener);
  }
}

第五層:實際的狀態管理庫

// Redux Observable 實作
const reduxStore = createStore(userReducer);
const reduxAdapter = new ReduxObservableAdapter(reduxStore);

// Zustand 實作
const zustandStore = create((set) => ({
  user: null,
  isLoading: false,
  setState: (partial) => set(partial)
}));
const zustandAdapter = new ZustandAdapter(zustandStore);

// 註冊到管理器
storeManager.registerStore('user', reduxAdapter);  // 初期使用 Redux
// 後來遷移
storeManager.replaceStore('user', zustandAdapter); // 替換為 Zustand

資料流向

使用者操作 → Component → Hook → StoreManager → Adapter → Redux/Zustand
                ↑                                        ↓
           UI 更新 ← Hook ← StoreManager ← Adapter ← 狀態變化通知

總結

這個架構實作了:

  1. SRP:每層只負責自己的職責
  2. OCP:可以新增新的 Adapter 而不修改現有程式碼
  3. LSP:所有 Adapter 行為一致,可以互相替換
  4. ISP:客戶端只依賴需要的介面
  5. DIP:高層模組依賴抽象,不依賴具體實作

這是經典的分層架構 + Adapter 模式的實際應用。

design patter adapter ghibli