iT邦幫忙

2025 iThome 鐵人賽

DAY 9
0
Build on AWS

AWS架構師的自我修養:30天雲端系統思維實戰指南系列 第 9

Day 8 | 畫面元件模組設計系統化:設計系統與原子化架構導入

  • 分享至 

  • xImage
  •  

Day 8 | 畫面元件模組設計系統化:設計系統與原子化架構導入

經過前七天從哲學思維到後端架構的完整建構,今天我們要解決一個關鍵問題:如何將後端的聚合邊界和業務邏輯在前端實現系統化的組件設計?

這不只是UI組件的技術問題,更是從領域模型到使用者介面的完整映射工程。每個前端組件都應該對應明確的業務概念,每個互動流程都應該反映領域邏輯的自然演進。

設計系統的本體論:從業務邏輯到視覺語言

重新定義「設計系統」的價值

傳統的設計系統往往只關注視覺一致性:

設計系統 = 顏色 + 字體 + 間距 + 組件庫

但基於DDD的設計系統應該體現業務語言到視覺語言的系統化翻譯

DDD設計系統 = 領域概念 + 使用者意圖 + 互動模式 + 視覺表達

從聚合邊界到前端組件域的映射

以你在統智科技的電子票券系統為例,我們可以建立這樣的映射關係:

票券聚合 → 前端組件域映射

// 後端聚合邊界
Ticket聚合 ↔ TicketDomain組件域
├── Ticket實體 ↔ TicketCard組件
├── RedemptionCode值對象 ↔ QRCodeDisplay組件  
└── TicketStatus值對象 ↔ StatusIndicator組件

User聚合 ↔ UserDomain組件域
├── User實體 ↔ UserProfile組件
├── Preferences值對象 ↔ SettingsPanel組件
└── AuthToken值對象 ↔ AuthStatus組件

這種映射確保了前端組件與業務邏輯的一致性,每個組件都有明確的職責邊界。

Atomic Design:領域概念的層次化實現

重新詮釋Atomic Design的DDD意義

Brad Frost的Atomic Design方法論需要在DDD語境下重新詮釋:

原子(Atoms):領域值對象的UI表達

  • Money、Status、Code等基礎業務概念
  • 這些是不可再分的業務意義單位

分子(Molecules):領域實體的屬性組合

  • 將相關的值對象組合成有意義的UI片段
  • 對應實體的屬性群組

有機體(Organisms):領域聚合的完整表達

  • 完整的業務功能實現
  • 對應聚合的核心能力

模板(Templates):領域服務的協調層

  • 跨聚合的業務流程
  • 複雜使用者旅程的結構

頁面(Pages):具體的業務場景實例

  • 特定角色在特定情境下的完整操作

7-11票券系統的Atomic Design實踐

graph TB
    subgraph "原子層 (Atoms) - 值對象UI"
        A1[StatusBadge]
        A2[QRCode] 
        A3[PriceDisplay]
        A4[ExpiryDate]
        A5[RedemptionButton]
    end
    
    subgraph "分子層 (Molecules) - 實體屬性"
        M1[TicketInfo]
        M2[RedemptionPanel]
        M3[PriceInfo]
        M4[StatusPanel]
    end
    
    subgraph "有機體層 (Organisms) - 聚合功能"
        O1[TicketCard]
        O2[RedemptionFlow]
        O3[TicketList]
        O4[UserDashboard]
    end
    
    subgraph "模板層 (Templates) - 領域服務"
        T1[TicketRedemptionWorkflow]
        T2[UserTicketManagement]
        T3[StoreIntegration]
    end
    
    subgraph "頁面層 (Pages) - 業務場景"
        P1[CoffeeRedemptionPage]
        P2[TicketHistoryPage]
        P3[StoreDashboardPage]
    end
    
    A1 --> M1
    A2 --> M2
    A3 --> M1
    A4 --> M1
    A5 --> M2
    
    M1 --> O1
    M2 --> O2
    M3 --> O1
    M4 --> O3
    
    O1 --> T2
    O2 --> T1
    O3 --> T2
    
    T1 --> P1
    T2 --> P2
    T1 --> P3

具體實現範例

// 原子層:Status值對象的UI表達
interface StatusBadgeProps {
  status: TicketStatus;
  size?: 'sm' | 'md' | 'lg';
  variant?: 'filled' | 'outlined';
}

const StatusBadge: React.FC<StatusBadgeProps> = ({ 
  status, 
  size = 'md',
  variant = 'filled' 
}) => {
  const getStatusConfig = (): StatusConfig => {
    switch (status) {
      case TicketStatus.AVAILABLE:
        return { color: 'green', label: '可使用', icon: 'check-circle' };
      case TicketStatus.USED:
        return { color: 'gray', label: '已使用', icon: 'check' };
      case TicketStatus.EXPIRED:
        return { color: 'red', label: '已過期', icon: 'x-circle' };
      case TicketStatus.PENDING:
        return { color: 'yellow', label: '處理中', icon: 'clock' };
    }
  };

  const config = getStatusConfig();
  
  return (
    <span className={`status-badge ${config.color} ${size} ${variant}`}>
      <Icon name={config.icon} />
      {config.label}
    </span>
  );
};

// 分子層:Ticket實體的屬性組合
interface TicketInfoProps {
  ticket: Ticket;
  showDetails?: boolean;
}

const TicketInfo: React.FC<TicketInfoProps> = ({ ticket, showDetails = true }) => {
  return (
    <div className="ticket-info">
      <div className="ticket-header">
        <h3>{ticket.productName}</h3>
        <StatusBadge status={ticket.status} />
      </div>
      
      {showDetails && (
        <div className="ticket-details">
          <PriceDisplay amount={ticket.value} originalPrice={ticket.originalPrice} />
          <ExpiryDate date={ticket.expiryDate} />
          <div className="store-info">
            <span>適用店家:{ticket.applicableStores.join(', ')}</span>
          </div>
        </div>
      )}
    </div>
  );
};

// 有機體層:Ticket聚合的完整表達
interface TicketCardProps {
  ticket: Ticket;
  onRedeem?: (ticket: Ticket) => void;
  onViewDetails?: (ticket: Ticket) => void;
}

const TicketCard: React.FC<TicketCardProps> = ({
  ticket,
  onRedeem,
  onViewDetails
}) => {
  const canRedeem = ticket.isRedeemable();
  const isNearExpiry = ticket.isNearExpiry();

  return (
    <div className={`ticket-card ${isNearExpiry ? 'near-expiry' : ''}`}>
      <TicketInfo ticket={ticket} />
      
      {canRedeem && (
        <RedemptionPanel 
          ticket={ticket}
          onRedeem={onRedeem}
        />
      )}
      
      <div className="ticket-actions">
        <button 
          onClick={() => onViewDetails?.(ticket)}
          className="btn-secondary"
        >
          查看詳情
        </button>
        
        {canRedeem && (
          <button 
            onClick={() => onRedeem?.(ticket)}
            className="btn-primary"
          >
            立即使用
          </button>
        )}
      </div>
      
      {isNearExpiry && (
        <div className="expiry-warning">
          <Icon name="alert-triangle" />
          即將過期,請盡快使用
        </div>
      )}
    </div>
  );
};

Core-Share-Feature 架構:前端領域邊界的實現

架構層次的DDD對應

Core層:領域核心概念

  • 跨所有feature共享的基礎概念
  • 對應DDD的共享內核(Shared Kernel)

Shared層:通用UI組件和服務

  • 跨多個feature使用但非核心業務邏輯
  • 對應DDD的公開語言(Published Language)

Feature層:具體業務功能實現

  • 對應DDD的界限上下文(Bounded Context)

7-11票券系統的Core-Share-Feature實現

src/
├── core/                           # 核心領域概念
│   ├── domain/
│   │   ├── entities/              # 領域實體
│   │   │   ├── Ticket.ts
│   │   │   ├── User.ts
│   │   │   └── Store.ts
│   │   ├── value-objects/         # 值對象
│   │   │   ├── Money.ts
│   │   │   ├── TicketStatus.ts
│   │   │   └── RedemptionCode.ts
│   │   └── events/                # 領域事件
│   │       ├── TicketRedeemed.ts
│   │       └── TicketExpired.ts
│   ├── infrastructure/
│   │   ├── api/                   # API客戶端
│   │   ├── storage/               # 本地存儲
│   │   └── payment/               # 支付集成
│   └── types/                     # 型別定義
│
├── shared/                         # 共享組件和服務
│   ├── components/
│   │   ├── atoms/                 # 原子組件
│   │   │   ├── StatusBadge.tsx
│   │   │   ├── PriceDisplay.tsx
│   │   │   ├── QRCode.tsx
│   │   │   └── ExpiryDate.tsx
│   │   ├── molecules/             # 分子組件
│   │   │   ├── TicketInfo.tsx
│   │   │   ├── RedemptionPanel.tsx
│   │   │   └── StoreLocator.tsx
│   │   └── layout/                # 佈局組件
│   │       ├── Header.tsx
│   │       ├── Navigation.tsx
│   │       └── Footer.tsx
│   ├── hooks/                     # 共享Hook
│   │   ├── useLocalStorage.ts
│   │   ├── useApiQuery.ts
│   │   └── useGeolocation.ts
│   ├── services/                  # 共享服務
│   │   ├── NotificationService.ts
│   │   ├── AnalyticsService.ts
│   │   └── CacheService.ts
│   └── utils/                     # 工具函數
│       ├── formatters.ts
│       ├── validators.ts
│       └── dateHelpers.ts
│
├── features/                       # 功能特性模組
│   ├── tickets/                   # 票券管理功能
│   │   ├── components/
│   │   │   ├── TicketCard.tsx
│   │   │   ├── TicketList.tsx
│   │   │   └── TicketFilter.tsx
│   │   ├── hooks/
│   │   │   ├── useTickets.ts
│   │   │   └── useTicketRedeem.ts
│   │   ├── services/
│   │   │   └── TicketService.ts
│   │   └── pages/
│   │       ├── TicketsPage.tsx
│   │       └── TicketDetailPage.tsx
│   │
│   ├── redemption/                # 兌換功能
│   │   ├── components/
│   │   │   ├── RedemptionFlow.tsx
│   │   │   ├── QRScanner.tsx
│   │   │   └── RedemptionResult.tsx
│   │   ├── hooks/
│   │   │   ├── useQRScanner.ts
│   │   │   └── useRedemption.ts
│   │   ├── services/
│   │   │   └── RedemptionService.ts
│   │   └── pages/
│   │       └── RedemptionPage.tsx
│   │
│   └── store/                     # 店家功能
│       ├── components/
│       │   ├── StoreDashboard.tsx
│       │   ├── RedemptionHistory.tsx
│       │   └── StoreAnalytics.tsx
│       ├── hooks/
│       │   └── useStoreData.ts
│       ├── services/
│       │   └── StoreService.ts
│       └── pages/
│           └── StoreDashboardPage.tsx

Feature內部的DDD結構實現

Tickets Feature的內部組織

// features/tickets/domain/TicketAggregate.ts
export class TicketAggregate {
  constructor(
    private tickets: Ticket[],
    private user: User,
    private redemptionHistory: RedemptionHistory
  ) {}
  
  // 聚合內的業務邏輯
  validateRedemption(ticketId: string, storeId: string): ValidationResult {
    const ticket = this.tickets.find(t => t.id === ticketId);
    if (!ticket) {
      return ValidationResult.failed("票券不存在");
    }
    
    if (!ticket.isRedeemable()) {
      return ValidationResult.failed("票券無法使用");
    }
    
    if (!ticket.isValidForStore(storeId)) {
      return ValidationResult.failed("票券不適用於此店家");
    }
    
    return ValidationResult.success();
  }
  
  // 聚合狀態變更
  redeemTicket(ticketId: string, storeId: string): RedemptionResult {
    const validation = this.validateRedemption(ticketId, storeId);
    if (!validation.isValid) {
      throw new Error(validation.errorMessage);
    }
    
    const ticket = this.tickets.find(t => t.id === ticketId);
    ticket.markAsUsed();
    
    const redemption = new Redemption(ticketId, storeId, new Date());
    this.redemptionHistory.addRedemption(redemption);
    
    return RedemptionResult.success(redemption);
  }
}

// features/tickets/hooks/useTicketAggregate.ts
export const useTicketAggregate = (userId: string) => {
  const [aggregate, setAggregate] = useState<TicketAggregate | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  
  // 載入聚合
  useEffect(() => {
    TicketService.loadUserTickets(userId)
      .then(data => {
        const aggregate = new TicketAggregate(
          data.tickets,
          data.user,
          data.redemptionHistory
        );
        setAggregate(aggregate);
      })
      .catch(setError)
      .finally(() => setLoading(false));
  }, [userId]);
  
  // 聚合操作方法
  const redeemTicket = useCallback(async (ticketId: string, storeId: string) => {
    if (!aggregate) return;
    
    try {
      // 樂觀更新
      const newAggregate = aggregate.clone();
      const result = newAggregate.redeemTicket(ticketId, storeId);
      setAggregate(newAggregate);
      
      // 同步到伺服器
      await TicketService.redeemTicketOnServer(ticketId, storeId);
      
      // 通知使用者
      NotificationService.success('票券使用成功!');
      
      return result;
    } catch (error) {
      // 回滾樂觀更新
      setAggregate(aggregate);
      NotificationService.error(error.message);
      throw error;
    }
  }, [aggregate]);
  
  return {
    aggregate,
    loading,
    error,
    redeemTicket
  };
};

// features/tickets/components/TicketList.tsx
export const TicketList: React.FC<Props> = ({ userId }) => {
  const { 
    aggregate, 
    loading, 
    error, 
    redeemTicket 
  } = useTicketAggregate(userId);
  
  if (loading) return <TicketListSkeleton />;
  if (error) return <ErrorDisplay error={error} />;
  if (!aggregate?.tickets.length) return <EmptyTicketList />;
  
  return (
    <div className="ticket-list">
      {aggregate.tickets.map(ticket => (
        <TicketCard
          key={ticket.id}
          ticket={ticket}
          onRedeem={(ticket) => redeemTicket(ticket.id, currentStoreId)}
        />
      ))}
    </div>
  );
};

設計令牌系統:業務語言的視覺化

從領域概念到設計令牌的映射

設計令牌不應該只是視覺變數,而應該承載業務語義:

// design-tokens/semantic-tokens.ts
export const TicketSemanticTokens = {
  // 顏色:反映票券狀態
  colors: {
    status: {
      available: '#10B981',    // 可用:綠色
      pending: '#F59E0B',      // 待處理:橙色
      used: '#6B7280',         // 已使用:灰色
      expired: '#EF4444',      // 過期:紅色
      nearExpiry: '#DC2626'    // 即將過期:深紅
    },
    value: {
      high: '#7C3AED',         // 高價值:紫色
      medium: '#3B82F6',       // 中價值:藍色
      low: '#059669'           // 低價值:綠色
    },
    category: {
      coffee: '#92400E',       // 咖啡:棕色
      food: '#DC2626',         // 食物:紅色
      retail: '#7C3AED',       // 零售:紫色
      service: '#059669'       // 服務:綠色
    }
  },
  
  // 字體:反映信息層次
  typography: {
    price: {
      major: {                 // 主要價格顯示
        fontSize: '1.5rem',
        fontWeight: '700',
        fontFamily: '"SF Pro Display", sans-serif'
      },
      discount: {              // 折扣價格
        fontSize: '1rem',
        fontWeight: '600',
        color: '#DC2626'
      }
    },
    status: {
      active: {                // 活躍狀態
        fontSize: '0.875rem',
        fontWeight: '600',
        textTransform: 'uppercase'
      }
    }
  },
  
  // 間距:反映功能關係
  spacing: {
    card: {
      padding: '1rem',         // 卡片內間距
      margin: '0.75rem'        // 卡片間間距
    },
    section: {
      padding: '1.5rem',       // 區段內間距
      margin: '2rem'           // 區段間間距
    }
  },
  
  // 陰影:反映層次和狀態
  shadows: {
    card: {
      normal: '0 1px 3px rgba(0, 0, 0, 0.1)',
      hover: '0 4px 6px rgba(0, 0, 0, 0.1)',
      selected: '0 0 0 2px #3B82F6'
    },
    nearExpiry: '0 0 8px rgba(220, 38, 38, 0.3)'
  }
};

// 業務組件中的使用
const TicketCard: React.FC<TicketCardProps> = ({ ticket }) => {
  const getCardStyle = () => {
    const tokens = TicketSemanticTokens;
    const baseStyle = {
      padding: tokens.spacing.card.padding,
      margin: tokens.spacing.card.margin,
      boxShadow: tokens.shadows.card.normal
    };
    
    if (ticket.isNearExpiry()) {
      return {
        ...baseStyle,
        boxShadow: tokens.shadows.nearExpiry,
        borderColor: tokens.colors.status.nearExpiry
      };
    }
    
    return baseStyle;
  };
  
  return (
    <div style={getCardStyle()}>
      <TicketInfo ticket={ticket} />
      <RedemptionPanel ticket={ticket} />
    </div>
  );
};

組件測試策略:業務邏輯的驗證

DDD導向的組件測試

組件測試應該驗證業務邏輯而非實現細節:

// TicketCard.test.tsx
describe('TicketCard Component', () => {
  describe('業務邏輯顯示', () => {
    it('應該根據票券狀態顯示正確的視覺提示', () => {
      const availableTicket = TicketTestFactory.createAvailable();
      
      render(<TicketCard ticket={availableTicket} />);
      
      expect(screen.getByTestId('status-badge')).toHaveTextContent('可使用');
      expect(screen.getByTestId('status-badge')).toHaveClass('status-available');
      expect(screen.getByTestId('redeem-button')).toBeEnabled();
    });
    
    it('應該為即將過期的票券顯示警告', () => {
      const nearExpiryTicket = TicketTestFactory.createNearExpiry();
      
      render(<TicketCard ticket={nearExpiryTicket} />);
      
      expect(screen.getByTestId('expiry-warning')).toBeInTheDocument();
      expect(screen.getByTestId('ticket-card')).toHaveClass('near-expiry');
    });
    
    it('已使用的票券不應該顯示兌換按鈕', () => {
      const usedTicket = TicketTestFactory.createUsed();
      
      render(<TicketCard ticket={usedTicket} />);
      
      expect(screen.queryByTestId('redeem-button')).not.toBeInTheDocument();
      expect(screen.getByTestId('status-badge')).toHaveTextContent('已使用');
    });
  });
  
  describe('使用者互動', () => {
    it('點擊兌換按鈕應該觸發兌換流程', async () => {
      const ticket = TicketTestFactory.createAvailable();
      const onRedeem = jest.fn();
      
      render(<TicketCard ticket={ticket} onRedeem={onRedeem} />);
      
      fireEvent.click(screen.getByTestId('redeem-button'));
      
      expect(onRedeem).toHaveBeenCalledWith(ticket);
    });
  });
});

// TicketList.test.tsx - 聚合層級的測試
describe('TicketList Component', () => {
  describe('聚合狀態顯示', () => {
    it('應該顯示使用者的所有可用票券', () => {
      const tickets = [
        TicketTestFactory.createAvailable({ productName: '經典美式咖啡' }),
        TicketTestFactory.createAvailable({ productName: '拿鐵咖啡' })
      ];
      
      render(<TicketList userId="user1" tickets={tickets} />);
      
      expect(screen.getByText('經典美式咖啡')).toBeInTheDocument();
      expect(screen.getByText('拿鐵咖啡')).toBeInTheDocument();
      expect(screen.getAllByTestId('redeem-button')).toHaveLength(2);
    });
  });
  
  describe('聚合操作', () => {
    it('應該正確處理票券兌換流程', async () => {
      const mockRedemption = jest.fn().mockResolvedValue({ success: true });
      const tickets = [TicketTestFactory.createAvailable()];
      
      render(
        <TicketList 
          userId="user1" 
          tickets={tickets}
          onTicketRedeem={mockRedemption}
        />
      );
      
      fireEvent.click(screen.getByTestId('redeem-button'));
      
      await waitFor(() => {
        expect(mockRedemption).toHaveBeenCalledWith(
          expect.objectContaining({
            id: tickets[0].id
          })
        );
      });
    });
  });
});

響應式設計的業務適配

業務功能的設備優先級

不同設備上的功能應該基於業務重要性進行分層:

// responsive/BusinessPriority.ts
export const TicketSystemPriorities = {
  mobile: {
    primary: [
      'ticket-redemption',      // 票券兌換(核心功能)
      'qr-code-display',        // QR碼顯示
      'ticket-status'           // 票券狀態
    ],
    secondary: [
      'ticket-list',            // 票券列表
      'expiry-notifications',   // 過期提醒
      'store-locator'           // 店家定位
    ],
    hidden: [
      'detailed-analytics',     // 詳細分析
      'bulk-operations',        // 批量操作
      'admin-functions'         // 管理功能
    ]
  },
  
  tablet: {
    primary: [
      'ticket-overview',        // 票券總覽
      'redemption-history',     // 兌換歷史
      'store-integration'       // 店家整合
    ],
    secondary: [
      'analytics-dashboard',    // 分析面板
      'user-management'         // 用戶管理
    ]
  },
  
  desktop: {
    primary: [
      'full-dashboard',         // 完整儀表板
      'advanced-analytics',     // 進階分析
      'system-administration'   // 系統管理
    ]
  }
};

// 響應式組件實現
const ResponsiveTicketCard: React.FC<Props> = ({ ticket, device }) => {
  const getPriorityLevel = () => {
    return TicketSystemPriorities[device] || TicketSystemPriorities.mobile;
  };
  
  const shouldShowFeature = (feature: string) => {
    const priorities = getPriorityLevel();
    return priorities.primary.includes(feature) || 
           priorities.secondary.includes(feature);
  };
  
  return (
    <div className={`ticket-card ${device}`}>
      <TicketInfo ticket={ticket} />
      
      {/* 核心功能:所有設備都顯示 */}
      <RedemptionPanel ticket={ticket} />
      
      {/* 條件功能:根據設備優先級顯示 */}
      {shouldShowFeature('detailed-info') && (
        <TicketDetails ticket={ticket} />
      )}
      
      {shouldShowFeature('analytics') && (
        <TicketAnalytics ticket={ticket} />
      )}
    </div>
  );
};

效能優化的系統化策略

組件載入的業務驅動優化

// performance/LazyLoading.ts
export const TicketSystemLazyComponents = {
  // 立即載入:核心業務功能
  immediate: [
    'TicketCard',
    'StatusBadge', 
    'RedemptionButton'
  ],
  
  // 延遲載入:次要功能
  deferred: [
    'TicketAnalytics',
    'RedemptionHistory',
    'StoreLocator'
  ],
  
  // 按需載入:管理功能
  onDemand: [
    'AdminDashboard',
    'BulkOperations',
    'SystemSettings'
  ]
};

// 智能預載入策略
const useSmartPreloading = (userBehavior: UserBehavior) => {
  useEffect(() => {
    // 基於用戶行為預測需要的組件
    if (userBehavior.frequentlyUsesAnalytics) {
      import('./TicketAnalytics').then(component => {
        // 預載入分析組件
      });
    }
    
    if (userBehavior.isStoreManager) {
      import('./StoreDashboard').then(component => {
        // 預載入店家面板
      });
    }
  }, [userBehavior]);
};

前後端協作邊界:實現Day 7的架構承諾

三系統的API設計策略:基於業務特性的技術選型

Day 7我們規劃了RESTful + GraphQL + WebSocket混合策略,現在針對三個核心案例分別實現最適合的API設計:

投資交易系統:極致性能的API架構

選型理由:毫秒級延遲要求、高併發、強一致性、實時數據流

// api/contracts/TradingContracts.ts
export interface TradingSystemAPI {
  // WebSocket主導:實時市場數據和交易執行
  websocket: {
    // 市場數據流(最高優先級)
    marketData: {
      subscribe: { symbols: string[]; depth?: number };
      events: {
        'price:update': { symbol: string; price: number; volume: number; timestamp: number };
        'orderbook:change': { symbol: string; bids: Order[]; asks: Order[] };
        'trade:executed': { symbol: string; price: number; quantity: number };
      };
    };
    
    // 交易操作(超低延遲)
    trading: {
      events: {
        'order:submitted': { orderId: string; status: 'pending' | 'rejected'; reason?: string };
        'order:filled': { orderId: string; fillPrice: number; fillQuantity: number };
        'portfolio:updated': { portfolioId: string; totalValue: number; positions: Position[] };
      };
    };
  };
  
  // REST輔助:配置和歷史查詢
  rest: {
    'GET /api/portfolios/{id}': {
      params: { id: string };
      response: { portfolio: Portfolio; holdings: Holding[] };
      cache: 'max-age=30'; // 30秒快取
    };
    
    'POST /api/orders': {
      body: { portfolioId: string; symbol: string; quantity: number; orderType: 'MARKET' | 'LIMIT' };
      response: { orderId: string; estimatedFillPrice?: number };
      timeout: 500; // 500ms超時
    };
    
    'GET /api/analytics/risk/{portfolioId}': {
      params: { portfolioId: string };
      response: { var: number; beta: number; sharpeRatio: number };
      cache: 'max-age=300'; // 5分鐘快取
    };
  };
  
  // GraphQL不適用:延遲過高,不利於高頻交易
}

// 高性能API客戶端實現
export class TradingAPIClient {
  private wsConnection: WebSocket;
  private messageQueue: PriorityQueue<WSMessage>;
  private reconnectStrategy: ExponentialBackoff;
  
  constructor() {
    // 建立多個WebSocket連線以降低延遲
    this.wsConnection = new WebSocket(WS_ENDPOINT, {
      // 關鍵配置:最小化延遲
      perMessageDeflate: false, // 關閉壓縮以降低CPU開銷
      maxPayload: 1024 * 16,    // 限制payload大小
      pingTimeout: 5000,        // 快速心跳檢測
    });
  }
  
  // 優先級訊息處理:交易>價格>其他
  private processMessage(message: WSMessage): void {
    switch (message.type) {
      case 'order:filled':
        this.handleOrderFill(message.data);
        break;
      case 'price:update':
        this.updateMarketData(message.data);
        break;
      default:
        this.handleGenericMessage(message);
    }
  }
  
  // 批次訂單提交:減少網路往返
  async submitOrderBatch(orders: OrderRequest[]): Promise<OrderResponse[]> {
    const batchId = generateBatchId();
    const promises = orders.map(order => 
      this.submitSingleOrder({ ...order, batchId })
    );
    
    return Promise.all(promises);
  }
}

家庭財務系統:簡單可靠的API架構

選型理由:成本敏感、開發簡單、維護容易、協作功能

// api/contracts/FamilyFinanceContracts.ts
export interface FamilyFinanceAPI {
  // REST主導:簡單CRUD,易於理解和維護
  rest: {
    // 家庭管理
    'GET /api/families/{id}': {
      params: { id: string };
      response: { family: Family; members: FamilyMember[]; permissions: Permission[] };
      cache: 'max-age=3600'; // 1小時快取
    };
    
    'POST /api/families/{id}/members': {
      params: { id: string };
      body: { email: string; role: 'ADMIN' | 'MEMBER' | 'CHILD' };
      response: { member: FamilyMember; inviteToken: string };
    };
    
    // 支出記錄
    'POST /api/expenses': {
      body: { 
        familyId: string; 
        amount: number; 
        category: string; 
        description: string;
        receiptImage?: string;
      };
      response: { expense: Expense; budgetRemaining: number };
    };
    
    'GET /api/expenses': {
      params: { 
        familyId: string; 
        startDate?: string; 
        endDate?: string; 
        category?: string;
        memberId?: string;
      };
      response: { expenses: Expense[]; summary: ExpenseSummary; pagination: Pagination };
      cache: 'max-age=300'; // 5分鐘快取
    };
    
    // 預算管理
    'PUT /api/budgets/{id}': {
      params: { id: string };
      body: { categoryLimits: CategoryLimit[]; alertThresholds: number[] };
      response: { budget: Budget; effectiveDate: string };
    };
  };
  
  // 基本WebSocket:協作通知
  websocket: {
    events: {
      'expense:added': { expense: Expense; addedBy: FamilyMember };
      'budget:exceeded': { category: string; currentAmount: number; limit: number };
      'member:joined': { member: FamilyMember; invitedBy: FamilyMember };
    };
  };
  
  // GraphQL不使用:增加複雜度,不符合簡化原則
}

// 簡化的API客戶端
export class FamilyFinanceAPIClient {
  constructor(private baseURL: string, private authToken: string) {}
  
  // 通用REST方法,減少程式碼重複
  private async request<T>(
    method: 'GET' | 'POST' | 'PUT' | 'DELETE',
    endpoint: string,
    data?: any
  ): Promise<T> {
    const response = await fetch(`${this.baseURL}${endpoint}`, {
      method,
      headers: {
        'Authorization': `Bearer ${this.authToken}`,
        'Content-Type': 'application/json',
      },
      body: data ? JSON.stringify(data) : undefined,
    });
    
    if (!response.ok) {
      throw new APIError(response.status, await response.text());
    }
    
    return response.json();
  }
  
  // 業務方法:語義清晰、易於理解
  async addExpense(expense: ExpenseInput): Promise<ExpenseResult> {
    return this.request('POST', '/api/expenses', expense);
  }
  
  async getFamilyExpenses(familyId: string, filters?: ExpenseFilters): Promise<ExpenseList> {
    const params = new URLSearchParams({ familyId, ...filters });
    return this.request('GET', `/api/expenses?${params}`);
  }
  
  async updateBudget(budgetId: string, budget: BudgetUpdate): Promise<Budget> {
    return this.request('PUT', `/api/budgets/${budgetId}`, budget);
  }
  
  // 簡單的WebSocket連線管理
  connectToFamilyUpdates(familyId: string, callback: (event: FamilyEvent) => void): void {
    const ws = new WebSocket(`${WS_ENDPOINT}/families/${familyId}`);
    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      callback(data);
    };
  }
}

健康監控系統:數據流驱動的API架構

選型理由:IoT數據流、長期存儲、異常檢測、時序分析

// api/contracts/HealthMonitorContracts.ts
export interface HealthMonitorAPI {
  // REST: 配置和查詢
  rest: {
    // 使用者健康檔案
    'GET /api/health-profiles/{userId}': {
      params: { userId: string };
      response: { 
        profile: HealthProfile; 
        devices: Device[]; 
        latestMetrics: HealthMetric[] 
      };
      cache: 'max-age=1800'; // 30分鐘快取
    };
    
    // 設備管理
    'POST /api/devices': {
      body: { 
        userId: string; 
        deviceType: 'HEART_RATE' | 'BLOOD_PRESSURE' | 'WEIGHT' | 'GLUCOSE';
        deviceId: string;
        calibrationData?: CalibrationData;
      };
      response: { device: Device; connectionToken: string };
    };
    
    // 歷史數據查詢
    'GET /api/health-data/{userId}': {
      params: { 
        userId: string; 
        startDate: string; 
        endDate: string; 
        metrics?: string[]; 
        aggregation?: 'HOURLY' | 'DAILY' | 'WEEKLY';
      };
      response: { 
        data: TimeSeriesData[]; 
        trends: TrendAnalysis; 
        anomalies: Anomaly[] 
      };
      cache: 'max-age=7200'; // 2小時快取(歷史數據變化較少)
    };
  };
  
  // GraphQL: 複雜關聯查詢和個性化分析
  graphql: {
    queries: {
      healthDashboard: {
        input: { userId: string; timeRange: TimeRange; preferences: DashboardPrefs };
        output: {
          user: {
            profile: HealthProfile;
            goals: HealthGoal[];
            alerts: Alert[];
          };
          metrics: Array<{
            type: MetricType;
            current: number;
            trend: 'IMPROVING' | 'STABLE' | 'DECLINING';
            history: TimeSeriesPoint[];
            benchmarks: Benchmark[];
          }>;
          insights: {
            correlations: Correlation[];
            recommendations: Recommendation[];
            riskFactors: RiskFactor[];
          };
        };
      };
    };
    
    subscriptions: {
      healthAlerts: {
        input: { userId: string };
        output: { 
          alertType: 'CRITICAL' | 'WARNING' | 'INFO';
          metric: MetricType;
          value: number;
          threshold: number;
          timestamp: Date;
        };
      };
    };
  };
  
  // WebSocket: IoT數據流和即時告警
  websocket: {
    // 設備數據流
    deviceData: {
      events: {
        'device:reading': { 
          deviceId: string; 
          userId: string; 
          readings: DeviceReading[]; 
          timestamp: number 
        };
        'device:offline': { deviceId: string; lastSeen: number };
        'device:battery_low': { deviceId: string; batteryLevel: number };
      };
    };
    
    // 健康告警
    healthAlerts: {
      events: {
        'health:anomaly_detected': { 
          userId: string; 
          metric: MetricType; 
          severity: AlertSeverity;
          recommendations: string[];
        };
        'health:goal_achieved': { userId: string; goal: HealthGoal };
        'health:trend_change': { userId: string; metric: MetricType; newTrend: TrendDirection };
      };
    };
  };
}

// 混合式API客戶端:針對不同數據類型優化
export class HealthMonitorAPIClient {
  constructor(
    private restClient: RestClient,
    private graphqlClient: GraphQLClient,
    private wsClient: WebSocketManager
  ) {}
  
  // REST: 簡單配置和歷史查詢
  async getHealthProfile(userId: string): Promise<HealthProfile> {
    return this.restClient.get(`/api/health-profiles/${userId}`);
  }
  
  async getHistoricalData(
    userId: string, 
    timeRange: TimeRange,
    options?: QueryOptions
  ): Promise<HealthHistoryResponse> {
    const params = { 
      startDate: timeRange.start.toISOString(),
      endDate: timeRange.end.toISOString(),
      ...options 
    };
    return this.restClient.get(`/api/health-data/${userId}`, { params });
  }
  
  // GraphQL: 複雜關聯查詢
  async getDashboardData(
    userId: string, 
    preferences: DashboardPrefs
  ): Promise<HealthDashboard> {
    const query = `
      query HealthDashboard($userId: ID!, $timeRange: TimeRangeInput!, $preferences: DashboardPrefsInput!) {
        healthDashboard(userId: $userId, timeRange: $timeRange, preferences: $preferences) {
          user {
            profile { id name age gender }
            goals { type target current deadline }
            alerts { type severity message timestamp }
          }
          metrics {
            type
            current
            trend
            history(limit: $preferences.historyPoints) {
              timestamp
              value
            }
            benchmarks {
              label
              value
              isHealthy
            }
          }
          insights {
            correlations {
              metrics
              strength
              description
            }
            recommendations {
              category
              priority
              action
              evidence
            }
          }
        }
      }
    `;
    
    return this.graphqlClient.query(query, { 
      userId, 
      timeRange: preferences.defaultTimeRange,
      preferences 
    });
  }
  
  // WebSocket: 即時數據流處理
  subscribeToDeviceReadings(
    userId: string, 
    callback: (reading: DeviceReading) => void
  ): UnsubscribeFunction {
    return this.wsClient.subscribe('device:reading', 
      { filter: { userId } }, 
      callback
    );
  }
  
  subscribeToHealthAlerts(
    userId: string,
    callback: (alert: HealthAlert) => void
  ): UnsubscribeFunction {
    return this.wsClient.subscribe('health:anomaly_detected',
      { filter: { userId } },
      callback
    );
  }
  
  // 智能數據同步:根據數據類型選擇最佳策略
  async syncDeviceData(deviceId: string): Promise<SyncResult> {
    // 1. 透過WebSocket接收即時數據
    const realtimeData = await this.getRealtimeReadings(deviceId);
    
    // 2. 透過REST獲取歷史數據補齊
    const historicalData = await this.getHistoricalReadings(deviceId);
    
    // 3. 合併並去重
    const mergedData = this.mergeAndDeduplicateReadings(realtimeData, historicalData);
    
    return { success: true, syncedReadings: mergedData.length };
  }
}

組件與API的邊界協調

責任邊界的具體劃分

// 後端責任 (Backend Concerns)
interface BackendResponsibilities {
  businessLogicValidation: {
    // 票券使用資格驗證
    validateTicketEligibility(ticketId: string, storeId: string): ValidationResult;
    // 防重複兌換檢查
    checkDuplicateRedemption(ticketId: string): boolean;
    // 庫存與限額控制
    enforceRedemptionLimits(storeId: string): LimitResult;
  };
  
  dataConsistency: {
    // 跨系統資料同步
    syncWithPOS(redemption: Redemption): Promise<void>;
    // 交易記錄完整性
    maintainAuditTrail(operation: Operation): Promise<void>;
    // 並發安全控制
    handleConcurrentRedemptions(ticketId: string): Promise<LockResult>;
  };
  
  integrationOrchestration: {
    // 支付系統整合
    processPaymentRefund(redemption: Redemption): Promise<PaymentResult>;
    // 第三方API協調
    notifyPartnerSystems(event: SystemEvent): Promise<void>;
    // 合規性處理
    handleComplianceRequirements(transaction: Transaction): Promise<void>;
  };
}

// 前端責任 (Frontend Concerns)
interface FrontendResponsibilities {
  userExperienceLogic: {
    // 使用者互動流程
    guideRedemptionProcess(ticket: Ticket): InteractionFlow;
    // 視覺狀態反饋
    provideFeedback(operation: Operation, result: Result): UIFeedback;
    // 錯誤恢復指導
    handleErrorRecovery(error: APIError): RecoveryAction;
  };
  
  clientSideOptimization: {
    // 樂觀更新策略
    optimisticallyUpdateUI(action: UserAction): void;
    // 本地快取管理
    manageCacheInvalidation(dataChange: DataChange): void;
    // 離線行為處理
    handleOfflineScenarios(operation: Operation): OfflineStrategy;
  };
  
  presentationLogic: {
    // 數據格式化展示
    formatDisplayData(rawData: APIResponse): DisplayData;
    // 條件性UI渲染
    renderConditionalElements(userContext: UserContext): ReactElement;
    // 響應式佈局適配
    adaptToDeviceConstraints(device: DeviceInfo): LayoutConfig;
  };
}

// 共享責任 (Shared Concerns) - 需要前後端協調
interface SharedResponsibilities {
  securityAndAuth: {
    // 前端:Token管理和刷新
    // 後端:Token驗證和權限檢查
    authTokenManagement: TokenStrategy;
  };
  
  errorHandling: {
    // 前端:使用者友善錯誤訊息
    // 後端:結構化錯誤回應
    errorResponseStrategy: ErrorHandlingStrategy;
  };
  
  performanceOptimization: {
    // 前端:請求合併和快取
    // 後端:批次處理和分頁
    requestOptimization: PerformanceStrategy;
  };
}

高併發準備:前端架構的擴展性設計

為了迎接Day 9的高併發挑戰,我們需要在前端架構中預先考慮:

併發安全的狀態管理

// stores/ConcurrentSafeTicketStore.ts
export class ConcurrentSafeTicketStore {
  private _tickets = new Map<string, Ticket>();
  private _pendingOperations = new Map<string, Promise<any>>();
  private _optimisticUpdates = new Map<string, OptimisticUpdate>();
  
  // 防止重複請求的併發控制
  async redeemTicket(ticketId: string, storeId: string): Promise<RedemptionResult> {
    const operationKey = `redeem:${ticketId}:${storeId}`;
    
    // 檢查是否已有相同操作在進行中
    if (this._pendingOperations.has(operationKey)) {
      return this._pendingOperations.get(operationKey);
    }
    
    // 建立新的操作Promise
    const operation = this._executeRedemption(ticketId, storeId);
    this._pendingOperations.set(operationKey, operation);
    
    try {
      const result = await operation;
      return result;
    } finally {
      this._pendingOperations.delete(operationKey);
    }
  }
  
  private async _executeRedemption(ticketId: string, storeId: string): Promise<RedemptionResult> {
    // 1. 樂觀更新本地狀態
    const originalTicket = this._tickets.get(ticketId);
    const optimisticUpdate = {
      ticketId,
      originalState: originalTicket?.clone(),
      newState: originalTicket?.clone().markAsRedeeming(),
      timestamp: Date.now()
    };
    
    this._optimisticUpdates.set(ticketId, optimisticUpdate);
    this._tickets.set(ticketId, optimisticUpdate.newState);
    
    try {
      // 2. 發送API請求
      const result = await TicketAPI.redeemTicket(ticketId, storeId);
      
      // 3. 更新為伺服器確認狀態
      const confirmedTicket = originalTicket?.clone().markAsRedeemed(result);
      this._tickets.set(ticketId, confirmedTicket);
      this._optimisticUpdates.delete(ticketId);
      
      return result;
    } catch (error) {
      // 4. 錯誤回滾
      if (optimisticUpdate.originalState) {
        this._tickets.set(ticketId, optimisticUpdate.originalState);
      }
      this._optimisticUpdates.delete(ticketId);
      
      throw error;
    }
  }
  
  // 處理網路重連後的狀態同步
  async reconcileAfterReconnection(): Promise<void> {
    const pendingUpdates = Array.from(this._optimisticUpdates.values());
    
    for (const update of pendingUpdates) {
      try {
        // 向伺服器確認樂觀更新的實際狀態
        const serverState = await TicketAPI.getTicketState(update.ticketId);
        this._tickets.set(update.ticketId, serverState);
        this._optimisticUpdates.delete(update.ticketId);
      } catch (error) {
        // 伺服器狀態不可用,保持樂觀更新或回滾
        console.warn(`Failed to reconcile ticket ${update.ticketId}:`, error);
      }
    }
  }
}

組件層的負載均衡策略

// components/LoadBalancedTicketList.tsx
export const LoadBalancedTicketList: React.FC<Props> = ({ userId }) => {
  const [tickets, setTickets] = useState<Ticket[]>([]);
  const [loadingStrategy, setLoadingStrategy] = useState<LoadingStrategy>('progressive');
  
  // 漸進式載入策略:避免初始負載過重
  useEffect(() => {
    const loadTicketsProgressively = async () => {
      // 1. 先載入關鍵票券(即將過期、高價值)
      const criticalTickets = await TicketAPI.getCriticalTickets(userId);
      setTickets(criticalTickets);
      
      // 2. 後台載入其他票券
      setTimeout(async () => {
        const remainingTickets = await TicketAPI.getRemainingTickets(userId);
        setTickets(prev => [...prev, ...remainingTickets]);
      }, 100);
    };
    
    loadTicketsProgressively();
  }, [userId]);
  
  // 虛擬滾動:處理大量票券的渲染性能
  const virtualizedRender = useVirtualization({
    items: tickets,
    itemHeight: 120,
    overscan: 5, // 預渲染5個項目
    renderItem: ({ item, index }) => (
      <TicketCard 
        key={item.id}
        ticket={item}
        lazy={index > 10} // 超過10個項目後啟用懶載入
      />
    )
  });
  
  return (
    <div className="ticket-list-container">
      {virtualizedRender}
    </div>
  );
};

// 智能快取策略:為高併發做準備
const useSmartCaching = (cacheKey: string) => {
  const [cache] = useState(() => new Map<string, CacheEntry>());
  
  const getCachedData = useCallback(<T,>(key: string): T | null => {
    const entry = cache.get(key);
    if (!entry) return null;
    
    // 檢查快取是否過期
    if (Date.now() - entry.timestamp > entry.ttl) {
      cache.delete(key);
      return null;
    }
    
    return entry.data as T;
  }, [cache]);
  
  const setCachedData = useCallback(<T,>(key: string, data: T, ttl: number = 300000) => {
    cache.set(key, {
      data,
      timestamp: Date.now(),
      ttl
    });
  }, [cache]);
  
  return { getCachedData, setCachedData };
};

為Day 9高併發設計的架構準備

前端併發處理的核心策略

// 請求去重和合併
export class RequestDeduplicator {
  private pendingRequests = new Map<string, Promise<any>>();
  
  async dedupe<T>(key: string, requestFn: () => Promise<T>): Promise<T> {
    if (this.pendingRequests.has(key)) {
      return this.pendingRequests.get(key) as Promise<T>;
    }
    
    const promise = requestFn();
    this.pendingRequests.set(key, promise);
    
    try {
      const result = await promise;
      return result;
    } finally {
      this.pendingRequests.delete(key);
    }
  }
}

// 批次請求優化
export class BatchRequestManager {
  private batchQueue = new Map<string, BatchRequest>();
  private batchTimeout = 50; // 50ms內的請求合併為一批
  
  async batchRequest<T>(
    batchKey: string,
    request: BatchableRequest<T>
  ): Promise<T> {
    return new Promise((resolve, reject) => {
      let batch = this.batchQueue.get(batchKey);
      
      if (!batch) {
        batch = {
          requests: [],
          timer: setTimeout(() => this.executeBatch(batchKey), this.batchTimeout)
        };
        this.batchQueue.set(batchKey, batch);
      }
      
      batch.requests.push({ request, resolve, reject });
    });
  }
  
  private async executeBatch(batchKey: string): Promise<void> {
    const batch = this.batchQueue.get(batchKey);
    if (!batch) return;
    
    this.batchQueue.delete(batchKey);
    clearTimeout(batch.timer);
    
    try {
      // 合併多個請求為單一批次請求
      const batchedRequests = batch.requests.map(r => r.request);
      const results = await API.executeBatch(batchKey, batchedRequests);
      
      // 分發結果到各個Promise
      batch.requests.forEach((r, index) => {
        r.resolve(results[index]);
      });
    } catch (error) {
      // 批次失敗時,讓所有請求都失敗
      batch.requests.forEach(r => r.reject(error));
    }
  }
}

為Day 9的高併發架構建立基礎

基於今天建立的前端架構,明天我們將深入探討:

資料密集型應用的前端策略

  • 本地快取與分散式快取的協調:如何在前端實現智能快取策略
  • 請求優化與批次處理:減少網路開銷,提升用戶體驗
  • 狀態同步與衝突解決:多用戶併發操作的前端處理

微服務架構的前端適配

  • 服務發現與路由:前端如何適應後端微服務的動態性
  • 斷路器模式:前端的故障隔離和優雅降級
  • 分散式追蹤:跨服務的用戶操作追蹤和除錯

AWS高併發架構的前端優化

  • CDN與邊緣計算:如何最大化CloudFront的效益
  • Lambda@Edge:前端邏輯的邊緣執行策略
  • API Gateway限流:前端如何優雅處理限流回應

今日的系統化收穫

  • 設計系統是業務語言的視覺翻譯:每個設計決策都應該反映業務概念
  • Atomic Design + DDD = 有機的組件層次:從值對象到聚合的自然映射
  • Core-Share-Feature體現領域邊界:前端架構應該對應後端聚合邊界
  • 組件測試驗證業務邏輯:測試應該關注業務價值而非技術實現
  • 響應式設計基於業務優先級:不同設備的功能分層應該反映業務重要性

記住:我們今天建立的不是UI框架,而是業務邏輯在視覺層的系統化表達。每個組件都承載著特定的業務語義,每個設計決策都應該服務於使用者的業務目標。


「組件不是UI的積木,而是業務概念的視覺化載體。我們設計的不是介面,而是使用者與業務領域對話的媒介。真正的設計系統,是讓複雜的業務邏輯變得直觀可感的翻譯藝術。」


上一篇
Day 7 | 畫出你的第一份系統藍圖:架構選型與設計
系列文
AWS架構師的自我修養:30天雲端系統思維實戰指南9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言