Галоўная / Артыкулы

Enumы і інтэрфейсы TypeScript як элементы для праектаў нізкага рэвялявання

Як enum у TypeScript адзіёўцуе багі, вызваныя «чародзейскімі» стрэлкамі, калі выбіраць тып чы раён, і як раёны, implements і структурны тыпаванне задаюць кантракты ў праектах нізкага рэвярсу.

Выдавец: REACTAPP.TOP7 хв чытання1599 слоў

Крыніцы: Day 1: (Enums and Interfaces) LLD With TS | Part 2

Пасля класоў і аб’ектаў наступнымі інструментамі для праектавання на нізкам рывень (LLD) у TypeScript ёсць enum’і і інтэрфейсы. Яны здаюцца простымі, але дапамагаюць падтрымаць цэласообразнась праекта: enum’і вказуюць дазволеныя значэння, а інтэрфейсы — умовы, якія павінны выпалняць аб’екты. У этай статыцэ показаны багі, якія кожны з іх запобегае, як выбраць між type і interface, а таксама што означае структурны тыпаванне для implements.

Проблема, яку рашаюць enum’і: розрозненыя „чародзкія“ строкі

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

PENDING
SUCCESS
REJECTED

Код у аднам модулі пераглядае замовленне, якое ў спадчыні, так:

if (order.status === "PENDING") {
  // ...
}

Іншы модулі пераглядае статус успеху:

if (order.status === "SUCCESS") {
  // ...
}

Усё працюе, пакуль новы колега не напіша перагляд, які выглядае майжэ так сама, але вікорыстоўвае маленькія літэры:

if (order.status === "pending") {
  // ...
}

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

"PENDING" !== "pending"

Умова ніколі не станавіцца правдзівай. Асновная прычына — значэння, запісаныя вручную ў всіх месцах: хто памятае, чы то было PENDING, pending чы Pending?

Адзін раз задаць дазволеныя значэння

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

enum OrderStatus {
  PENDING = "PENDING",
  SUCCESS = "SUCCESS",
  REJECTED = "REJECTED"
}

У замяну на порэшчанне з сырой стракай, як у гэтым пераглядзе,

if (order.status === "PENDING") {
  // ...
}

вы порэшчваеце з членам enum:

if (order.status === OrderStatus.PENDING) {
  // ...
}

То ж самае паслужыць для кожнага іншага перагляду статусу:

if (order.status === OrderStatus.SUCCESS) {
  // ...
}

Нікому не трэба памятаць точную артыкуляцыю, достатні толькі назва члена, яку автодапоўнюе рэдагувальнік:

OrderStatus.PENDING

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

Чаму гэта мае значэнне, калі кодавая база расце

Уявіце, што гэты статус праказваецца у двадзяці месцах. Без enum адпаведнасць залежыць ад дысцыпліны, і пачынаюць з’являвацца варыяцыі:

"PENDING"
"PENDING"
"pending"
"Pending"
"PENDING"

За дапамою enum кожна локалізацыя апілецца да той самай дэкларацыі:

OrderStatus.PENDING

Корача кажучы, enum — гэта імёнаваныя, загадзя заданыя наборы сталярных значэнняў.

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

type проты interface

Для LLD і спэкаванняях дастатнекі просты ментальны модэль гэтых двух паняў.

type: складанне і мадэляванне тыпаў

  • Гнучкі для сумешчання тыпаў.
  • Падрэбніця для аб’еднання, перасекання, туплів, прымітываў, карэктаваных тыпоў і іншага.
  • Ўзгодна, калі вы пераважна опісваеце або складаеце даны.
  • Тыпічны прыклад — аб’еднанне літэральных строк:

    type Status = "PENDING" | "SUCCESS" | "FAILED";
    

    Таксама ўзгодна ідэнтыфікатор, які можа быць або строкай, або чыслам:

    type ID = string | number;
    

    Інтэрфейс: кантракты для об’ектаў і класаў

    • Пераважна опісвае форму об’ектаў і кантракты, якія класы павінны выпалняць.
    • Можа быць рэалізаваны класам за дапамогою implements.
    • Ўзгодна, калі калькі рэалізацый павінны выпалняць той самы кантракт.
    • Падтрымлівае з’еднанне дэкларацый, таму две дэкларацыі з тым самым іменем з’еднаюцца ў адну.
    interface Payment {
      amount: number;
      pay(): void;
    }
    

    Практычная правіла

    Калі вам трэба складаць або мадэляваць тып, вжывайце type:

    Need to compose/model a type?
            ↓
          type
    

    Калі вам патрэбна угода межа класамі або об’ектамі, вжывайце interface:

    Need a contract between classes/objects?
            ↓
        interface
    

    Гэта ўказвач, а не закон: оба можу опісваць структуру об’ектаў.

    Інтэрфейсы опісваюць структуру об’екта

    Спакойны спосаб занурэння ў інтэрфейсы — гледзець на структуру об’екта. Ёсць просты JavaScript-об’ект:

    const student = {
      id: 101,
      name: "Rahul",
      age: 20
    };
    

    TypeScript дазваляе вам опісаць такую структуру, напрыклад, за дапамою псевдоніма тыпу:

    type Student = {
      id: number;
      name: string;
      age: number;
    };
    

    Потым можна пераканацца, чы розныя об’екты паспелі да той самай структуры:

    const student1: Student = {
      id: 101,
      name: "Rahul",
      age: 20
    };
    
    const student2: Student = {
      id: 102,
      name: "Aman",
      age: 21
    };
    

    Калі прыложэнне расце, называнне такой структуры як відновімаемая угода — гэта сама мета інтэрфейса.

    Інтэрфейс як угода

    Тая ж структура, выражаная як інтэрфейс:

    interface Student {
      id: number;
      name: string;
      age: number;
    }
    

    Будзь-якае значэнне, якое трэба спрацоўваць як Student, цяпер павінна яго задовольняць:

    const student: Student = {
      id: 101,
      name: "Rahul",
      age: 20
    };
    

    Не паведаміце неабяжлівую власнасць, як у гэтым прыкладзе,

    const student: Student = {
      id: 101,
      name: "Rahul"
    };
    

    і кампайляр паведамляе працою, таму што гэтая власнасць не ўсунута:

    age
    

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

    Інтарфейсы візначаюць шта, а не як

    Гэтыя разлікі ёсць ключовымі для LLD. Інтарфейс паведамляе, якім должна быць выгляд або што должна запростацаваць якась рэч, а не як гэта робіцца. Возьмімо контракт адплатаў:

    interface Payment {
      amount: number;
      pay(): void;
    }
    

    Ён гарантуе роўна два пункты:

    Payment must have:
    → amount
    → pay()
    

    У ём нічога не сказана пра тое, як работае pay(). Адна класа можа обрабоцаваць картку, пачынаючы з неабяжлівай власнасці amount,

    class CreditCardPayment implements Payment {
      amount: number;
    

    і аддаючы свой сопственны pay():

      pay() {
        console.log("Paying using credit card");
      }
    }
    

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

    class UPIPayment implements Payment {
      amount: number;
    

    і іншы pay():

      pay() {
        console.log("Paying using UPI");
      }
    }
    

    Той самы контракт, але разныя рэалізацыі, таму вызывачы можаць працаваць з будзь-якім спосабам адплата:

    Interface
       ↓
    Defines the contract
       ↓
    Multiple implementations
       ↓
    Different implementation details
    

    Ключавое слова implements

    implements чыткая паказывае зв’язок. З урахоўваннем контракта,

    interface Payment {
      amount: number;
      pay(): void;
    }
    

    клас заявляе, што ён яго выпалняе:

    class CreditCardPayment implements Payment {
      amount: number;  pay() {
        console.log("Paying with credit card");
      }
    }
    

    Клаза

    implements Payment
    

    прабуе, каб кампайлер пераканаўся, што клас падтрымляе контракт Payment. Калі гэта не так, напрыклад, таму што відсутня pay(),

    class CreditCardPayment implements Payment {
      amount: number;
    }
    

    кампайлер пазначае відсутнае

    pay()
    

    Метод implements ўсунэнцца толькі для пераконтроўкі пад час кампайлявання; ён нічога не дадае пад час выканання. Адзін момент, які трэба врачыць пад час працы з такімі фрагментамі: калі увімкнута опцыя strictPropertyInitialization, заявленая палатавае, як-то amount: number;, без значэння або прызначэння ў канстрактары, сама по сабе ёсць адной з адмов, таму неабходна задаць яе значэння.

    Структурны тыпаванне: форма перамага заявы

    У працоўнасці з Java або C# не так, як у TypeScript, якай выкарыстоўвае структурны тыпаванне. Клас не патрабуе заявляць

    implements Payment
    

    чыба ён не будзе прыйманы там, дзе патрабуецца Payment. Разглянем кантракт з адным методам:

    interface Payment {
      pay(): void;
    }
    

    і клас, які ніколі не згадвае яго:

    class UPIPayment {
      pay() {
        console.log("Paying through UPI");
      }
    }
    

    Нават без напісання

    implements Payment
    

    функцыі, якая патрабуе Payment

    function processPayment(payment: Payment) {
      payment.pay();
    }
    

    яна з радасцю прыймае экзанпляр таго класу:

    const payment = new UPIPayment();processPayment(payment);
    

    Этакі код компілюецца, таму што UPIPayment мае неабяжлівую структуру:

    {
      pay(): void;
    }
    

    Для TypeScript дастатнька проста зусімнаста структуры; явная дэкларацыя ўзельна.

    Тады чаму взагалі пісаць implements?

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

    interface Payment {
      amount: number;
      pay(): void;
    }
    

    і клас, які явна яго рэалізуе,

    class UPIPayment implements Payment {
      amount = 100;  pay() {
        console.log("Paying through UPI");
      }
    }
    

    падазрóўце, калі хтось пазней расшыруе інтэрфейс за дапамою валюты:

    interface Payment {
      amount: number;
      currency: string;
      pay(): void;
    }
    

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

    Ключовыя выводы

    • Enum-ы заменяюць розрозненыя магічныя стрынкі адной дэкларацыёй, ператвараючы памылкі напісання на памылкі кампайлявання.
    • Аб’яднанні стрынкавых літэралай ёсць лёгкі альтэрнатыўны спосаб, калі не патрэбен об’ект у часе выканання.
    • Для складання (аб’яднанняў, перасеканняў, карэльваных тыпаў) выкорыстоўваеце type, а для кантрактаў, якія рэалізуюць класы — interface.
    • Інтэрфейсы опісваюць, што дае об’ект, але не як, што дазволяе вярнавальнае рэалізаванне.
    • TypeScript пераказвае структуру тыпаў, таму implements ёсць необав’язковым, але ён пераносіць памылкі кантрактаў у адзначэнне класа, дзе іх лёгкая пасправіць.