Галоўная / Артыкулы / Шысь прыемаў дизайна JavaScript для адзбавлення ад «спагеты-кода»

Шысь прыемаў дизайна JavaScript для адзбавлення ад «спагеты-кода»

Паспрацаванне практычных шэсціх шаблонаў JavaScript — Strategy, Factory, Observer, Adapter, Composition і Pipeline — якія заменяюць заплутаны код на структуру, якая лёгка адтрымвалася.

2146 слоў

Ператвараць хаотычныя скрыпты ў прагнозаваныя, адрабатывальныя системы

Кожны, хто давэў час на стварэнне програмнага забезпечэння, ведае той некамфортны момент, калі пасля выпуску проекта чыраў яго зноў чераз калькі месцацоў і вже не можа зрозумець, як усё адносіцца адна да другой.

Падае ў хаос рэдка калі це рабится намерна. Спачатку дадаёцца швыдкі элемент керування для інтэрфейса, потым — запрашэнне дадзеных, пасля чаго — адпаведнае рашэнне для экстремальных ситуацыяў, індыкаторы завантажэння, а ўсё гэта дапамагае аналізу. Чыраў месцацоў пазней ваша ранейшая апрантная скрыпта ператварыцца ў каллаж з ад 800 і болей ліній, у якому є вялікая колькасць внутрошняях функцый, неконтрольваныя глобальныя зменнікі і нестабільныя ланцюгі if/else.

Гэтае і є сутніснае значэнне «спагеты-кода»: бізнес-правілы і логіка адображэння так супакоўваюцца, што змяна аднага разделу тыхама пашкоджае два іншых у іншым месцы.

Стварэнне JavaScript-кода, який можа расшырюватыся, не значыць прыкладчання важкіх абстракцый у стыле корпоратывных рашынкаў да кожнай функцыі. Галоўная мета — раздзелэнне задач і апытака да калькі надзеяных шаблонаў.

Нижчэ прыведзены шасць апробаваных шаблонаў дызайну і архітэктуры, якія практычна расплутваюць заплятаны JavaScript-код і дапамагаюць падтрымваць ваш кодбазу ў кантролі па меры яго росту.

1. Шаблон стратэгіі: Адмова ад вярстакавых умоў

Проблема

Калі логіка трэба роздзеляць залежна ад катагорыі пользователя, спосабу платэжа чы ад прыемлівага режыму, багато разработчыкаў інстынктыўна прыбегаюць да вярстакавых оператараў if/else чы до вялікіх блакоў switch.

// The Spaghetti Way
function calculateShipping(order) {
  if (order.type === 'standard') {
    return order.weight * 1.5;
  } else if (order.type === 'express') {
    return order.weight * 3.0 + 10;
  } else if (order.type === 'overnight') {
    return order.weight * 5.0 + 25;
  } else if (order.type === 'international') {
    return order.weight * 8.0 + 50;
  } else {
    throw new Error('Unknown shipping method');
  }
}

Кожны раз, калі ваша команда адагуе новы рэгламент доставкі, вас прыводзіць да неабяжнага змены штоўной цэнтральнай функцыі. Одна-едзина памылка чы саблужніця ў яе прыводзіць да таго, што расчыткі доставкі для всіх типаў замовленняў перастаюць работаць.

Рашэнне

Шаблон стратэгіі выделяе кожны алгорытм у самастоятельную функцыю і зберагае іх у спяльным об’екте для пошуку.

// The Scalable Way
const shippingStrategies = {
  standard: (order) => order.weight * 1.5,
  express: (order) => order.weight * 3.0 + 10,
  overnight: (order) => order.weight * 5.0 + 25,
  international: (order) => order.weight * 8.0 + 50,
};

function calculateShipping(order) {
  const strategy = shippingStrategies[order.type];

  if (!strategy) {
    throw new Error(`Unsupported shipping method: ${order.type}`);
  }

  return strategy(order);
}

Чаму гэта працюе у масштабе

  • Прынцып адкрытасі/закрытасі: дадаць дзесяток новых варыянтаў доставкі значыць толькі дадаць новыя елементы да shippingStrategies, без неабяжных зменын у самай функцыі calculateShipping.
  • Лёгкасц тэставання: кожную функцыю стратэгіі можна выеўнуць, прааналізаваць і абсалютна незалежна прыменіць на тэстах.

2. Шаблоны модуля і фабрыкі: контроль стану

Проблема

Глобальныя зменныкі з шырокім дыячым прасторам і спакульнаваныя об’екты ствараюць складныя багі. Калі калькі компонентаў інтэрфейсу можа свабодна чытаць і меняць той самы стан, з’ясаванне, калькі з іх паспрабаваў змяніць значэнне, ператвараецца на роботу детекtyва.

// The Spaghetti Way
let cart = [];
let total = 0;

function addItem(item) {
  cart.push(item);
  total += item.price;
}

function resetCart() {
  cart = [];
  total = 0;
}

Нічога не заважае якому-небудзь некаляванаму скрыпту на сторонцы выставіць cart на значэнне null або адкоректаваць total, не прабуючы адразу змяніць cart.

Рашэнне

Закрыцці ўнутры функцый-фабрык дазваляюць зберагчы стан у прыватным режыме, прадаючы толькі тыя операцыі, якія насправды патрабуе іншы код, а таксама повнасцю прыховваючы первісныя зменныкі.

// The Scalable Way
function createCart() {
  // Private variables protected inside the closure
  let items = [];

  return {
    addItem(product) {
      if (!product || typeof product.price !== 'number') {
        throw new Error('Invalid product payload');
      }
      items.push({ ...product, id: crypto.randomUUID() });
    },
    removeItem(productId) {
      items = items.filter((item) => item.id !== productId);
    },
    getItems() {
      // Return a shallow copy so external mutations don't alter state
      return [...items];
    },
    getTotal() {
      return items.reduce((sum, item) => sum + item.price, 0);
    },
    clear() {
      items = [];
    }
  };
}

const userCart = createCart();
userCart.addItem({ name: 'Mechanical Keyboard', price: 120 });
console.log(userCart.getTotal()); // 120

Чаму гэта працюе у масштабаванні

  • Няма выцэкаўых зменных: код званыя не можа безпасэчна перазапісаць items — ён павінен выконвацься через апрацоўаныя публічныя методы.
  • Безпечныя множыственные экземпляры: кожны вызов createCart() вяртае свой сабэйстандынтны стан, без рызыку таго, што адной экземпляр змусіць знішчыць іншы.

3. Шаблон спазірніка (Pub/Sub): Адпусканне строго сувязанага коду

Проблема

Калі пакупальнік нажымае „Задаць замовленне“, трэба, каб адразу ж выйшлі на практыку кальколька дзеянняў: кошык апошчаджваецца, паказваецца паведамленне пра падтверджэння, запускаецца піксель для стежэння, і сервер абавешчаецца. Адкладанне всей гэтай логіки ў адну функцыю прыводзіць да таго, што яна стае некерованым збірнікам усьго.

// The Spaghetti Way
async function handleCheckout(order) {
  await api.submitOrder(order);

  // UI logic mixed directly with tracking and data operations
  document.querySelector('#cart-count').textContent = '0';
  document.querySelector('#modal').classList.add('active');
  analytics.trackPurchase(order);
  notificationSystem.sendPush('Order confirmed');
}

Якщо скрыпт для стежэння выклікае адзіну з бягоў чы ў элементе DOM зменяецца назва, весь процес прайшчыць платежа можа зазнаць няудачы.

Рашэнне

// The Scalable Way
class EventEmitter {
  constructor() {
    this.events = new Map();
  }

  subscribe(eventName, listener) {
    if (!this.events.has(eventName)) {
      this.events.set(eventName, new Set());
    }
    this.events.get(eventName).add(listener);

    // Return an easy unsubscribe function
    return () => this.events.get(eventName).delete(listener);
  }

  publish(eventName, data) {
    const listeners = this.events.get(eventName);
    if (listeners) {
      listeners.forEach((listener) => {
        try {
          listener(data);
        } catch (err) {
          console.error(`Error executing listener for ${eventName}:`, err);
        }
      });
    }
  }
}

const appBus = new EventEmitter();

// Feature modules register their own behavior
appBus.subscribe('order:placed', (order) => {
  analytics.trackPurchase(order);
});

appBus.subscribe('order:placed', () => {
  document.querySelector('#cart-count').textContent = '0';
});

// The emitter stays minimal and decoupled
async function handleCheckout(order) {
  await api.submitOrder(order);
  appBus.publish('order:placed', order);
}

Чаму гэта працюе у масштабе

  • Няма залежнасцей між часткамі: функцыя прайшчыць платежа не ведае, хто ў яе запісаны. Можна дадаць новыя аналітычныя звесткі, паведамленні па электронной пошцы чы ефекты інтэрфейсу, не трагчыць handleCheckout.
  • Ізоляцыя бягоў: няудача ў аднам з слухальнікаў не паражае функцыю, якая запускала ўздзейсненне.

4. Шаблон адаптара: ізоляцыя вашага коду ад нестабільных залежнасцей

Проблема

Зовнішнія службы, пакеты npm і внутрэшніе канцэнтры часта змянюють своіі контракты без паведамлення. Якщо п’ятнаццац разлучных компанентаў запрашваюць даны корыстніка, а кожны з іх чытае поля рэспансу без ператворэння, перайменаванне адной-ўсёго атрыбута — напрыклад, user_id на id — спрычынае переработку, якая паўтараецца ў всім кодбэсе.

// The Spaghetti Way: scattered across multiple UI components
function renderProfile(rawApiResponse) {
  // Directly tied to backend-specific naming conventions
  const name = `${rawApiResponse.first_name} ${rawApiResponse.last_name}`;
  const address = rawApiResponse.shipping_address_line_1;
  const avatar = rawApiResponse.meta_info.profile_image_url;
}

Рашэнне

Даўжыце шар адаптэра між зовнішнім джерелам даных і внутрэшняй логікай вашага прыемлівача. Ператворыце даны, якія прыходзяць зза меж аплікацыі, у стабільную, прагнозавальную структуру перш чым яны будуць выкарыстоўваны.

// The Scalable Way
function userAdapter(externalUser) {
  return {
    id: externalUser.user_id || externalUser.id,
    fullName: `${externalUser.first_name || ''} ${externalUser.last_name || ''}`.trim(),
    address: externalUser.shipping_address_line_1 || externalUser.street || 'N/A',
    avatar: externalUser.meta_info?.profile_image_url || '/assets/default-avatar.png',
  };
}

// Your components only ever consume normalized models
async function getUserProfile(userId) {
  const response = await fetch(`/api/v1/users/${userId}`);
  const rawData = await response.json();
  return userAdapter(rawData);
}

Чаму гэта працюе у масштабе

  • Адаптавацыя толькі ў аднам месца: якщо серверная частка наступнай недзе змяні схему сваёй адпаведзі, вам дастаткова будзе зменіць userAdapter аднойчы, у замест на тое, каб пераследаваць чырвантсць компонентаў, якія перасталі працаваць.
  • Простыя альтэратывы для тэстаў: тэсты інтэрфейсу трэба працаваць з нормалізаваным форматам, а не з постаўляючымся зменяюцымся званоўным форматам.

5. Перакананне ў кампозіцыі замест на спадчынаў: складанне функцыйяй як будовых блакоў

Проблема

Глыбокія іерархіі класаў часта не вытрымаюць свайго ж сложнасці. Падазроўваю, што вы пачынаеце з гэнерычнага класа User і далей раздзеляеце яго на AdminUser, ModeratorUser і GuestUser. Усё пачынае ламацца тады, калі патрабуецца GuestModerator — чалавек, у якога є калькі змагчыццяў модератора, але не весь набор.

// The Spaghetti Way: Deep Inheritance
class BaseUser {
  login() { /* ... */ }
}

class Admin extends BaseUser {
  deleteContent() { /* ... */ }
  manageBilling() { /* ... */ }
}

// What happens when you need a "BillingAgent" who cannot delete content?

Дзейнствыя спадчыны з’еднаюць разныя можлівасці спосабамі, якія не адпаважаюць рэальнасці, і падкласы ў канечнай лініі успадковуюць методы, якія для іх не маюць сэнсу.

Рашэнне

У замен на гэта выкорыстоўваюце складанне об’ектаў: маленькія, можна перызначаць функцыі поведэння (міксіны), якія можна прыўязаць да об’екта за патрэбай. Ствараюце наборы можлівасцей на адной засадзе таго, калі ўжо ўмее ўрадзіць аб’ект, а не прымушваеце яго парадкавацца ў жорсткай іерархіі я ёсць.

// The Scalable Way: Composable Behaviors
const canAuthenticate = (state) => ({
  login: () => console.log(`${state.email} logged in`),
  logout: () => console.log(`${state.email} logged out`),
});

const canModerateContent = () => ({
  deletePost: (postId) => console.log(`Post ${postId} deleted`),
  banUser: (userId) => console.log(`User ${userId} banned`),
});

const canManageBilling = () => ({
  processInvoice: (amount) => console.log(`Invoice processed: ${amount}`),
});

// Build specialized actors on demand
function createSupportStaff(email) {
  const state = { email };
  return {
    email,
    ...canAuthenticate(state),
    ...canModerateContent(),
  };
}

function createSuperAdmin(email) {
  const state = { email };
  return {
    email,
    ...canAuthenticate(state),
    ...canModerateContent(),
    ...canManageBilling(),
  };
}

const moderator = createSupportStaff('support@example.com');
moderator.login();
moderator.deletePost(404);
// moderator.processInvoice is undefined - zero privilege leakage

Чаму гэта працюе у масштабе

  • Няма іерархіі, якую трэба кантролюваць: поведэнняя элементы з’еднаюцца на месцы, без патрэбы заздалегідь планаваць структуру класоў.
  • Можна выкарыстоўваць у розных сэтанкі: такое паведанне, як canAuthenticate, аднакова добра працуе як у аблікацыях клёнтаў, так і ў внутрашнях аблікацыях спаведамальнікаў, а таксама ў аблікацыях автаматызаваных ботаў.
  • 6. Шаблон Pipeline: асінхронныя крокі па спрабах

    Проблема

    Спаяванне кальколька асінхронных трансфармацый часта вылучае глыбокая аранжаваная, складная для разумэння код, які спалучае некалякуюцяяся элементы.

    // The Spaghetti Way
    async function handleImageUpload(file) {
      if (file.size > 5000000) {
        throw new Error('Too large');
      }
      const compressed = await compressImage(file);
      const metadata = await extractExif(compressed);
      const tagged = await tagCategories(compressed, metadata);
      const uploadResult = await uploadToS3(tagged);
      return uploadResult;
    }
    

    Это можна кераваць за дапамою трох крокаў, але калі пачынаеш дадаваць логгіранне, тэлеметрыю, прабыткі і перакананне, весь працэс стае складным для разумэння.

    Рашэнне

    Застосавайце шаблон паіплайну: прымкніце кожную трансфармацыю як маленькую, спецыялізаваную функцыю і складзіце ў ланцоўку, так каб весь процес быў зрозумілы з пачатку да канца, зверху вярху або злева напраўо.

    // The Scalable Way
    const pipeAsync = (...functions) => (initialValue) =>
      functions.reduce(
        (currentPromise, currentFunction) => currentPromise.then(currentFunction),
        Promise.resolve(initialValue)
      );
    
    // Each step is an isolated, testable transformation
    const validateSize = async (file) => {
      if (file.size > 5 * 1024 * 1024) throw new Error('File exceeds 5MB limit');
      return file;
    };
    
    const compress = async (file) => compressImage(file);
    const attachWatermark = async (image) => applyWatermark(image);
    const upload = async (finalImage) => uploadToCloud(finalImage);
    
    // Create the pipeline
    const processUserImage = pipeAsync(
      validateSize,
      compress,
      attachWatermark,
      upload
    );
    
    // Usage
    processUserImage(rawFileInput)
      .then((res) => console.log('Upload complete:', res))
      .catch((err) => console.error('Pipeline failed:', err.message));
    

    Чаму гэта працюе ў масштабе

    • Простае пераранжаванне: дадаць, адмаўіць або змяніцы порядак крока — напрыклад, дадаць етап стварэння тамбнела — не выклікае значных зусиль.
    • Паслякрокавая дыбаггінг: вы можете дадаць простую функцыю логавання ў будзь-які момент паіплайну, каб пераканацца, што прыходзіць і што выходзіць.

    Як павэрнуцца ад заплутанага коду

    Код не становіцца заплутанным таму, што людзі, які яго пішлі, не маюць спаку. Ён становіцца заплутанным таму, што системы расту пад тым прытыскам часу, і разработчыкі выбіраюць тую лінію коду, якая найшырэй рашае трыбутны проблему.

    Праўы ключ да чыстай архітектуры — гэта не прыўязка якога-небудзь масівага корпоратыўскага фрэймворку. Гэта застосаванне маленькіх, рашуча выбраных структур там, дзе яны падходяць:

    1. Паспраўляйце шаблон да рэальнага сімптому: выберыце Strategy, калі умовныя оператары выходзяць з-пад контролю, вжывайце pub/sub, калі модулі начынаюць вызываць адзін другога цікламі, і викорыстоўвайце adapter, калі умовы сторонньяго партнера загрожуюць дэстабілізаваць ваш UI.
    2. Валічыце простасць над хитроумнасцю: вам не патрабуецца кожны шаблон з самага першага дня. Дазвольце фрагменту логіки павтарыцца два разы, прычаму перш чым працаваць над його абстракцыяй у трэці раз.
    3. Зберагаюце функцыі чыстымі, а інтэрфейсы — чытаўна вялізначанымі: прагнозаваныя вхідныя та выходныя даны робяць будучыя переработы значна менш болючымі.

    Чысты код — это не тое, што можна напісаць ідеальна адразу; цэў код, які за шасць месцаў будзе ўсё такі ж лёгкі да змены.

    Спадневаная літэратура

  • Cursor, Claude Code і Codex: Выбір інструмента для програмавання на AI для JS — У цым аналізе разбіраецца, як Cursor, Claude Code і Codex падходзяць да розных заведамаў у JavaScript, ад програмавання ў рэдагарыі да задач, якія выканаюць автонамныя агенты.
  • Дзесяць распашчаёных шаблонаў JavaScript і наследкі ўжыцтва кожнага з іх — Guard clauses, lookup maps, result objects, AbortController, allSettled, factories і маленькія кэшы: калі кожны шаблон єфектыўны, а калі прыносіць наследкі.