Skip to main content

ماژولاریتی (Modularity)

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

در این پروژه، هر بخش از کد (Component، Hook، Utility، Type و ...) باید در قالب یک ماژول (Module) تعریف شود.


ماژول چیست؟

یک ماژول در این معماری:

یک پوشه‌ی self-contained است که تمام منطق، انواع، ابزارها و وابستگی‌های مرتبط با یک مسئولیت مشخص را در خود نگه می‌دارد و از طریق یک Public API با بیرون ارتباط برقرار می‌کند.


اصل Colocation در ماژول‌ها

در این پروژه از الگوی Module Colocation استفاده می‌شود.

منظور از Colocation این است که:

تمام فایل‌هایی که به یک feature، component یا responsibility مشخص مربوط هستند، باید تا حد ممکن کنار هم و داخل همان ماژول قرار بگیرند.

به‌جای پراکنده کردن فایل‌ها بر اساس نوع آن‌ها (مثل جدا کردن همه hooks، همه types یا همه utilities در سطح پروژه)، هر ماژول باید وابستگی‌ها و منطق مرتبط با خودش را درون همان ساختار نگه دارد.


چرا از Colocation استفاده می‌کنیم؟

این ساختار باعث می‌شود:

  • ارتباط فایل‌ها واضح‌تر باشد
  • پیدا کردن کد ساده‌تر شود
  • وابستگی‌ها بهتر مدیریت شوند
  • حذف یا تغییر featureها راحت‌تر انجام شود
  • refactor کردن ساده‌تر شود
  • scalability پروژه در بلندمدت حفظ شود

همچنین این رویکرد باعث می‌شود هر ماژول تا حد ممکن مستقل باقی بماند و coupling بین بخش‌های مختلف پروژه کاهش پیدا کند.


Public API (فایل index)

هر ماژول باید index.ts یا index.tsx داشته باشد

این فایل تنها نقطه‌ی دسترسی مجاز به داخل ماژول است.

هر چیزی که قرار است خارج از ماژول استفاده شود، باید فقط از طریق این فایل export شود.

// @components/button/index.ts
export { Button } from './Component'
export type { ButtonProps } from './types'

ممنوعیت دسترسی مستقیم به فایل‌های داخلی

هیچ ماژولی نباید به فایل‌های داخلی ماژول دیگر دسترسی مستقیم داشته باشد.

نمونه نامناسب
import { ButtonProps } from '@components/button/types'
import { Button } from '@components/button/Component'
نمونه مناسب
import { Button, ButtonProps } from '@components/button'

ممنوعیت استفاده از Wildcard Export

برای حفظ Tree-shaking و جلوگیری از ورود کدهای غیرضروری به باندل:

استفاده از * export کاملاً ممنوع است.

نمونه نامناسب
// @components/button/index.ts
export * from './Component'
export * from './types'
نمونه مناسب
// @components/button/index.ts
export { Button } from './Component'
export type { ButtonProps } from './types'

ساختار داخلی ماژول

ما از فایل‌های بزرگ و چندمنظوره پرهیز می‌کنیم.

هر مسئولیت باید در فایل جداگانه قرار بگیرد.

نمونه نامناسب
// Button.tsx
// شامل JSX + types + constants + utils 😬
نمونه مناسب
Button/
├── Component.tsx
├── index.ts
├── types.ts
├── constants.ts
├── utilities.ts
├── enums.ts
فایلتوضیحاجباری
index.tsPublic API
Component.tsxUI و JSXبرای کامپوننت‌ها
types.tsTypeها و Interfaceها
constants.tsمقادیر ثابت
utilities.tshelperها
enums.tsenumها

نمونه ماژول‌های عمومی

Types
/types/html/
├── index.ts
└── types.ts
Hooks
/hooks/use-media-query/
├── index.ts
├── hook.ts
├── types.ts
Utilities
/utilities/is-server/
├── index.ts
└── utilities.ts

ماژول‌های تو در تو (Recursive Modularity)

گاهی یک ماژول با بزرگ‌تر شدن نیاز پیدا می‌کند که از ساب ماژول‌ها (Submodules) استفاده کند؛ برای مثال یک کامپوننت ممکن است به چند کامپوننت داخلی یا یک هوک اختصاصی نیاز داشته باشد.

در این معماری، ساب ماژول‌ها مستقیماً به عنوان پوشه‌های مستقل داخل همان ماژول قرار می‌گیرند و نیازی به ایجاد پوشه‌های واسط مانند components/ یا hooks/ وجود ندارد.

دلیل این تصمیم این است که نام‌گذاری ماژول‌ها خودشان نوع آن‌ها را مشخص می‌کند:

  • کامپوننت‌ها با PascalCase نام‌گذاری می‌شوند
  • هوک‌ها با پیشوند use- شروع می‌شوند

بنابراین ساختار پوشه به‌صورت طبیعی و بدون نیاز به دسته‌بندی اضافی قابل درک است.

به طور مثال، فرض کنید یک صفحه‌ی Landing داریم که با رشد پروژه نیاز به ساب ماژول‌هایی پیدا کرده است.

Landing/
├── Hero/
│ ├── Component.tsx
│ ├── index.ts
│ └── types.ts
├── use-landing-logic/
│ ├── hook.ts
│ ├── index.ts
│ └── types.ts
├── Component.tsx
├── index.ts

در این مثال:

  • Hero یک کامپوننت داخلی برای LandingPage است
  • use-landing-logic یک هوک اختصاصی برای همین ماژول است

هرکدام از این‌ها نیز خودشان یک ماژول کامل محسوب می‌شوند و باید تمام قوانین ماژولار بودن را رعایت کنند:

  • داشتن index.ts به عنوان Public API
  • عدم استفاده از wildcard export
  • تفکیک فایل‌ها (types، utilities و …)
نکته مهم

این الگو به‌صورت بازگشتی (Recursive) قابل ادامه است. اگر یکی از ساب ماژول‌ها نیز پیچیده‌تر شود، می‌تواند ساب ماژول‌های خودش را داشته باشد.


انعطاف‌پذیری در آینده (Future Flexibility)

هدف از طراحی این ساختار فقط نظم‌دهی فعلی نیست؛ بلکه آینده‌پذیر بودن (Future‑proofing) سیستم است.

در ابتدا ممکن است یک ماژول بسیار ساده باشد:

Profile/
├── Component.tsx
└── index.ts

اما با رشد پروژه و افزایش پیچیدگی، می‌توان بدون شکستن ساختار فعلی آن را گسترش داد:

Profile/
├── Avatar/
├── UserStats/
├── use-profile-data/
├── use-profile-actions/
├── Component.tsx
├── constants.ts
├── types.ts
├── index.ts
نکته

در صورت نیاز حتی می‌توان دسته‌بندی‌های جدیدی اضافه کرد بدون این‌که نیاز به refactor گسترده یا جابه‌جایی فایل‌ها باشد.


چک‌لیست

  • ماژول index.ts یا index.tsx دارد
  • هیچ * export وجود ندارد
  • هیچ import مستقیم از فایل داخلی انجام نشده
  • فایل‌ها تفکیک مسئولیت دارند
  • ساختار پوشه‌ای رعایت شده
  • ساب ماژول‌ها به درستی تعریف شده‌اند

جمع‌بندی

این ساختار باعث می‌شود:

  • هر پوشه یک ماژول مستقل باشد و مانند یک پکیج مستقل رفتار کند
  • هر ماژول یک Public API مشخص (index.ts یا index.tsx) داشته باشد
  • کدها قابل پیش‌بینی و توسعه تیمی ساده‌تر باشند
  • refactor کم‌هزینه‌تر شود
  • tree-shaking به‌درستی کار کند
  • ساب ماژول‌ها مستقیماً داخل ماژول والد قرار بگیرند
  • نام‌گذاری (PascalCase برای کامپوننت و use- برای هوک) نوع ساب ماژول را مشخص کند

در نتیجه ساختاری ایجاد می‌شود که:

  • ساده است
  • قابل فهم است
  • و بدون نیاز به بازطراحی اساسی، می‌تواند در طول زمان رشد کند