今天要介紹SOLID的第四篇- 介面分離原則(Interface Segregation Principle, ISP)
,一開始第一次學習接觸會覺得SOLID這幾個原則,在實際應用上會有幾分類似。
不過這些前輩提出的理論,可能是在當時軟體開發背景下碰到的另一個問題,用當時的觀察角度所提出的軟體開發建議,我們反而不要以反終為始的態度,覺得它們應用層面很窄,就就會稍稍懂其實主軸還是圍繞在讓程式碼好維護就行~~~
Clients should not be forced to depend upon interfaces that they don't use.
來分析一下ISP的定義: 不應該要求客戶端讓他們依賴用不到接口(介面)。
其實翻成白話應該描述成 「使用方不應被迫使用對其而言無用的方法或功能」
,對於開發者來說則可以解讀成「為個別的使用方設置其專屬的功能介面,來避免多個介面彼此干擾」
。
來看一下漫畫圖會比較好理解:
ISP原理同樣是 Robert Martin(Bob大叔) 在一家Xerox印刷公司當軟體設計顧問時遇到問題所提出:
該印表機的系統的功能是,用戶使用客戶端編輯列印任務後,會發送給該印表機處理、印出、裝訂等,所有機器的運作都由該系統負責處理。 但隨著系統功能增加,所有軟體功能的類別(當時是用C++開發),是集中在一個大類別集合時,像是 掃描
、 影印
和 傳真
都包括在內。
但當只有一項動線功能例如影印需要調整時,開發影印功能的程式人員,無法知道修改的部分,是否被其他功能(掃描、裝訂、傳真)耦合所使用。因為進行了「影印功能」的變更,其他功能也有可能引此損壞,導致出現了巨大的測試與 debug 等維護成本。
有細細看前面單一職責文章,會發現不是跟單一職責所提到的,因為承攬太多責任導致改A壞B的情境又出現了
?
先談到上面介面隔離(ISP)實際的作法:
隔離(Segregation) :當有一個功能豐富的類別 class 要給多個使用者(用戶或開發人員)提供功能時,應該透過介面與抽象類 (Interface),來把要提供給「不同調用方」的「不同功能」,透過「不同的介面」給「隔離」開來;不讓多個調用方因為能夠使用與其無關的 API 而造成無謂的維護性問題。
介面分離原則更像是對一段包含商業邏輯或經過組裝的使用流程
進行分析,從中檢視組合使用的子功能是否引入了多餘的類別或函式,造成效能浪費或增加變更成本。這是從元件使用者(Component Client)的角度出發,關注如何保持運行效率並降低維護難度。
單一職責原則更注重以開發者角度來檢視程式碼中的問題
,特別是在基礎元件或函式過於龐大、複雜的情況下進行分析。透過釐清職責並將其分離,讓每個元件或函式更專注於特定的功能,使程式碼結構更簡單、清晰。
有搜尋到另一篇文章介紹的圖片說明隔離的作用
:
如果當產品規模越來越龐大,模組功能也越來越豐富時,但實際上使用者或開發者使用API時,只會用到一小段module,之後這條功能需求要組裝變動時,因為直接觸及最上面的原始模組,耗費成本相對來說比較大。
接下來就想像在 Vue 中那些可以用介面分離思維去實踐:
將大型組件拆分為多個小型、具體單一職責的組件。每個組件只負責特定的功能或顯示,並且檔案中使用某段功能時,不相關的邏輯片段不應該強迫出現。
將複雜的邏輯從組件中抽離,封裝成 composable,並只在需要的地方使用這些 composable。
假設我們接到有一個需求需要讓用戶註冊並且付款,其中包括用戶資訊輸入
、地址輸入
以及付款資訊輸入
等步驟。
如果不進行拆分,這些步驟可能會被寫在一個大型的 Form Component
元件中。這樣的組件會有複雜的邏輯,並且維護困難。
哪張表單故障或需要修改,找到負責的元件就行,不僅提升了代碼的可讀性和可維護性,也會增加了組件的重用性,因為未來可能表單的組合排列又不一樣。
<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>
如果一個表單介面上不是同時出現,是使用者有步驟性填選
時,該怎麼讓元件不要一次出現一堆不相關步驟的表單呢?
可以使用 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,
};
}
利用動態元件的切換
,讓使用者(或者說開發人員),在使用表單開發時一次只會看到某個步驟表單的邏輯,這樣能使頁面元件集中管理所有狀態變更,符合單一責任原則(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>
最終範例參考
上圖表單的關係如果開發上比擬介面分離會類似:
介面分離原則(ISP)
感覺跟先前提到的單一職責(SRP)
有點重疊的感覺,兩者都強調避免單一模組或接口過於肥大複雜或承擔多重責任,這樣可以提升系統的可維護性和擴展性。
單一職責原則 (SRP):
主要用於指導元件的設計。它從開發者的角度出發,要求元件的整體內容應該高度內聚,專注於一個特定的任務,避免不相關的邏輯或功能混雜在一起,以提升可維護性和可讀性。
介面分離原則 (ISP):
則是站在更高層次的角度,通常應用於有業務邏輯的流程或功能設計上。它強調為特定的使用者或功能組織設計專屬的介面,避免不必要的依賴。這樣的介面設計應當只提供相關的方法,並且避免將無關模組或元件的功能暴露給不需要的使用者,從而降低耦合度並提升效率。
希望我探索找到的案例大家還能夠理解,如果您有更多更好的案例或觀點,也可以分享喲~~~繼續一起加油!