iT邦幫忙

2024 iThome 鐵人賽

DAY 24
0
JavaScript

Vue.js學習中的細節陷阱:30天自我學習指南系列 第 24

Day 24: SOLID - 介面分離原則(ISP) 和 Vue 的動態元件切換

  • 分享至 

  • xImage
  •  

今天要介紹SOLID的第四篇- 介面分離原則(Interface Segregation Principle, ISP),一開始第一次學習接觸會覺得SOLID這幾個原則,在實際應用上會有幾分類似。

不過這些前輩提出的理論,可能是在當時軟體開發背景下碰到的另一個問題,用當時的觀察角度所提出的軟體開發建議,我們反而不要以反終為始的態度,覺得它們應用層面很窄,就就會稍稍懂其實主軸還是圍繞在讓程式碼好維護就行~~~

今日學習目標

  1. 認識介面分離原則(ISP)
  2. 和單一職責(SRP)關注點的不同
  3. Vue頁面元件組裝-介面分離(ISP)實踐練習

介面分離原則(Interface Segregation Principle, ISP)

Clients should not be forced to depend upon interfaces that they don't use.

來分析一下ISP的定義: 不應該要求客戶端讓他們依賴用不到接口(介面)。

其實翻成白話應該描述成 「使用方不應被迫使用對其而言無用的方法或功能」,對於開發者來說則可以解讀成「為個別的使用方設置其專屬的功能介面,來避免多個介面彼此干擾」

來看一下漫畫圖會比較好理解:

https://ithelp.ithome.com.tw/upload/images/20241007/201452511lGzMXJY7A.png

ISP原理同樣是 Robert Martin(Bob大叔) 在一家Xerox印刷公司當軟體設計顧問時遇到問題所提出:

該印表機的系統的功能是,用戶使用客戶端編輯列印任務後,會發送給該印表機處理、印出、裝訂等,所有機器的運作都由該系統負責處理。 但隨著系統功能增加,所有軟體功能的類別(當時是用C++開發),是集中在一個大類別集合時,像是 掃描影印傳真 都包括在內。

但當只有一項動線功能例如影印需要調整時,開發影印功能的程式人員,無法知道修改的部分,是否被其他功能(掃描、裝訂、傳真)耦合所使用。因為進行了「影印功能」的變更,其他功能也有可能引此損壞,導致出現了巨大的測試與 debug 等維護成本。


和單一職責(SRP)關注點的不同

有細細看前面單一職責文章,會發現不是跟單一職責所提到的,因為承攬太多責任導致改A壞B的情境又出現了?

先談到上面介面隔離(ISP)實際的作法:

隔離(Segregation) :當有一個功能豐富的類別 class 要給多個使用者(用戶或開發人員)提供功能時,應該透過介面與抽象類 (Interface),來把要提供給「不同調用方」的「不同功能」,透過「不同的介面」給「隔離」開來;不讓多個調用方因為能夠使用與其無關的 API 而造成無謂的維護性問題。

  • ISP 的出發點:以使用者從使用功能角度來思考(可能已經有一台機器或 MVP 產品)

介面分離原則更像是對一段包含商業邏輯或經過組裝的使用流程進行分析,從中檢視組合使用的子功能是否引入了多餘的類別或函式,造成效能浪費或增加變更成本。這是從元件使用者(Component Client)的角度出發,關注如何保持運行效率並降低維護難度。

  • SRP 的出發點:從開發者角度去思考單一函式或類別的責任(開發完一小段功能)

單一職責原則更注重以開發者角度來檢視程式碼中的問題,特別是在基礎元件或函式過於龐大、複雜的情況下進行分析。透過釐清職責並將其分離,讓每個元件或函式更專注於特定的功能,使程式碼結構更簡單、清晰。

有搜尋到另一篇文章介紹的圖片說明隔離的作用:

如果當產品規模越來越龐大,模組功能也越來越豐富時,但實際上使用者或開發者使用API時,只會用到一小段module,之後這條功能需求要組裝變動時,因為直接觸及最上面的原始模組,耗費成本相對來說比較大。

https://ithelp.ithome.com.tw/upload/images/20241007/201452517NWnVdwQyr.jpg


Vue頁面元件組裝-介面分離(ISP)實踐練習

接下來就想像在 Vue 中那些可以用介面分離思維去實踐:

元件拆分:

將大型組件拆分為多個小型、具體單一職責的組件。每個組件只負責特定的功能或顯示,並且檔案中使用某段功能時,不相關的邏輯片段不應該強迫出現。

Composable 組合式函式邏輯分離:

將複雜的邏輯從組件中抽離,封裝成 composable,並只在需要的地方使用這些 composable。

假設我們接到有一個需求需要讓用戶註冊並且付款,其中包括用戶資訊輸入地址輸入以及付款資訊輸入等步驟。

如果不進行拆分,這些步驟可能會被寫在一個大型的 Form Component 元件中。這樣的組件會有複雜的邏輯,並且維護困難。

step 1: 拆分元件讓維護性提高

哪張表單故障或需要修改,找到負責的元件就行,不僅提升了代碼的可讀性和可維護性,也會增加了組件的重用性,因為未來可能表單的組合排列又不一樣。

<template>
 <div>
 <UserInfo :userInfo="userInfo" />
 <AddressInfo :address="address" />
 <PaymentInfo :payment="payment" />
 </div>
</template>
<script setup>
  import { reactive } from 'vue';
  import UserInfo from './UserInfo.vue';
  import AddressInfo from './AddressInfo.vue';
  import PaymentInfo from './PaymentInfo.vue';
  // data
  const userInfo = ref({ name: '', email: '' });
  const address = ref({ street: '', city: '' });
  const payment = ref({ cardNumber: '', expiry: '' });
</script>

step 2: 有順序步驟的表單,讓使用者關注點分離

如果一個表單介面上不是同時出現,是使用者有步驟性填選時,該怎麼讓元件不要一次出現一堆不相關步驟的表單呢?

可以使用 Vue動態元件功能(Dynamic component) 來實現步驟之間的切換,這樣對開發者來說,也會有介面分離的效果,避免出現很多判斷式v-if進行切換,開發的工程師在維護上會更輕鬆。

  • 子元件表單UserInfo、AddressInfo和PaymentInfo
    實作細節上還是要注意由父元件props傳遞下來,子元件透過emits向上更新的原則。

  • 表單互動功能集中到組合式函式,控制表單操作邏輯
    我們可以將切換表單步驟表單資料收集至 組合函式useMutipleForm,它的作用會有點類似一層操作介面userInterface,我們在裡面做那些表單元件要放進來,步驟流程該怎麼切換等。

import { ref, computed,shallowRef } from 'vue';
import UserInfo from './UserInfo.vue';
import AddressInfo from './AddressInfo.vue';
import PaymentInfo from './PaymentInfo.vue';

export function useMultiStepForm() {

  const steps = ref([
    { name: 'UserInfo', data: { name: '', email: '' }, component: shallowRef(UserInfo) },
    { name: 'AddressInfo', data: { street: '', city: '' }, component: shallowRef(AddressInfo) },
    { name: 'PaymentInfo', data: { cardNumber: '', expiry: ''} , component: shallowRef(PaymentInfo)} ,
  ]);

  const currentStepIndex = ref(0);
  
  const currentStep = computed(() => steps.value[currentStepIndex.value]);

  function nextStep() {
    if (currentStepIndex.value < steps.value.length - 1) {
      currentStepIndex.value++;
    }
  }

  function prevStep() {
    if (currentStepIndex.value > 0) {
      currentStepIndex.value--;
    }
  }

  function updateUserInfo(updatedUserInfo) {
    currentStep.value.data = updatedUserInfo;
  }

  function updateAddress(updatedAddress) {
    currentStep.value.data = updatedAddress;
  }

  function updatePayment(updatedPayment) {
    currentStep.value.data = updatedPayment;
  }

  return {
    currentStep,
    nextStep,
    prevStep,
    updateUserInfo,
    updateAddress,
    updatePayment,
    currentStepIndex,
    steps,
  };
}

step 3: 將邏輯集中動態元件(Dynamic component)上

利用動態元件的切換,讓使用者(或者說開發人員),在使用表單開發時一次只會看到某個步驟表單的邏輯,這樣能使頁面元件集中管理所有狀態變更,符合單一責任原則(Single Responsibility Principle),而並動態元件切換保持著一層介面分離原則(Interface Segregation Principle, ISP)

<template>
  <div>
    <component 
      :is="currentStep.component" 
      v-bind="currentStep.data" 
      @update:userInfo="updateUserInfo"
      @update:address="updateAddress"
      @update:payment="updatePayment"
    />

    <div class="navigation">
      <button @click="prevStep" :disabled="currentStepIndex === 0">上一步</button>
      <button @click="nextStep" :disabled="currentStepIndex === steps.length - 1">下一步</button>
    </div>
  </div>
</template>

<script setup>

// 類似使用者在使用操作的介面
import { useMultiStepForm } from './useMultiStepForm.js';


const {
  currentStep,
  nextStep,
  prevStep,
  updateUserInfo,
  updateAddress,
  updatePayment,
  currentStepIndex,
  steps,
} = useMultiStepForm();

</script>

最終範例參考
上圖表單的關係如果開發上比擬介面分離會類似:
https://ithelp.ithome.com.tw/upload/images/20241007/20145251nlgEWNyPPK.png


總結

介面分離原則(ISP)感覺跟先前提到的單一職責(SRP)有點重疊的感覺,兩者都強調避免單一模組或接口過於肥大複雜或承擔多重責任,這樣可以提升系統的可維護性和擴展性。

  • 單一職責原則 (SRP):
    主要用於指導元件的設計。它從開發者的角度出發,要求元件的整體內容應該高度內聚,專注於一個特定的任務,避免不相關的邏輯或功能混雜在一起,以提升可維護性和可讀性。

  • 介面分離原則 (ISP):
    則是站在更高層次的角度,通常應用於有業務邏輯的流程或功能設計上。它強調為特定的使用者或功能組織設計專屬的介面,避免不必要的依賴。這樣的介面設計應當只提供相關的方法,並且避免將無關模組或元件的功能暴露給不需要的使用者,從而降低耦合度並提升效率。

希望我探索找到的案例大家還能夠理解,如果您有更多更好的案例或觀點,也可以分享喲~~~繼續一起加油!


學習資源

  1. https://learn-vuejs.github.io/vue-patterns/patterns/?source=post_page-----9c2ec89a8006--------------------------------#dynamic-component
  2. https://medium.com/@radheshyamsingh_83359/interface-segregation-principle-solid-design-12ecfd29f906
  3. https://coffe1891.gitbook.io/frontend-hard-mode-interview/6/6.1.2
  4. https://medium.com/@f40507777/介面隔離原則-interface-segregation-principle-isp-6854c5b3b42c
  5. https://medium.com/@chengyang1380/什麼是好的程式碼-從-solid-的設計原則出發-五-介面隔離原則-總結-interface-segregation-principle-isp-5cec1f5ae769
  6. https://gamma-ray-studio.blogspot.com/2021/07/solid-srpocplspispdip.html
  7. https://www.jyt0532.com/2020/03/23/isp/

上一篇
Day 23: SOLID - 里式替換原則(LSP) 和 Vue的組合式函式擴充
下一篇
Day 25: SOLID - 依賴反轉原則(DIP) 和 Vue 的依賴注入模式
系列文
Vue.js學習中的細節陷阱:30天自我學習指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言