Myšlení v datových smlouvách

Tento příspěvek byl poprvé publikován na blogu TK .

Při vývoji frontendu je běžné spotřebovávat data API a používat je k vykreslování uživatelských rozhraní. Někdy však data API nejsou přesně to, s čím chceme pracovat. Daří se nám tedy mapovat data API do stavu aplikace.

Ale nemusí to být složité. Může to být jen jednoduchá funkce mapovače, smlouva o datech API a smlouva o stavu aplikace.

Ukážu příklad v kontextu aplikace Redux a jak můžeme zajistit, aby byl stav konzistentní.

Začínáme počáteční státní smlouvou.

type Person = {
  id: number,
  name: string,
  email: string
};

A použijte typ smlouvy v definici počátečního stavu:

const initialState: Person = {
  id: 0,
  name: 'TK',
  email: '[email protected]'
};

Po definici stavu aplikace můžeme uvažovat o smlouvě API. Můžeme jen implementovat typ PersonAPI se všemi typy potřebnými pro data.

type PersonAPI = {
  id: number,
  name: string,
  email: string
};

Nyní, když máme naši smlouvu definovanou, můžeme pracovat s mapováním dat. nemusí to být super komplexní třída. Může to být jednoduchá čistá funkce, která přijímá PersonAPI data a transformovat je do Person data.

const fromAPI = (person: PersonAPI): Person => ({
  id: person.id,
  name: person.name,
  email: person.email
});

To je docela jednoduché! A jak to používáme?

const payloadAPI = {
  id: 1,
  name: 'TK',
  email: '[email protected]'
};

const person: Person = fromAPI(payloadAPI); // { id: 1, name: 'TK', email: '[email protected]' }

Data přicházejí. Data vycházejí. Všechno čisté.

Zde máme velmi jednoduché mapování, žádná logika. Ale co když data API, která přijímáme, nemají name , ale firstName a lastName ? Chceme transformovat firstName a lastName do name atribut v Person smlouvy.

PersonAPI typ:

type PersonAPI = {
  id: number,
  firstName: string,
  lastname: string,
  email: string
};

Person typ:

type Person = {
  id: number,
  name: string,
  email: string
};

V našem name , musíme spojit řetězce. V zásadě se provádí interpolace řetězců:

`${person.firstName} ${person.lastName}`

Naše mapovací funkce by tedy byla něco takového:

const fromAPI = (person: PersonAPI): Person => ({
  id: person.id,
  name: `${person.firstName} ${person.lastName}`,
  email: person.email
});

Skvělý! Transformace dat pro vykreslování uživatelského rozhraní.

Další krok:představte si naše lastName je volitelný sloupec databáze. Takže koncový bod API to může vrátit... nebo ne!

Můžeme použít Typescript Optional Property . Říká nám:"Je to volitelná vlastnost, má tento typ, ale data zde mohou být... ​​nebo ne!"

Takže to používáme v naší smlouvě API:

type PersonAPI = {
  id: number,
  firstName: string,
  lastName?: string,
  email: string
};

Pěkný! Nyní víme, že pro vytvoření name musíme udělat nějakou logiku atribut.

  • lastName vlastnost:concat firstName a lastName
  • Nemá lastName :stačí vrátit firstName hodnotu
const fromAPI = (person: PersonAPI): Person => {
  let name: string;
  if (person.lastName) {
    name = `${person.firstName} ${person.lastName}`
  } else {
    person.firstName
  }
  return {
    id: person.id,
    name,
    email: person.email
  };
};

Ale můžeme také transformovat tento let příkaz do const provedením ternární operace:

const fromAPI = (person: PersonAPI): Person => {
  const name: string = person.lastName
    ? `${person.firstName} ${person.lastName}`
    : person.firstName;
  return {
    id: person.id,
    name,
    email: person.email
  };
};

Nebo lépe:rozdělte jeho odpovědnost do funkce, která buduje jméno!

const buildPersonName = (person: PersonAPI): string =>
  person.lastName
    ? `${person.firstName} ${person.lastName}`
    : person.firstName;
const fromAPI = (person: PersonAPI): Person => {
  const name: string = buildPersonName(person);
  return {
    id: person.id,
    name,
    email: person.email
  };
};

Oddělujeme odpovědnost za každou funkci. Skvělý! Nyní je snazší otestovat naše funkce.

Další fáze:použití dat API k vytvoření nového stavu aplikace. Představte si, že chceme vědět, zda je daný člověk aktivní. Obchodní pravidlo zní:stav osoby by měl být active a poslední návštěva by měla být během tohoto týdne (za posledních 7 dní).

Nejprve naše smlouva o API:

type PersonAPI = {
  id: number,
  firstName: string,
  lastName?: string,
  email: string,
  status: string,
  lastVisit: Date
};

Použijeme tyto vlastnosti:status a lastVisit .

Za druhé naše smlouva o stavu aplikace:

type Person = {
  id: number,
  name: string,
  email: string,
  active: boolean
};

Obchodní pravidlo nyní:

  • Stav osoby by měl být active
person.status === 'active'
  • Poslední návštěva osoby by měla být za posledních 7 dní
person.lastVisit >= new Date(Date.now() - 7 * 24 * 3600 * 1000);

Nyní naše mapovací funkce:

const fromAPI = (person: PersonAPI): Person => {
  const name: string = buildPersonName(person);
  const active: boolean = person.status === 'active' && person.lastVisit >= new Date(Date.now() - 7 * 24 * 3600 * 1000);
  return {
    id: person.id,
    name,
    email: person.email,
    active
  };
};

Pojďme to zrefaktorovat! Začneme s status věc. 'active' je řetězec. K jeho definování v datové struktuře a umožnění opětovné použitelnosti můžeme použít Typescript's Enum.

enum PersonStatus {
  Active = 'active',
  Inactive = 'inactive'
};

Používáme to takto:

PersonStatus.Active // 'active'
PersonStatus.Inactive // 'inactive'

Logika stavu osoby je s touto funkcí snadná:

person.status === PersonStatus.Active;

Nyní poslední návštěva. Místo náhodných čísel, co to udělat trochu popisnější? Toto je 1 den v milisekundách:

const oneDayInMilliseconds: number = 24 * 3600 * 1000;

Toto je 7 dní v milisekundách:

const sevenDaysInMilliseconds: number = oneDayInMilliseconds * 7;

A to je před týdnem:

const weekAgo: Date = new Date(Date.now() - sevenDaysInMilliseconds);

Nyní je naše logika snadná:

person.lastVisit >= weekAgo;

Nyní můžeme vše spojit do funkce nazvané isActive to vrací boolean?

const isActive = (person: PersonAPI): boolean => {
  const oneDayInMilliseconds: number = 24 * 3600 * 1000;
  const sevenDaysInMilliseconds: number = oneDayInMilliseconds * 7;
  const weekAgo: Date = new Date(Date.now() - sevenDaysInMilliseconds);
  return person.status === PersonStatus.Active &&
    person.lastVisit >= weekAgo;
};

Opravdu chci oddělit weekAgo „logiku“ do nové funkce. A chci také jmenovat výroky.

const getWeekAgo = (): Date => {
  const oneDayInMilliseconds: number = 24 * 3600 * 1000;
  const sevenDaysInMilliseconds: number = oneDayInMilliseconds * 7;
  return new Date(Date.now() - sevenDaysInMilliseconds);
};
const weekAgo: Date = getWeekAgo();

Když pojmenujeme naše prohlášení, vypadá to takto:

const hasActiveStatus: boolean = person.status === PersonStatus.Active;
const lastVisitInSevenDays: boolean = person.lastVisit >= weekAgo;

Takže naše finální isActive funkce vypadá krásně:

const isActive = (person: PersonAPI): boolean => {
  const weekAgo: Date = getWeekAgo();
  const hasActiveStatus: boolean = person.status === PersonStatus.Active;
  const lastVisitInSevenDays: boolean = person.lastVisit >= weekAgo;
  return hasActiveStatus && lastVisitInSevenDays;
};

A naše mapovací funkce je jednoduchá:

const fromAPI = (person: PersonAPI): Person => {
  const name: string = buildPersonName(person);
  const active: boolean = isActive(person);
  return {
    id: person.id,
    name,
    email: person.email,
    active
  };
};

Několik vylepšení:Zkratka hodnoty vlastnosti objektu pro id a email .

const fromAPI = (person: PersonAPI): Person => {
  const { id, email }: PersonAPI = person;
  const name: string = buildPersonName(person);
  const active: boolean = isActive(person);
  return {
    id,
    name,
    email,
    active
  };
};

Učení

Co jsme se tu tedy naučili?

  • Datové smlouvy nám pomáhají lépe definovat naše datové struktury, tedy stav, který chceme, aby naše rozhraní správně vykreslovalo uživatelské rozhraní.
  • Slouží také jako dobrá dokumentace:lepší porozumění naší odpovědi API a stavu aplikace, se kterým se musíme vypořádat.
  • Další skvělou výhodou je, když definujeme datové typy a používáme je v počátečním stavu. Náš systém je skutečně konzistentní, pokud zachováváme státní smlouvu napříč aplikací.
  • Nemusí to být složité. Pouze jednoduché a čisté funkce. Oddělte odpovědnost za každou funkci a můžeme jít. Pomáhá nám to i při testování.

Doufám, že jsem mohl ukázat dobrý přehled datových smluv, jednoduchých funkcí a principu jediné odpovědnosti. V softwarovém inženýrství je opravdu snadné udělat vše složité a pokazit to. Ale pokud se pečlivě zamyslíme nad našimi daty, datovými strukturami, které používáme, a tím, jak zvládáme složitost a logiku, myslím, že máme dobrou šanci vybudovat dobrý software.

Zdroje

  • Kurz JavaScript pro začátečníky
  • Kurz React pro začátečníky
  • Pokročilý kurz React
  • Kurz ES6
  • Kurz JavaScriptu od OneMonth