Joe Chasinga
Joe ChasingaFounder / CEO @ Referkit

Jak dobrze refaktoryzować swój kod

Prześledź historię pewnej refaktoryzacji kodu w JavaScript i zobacz, jak prawidłowo podejść do tego procesu.
10.04.20204 min
Jak dobrze refaktoryzować swój kod

Refaktoryzacja kodu ma fundamentalne znaczenie w pracy każdego programisty. Niemniej jednak natknąłem się na stosunkowo niewiele źródeł, które ten temat dogłębnie analizują. Postanowiłem napisać ten artykuł po refaktoryzacji mojego kodu w JavaScript. Cały proces trwał niecałe pół godziny, ale podekscytowałem się na tyle, żeby to opisać. 

Tak zaczęła się wielka refaktoryzacja

Na początku w kodzie były dwie funkcje fetch. Były one porozrzucane po całej bazie kodu z różnymi nazwami. Chciałem je przekształcić w jeden moduł z funkcjami, które mógłbym używać wielokrotnie. Oto dwie z pierwotnych funkcji:

async function postLoginData(data) {
  const loginUrl = `${apiBaseUrl}/login`;
  let response = await fetch(loginUrl, {
    method: "POST",
    mode: "cors",
    cache: "no-cache",
    credentials: "same-origin",
    headers: {
      "Content-Type": "application/json; charset=utf-8",
    },
    redirect: "follow",
    referrer: "no-referrer",
    body: JSON.stringify(data),
  });
  return response;
}

// Get the user's data based on user id.
async function getUser(userId) {
  const userUrl = `${apiBaseUrl}/users/${userId}`;
  let response = await fetch(userUrl, {
    method: "GET",
    mode: "cors",
    cache: "no-cache",
    credentials: "same-origin",
    headers: {
      "Content-Type": "application/json; charset=utf-8",
    },
    redirect: "follow",
    referrer: "no-referrer",
  });
  return response;
}


Nie jestem skrajnym zwolennikiem DRY, ale wygląda to raczej niezręcznie. Żadna z tych funkcji nie robi zbyt wiele w porównaniu z tym, co można osiągnąć za pomocą fetch, który je opakowuje. Oprócz hermetyzacji URL i punktów końcowych oraz właściwości method te dwie funkcje wyglądają dokładnie tak samo i powinny się nadać do ponownego wykorzystania we wszystkich tego typu przypadkach w kodzie.

Funkcje powinny być czyste, jeśli to możliwe

Moim pierwszym i najważniejszym kryterium dla funkcji jest to, że po refaktoryzacji powinna być czysta. Czystość oznacza możliwość ponownego użycia. Jeśli funkcja musi zmieniać dowolny współdzielony stan, to może być wtedy metodą. Dzięki temu funkcje są łatwe do przetestowania i nadają się do wielokrotnego użycia, a funkcje takie jak postLoginData to naruszają. Oto kilka sposobów na refaktoryzację bez zastanawiania się nad implementacją:

  • user.login()
  • login(user)
  • post(loginUrl, user)


Powyższa lista została uporządkowana od najmniej ogólnego do najbardziej ogólnego przypadku. W rzeczywistości pierwsze dwa mają ten sam poziom ogólności. Tylko ostatnia z nich jest funkcją wielokrotnego użytku — i o to właśnie chodziło.

Teraz możecie zobaczyć, że moje dwie funkcje są bardzo słabym rozwiązaniem. Czasami zajmujesz się wieloma sprawami i gubisz się w gąszczu priorytetów. Można się jak najbardziej spieszyć, o ile od czasu do czasu po sobie sprzątasz.

Uzasadnij refaktoryzację

Aby zdecydować, czy coś powinno być refaktoryzowane, myślę o intencji i wartości stworzenia dla tego czegoś funkcji.

Na przykład, funkcja, która robi „POST” danych, będzie miała inne intencje od tej, która robi „GET”, niezależnie od niewielkiej różnicy w implementacji. Intencje na tyle się różnią, że można stworzyć dwie funkcje.

Opakowanie dowolnego adresu URL wewnątrz funkcji (na przykład punktu końcowego API logowania), a następnie nazwanie funkcji postLoginData nie wnosi do niej zbyt dużo, biorąc pod uwagę jej mniejszą ogólność.

Adres URL, oprócz tego, że jest ciągiem znaków, powinien być „opowieścią” wywołującego. Pomyśl o artyście z farbami olejnymi, paletą i pędzlami. To, co artysta chce namalować, powinno być jego opowieścią.

Paleta, farby i pędzle powinny być na tyle zróżnicowane, by pozwolić na namalowanie tematu. To pewnie dasz radę sobie wyobrazić. A co powiesz o namalowaniu statku? To już nie jest takie łatwe, bo ciężko o hermetyzację wszystkich szczegółów dotyczących statku.

Czas na refaktoryzację: 

const baseConfig = {
  mode: "cors",
  cache: "no-cache",
  credentials: "same-origin",
  headers: {
    "Content-Type": "application/json; charset=utf-8", 
  },
  redirect: "follow",
  referrer: "no-referrer",
};

// Configurable POST with predefined config
async function post(uri, data, config = {}) {
  config = Object.assign({
    method: "POST",
    body: JSON.stringify(data),
    ...baseConfig,
  }, config);
  return await fetch(uri, config)
}

// Configurable GET with predefined config
async function get(uri, config = {}) {
  config = Object.assign({
    method: "GET",
    ...baseConfig,
  }, config);
  return await fetch(uri, config);
}

export {get, post};


Teraz wygląda to znacznie lepiej. A to dzięki wyłączeniu powtarzających się opcji konfiguracji do stałej baseConfig. Dodałem także opcjonalny parametr config do każdej funkcji, aby można ją było konfigurować z zewnątrz. Object.assign służy do połączenia niestandardowej konfiguracji z baseConfig.

Widzimy także rozwinięcia obiektu w akcji. W tym momencie byłem bardzo zadowolony, ale w wolnym czasie postanowiłem sprawdzić, czy uda mi się zrobić z tym coś więcej.

Oto ostatnia refaktoryzacja:

const baseConfig = {
  mode: "cors",
  cache: "no-cache",
  credentials: "same-origin",
  headers: {
    "Content-Type": "application/json; charset=utf-8",
  },
  redirect: "follow",
  referrer: "no-referrer",
};

const send = (method, payload) => (
  async function(uri, config) {
    // Create an array of source config objects to be merged.
    let sources = [config];
    if (method === "POST") {
      sources.push({ body: JSON.stringify(payload) });
    }
    config = Object.assign({
      method: method,
      ...baseConfig,
    }, ...sources);

    return await fetch(uri, config);
  }
);

const get = (uri, config = {}) => (
  send("GET")(uri, config)
);


const post = (uri, data, config = {}) => (
  send("POST", data)(uri, config)
);

export {get, post};


Osobiście najbardziej podoba mi się ostatnia wersja, ponieważ funkcje get i post są bardzo cienkimi opakowaniami nowo utworzonej funkcji send (która nie jest eksportowana). To sprawia, że jest jedynym punktem, w którym będziemy debugować, jeżeli pojawią się błędy.

Refaktoryzacja to ciężka sprawa, i to nie dlatego, że jest trudna do wykonania, ale ponieważ wymaga głębszego myślenia o projekcie. Niech Ci się nie wydaje, że wszyscy będą zadowoleni. Refaktoryzacja kodu, by nadawał się do ponownego użycia, może się niektórym nie podobać, szczególnie jeśli wprowadzi to kompromisy, które zniwelują zysk z takiego podejścia. 

Dążymy tutaj zatem do równowagi. Takie czynniki jak konwencje nazewnictwa i parametry funkcji mogą bardzo pomóc, dlatego powinno się je dobrze przemyśleć. 

<p>Loading...</p>