اصول SOLID در Vue

اصول SOLID در Vue

به‌عنوان یک توسعه‌دهنده فرانت‌اند، همیشه از مفاهیم کلی توسعه نرم‌افزار کمی هراس داشتم. ما اغلب مهم‌ترین اصول توسعه نرم‌افزار را نادیده می‌گیریم، تنها به این دلیل که یک باور نادرست و نانوشته وجود دارد که توسعه فرانت‌اند را از پارادایم توسعه نرم‌افزار جدا می‌کند. ممکن است بگویید این برداشت چندان رایج نیست، اما باور کنید، من به اندازه کافی دیده‌ام که نگران باشم!

برویم سر اصل مطلب؛ ما اینجا هستیم تا به‌صورت مختصر اصول SOLID را بررسی کنیم و ببینیم چگونه می‌توانیم آن‌ها را در فریم‌ورک Vue 3 به کار ببریم. اصول SOLID مجموعه‌ای از دستورالعمل‌ها برای نوشتن کدی نگهداری‌پذیر و مقیاس‌پذیر هستند. این اصول محدود به زبان‌های برنامه‌نویسی یا فناوری‌های خاصی نیستند و می‌توان آن‌ها را در هر نوع توسعه نرم‌افزاری، از جمله توسعه فرانت‌اند، اعمال کرد.

در ادامه، درباره هر اصل به تفصیل صحبت می‌کنیم و نگاهی دقیق‌تر به نقش آن‌ها در بهبود کار با Vue خواهیم داشت.


۱. اصل مسئولیت تک‌گانه (Single Responsibility Principle - SRP)

همیشه به نظر می‌رسد نوشتن یک کلاس یا تابع که تمام بار منطق کسب‌وکار را بر دوش بکشد، ایده درستی است. مشکل این روش این است که کد شما غیرقابل پیش‌بینی و اصطلاحاً «غیرقابل نگهداری» می‌شود. فرض کنید یک کلاس بسیار پیچیده دارید که وظایف متعددی را انجام می‌دهد و شما نیاز دارید تغییری جزئی در یکی از این عملکردها ایجاد کنید. چطور می‌توانید تأثیر این تغییر را بر کل رفتار این کلاس ردیابی کنید؟

اینجاست که قانون SRP وارد عمل می‌شود. اگر هر کلاس را به یک وظیفه محدود کنید، دیگر لازم نیست نگران عواقب تغییرات در یک کلاس باشید، زیرا درک تأثیر این تغییرات در کلاس بسیار آسان‌تر خواهد شد.

یک مثال ساده:

<template>
  <div>
    <ProductList :products="products" @product-selected="handleProductSelected" />
    <ProductFilter :categories="categories" @category-selected="handleCategorySelected" />
    <ProductDetail v-if="selectedProduct" :product="selectedProduct" />
  </div>
</template>

<script setup>
import { ref } from "vue";

const products = ref([])
const categories = ref([])
const selectedProduct = ref(null)

const handleProductSelected = (product) => {
  selectedProduct.value = product;
};

const handleCategorySelected = (category) => {
  // Filter products by category
};
</script>

 

همان‌طور که می‌بینید، سه کامپوننت مختلف داریم که هر کدام مسئولیت خاص خود را دارند: ProductList، ProductFilter و ProductDetail.

  • کامپوننت ProductList مسئول نمایش لیستی از محصولات و انتشار یک رویداد به نام product-selected است، زمانی که یک محصول انتخاب شود.
  • کامپوننت ProductFilter مسئول نمایش لیستی از دسته‌بندی‌ها و انتشار یک رویداد به نام category-selected است، زمانی که یک دسته‌بندی انتخاب شود.
  • کامپوننت ProductDetail مسئول نمایش اطلاعات دقیق درباره محصول انتخاب‌شده است.

با جدا کردن مسئولیت‌ها در کامپوننت‌های مجزا، کد خواناتر، قابل نگهداری‌تر و قابل آزمایش‌تر می‌شود. هر کامپوننت را می‌توان به‌صورت جداگانه تست کرد و تغییرات در یک کامپوننت تأثیری روی کامپوننت‌های دیگر نخواهد داشت.


۲. اصل باز/بسته (Open/Closed Principle - OCP)

این اصل نسبتاً ساده است. بهترین توضیحی که می‌توان ارائه داد، همان چیزی است که در کتاب Clean Code JavaScript آمده است:

موجودیت‌های نرم‌افزاری (کلاس‌ها، ماژول‌ها، توابع و ...) باید برای گسترش باز و برای تغییر بسته باشند.

این اصل به این معناست که باید به کاربران اجازه دهید قابلیت‌های جدیدی به نرم‌افزار اضافه کنند، بدون اینکه کد موجود را تغییر دهند.

فرض کنید یک کامپوننت دارید که لیستی از آیتم‌ها را نمایش می‌دهد و می‌خواهید قابلیت مرتب‌سازی آیتم‌ها را اضافه کنید. با در نظر گرفتن اصل باز/بسته، می‌توانید رفتار کامپوننت را بدون تغییر کد آن گسترش دهید. به‌عنوان مثال، یک Composable ایجاد کنید که قابلیت مرتب‌سازی را به کامپوننت اضافه کند.

این کامپوننت List.vue ماست:

<template>
  <ul>
    <li v-for="item in items" :key="item.id">{{ item.text }}</li>
  </ul>
</template>

<script setup>
defineProps({
  items: Array
})
</script>

با استفاده از composable (useSorting.js)

import { ref, onMounted } from 'vue'

export const useSorting = (items) => {
  const sortOrder = ref('ascending')
  const sortedItems = ref([])
  
  function toggleSortOrder () {
    sortOrder.value = sortOrder.value === 'ascending' ? 'descending' : 'ascending'
  }

  onMounted(() => {
    sortedItems.value = items.value.sort((a, b) => {
      if (sortOrder.value === 'ascending') {
        return a.text.localeCompare(b.text)
      } else {
        return b.text.localeCompare(a.text)
      }
    })
  })

  return { sortOrder, sortedItems, toggleSortOrder }
}

و کمپوننت App که از مؤلفه لیست استفاده می کند و با استفاده از useSorting composable قابلیت مرتب سازی را به آن اضافه می کند:

<template>
  <div>
    <button @click="toggleSortOrder">Toggle sort order</button>
    <List :items="sortedItems"/>
  </div>
</template>

<script setup>
import useSorting from './useSorting.js'

const items: [
  { id: 1, text: 'Item 1' },
  { id: 2, text: 'Item 2' },
  { id: 3, text: 'Item 3' }
]

const { sortOrder, sortedItems, toggleSortOrder } = useSorting(items)
</script>

این مثال اصل باز/بسته (OCP) را نشان می‌دهد، زیرا رفتار کامپوننت List از طریق اضافه کردن کد جدید گسترش یافته است، نه با تغییر کد اصلی آن.

به بیان ساده، ما این اصل را به‌صورت مستقیم پیاده‌سازی نمی‌کنیم. معماری مبتنی بر کامپوننت Vue و سیستم داده‌های واکنش‌پذیر آن به طور طبیعی راهی برای پیروی از اصل باز/بسته در کدمان فراهم می‌کند.

۳. اصل جایگزینی لیسکوف (Liskov Substitution Principle - LSP)

با وجود نام ترسناکش، این اصل به سادگی بیان می‌کند که اشیای یک کلاس والد باید قابل جایگزینی با اشیای کلاس‌های فرزند آن باشند، بدون اینکه درستی برنامه را تحت تأثیر قرار دهند.

بیایید ببینیم چطور می‌توان این اصل را در کامپوننت‌های Vue اعمال کرد. به عنوان مثال، ما یک کامپوننت داریم که یک فرم را با دکمه ارسال نمایش می‌دهد (Form.vue):

<template>
  <form @submit.prevent="submit">
    <slot />
    <button type="submit">Submit</button>
  </form>
</template>

<script setup>
function submit() {
  console.log('Form submitted!')
}
</script>

در اینجا یک زیر کلاس وجود دارد که مؤلفه پایه را گسترش می دهد و دو فیلد متنی اضافه می کند:

<template>
  <Form>
    <input type="text" v-model="username" />
    <input type="text" v-model="password" />
  </Form>
</template>

<script setup>
const username = ref('')
const password = ref('')
</script>

 

در این زیرکلاس، دو فیلد متنی برای وارد کردن نام کاربری و رمز عبور اضافه شده است. زیرکلاس می‌تواند به‌جای کامپوننت پایه مورد استفاده قرار گیرد، بدون اینکه عملکرد کلی برنامه را تحت تأثیر قرار دهد. زمانی که فرم ارسال می‌شود، متد submit از کامپوننت پایه همچنان فراخوانی می‌شود و پیام مربوطه در کنسول ثبت خواهد شد.

با پیروی از اصل جایگزینی لیسکوف (LSP) به این روش، توسعه رابط کاربری انعطاف‌پذیرتر و مقیاس‌پذیرتر می‌شود و نگهداری و گسترش برنامه در طول زمان آسان‌تر خواهد شد.

مشابه اصل باز/بسته، اصل جایگزینی لیسکوف نیز به‌طور طبیعی در معماری Vue گنجانده شده است، و ما بدون اینکه بدانیم این اصل دقیقاً چیست، از آن استفاده می‌کنیم.

۴. اصل تفکیک رابط (Interface Segregation Principle - ISP)

این اصل بیان می‌کند که کلاینت‌ها نباید مجبور باشند به متدهایی وابسته باشند که از آن‌ها استفاده نمی‌کنند. به عبارت دیگر، بهتر است رابط‌های کوچک‌تر و مشخص‌تری داشته باشیم که متناسب با نیازهای هر کلاینت طراحی شده باشند، به جای یک رابط بزرگ که شامل متدهایی باشد که برای همه کلاینت‌ها مرتبط نیست.

این کار باعث کاهش وابستگی میان بخش‌های مختلف سیستم می‌شود و نگهداری و تغییر کد را در طول زمان آسان‌تر می‌کند.

اصل ISP یکی از دلایل کلیدی استفاده از کامپوننت‌ها در Vue (یا هر فریم‌ورک مبتنی بر کامپوننت) است. این اصل به ما این امکان را می‌دهد که ساختارهای پیچیده را به کامپوننت‌های کوچک‌تر با رفتارهای ساده‌تر و مسئولیت‌های مشخص‌تر تقسیم کنیم.

به مثال زیر توجه کنید:

 

<template>
  <div>
    <Filter @change="setFilter" />
    <List :items="filteredItems" @select="selectItem" />
    <ItemDetail v-if="selectedItem" :item="selectedItem" @close="deselectItem" />
  </div>
</template>

<script setup>
import { reactive, toRefs } from 'vue'

const state = reactive({
  items: [...], // an array of items
  filter: '',
  selectedItem: null
})

const filteredItems = computed(() => {
  return state.items.filter(item => item.name.includes(state.filter))
})

function setFilter(filter) {
  state.filter = filter
}

function selectItem(item) {
  state.selectedItem = item
}

function deselectItem() {
  state.selectedItem = null
}

const { items, filter, selectedItem } = toRefs(state)
</script>

تصور کنید بخواهید فیلتر کردن، لیست کردن، و نمایش جزئیات یک آیتم را در یک کامپوننت واحد مدیریت کنید. در نهایت، یک کامپوننت بزرگ و پیچیده خواهید داشت که هر تغییری کوچک در بخشی از آن باعث تغییراتی در بخش‌های دیگر می‌شود و ردیابی این تغییرات بسیار دشوار خواهد بود.


۵. اصل وارونگی وابستگی (Dependency Inversion Principle - DIP)

این اصل بیان می‌کند که ماژول‌های سطح بالا نباید به ماژول‌های سطح پایین وابسته باشند؛ بلکه هر دو باید به انتزاعات وابسته باشند. انتزاعات نباید به جزئیات وابسته باشند، بلکه جزئیات باید به انتزاعات وابسته باشند.
این اصل به ما کمک می‌کند تا کدی با وابستگی‌های کمتر و نگهداری‌پذیرتر ایجاد کنیم.

برای درک بهتر این اصل، بیایید کمی به عقب برگردیم و فرآیند ایجاد برنامه‌های وب را مرور کنیم. یک برنامه وب معمولاً شامل چهار بخش است: پایگاه داده، بک‌اند، API و فرانت‌اند. این تقسیم‌بندی یک مثال از پیاده‌سازی DIP است.
به‌عنوان مثال، در زمینه DIP، API به‌عنوان یک قرارداد بین دو بخش از برنامه وب عمل می‌کند: یعنی فرانت‌اند و بک‌اند. کامپوننت‌های فرانت‌اند به جزئیات خاص داده‌های ذخیره‌شده در پایگاه داده وابسته نیستند. آن‌ها تنها از طریق دستورات و متدهای تعریف‌شده در API با بک‌اند ارتباط برقرار می‌کنند. سپس داده‌های ذخیره‌شده بر اساس این دستورها تغییر می‌کنند، که این خود نشان‌دهنده وابستگی جزئیات به انتزاعات است.

ارتباط DIP با Vue

معماری مبتنی بر کامپوننت Vue خود نوعی پیروی از اصل DIP است. ما کامپوننت‌های قابل استفاده مجدد ایجاد می‌کنیم تا نحوه نمایش داده‌ها به کاربران را تعریف کنیم.

یک مثال دیگر استفاده از props در Vue است. ما از props برای ارسال داده به کامپوننت‌ها استفاده می‌کنیم. props در واقع پیاده‌سازی اصل DIP هستند، زیرا به‌عنوان یک قرارداد بین دو کامپوننت عمل می‌کنند تا نحوه نمایش داده‌ها را تغییر دهند.

سرویس‌ها در Vue نیز مثال عالی دیگری از پیروی از DIP هستند. کامپوننت‌های Vue از سرویس‌ها برای دسترسی به داده‌ها استفاده می‌کنند، ممکن است این داده‌ها هنگام دریافت تغییر کنند و در نهایت به کاربران نمایش داده شوند.

 

منبع مدیوم معین میرکیانی

۳۶
۱۴۰۳/۱۱/۸