Bất biến (immutability) và tránh side effect: viết code ít bug hơn

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Một lớp lỗi rất khó tìm là khi dữ liệu bị thay đổi âm thầm ở một nơi nào đó trong chương trình, và nơi khác dùng nó nhận kết quả bất ngờ. Hai khái niệm giúp phòng tránh: tính bất biến (immutability) và hạn chế side effect (tác dụng phụ). Bài này giải thích và đưa ra ví dụ dễ áp dụng.

Side effect là gì?

Một hàm có side effect khi nó làm gì đó ngoài việc trả về giá trị: thay đổi biến bên ngoài, sửa đối số truyền vào, ghi file, gọi mạng, in ra màn hình. Không phải side effect nào cũng xấu, chương trình hữu ích phải có (ghi database, hiển thị). Vấn đề là những side effect ẩn, bất ngờ.

// Có side effect ẩn: sửa trực tiếp mảng truyền vàofunction addTax(prices) {for (let i = 0; i < prices.length; i++) {prices[i] = prices[i] * 1.1;}return prices;}const original = [100, 200];const withTax = addTax(original);console.log(original); // [110, 220] - mảng gốc bị đổi!

Ai đó dùng original ở chỗ khác sẽ nhận giá trị đã bị đổi mà không hiểu vì sao.

Hàm thuần (pure function)

Hàm thuần có hai đặc điểm: với cùng đầu vào luôn cho cùng đầu ra, và không gây side effect. Phiên bản thuần của ví dụ trên:

function addTax(prices) {return prices.map(price => price * 1.1);}const original = [100, 200];const withTax = addTax(original);console.log(original); // [100, 200] - nguyên vẹnconsole.log(withTax);  // khoảng [110, 220]

Hàm thuần dễ test (đưa vào, kiểm tra kết quả), dễ hiểu (chỉ cần xem tham số), dễ tái sử dụng và an toàn khi chạy song song.

Bất biến: không sửa dữ liệu, tạo bản mới

Thay vì sửa đối tượng hay mảng tại chỗ, bạn tạo bản sao với thay đổi. Trong JavaScript:

const user = { name: 'An', age: 20, address: { city: 'Hà Nội' } };// Cập nhật thuộc tính bằng spread (bản sao nông)const olderUser = { ...user, age: 21 };// Thêm, xóa phần tử mảng không đổi mảng gốcconst list = [1, 2, 3];const added = [...list, 4];const removed = list.filter(n => n !== 2);const doubled = list.map(n => n * 2);

Lưu ý spread chỉ sao chép nông: user.address vẫn là cùng một đối tượng được chia sẻ giữa hai bản. Với cấu trúc lồng nhau, bạn phải sao chép cả tầng bên trong cần đổi (hoặc dùng structuredClone cho bản sao sâu, hay thư viện chuyên dụng).

const moved = {...user,address: { ...user.address, city: 'Đà Nẵng' },};

Công cụ ngôn ngữ hỗ trợ bất biến

  • JavaScript: const chỉ ngăn gán lại biến, không ngăn sửa nội dung. Object.freeze() khóa nông một đối tượng. Phương thức map, filter, slice, concat trả bản mới, còn push, splice, sort, reverse sửa tại chỗ (chú ý!).
  • TypeScript: readonly và ReadonlyArray giúp trình biên dịch bắt lỗi sửa nhầm.
  • Python: kiểu tuple, frozenset, @dataclass(frozen=True) là bất biến. Cẩn thận với đối số mặc định là list hoặc dict (dùng chung giữa các lần gọi).
  • Java: từ khóa final, record (từ phiên bản mới), danh sách không thể sửa như List.of.
  • Kotlin, Swift, Rust: ưu tiên val, let, mặc định bất biến.
from dataclasses import dataclass@dataclass(frozen=True)class Point:x: inty: intp = Point(1, 2)# p.x = 5  # gây lỗi FrozenInstanceError

Cạm bẫy kinh điển: đối số mặc định có thể sửa trong Python

# Sai: danh sách mặc định được tạo một lần và dùng chungdef add_item(item, items=[]):items.append(item)return items# Đúngdef add_item(item, items=None):if items is None:items = []return items + [item]

Khi nào bất biến thật sự đáng giá?

  1. Quản lý trạng thái giao diện: các thư viện như React dựa vào so sánh tham chiếu, sửa tại chỗ khiến giao diện không cập nhật.
  2. Chương trình đa luồng hoặc bất đồng bộ: dữ liệu không đổi thì không cần khóa và không có tranh chấp.
  3. Cần lịch sử và hoàn tác: giữ các phiên bản trạng thái cũ dễ dàng.
  4. Dễ suy luận và debug: giá trị không đổi nên nhìn vào chỗ khai báo là biết nó là gì.

Cân bằng thực tế

Không cần cực đoan. Sao chép dữ liệu lớn liên tục có thể tốn bộ nhớ và thời gian, trong vòng lặp hiệu năng cao, sửa tại chỗ cục bộ (biến chỉ tồn tại trong hàm) là chấp nhận được. Nguyên tắc thực dụng: đừng sửa dữ liệu mà bạn không sở hữu (đối số, trạng thái chia sẻ); cô lập các side effect ở rìa chương trình (đọc ghi file, mạng, database) và giữ phần logic ở giữa là hàm thuần.

Kết luận

Ưu tiên hàm thuần, tạo bản mới thay vì sửa tại chỗ và để side effect nằm ở ranh giới hệ thống. Chỉ vài thói quen này đã loại bỏ được cả một lớp bug khó chịu. Hãy rà lại code của bạn xem có hàm nào đang âm thầm sửa đối số hay không.

Nếu bạn muốn học JavaScript, Python hay tư duy lập trình nền tảng với bài tập thực hành, hãy xem qua codebem.com.

author_photo
Nguyễn Đình Cường

0 comment