Cách đặt tên biến và tách hàm: refactor an toàn nhờ có test

Cách đặt tên biến và tách hàm khi dọn code cũ: viết test đặc tả trước, đổi tên rồi mới tách, chạy test sau mỗi bước để chắc hành vi không đổi.

Cách đặt tên biến cho dễ đọc là gì? Tên phải nói biến đó chứa gì. Nhưng đổi tên trong code cũ chỉ an toàn khi có test chứng minh hành vi không đổi. Bạn đã bao giờ sửa "cho đẹp" rồi làm hỏng thứ đang chạy chưa? Cùng mình dọn một file nhé!

Anh Khoa chê file nào?

Ở bài trước, pull request sửa lỗi "đơn 1044 tổng 70 đồng" của hệ thống đơn hàng quán Mộc đã được duyệt và merge. Mộc là một quán cà phê nhỏ bán online, còn quán và anh Khoa, người review, là minh họa. Code và mọi kết quả chạy trong bài là thật.

Kèm lời duyệt là một ghi chú: "Lần sau sửa server.ts mình nên dọn luôn. File này đọc mất thời gian: tên biến kiểu rawOrders, orders, body không nói nó chứa gì, và một hàm vừa đọc file, vừa chuẩn hóa, vừa trả HTTP."

Đây là toàn bộ file đó. Nó là một máy chủ HTTP nhỏ viết bằng module node:http có sẵn của Node.js. Khi khởi động, nó đọc file data/orders.json, đổi mọi giá dạng chuỗi như "35.000" thành số bằng hàm normalizeOrder, rồi trả danh sách đơn ở đường dẫn /don-hang:

src/server.ts (trước khi dọn)
import { readFileSync } from "node:fs";
import { createServer } from "node:http";

import { listOrders, normalizeOrder, type RawOrder } from "./orders.ts";

const dataFile = new URL("../data/orders.json", import.meta.url);
// Chuẩn hóa ngay khi đọc file: từ đây trở đi, giá luôn là số.
const rawOrders: RawOrder[] = JSON.parse(readFileSync(dataFile, "utf8"));
const orders = rawOrders.map(normalizeOrder);

export const server = createServer((req, res) => {
  if (req.url === "/don-hang") {
    try {
      const body = JSON.stringify(listOrders(orders), null, 2);
      res.writeHead(200, { "content-type": "application/json; charset=utf-8" }).end(body);
    } catch (error) {
      console.error(error);
      res.writeHead(500, { "content-type": "text/plain; charset=utf-8" }).end("Lỗi máy chủ");
    }
    return;
  }
  res.writeHead(404).end();
});

const port = Number(process.env.PORT ?? 3000);
server.listen(port, () => console.log(`Đơn hàng Mộc: http://localhost:${port}/don-hang`));

Chạy nó lên rồi mở http://localhost:3000/don-hang, bạn nhận được bốn đơn của sáng 3/11. Mỗi đơn có mã, tên khách viết hoa và tổng tiền:

curl localhost:3000/don-hang
[
  { "id": 1041, "khach": "NGUYỄN AN", "tong": 83000 },
  { "id": 1042, "khach": "TRẦN BÌNH", "tong": 32000 },
  { "id": 1043, "khach": "KHÁCH TẠI QUẦY", "tong": 25000 },
  { "id": 1044, "khach": "LÊ CHI", "tong": 70000 }
]

Mình gộp mỗi đơn về một dòng cho gọn, còn máy chủ thật in mỗi trường một dòng. File chỉ có 25 dòng, nên bạn có thể thắc mắc vì sao phải dọn. Mình nghĩ lý do nằm ở chỗ file này là điểm vào của cả hệ thống. Ai mới vào dự án cũng mở nó đầu tiên, và hiện tại họ phải đọc hết từng dòng mới biết nó làm ba việc khác nhau. Hệ thống sắp có thêm tính năng mới, và file 25 dòng hôm nay sẽ thành 200 dòng nếu không ai tách nó ra.

Refactor là gì, và nó khác sửa lỗi ở đâu?

Martin Fowler, người viết cuốn sách nổi tiếng về chủ đề này, định nghĩa refactor là thay đổi cấu trúc bên trong của code mà không thay đổi hành vi bên ngoài của nó. Vế sau mới là phần khó. Sau khi refactor, máy chủ phải trả về đúng từng byte như trước, đúng mã trạng thái, đúng header, và vẫn dừng khi gặp giá sai.

Vậy làm sao biết hành vi không đổi? Hệ thống đã có 15 test viết bằng Vitest từ bài về test đầu tiên và bài code review. Nhưng cả 15 test đều kiểm tra orders.ts, tức các hàm đọc giá và tính tổng. Không test nào import server.ts. Nếu mình dọn server.ts và lỡ đổi đường dẫn /don-hang thành /don-hang/, cả 15 test vẫn xanh.

Vì vậy việc đầu tiên không phải là đổi tên. Việc đầu tiên là viết test cho thứ sắp bị sửa.

Viết test gì khi chưa biết code "đúng" là thế nào?

Loại test mình cần ở đây có tên là test đặc tả (characterization test). Nó không hỏi code có đúng không. Nó chỉ chụp lại code đang làm gì, để sau khi sửa mình so được với trước khi sửa. Nếu code cũ có một hành vi kỳ cục, test đặc tả vẫn ghi lại hành vi kỳ cục đó, vì refactor không phải lúc để sửa nó.

Có một trở ngại. server.ts gọi server.listen ngay khi được import, ở cổng 3000. Nếu bạn đang chạy máy chủ ở cổng đó trong một cửa sổ terminal khác, test sẽ lỗi vì cổng đã có người dùng. Mình tránh bằng cách đặt biến môi trường PORT thành 0 trước khi import. Tài liệu của Node.js ghi rằng khi cổng là 0, hệ điều hành tự chọn một cổng trống.

src/server.test.ts
// Test đặc tả: chụp lại hành vi HIỆN TẠI của server.ts trước khi dọn code.
// Không hỏi "code có đúng không", chỉ hỏi "sau khi dọn, máy chủ có trả lời y như trước không".
import type { AddressInfo } from "node:net";
import { afterAll, beforeAll, expect, test } from "vitest";

let baseUrl = "";
let close = () => {};

beforeAll(async () => {
  process.env.PORT = "0"; // cổng 0: hệ điều hành chọn một cổng trống, không đụng máy chủ đang chạy ở 3000
  const { server } = await import("./server.ts");
  if (!server.listening) await new Promise((resolve) => server.once("listening", resolve));
  baseUrl = `http://localhost:${(server.address() as AddressInfo).port}`;
  close = () => server.close();
});

afterAll(() => close());

test("GET /don-hang trả danh sách đơn đúng như trước khi dọn", async () => {
  const response = await fetch(`${baseUrl}/don-hang`);
  expect(response.status).toBe(200);
  expect(response.headers.get("content-type")).toBe("application/json; charset=utf-8");
  expect(await response.json()).toEqual([
    { id: 1041, khach: "NGUYỄN AN", tong: 83000 },
    { id: 1042, khach: "TRẦN BÌNH", tong: 32000 },
    { id: 1043, khach: "KHÁCH TẠI QUẦY", tong: 25000 },
    { id: 1044, khach: "LÊ CHI", tong: 70000 },
  ]);
});

test("đường dẫn khác trả 404", async () => {
  const response = await fetch(`${baseUrl}/khong-co`);
  expect(response.status).toBe(404);
});

Mình dùng import() bên trong beforeAll thay cho dòng import ở đầu file, vì import ở đầu file chạy trước mọi dòng code khác, tức là trước khi PORT kịp được đặt thành 0. Bốn đơn trong test là bốn đơn thật trong data/orders.json, chép từ output ở trên.

npx vitest run src
 Test Files  2 passed (2)
      Tests  17 passed (17)

Lúc chạy, Vitest in kèm dòng log của máy chủ, và dòng đó làm mình bật cười:

stdout của test
Đơn hàng Mộc: http://localhost:0/don-hang

Log in ra cổng mình yêu cầu chứ không in cổng thật mà hệ điều hành chọn. Đó là một lỗi nhỏ, nhưng sửa nó thì hành vi đổi, nên nó không thuộc lần refactor này. Mình ghi lại để làm ở một commit riêng.

Commit đầu tiên của nhánh refactor/don-server-ts chỉ chứa file test này. Nhờ vậy, ai đọc lịch sử cũng thấy test có trước, và có thể checkout commit đó để tự xác nhận test xanh trên code cũ.

Đặt tên biến thế nào cho người sau đọc hiểu?

Giờ mới tới phần đổi tên. Mình đi qua từng tên anh Khoa nhắc, và với mỗi tên, mình tự hỏi một câu: người đọc cần biết gì về giá trị này mà cái tên hiện tại không nói?

Tên body chỉ nói vị trí của giá trị trong một response HTTP, phần thân. Nó không nói thân đó chứa gì. Người đọc phải nhìn sang vế phải mới biết đây là danh sách đơn đã đổi thành chuỗi JSON. Mình đổi nó thành orderListJson, nghĩa là danh sách đơn dạng JSON. Tên dài hơn, nhưng đọc tới cuối dòng .end(orderListJson) là hiểu máy chủ gửi đi cái gì.

Cặp rawOrders và orders khó hơn, vì chúng là cùng một dữ liệu ở hai trạng thái. "Raw" là thô, nhưng thô theo nghĩa nào? Mình đọc kiểu RawOrder mới biết nó nghĩa là "đơn như được lưu trong file, giá có thể là chuỗi". Mình đổi rawOrders thành ordersInFile, đơn như trong file, vì tên đó nói luôn nguồn gốc của nó. Còn orders thì mình giữ, và ở phần tách hàm bên dưới bạn sẽ thấy vì sao giữ được.

Tên dataFile thì không sai, nhưng hệ thống sau này sẽ có thêm file khác, như file menu hay file khách hàng. Mình đổi thành ORDERS_FILE. Phần viết hoa là quy ước phổ biến trong JavaScript cho một hằng số cấu hình đặt ở đầu module, để người đọc biết giá trị này cố định suốt lúc chạy.

req và res là hai tên viết tắt rất quen trong code Node.js, nên mình phân vân khá lâu. Cuối cùng mình đổi thành request và response, vì phần còn lại của dự án không viết tắt chỗ nào, và một bạn mới học sẽ không phải đoán. Nếu nhóm của bạn đã quen req/res thì giữ nguyên cũng không sao. Thống nhất trong cả dự án quan trọng hơn chọn đúng một kiểu.

Cuối cùng, orders.ts có một hàm nhỏ tên hienThi. Nó là tên tiếng Việt duy nhất trong một file mà mọi tên khác đều bằng tiếng Anh, và "hiển thị" cũng không nói hàm làm gì. Hàm này bọc chuỗi trong ngoặc kép và giữ nguyên số, để thông báo lỗi phân biệt được giá "35" (chuỗi) với 35 (số). Mình đổi thành quoteIfText, tức "thêm ngoặc kép nếu là chữ". Tên hàm bắt đầu bằng động từ để người đọc biết nó làm một việc, chứ không phải chứa một giá trị.

Nếu gom lại thành vài câu hỏi để tự kiểm tra khi đặt tên, mình dùng bốn câu sau:

  1. Đọc mỗi cái tên, không nhìn vế phải, mình có đoán được nó chứa gì không?
  2. Nếu cùng một dữ liệu xuất hiện ở hai trạng thái, hai cái tên có nói rõ trạng thái nào là trạng thái nào không?
  3. Tên hàm có bắt đầu bằng động từ, và động từ đó có đúng là việc hàm làm không?
  4. Tên mới có cùng ngôn ngữ và cùng kiểu viết với phần còn lại của dự án không?

Đổi tên "tiện tay" và lần test đỏ đầu tiên

Trong lúc đổi tên, mình nhớ ra bình luận số 11 của anh Khoa ở bài trước. Hàm listOrders trả về key tiếng Việt khach và tong, trong khi phần còn lại của code đặt tên bằng tiếng Anh. Đang đổi tên mà, sao không đổi luôn thành customer và total? Mình sửa hai dòng trong listOrders rồi chạy test:

npx vitest run src (sau khi đổi khach/tong)
 FAIL  src/orders.test.ts > dữ liệu thật trong data/orders.json > tổng tiền của cả bốn đơn
AssertionError: expected [ { id: 1041, tong: undefined }, …(3) ] to deeply equal [ { id: 1041, tong: 83000 }, …(3) ]

 FAIL  src/server.test.ts > GET /don-hang trả danh sách đơn đúng như trước khi dọn
- Expected
+ Received

  [
    {
+     "customer": "NGUYỄN AN",
      "id": 1041,
-     "khach": "NGUYỄN AN",
-     "tong": 83000,
+     "total": 83000,
    },

Mình cắt bớt output, vì phần sau lặp lại y hệt cho ba đơn còn lại. Hai test cùng đỏ, và cả hai đều đúng. Đổi tên một biến bên trong hàm thì không ai bên ngoài thấy. Còn khach và tong là tên của các trường trong JSON mà máy chủ trả ra. Trang quản lý đơn của quán đang đọc đúng hai cái tên đó. Đổi chúng là đổi hành vi bên ngoài, tức là đổi API, và bước đó cần báo cho người làm trang quản lý, cần thời gian chuyển tiếp, và cần một pull request riêng.

Mình hoàn tác hai dòng đó. Ranh giới mà lần test đỏ này vạch ra rất rõ: tên bên trong thì tự do đổi, còn tên mà code khác dựa vào thì là một phần của hành vi. Nếu chưa có server.test.ts, mình đã đẩy thay đổi đó lên và trang quản lý của quán sẽ hiện ra một cột tổng tiền trống.

Sau khi hoàn tác, 17 test xanh trở lại, và mình commit bước đổi tên. Commit này chỉ đổi tên, chưa di chuyển dòng code nào:

git diff (phần server.ts trong commit đổi tên)
-const dataFile = new URL("../data/orders.json", import.meta.url);
+const ORDERS_FILE = new URL("../data/orders.json", import.meta.url);
 // Chuẩn hóa ngay khi đọc file: từ đây trở đi, giá luôn là số.
-const rawOrders: RawOrder[] = JSON.parse(readFileSync(dataFile, "utf8"));
-const orders = rawOrders.map(normalizeOrder);
+const ordersInFile: RawOrder[] = JSON.parse(readFileSync(ORDERS_FILE, "utf8"));
+const orders = ordersInFile.map(normalizeOrder);

-export const server = createServer((req, res) => {
-  if (req.url === "/don-hang") {
+export const server = createServer((request, response) => {
+  if (request.url === "/don-hang") {
     try {
-      const body = JSON.stringify(listOrders(orders), null, 2);
-      res.writeHead(200, { "content-type": "application/json; charset=utf-8" }).end(body);
+      const orderListJson = JSON.stringify(listOrders(orders), null, 2);
+      response.writeHead(200, { "content-type": "application/json; charset=utf-8" }).end(orderListJson);

Mình tách đổi tên và di chuyển code thành hai commit vì người review đọc chúng theo hai cách khác nhau. Một commit chỉ đổi tên thì đọc rất nhanh, vì mỗi dòng đỏ có đúng một dòng xanh tương ứng. Nếu trộn với việc di chuyển code, diff sẽ thành một khối lớn mà người đọc phải so từng ký tự.

Tách hàm theo câu hỏi "ai cần thay đổi chỗ này?"

Phần thứ hai trong ghi chú của anh Khoa là server.ts làm nhiều việc. Mình đếm thì thấy ba việc: đọc và chuẩn hóa file, trả lời request HTTP, và khởi động (chọn file, chọn cổng, mở cổng). Ba việc này thay đổi vì ba lý do khác nhau. Khi quán đổi từ file JSON sang database, chỉ việc thứ nhất đổi. Khi thêm một đường dẫn mới, chỉ việc thứ hai đổi. Khi đưa lên máy chủ thật với cổng khác, chỉ việc thứ ba đổi.

Mình tách mỗi việc vào một chỗ riêng. Việc đọc file vào order-file.ts:

src/order-file.ts
import { readFileSync } from "node:fs";

import { normalizeOrder, type Order, type RawOrder } from "./orders.ts";

// Đọc file đơn hàng và chuẩn hóa ngay: từ đây trở đi, giá luôn là số.
// Một giá sai làm cả hàm ném lỗi, nên máy chủ dừng thay vì bỏ qua đơn.
export function loadOrders(file: URL): Order[] {
  const ordersInFile: RawOrder[] = JSON.parse(readFileSync(file, "utf8"));
  return ordersInFile.map(normalizeOrder);
}

Đây là lý do orders không cần đổi tên. Trước đây đơn "trong file" và đơn "đã chuẩn hóa" sống cạnh nhau trong cùng một phạm vi, nên tên phải phân biệt được chúng. Giờ ordersInFile chỉ sống bên trong loadOrders, và mọi chỗ khác trong dự án chỉ thấy đơn đã chuẩn hóa. Khi trong một phạm vi chỉ còn một loại đơn, cái tên ngắn orders là đủ rõ. Tách hàm thường làm việc đặt tên dễ hơn theo đúng cách đó.

Việc trả lời request vào order-server.ts. Hàm createOrderServer nhận danh sách đơn qua tham số thay vì tự đọc file, và trả về máy chủ mà không tự mở cổng:

src/order-server.ts
import { createServer, type Server, type ServerResponse } from "node:http";

import { listOrders, type Order } from "./orders.ts";

const JSON_HEADERS = { "content-type": "application/json; charset=utf-8" };
const TEXT_HEADERS = { "content-type": "text/plain; charset=utf-8" };

// Máy chủ HTTP cho danh sách đơn. Nhận đơn đã chuẩn hóa, không tự đọc file, không tự mở cổng.
export function createOrderServer(orders: Order[]): Server {
  return createServer((request, response) => {
    if (request.url === "/don-hang") return sendOrderList(response, orders);
    response.writeHead(404).end();
  });
}

function sendOrderList(response: ServerResponse, orders: Order[]): void {
  try {
    const orderListJson = JSON.stringify(listOrders(orders), null, 2);
    response.writeHead(200, JSON_HEADERS).end(orderListJson);
  } catch (error) {
    console.error(error);
    response.writeHead(500, TEXT_HEADERS).end("Lỗi máy chủ");
  }
}

Phần xử lý /don-hang được tách thành hàm sendOrderList, nên phần định tuyến chỉ còn hai dòng. Đọc createOrderServer là thấy ngay máy chủ có những đường dẫn nào, mà không phải lướt qua khối try/catch. Khi thêm một đường dẫn mới, bạn thêm một dòng if và một hàm send... mới.

Còn server.ts chỉ giữ việc khởi động:

src/server.ts (sau khi dọn)
// Điểm khởi động: đọc dữ liệu, dựng máy chủ, mở cổng. Không còn logic nào khác ở đây.
import { loadOrders } from "./order-file.ts";
import { createOrderServer } from "./order-server.ts";

const ORDERS_FILE = new URL("../data/orders.json", import.meta.url);
const port = Number(process.env.PORT ?? 3000);

export const server = createOrderServer(loadOrders(ORDERS_FILE));
server.listen(port, () => console.log(`Đơn hàng Mộc: http://localhost:${port}/don-hang`));

Mình giữ export const server với đúng cái tên cũ, vì server.test.ts đang import nó. Đó là điều kiện để test đặc tả chạy được trên cả code cũ lẫn code mới mà không sửa dòng nào. Nếu phải sửa test để nó xanh sau refactor, test đó không còn chứng minh được gì nữa.

npx vitest run src (sau khi tách)
 Test Files  2 passed (2)
      Tests  17 passed (17)

Mình chạy máy chủ thật thêm một lần và gọi /don-hang. Kết quả giống hệt danh sách ở đầu bài, từ thứ tự trường tới tên khách viết hoa.

Tách xong thì viết được những test nào mới?

Đây là phần mình thích nhất của lần dọn này. Trước khi tách, muốn test máy chủ thì phải chạy nó với đúng file data/orders.json. Muốn thử một đơn có giá sai, mình phải sửa file dữ liệu thật. Giờ hai hàm đều nhận dữ liệu từ bên ngoài, nên mình đưa vào gì cũng được.

Test cho loadOrders ghi một file tạm, để thử với dữ liệu bất kỳ mà không đụng file thật. Test thứ hai khóa lại quyết định ở bình luận số 6 của bài trước, rằng máy chủ phải dừng khi gặp giá sai chứ không bỏ qua đơn:

src/order-file.test.ts
import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { expect, test } from "vitest";

import { loadOrders } from "./order-file.ts";

// Ghi một file đơn hàng tạm, để thử loadOrders với dữ liệu bất kỳ mà không đụng data/orders.json.
function tempOrdersFile(orders: unknown): URL {
  const file = join(mkdtempSync(join(tmpdir(), "don-hang-")), "orders.json");
  writeFileSync(file, JSON.stringify(orders));
  return pathToFileURL(file);
}

test("đọc file và đổi giá dạng chuỗi thành số", () => {
  const file = tempOrdersFile([
    { id: 2001, createdAt: "2026-11-13T08:00:00+07:00", source: "may-cu", items: [{ name: "Bạc xỉu", price: "32.000", quantity: 1 }] },
  ]);
  expect(loadOrders(file)[0].items[0].price).toBe(32000);
});

test("một giá sai làm cả file bị từ chối, máy chủ không khởi động với dữ liệu hỏng", () => {
  const file = tempOrdersFile([
    { id: 2002, createdAt: "2026-11-13T08:05:00+07:00", source: "may-cu", items: [{ name: "Cà phê muối", price: 35, quantity: 2 }] },
  ]);
  expect(() => loadOrders(file)).toThrow("Giá không hợp lệ: 35");
});

Test cho createOrderServer dựng máy chủ với đúng những đơn mà từng test cần. Test thứ ba thử nhánh catch trả lỗi 500, một nhánh mà trước đây không cách nào chạm tới bằng dữ liệu thật:

src/order-server.test.ts
import type { AddressInfo } from "node:net";
import { expect, test } from "vitest";

import { createOrderServer } from "./order-server.ts";
import type { Order } from "./orders.ts";

// Dựng máy chủ với đúng những đơn cần cho từng test, mở ở một cổng trống, gửi một request rồi đóng.
async function get(orders: Order[], path: string): Promise<Response> {
  const server = createOrderServer(orders);
  await new Promise<void>((resolve) => server.listen(0, resolve));
  try {
    return await fetch(`http://localhost:${(server.address() as AddressInfo).port}${path}`);
  } finally {
    server.close();
  }
}

const counterOrder: Order = {
  id: 2003,
  createdAt: "2026-11-13T08:10:00+07:00",
  source: "may-cu",
  items: [{ name: "Cà phê đen", price: 25000, quantity: 2 }],
};

test("không có đơn nào thì trả danh sách rỗng", async () => {
  const response = await get([], "/don-hang");
  expect(await response.json()).toEqual([]);
});

test("đơn tại quầy không có khách hàng vẫn hiện được", async () => {
  const response = await get([counterOrder], "/don-hang");
  expect(await response.json()).toEqual([{ id: 2003, khach: "KHÁCH TẠI QUẦY", tong: 50000 }]);
});

test("dữ liệu khách hàng hỏng thì trả 500, không làm sập máy chủ", async () => {
  // Khách hàng có số điện thoại nhưng thiếu tên: customerLabel gọi toUpperCase trên undefined.
  const brokenOrder = { ...counterOrder, customer: { phone: "0901000009" } } as unknown as Order;
  const response = await get([brokenOrder], "/don-hang");
  expect(response.status).toBe(500);
  expect(await response.text()).toBe("Lỗi máy chủ");
});
npx vitest run src
 Test Files  4 passed (4)
      Tests  22 passed (22)

Test mới thì cũng cần được kiểm tra. Như bài về Vitest đã làm, mình cố tình làm hỏng code để xem test có đỏ không. Mình sửa loadOrders để nó trả nguyên dữ liệu trong file mà không chuẩn hóa, tức là xóa đúng dòng quan trọng nhất của hàm:

npx vitest run src/order-file.test.ts (loadOrders không chuẩn hóa)
   × đọc file và đổi giá dạng chuỗi thành số 7ms
   × một giá sai làm cả file bị từ chối, máy chủ không khởi động với dữ liệu hỏng 3ms
AssertionError: expected '32.000' to be 32000 // Object.is equality
AssertionError: expected [Function] to throw an error

Cả hai test đỏ, mỗi test vì đúng lý do của nó. Mình trả lại dòng đó và commit.

Bốn commit, và vì sao chia nhỏ như vậy

Lịch sử của nhánh sau khi dọn trông như sau:

git log --oneline main..HEAD
1930a11 test: thêm test cho loadOrders và createOrderServer
1daea44 refactor: tách việc đọc file và dựng máy chủ khỏi server.ts
1a3e69a refactor: đặt tên biến nói rõ nó chứa gì
d9cd257 test: chụp lại hành vi của server.ts trước khi dọn

Mỗi commit để test xanh. Nếu một ngày ai đó phát hiện máy chủ trả lời khác đi, họ dùng git bisect hoặc checkout từng commit là tìm ra ngay bước gây lỗi. Hai commit refactor có tiền tố refactor: để người review biết trước rằng họ không cần tìm thay đổi hành vi trong đó, chỉ cần kiểm tra code có dễ đọc hơn không.

Bạn cũng để ý là lỗi log in "localhost:0" chưa được sửa. Nó sẽ là một commit fix: riêng, nhỏ và dễ đọc, sau khi nhánh refactor được duyệt. Trộn một thay đổi hành vi vào giữa các commit refactor là cách nhanh nhất để người review bỏ sót nó.

Refactor đến đâu thì dừng?

Còn nhiều chỗ mình có thể dọn tiếp. listOrders vẫn trả key tiếng Việt. customerLabel vẫn sập khi khách thiếu tên, và test mới chỉ chứng minh rằng máy chủ không sập theo. Mình dừng ở đây vì ghi chú của anh Khoa nói về server.ts, và pull request này đã làm xong đúng việc đó. Một pull request refactor càng lớn thì càng khó review, và càng dễ có người nói "để đó tuần sau đọc".

Nếu muốn làm lại toàn bộ trên máy, bạn cần thư mục dự án với data/orders.json và src/orders.ts từ các bài trước, rồi tạo bốn file mới theo đúng code trong bài: server.test.ts, order-file.ts, order-server.ts và hai file test. Máy cần Node.js 22.18 trở lên để chạy thẳng file .ts, và Vitest cài bằng npm install -D vitest.

Đơn hàng sắp đến từ một nơi khác

Code giờ đã dễ đọc, và nhờ createOrderServer nhận đơn qua tham số, thêm một đường dẫn mới không còn là việc đáng sợ. Đúng lúc đó, chủ quán Mộc nhắn: website của quán sắp gửi đơn trực tiếp tới máy chủ này qua một request POST, thay vì đợi ai đó xuất file JSON mỗi sáng.

Câu hỏi đầu tiên mình nghĩ tới là dữ liệu gửi lên sẽ trông như thế nào. Máy bán hàng cũ đã từng gửi giá dạng "35.000". Website mới có thể gửi thiếu trường, gửi số lượng là chuỗi, hay gửi một mảng rỗng. parsePrice chỉ kiểm tra được giá, còn phần còn lại của đơn thì chưa ai kiểm tra. Kiểm tra cả một đơn hàng gửi lên mà không phải viết hàng chục câu if là việc của các bài viết sau.

Bạn theo dõi cả câu chuyện ở series Nhận dự án cũ. Nếu đang ôn phỏng vấn, các câu hỏi về code sạch và làm việc nhóm có trong ngân hàng câu hỏi phỏng vấn.

Nhận bài tiếp theo qua email.

Một email mỗi tuần, chỉ khi có điều đáng đọc. Không spam.