ماژولاریتی (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.ts | Public API | ✅ |
Component.tsx | UI و JSX | برای کامپوننتها |
types.ts | Typeها و Interfaceها | ❌ |
constants.ts | مقادیر ثابت | ❌ |
utilities.ts | helperها | ❌ |
enums.ts | enumها | ❌ |
نمونه ماژولهای عمومی
/types/html/
├── index.ts
└── types.ts
/hooks/use-media-query/
├── index.ts
├── hook.ts
├── types.ts
/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-برای هوک) نوع ساب ماژول را مشخص کند
در نتیجه ساختاری ایجاد میشود که:
- ساده است
- قابل فهم است
- و بدون نیاز به بازطراحی اساسی، میتواند در طول زمان رشد کند