چگونه فایل‌های جاوااسکریپتی را ساختاربندی کنیم؟
ﺯﻣﺎﻥ ﻣﻄﺎﻟﻌﻪ: 7 دقیقه

چگونه فایل‌های جاوااسکریپتی را ساختاربندی کنیم؟

افرادی وجود دارند که از من می‌پرسند که چگونه فایل‌های جاوااسکریپتی‌ام را می‌نویسم - خب این موضوع یک دروغ بود، کسی از من این سوال را نپرسیده است اما اگر زمانی کسی در این رابطه بپرسد، آن‌ها را به خواندن این مطلب دعوت می‌کنم. من شیوه کدنویسی‌ام را بعد از سال‌ها تعیین کردم. کتاب‌های مختلفی در رابطه با کدنویسی تمیز -Clean Code- خوانده‌ام. از جوامع مختلفی در اینترنت کمک گرفتم. سال‌ها PHP نوشتم و استایل‌های مختلف کدنویسی را امتحان نمودم.

ساختار به ماژول‌های جاوااسکریپت وابسته نیست اما واقعیت را بگویم من تنها دوست دارم ماژول‌هایی را بنویسیم که از آن‌ها استفاده می‌کنم. ساختار کلی به صورت زیر است:

 //imports
    import fs from 'fs';
    import utils from 'utils';

    import db from '../../../db';

    import { validatePath } from './readerHelpers';

    // constants
    const readDir = utils.promisify(fs.readDir);
    const knex = db.knex;

    // main exports
    export async function fileReader(p) {
      validatePath(p);

      return await readFile(p);
    }

    // core logic
    function readFile(p) {
     // logic
    }

Importها

در اولین قسمت از فایل‌ها import قرار دارد. آن‌ها بالاتر از هر چیز دیگری قرار گرفته و مطمئنا دلیل آن را می‌دانیم. ترتیب importها تا زمانی که از hookها (مانند babel hook) استفاده نکنیم مهم نیست، بنابراین من ترجیح می‌دهم که عملیات import را به شکل زیر انجام دهم:

  • ماژول‌های محلی — Node
  • ماژول‌های کتابخانه — lodash, knex
  • کتابخانه‌های محلی —  ../db
  • فایل‌های محلی — ./helpers or similar

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

در رابطه با ساختاربندی فایل‌ها من به موضوع «ترتیب الفبا» هیچ دقتی نمی‌کنم و حقیقتا مسئله‌ای مهم برای‌م نیست. 

ماژول‌های محلی

همواره دوست دارم که ماژول‌های محلی را در بالای تمام ماژول‌ها نگه دارم و آن‌ها را با یک ساختار مرتب در کنار یکدیگر قرار دهم:

    import path from 'path';
    import fs from 'fs';

    import util from 'util';

زمانی که در مرورگر کدنویسی می‌کنم این قسمت‌ها را به کلی فراموش می‌کنم.

ماژول‌های کتابخانه

همواره سعی می‌کنم که تنها چیزهای مورد نیاز از یک کتابخانه را import کنم. اما در هر حال آن‌ها را نیز به صورت مرتب دسته‌بندی می‌کنم:

import knex from 'knex';
import { clone } from 'lodash';

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

کتابخانه‌های داخلی/محلی

منظور از کتابخانه‌های محلی یا داخلی ماژول‌هایی است که به صورت محلی به اشتراک گذاشته شده‌اند مانند db.js که به یک اپلیکیشن متصل شده است. یا در کارهای من کتابخانه‌هایی وجود دارد که برای کار کردن با آن‌ها نیاز است که در تمام قسمت‌های محصول قابل دسترس باشند. 

import db from '../../../db';
import calculators from '../../../lib/calculators';

فایل‌های محلی

در پایان، من فایل‌های محلی که در پوشه مربوط به فایل اصلی قرار دارد را import می‌کنم. البته می‌شود به فایل‌های دیگر در پوشه‌های دیگر از پروژه نیز ارجاع داده شود:

import { assignValue, calculateTotal } from './calculationReducerHelpers';

ثابت‌ها

بعد از اینکه تمام ماژول‌ها و مستقلات را import کردم، محتوایی را آماده می‌کنم که در ادامه ماژول‌نویسی به آن‌ها نیاز دارم. در اینجا من از ثابت‌ها استفاده می‌کنم:

const knex = db.knex;

const pathToDir = '../../data-folder/'; 

Exportها

بعد از اینکه در نهایت تمام سطوح مستقلات مربوط به ماژول‌ها را پیاده‌سازی کردم: خواه که مقادیر ثابتی بوده‌اند یا کتابخانه‌های import شده، سعی می‌کنم که آن‌ها را به صورت export در بالای فایل قرار دهم. 

   import { COUNT_SOMETHING } from './calculationActions';
    import helpers from './calculationHelpers';

    export function calculationReducer(state, action) {
      switch (action.type) {
        case COUNT_SOMETHING:
          return calculateSomething(state, action);
      }
    }

البته این تنها قسمتی نیست که من توابع را export می‌کنم. 

احساس می‌کنم سیستمی که ماژول‌ها با استفاده از آن کار می‌کنند تشخیص APIها و توابع خروجی را در تست‌ها را سخت می‌کند. برای مثال در کدهای بالا من هیچ گاه قصد استفاده از calculateSomething در بیرون از ماژول را ندارم. از اینکه برنامه‌نویسی شیءگرا به چه صورت تست‌های توابع private را مدیریت می‌کنم مطمئن نیستم. این موضوع ممکن است به یک مشکل تبدیل شد.

منطق اصلی

ممکن است این موضوع برای‌تان عجیب باشد اما منطق اصلی برنامه برای من در آخر آن تعیین می‌شود. این مورد برای من به چند دلیل بسیار به خوبی کار می‌کند.

وقتی یک فایل را باز می‌کنم تابع سطح بالا به من اتفاقاتی که در مرحله انتزاع می‌افتد را می‌گوید. دوست دارم این موضوع که برنامه در یک چشم انداز کلی چه کاری را انجام می‌دهد را بدانم. من بروزرسانی‌ها و اضافه کردن‌های بسیاری را با استفاده از CSV در DB انجام می‌دهم و در تابع سطح بالا پروسه انجام این کار همواره آسان‌تر است. ما به صورت زیر عمل می‌کنیم:

fetchCSV → aggregateData → insertData → terminate script

منطق اصلی همواره چیزی که در exportها اتفاق می‌افتد را نشان می‌دهد. بنابراین در این حالت ما چیزی شبیه به زیر را خواهیم داشت:

    export async function importCSV(csvPath) {
      const csv = await readCSV(csvPath);
      const data = aggregateData(csv);

      return await insertData(data);
    }

    function aggregateData(csv) {
      return csv
        .map(row => {
         return {
           ...row,
           uuid: uuid(),
           created_at: new Date(),
           updated_at: new Date(),
         };
        })
      ;
    }

    function insertData(data) {
      return knex
        .batchInsert('data_table', data)
      ;
    }

نکته اینجاست که readCSV در این مثال قرار ندارد. بجای آن از طریق یک فایل کمکی آن را import کرده‌ایم. من دوست ندارم که aggregateData از بیرون ماژول قابل دسترس باشد با این حال هنوز قصد تست کردن آن را دارم. به همین دلیل از حالت export در ابتدای تابع استفاده نموده‌ام.

خارج از آن من یک تابع را نیز در بالا و پایین آن تعریف کرده‌ام. اولویت قرار گیری کدهای بالا به صورت زیر خواهد بود:

  • توابع اصلی - توابعی که با استفاده از export در سطح بالا استفاده می‌شود. 
  • توابع ساده‌تر و کوچکتر - توابعی که توسط توابع اصلی استفاده می‌شوند. 
  • توابع کاربردی - توابع کوچکی که در جاهای مختلف از یک ماژول استفاده می‌شود. (این توابع exportشده نیستند.)

توابع منطقی اصلی (Core-logic)

توابع منطقی اصلی مانند توابع export شده هستند. براساس پیچیدگی ماژول‌های‌تان این توابع ممکن است وجود داشته و یا نداشته باشند. قطعه کردن توابع در یک ماژول ضروری نیست اما وقتی ماژول رشد کند و بزرگ‌تر شود، توابع Core-logic مانند قطعاتی از تابع اصلی خواهند بود.

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

اگر در انگولار کدنویسی بکنید، دسته‌بندی کردن چنین توابعی در یک کلاس بسیار بهتر از استفاده از آن‌ها در کل فایل خواهد بود. 

    export FormComponent extends Component {
      function constructor() { }
      onHandleInput($event) {
        //  logic
      }
    }

توابع کوچکتر/ساده‌تر

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

به عنوان مثال در زیر یک تابع کوچک‌تر ایجاد شده که می‌توانید آن را در فرم یک اپلیکیشن نسبتا پیچیده مشاهده کنید:

   export FormComponent extends Component {
      onHandleInput($event) {
        try {
          validateFormInput($event);
        } catch (e) {

        }
      }

      validateFormInput($event) {
        if (this.mode === 'strict-form') {
          throw new Error();
        }
      }
    }

توابع کاربردی

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

یک تابع کاربردی همواره باید یک متد واضح باشد، به این معنا که نباید به متغیرهای خارج از scope دسترسی داشته باشد و نباید متکی به داده‌هایی باشد که در آن قرار گرفته است. البته این خارج از حالتی است که بخواهید به یک API یا DB دسترسی داشته باشید.

 function splitDataByType(data) {
      return data
        .reduce((typeCollection, item) => {
          if (!typeCollection[item.type]) {
            typeCollection[item.type] = [];
          }

          typeCollection[item.type].push(item);

          return typeCollection;
        }, {});
    }

    function insertData(data, knex) {
      return knex
        .batchInsert('data', data);
    }

چیزهای دیگری نیز وجود دارد؟

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

منبع

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

خیلی بد
بد
متوسط
خوب
عالی
3 از 1 رای

/@arastoo
ارسطو عباسی
کارشناس تولید و بهینه‌سازی محتوا

کارشناس ارشد تولید و بهینه‌سازی محتوا و تکنیکال رایتینگ - https://arastoo.net

دیدگاه و پرسش

برای ارسال دیدگاه لازم است وارد شده یا ثبت‌نام کنید ورود یا ثبت‌نام

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید