تقسیم کد TypeScript به ماژول‌های جداگانه یکی از مهارت‌های کلیدی مهندسان ارشد است که مستقیماً بر نگهداشت‌پذیری، تست‌پذیری، مقیاس‌پذیری و بهره‌وری تیم اثر می‌گذارد. تقسیم‌بندی کد، فراتر از جداسازی فایل‌ها است؛ یک تصمیم معماری است که مرزهای مسئولیت، جهت وابستگی و نقاط اتصال را تعریف می‌کند. در پروژه‌های TypeScript، تقسیم کد می‌تواند به روش‌های مختلف انجام شود: بر اساس Feature، بر اساس Layer، بر اساس Domain یا ترکیبی از این‌ها. هر رویکرد، مزایا و چالش‌های خود را دارد و انتخاب درست، بستگی به زمینه‌ی پروژه دارد. در این راهنما، استراتژی‌های تقسیم کد TypeScript، معیارهای تصمیم‌گیری، الگوهای پیاده‌سازی و اشتباهات رایج بررسی می‌شود.

در یکی از پروژه‌های سازمانی، فایلی با ۳۰۰۰ خط کد شامل تمام منطق فروشگاه بود. پس از تقسیم به Moduleهای Feature-based، زمان Build ۴۰٪ کاهش یافت و تست‌نویسی ۳ برابر سریع‌تر شد. این تجربه نشان می‌دهد که تقسیم کد، یک سرمایه‌گذاری مهندسی است، نه یک بازآرایی ظاهری.

چرا کد را تقسیم کنیم

تقسیم کد به Moduleهای کوچک، مزایای متعددی دارد:

  • نگهداشت‌پذیری: تغییر در یک Module، بر سایر Moduleها اثر کمتری می‌گذارد.
  • تست‌پذیری: Moduleهای کوچک، Unit Test ساده‌تری دارند.
  • قابلیت استفاده‌ی مجدد: Moduleهای عمومی در پروژه‌های دیگر قابل استفاده‌اند.
  • موازی‌سازی: تیم‌های مختلف می‌توانند روی Moduleهای جداگانه کار کنند.
  • Tree Shaking: Bundler می‌تواند کدهای استفاده‌نشده را حذف کند.
  • Code Splitting: امکان Lazy Loading برای بهبود عملکرد.
  • کاهش Coupling: وابستگی‌های غیرضروری حذف می‌شوند.
  • کاهش Cognitive Load: توسعه‌دهنده جدید سریع‌تر با پروژه آشنا می‌شود.

اصول Cohesion و Coupling

دو اصل بنیادین در تقسیم کد:

Cohesion (چسبندگی)

Cohesion به میزان ارتباط منطقی اجزای یک Module اشاره دارد. Cohesion بالا یعنی اجزای یک Module به یک مسئولیت واحد خدمت می‌کنند.

  • Functional Cohesion: بالاترین سطح؛ تمام اجزا به یک وظیفه‌ی واحد خدمت می‌کنند.
  • Sequential Cohesion: خروجی یک جزء، ورودی جزء بعدی است.
  • Communicational Cohesion: اجزا روی یک داده‌ی مشترک کار می‌کنند.
  • Logical Cohesion: اجزا منطقاً مشابه‌اند اما مستقل عمل می‌کنند.
  • Coincidental Cohesion: پایین‌ترین سطح؛ اجزا ارتباط منطقی ندارند.

Coupling (وابستگی)

Coupling به میزان وابستگی بین Moduleها اشاره دارد. Coupling پایین یعنی Moduleها مستقل‌ترند.

  • Data Coupling: Moduleها فقط داده رد و بدل می‌کنند.
  • Stamp Coupling: Moduleها یک ساختار داده‌ی کامل رد و بدل می‌کنند.
  • Control Coupling: یک Module جریان Module دیگر را کنترل می‌کند.
  • External Coupling: Moduleها به یک منبع خارجی مشترک وابسته‌اند.
  • Common Coupling: Moduleها از یک Global State مشترک استفاده می‌کنند.
  • Content Coupling: بالاترین سطح؛ یک Module به داخل Module دیگر دسترسی دارد.

هدف: Cohesion بالا، Coupling پایین.

استراتژی‌های تقسیم کد

استراتژی مزایا معایب مناسب برای
Feature-based Cohesion بالا، تمرکز بر دامنه احتمال تکرار کد اپلیکیشن‌های بزرگ
Layered جداسازی مسئولیت‌ها Boilerplate بیشتر پروژه‌های سازمانی
Domain-Driven هماهنگی با کسب‌وکار پیچیدگی اولیه سیستم‌های پیچیده
Type-based سادگی Cohesion پایین پروژه‌های کوچک
Hybrid انعطاف‌پذیری نیاز به Disciplined پروژه‌های متوسط تا بزرگ

Feature-based Structure

در این استراتژی، کد بر اساس Featureهای کسب‌وکار تقسیم می‌شود:

src/
├── features/
│   ├── auth/
│   │   ├── components/
│   │   │   ├── LoginForm.tsx
│   │   │   └── RegisterForm.tsx
│   │   ├── hooks/
│   │   │   └── useAuth.ts
│   │   ├── services/
│   │   │   └── authService.ts
│   │   ├── types/
│   │   │   └── index.ts
│   │   ├── utils/
│   │   │   └── validators.ts
│   │   └── index.ts
│   ├── products/
│   │   ├── components/
│   │   ├── hooks/
│   │   ├── services/
│   │   ├── types/
│   │   └── index.ts
│   └── cart/
├── shared/
│   ├── ui/
│   │   ├── Button/
│   │   ├── Input/
│   │   └── Modal/
│   ├── utils/
│   │   ├── date.ts
│   │   └── format.ts
│   └── types/
└── app/
    ├── routes/
    └── providers/

اصول Feature-based:

  • هر Feature یک Public API مشخص دارد (index.ts).
  • کد داخلی Feature از خارج قابل دسترسی نیست.
  • Featureها از طریق Public API با هم تعامل می‌کنند.
  • کد مشترک در shared/ قرار می‌گیرد.

Layered Architecture

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

src/
├── presentation/
│   ├── components/
│   ├── pages/
│   └── hooks/
├── application/
│   ├── use-cases/
│   └── services/
├── domain/
│   ├── entities/
│   ├── value-objects/
│   └── repositories/
└── infrastructure/
    ├── api/
    ├── persistence/
    └── config/

قواعد لایه‌بندی:

  • جهت وابستگی از بالا به پایین است.
  • لایه‌های پایین‌تر نباید به لایه‌های بالاتر وابسته باشند.
  • Domain Layer مستقل از Framework است.
  • Infrastructure Layer پیاده‌سازی Repository را فراهم می‌کند.

Domain-Driven Design

در DDD، تقسیم بر اساس Bounded Context انجام می‌شود:

src/
├── contexts/
│   ├── identity/
│   │   ├── domain/
│   │   ├── application/
│   │   ├── infrastructure/
│   │   └── presentation/
│   ├── catalog/
│   │   ├── domain/
│   │   ├── application/
│   │   ├── infrastructure/
│   │   └── presentation/
│   └── ordering/
└── shared/
    └── kernel/

هر Bounded Context یک Unit مستقل است که می‌تواند توسط تیم جداگانه‌ای توسعه یابد.

Code Splitting و Lazy Loading

Code Splitting در سطح Module، عملکرد را بهبود می‌دهد:

// Static Import
import { HeavyComponent } from "./HeavyComponent";

// Dynamic Import (Lazy Loading)
const HeavyComponent = lazy(() => import("./HeavyComponent"));

// در React Router
const ProductPage = lazy(() => import("./pages/ProductPage"));

<Suspense fallback={<Spinner />}>
  <Route path="/products" element={<ProductPage />} />
</Suspense>

مزایای Code Splitting:

  • کاهش Initial Bundle Size.
  • Improve First Contentful Paint (FCP).
  • کاهش زمان بارگذاری اولیه.
  • امکان Lazy Loading برای Routeها.

برای مطالعه‌ی بیشتر، پست بهینه سازی HTML و بهینه سازی CSS مفید هستند.

Naming Convention و Barrel Files

Naming Convention در تقسیم کد:

  • PascalCase برای Componentها: LoginForm.tsx.
  • camelCase برای Utilityها: formatDate.ts.
  • kebab-case برای دایرکتوری‌ها: user-profile/.
  • index.ts برای Public API.
  • types.ts برای Typeهای محلی.

Barrel Files (index.ts) در سطح Feature:

// features/auth/index.ts
export { LoginForm } from "./components/LoginForm";
export { RegisterForm } from "./components/RegisterForm";
export { useAuth } from "./hooks/useAuth";
export type { User, Credentials } from "./types";

توصیه: Barrel File در سطح Feature استفاده شود، نه در سطح Root پروژه.

Refactoring تدریجی

Refactoring یک پروژه‌ی بزرگ به Moduleهای کوچک باید تدریجی انجام شود:

  1. شناسایی Featureها: نقشه‌برداری از قابلیت‌های کسب‌وکار.
  2. تعریف مرزها: مشخص کردن Public API هر Feature.
  3. استخراج کد مشترک: انتقال به shared/.
  4. انتقال تدریجی: هر Feature در یک PR.
  5. به‌روزرسانی Importها: استفاده از Path Mapping.
  6. حذف کد قدیمی: پس از تأیید تست‌ها.
  7. پایش Dependency Graph: پس از هر مرحله.

ابزارهای تحلیل Dependency

ابزار کاربرد خروجی
madge تحلیل Dependency و Circular Graph، Text
dependency-cruiser تحلیل و Enforce قواعد Graph، Rules
dpdm تحلیل Circular Text
webpack-bundle-analyzer تحلیل Bundle Treemap
rollup-plugin-visualizer تحلیل Bundle برای Rollup Treemap
ts-unused-exports شناسایی Exportهای استفاده‌نشده Text
# Enforce rules با dependency-cruiser
# .dependency-cruiser.js
module.exports = {
  forbidden: [
    {
      name: "no-circular",
      severity: "error",
      from: {},
      to: { circular: true },
    },
    {
      name: "no-orphans",
      severity: "warn",
      from: {
        orphan: true,
        pathNot: ["\.d\.ts$", "index\.ts$"],
      },
      to: {},
    },
    {
      name: "shared-not-to-features",
      severity: "error",
      from: { path: "^src/shared" },
      to: { path: "^src/features" },
    },
  ],
};

پرسش‌های پرتکرار

چه زمانی باید کد را تقسیم کنیم؟

وقتی یک فایل از ۳۰۰ خط فراتر می‌رود، یا وقتی چند مسئولیت در یک Module قرار می‌گیرد.

Feature-based بهتر است یا Layered؟

Feature-based برای اپلیکیشن‌های بزرگ، Layered برای سیستم‌های سازمانی. Hybrid نیز رایج است.

چگونه از Circular Dependency جلوگیری کنیم؟

با لایه‌بندی دقیق، استفاده از import type، و Enforce قواعد با dependency-cruiser.

آیا Barrel Files توصیه می‌شوند؟

در سطح Feature بله، در سطح Root پروژه خیر.

چگونه کد مشترک را مدیریت کنیم؟

در پوشه‌ی shared/ با مرزهای مشخص. کد مشترک نباید به Featureها وابسته باشد.

آیا تقسیم کد بر عملکرد اثر دارد؟

بله، با Code Splitting و Tree Shaking می‌توان Bundle را کوچک‌تر کرد.

چگونه Naming Convention را Enforce کنیم؟

با ESLint Rules مانند eslint-plugin-filenames و @typescript-eslint/naming-convention.

اشتباهات رایج

اشتباه علت راه‌حل
تقسیم بر اساس Type (همه‌ی utils در یک پوشه) سادگی ظاهری Feature-based Structure
Circular Dependency عدم لایه‌بندی Enforce با dependency-cruiser
Barrel File سراسری سادگی Import Barrel در سطح Feature
کد مشترک وابسته به Feature عدم تعریف مرز shared/ مستقل
عدم استفاده از Path Mapping Importهای طولانی تنظیم paths در tsconfig
Refactoring یک‌باره شتاب در تغییر Refactoring تدریجی
عدم تحلیل Dependency عدم پایش سلامت معماری تحلیل در CI/CD

ملاحظات پیشرفته

در سطح معماری، تقسیم کد TypeScript نیازمند یک استراتژی جامع است:

۱. Architectural Fitness Functions: تعریف قواعد خودکار برای Enforce معماری.

۲. Dependency Inversion Principle: وابستگی به Abstraction به‌جای Concrete.

۳. Hexagonal Architecture: جداسازی Domain از Infrastructure.

۴. Module Federation: برای Micro-frontend.

۵. Monorepo با Workspaces: تقسیم کد بین Packageها.

۶. Code Ownership: تعریف مالکیت هر Module با CODEOWNERS.

۷. Progressive Migration: مهاجرت تدریجی از Structur قدیمی به جدید.

۸. Automated Analysis: تحلیل خودکار در CI/CD.

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

نتیجه

تقسیم کد TypeScript به Moduleهای جداگانه یک تصمیم معماری است که با اصول Cohesion و Coupling شکل می‌گیرد. انتخاب استراتژی مناسب (Feature-based، Layered، DDD یا Hybrid)، تعریف مرزهای مشخص، Enforce قواعد با ابزارها و Refactoring تدریجی، ستون‌های یک معماری سالم هستند. Code Splitting و Tree Shaking بهره‌وری Bundle را افزایش می‌دهند.

💡 اگر تجربه‌ای در تقسیم کد TypeScript در پروژه‌های خود داشته‌اید، برای ما جالب است بدانیم کدام استراتژی بیشترین اثر را داشت: Feature-based، Layered یا DDD. تجربه‌ی خودتان را در دیدگاه‌ها بنویسید.