Coding

Phân biệt giữa readableunderstandable
plain text human có thể đọc được nhưng chưa chắc hiểu được.
Cố gắng biểu diễn dữ liệu dưới dạng human có thể đọc hiểu, là tốt nhất. Sử dụng binary format hay các định dạng đặc biệt yêu cần người đọc cần sử dụng tool cần thiết để mở thì nó hơi bất tiện.

Debug Mindset

  • Mình tìm lỗi để fix chứ không phải để đổ lỗi.
  • Bình tĩnh, nếu cảm thấy mệt hãy nghỉ ngơi, thư giãn rồi quay lại làm việc. Tránh căng thẳng rồi lại phát sinh thêm lỗi khác, hoặc không tìm ra vấn đề.
  • Cố gắng suy nghĩ theo hướng nguyên nhân phát sinh ra lỗi. Lưu ý, lỗi show ra chưa chắc là nguyên nhân chính.
    • Ví dụ: hàm báo lỗi khi param đầu vào bị undefined. Không nên lao vào sửa lỗi ngay bằng cách đặt if not undefined, mà hãy tìm hiểu xem tại sao input lại bị undefined, như vậy có đúng với flow không, hay là lỗi từ các bước phía trước đó.
  • Don't Assume It - Prove It: ngoài việc fix lỗi, cần đặt ra câu hỏi tại sao lỗi này trước kia không xuất hiện?? Cần fix tận gốc vấn đề thay vì chỉ fix bề nổi.
  • Nếu việc fix bug tốn nhiều thời gian, thì nên tìm cách để tránh trường hợp này xảy ra lần sau, bằng cách đưa nó vào test case, hoặc làm cách nào đó khác? viết assertion? Document lại?

Automation

  • Để tăng hiệu suất công việc, mình có thể viết các scripts nhỏ hoặc tool để hỗ trợ. Nhưng cần lưu ý về thời gian/công sức cho nó. Dành một ngày để viết một component quan trọng cho project thì cũng chấp nhận được, nhưng dành cả tuần thì không tốt lắm. Cần đánh giá xem nó có đáng hay không? scripts đó, tool đó, giúp giảm bao nhiêu % công việc, bao lâu?

Engineering Daybooks

Có thể là một cuốn note, ghi lại hôm đó mình làm đã làm gì, vấn đề mình gặp phải là gì, ý tưởng. Đặt hết những gì mình suy nghĩ xuống giấy sẽ giúp mình giải phóng bộ nhớ não, giúp não bớt căng thẳng, bớt tốn năng lượng để switch từ task này qua task kia (lúc switch nó phải tìm lại ký ức đó), nếu mình để ra giấy giống như mình đã gán nó một địa chỉ sẽ dễ nhớ hơn.

Kỹ năng quan trọng: biết phải note cái gì, note như thế nào. Cái này thì tùy mỗi cá nhân, từ kinh nghiệm bản thân, từ kiến thức của mình cái nào không biết cái nào biết quá rõ, rồi chọn lọc mà note xuống.

Ngoài ra, lợi ích của việc ghi xuống note giúp mình có được thông tin chính xác hơn. Ký ức có thể bị thay đổi (mà chính mình cũng không thể nhận ra được). Theo nghiên cứu (đọc ở đâu đó, quên rồi) trí nhớ con người giới hạn nên nó sẽ chọn lọc lưu trữ, không lưu hết toàn bộ chi tiết, và não mình sẽ tự động điền vào chỗ trống, do đó ký ức có thể sẽ khác với hiện thực.

Hãy thử một thời gian, để xem nó có hiệu quả và tìm cách phù hợp với bản thân.

Design by Contract

Code cho máy hiểu được thì đơn giản, code cho người đọc hiểu được mới khó.
Sự tương tác, kết hợp người với người là điều khó khăn, làm sao mỗi người biết được mình nên làm gì, ai chịu trách nhiệm phần nào, nên nó sinh ra cái gọi là contract.

A contract defines your rights and responsibilities
Một contract sẽ cho biết quyền và nghĩa vụ của bạn

Trong software development, làm sao để biết chương trình đó đúng? Nó đúng là khi nó thỏa các điều kiện được đặt ra, không hơn không kém.

Kém hơn thì tất nhiên là không đúng rồi. Nhưng tại sao hơn thì không được?

Đối với cái hơn, làm sao mình biết là nó đúng hay không? có lợi hay có hại? nếu nó không có trong yêu cầu thì làm sao để đánh giá? Nó có thể tốt hơn, nhưng cũng có thể không đúng với mong muốn của người ta. Do đó hơn cũng không được.

#TODO: so sánh giữa DBC và TDD và Defensive programming

Dead Programs Tell No Lies

Một assumption thường hay bị mắc phải là nghĩ Nó không thể xảy ra. Điều gì dẫn tới việc kết luận như vậy? Có dẫn chứng gì không? Mình có kiểm soát được nó hay không? hay là một module khác kiểm soát điều đó --> vậy là nó vẫn có thể xảy ra đối với mình.
Nếu tuân thủ DRY - và viết unit test cho module đó mình sẽ xác nhận được là nó có thể xảy ra hay không, nếu input đó vẫn hợp lệ khi truyền vào, mà code mình không handle thì đó là lúc mình bị lừa bởi chính mình, điều không thể xảy ra nhưng thực sự vẫn có thể xảy ra.

Assertive Programming

Khi mình cảm thấy đoạn này nó không để sai, dữ liệu đầu vào nó luôn là format như này. Thì hãy đặt assertion tại đó để kiểm tra. Nếu có lỗi nó sẽ cho mình biết ngay là assumption mình bị sai.
Trong produciton, vẫn nên để assertion bật, không nên chỉ bật trong lúc dev hay test. Lỗi có thể đến từ dữ liệu hoặc một chuỗi hành động nào đó, do vậy vẫn nên để + kết hợp logging mình có thể biết được và tracking issue.

Một số ví dụ hãy xem thử những điều dưới đây có phải là không thể xảy ra không?

  1. Tồn tại một tháng có ít hơn 28 ngày: có tồn tại một tháng ít hơn 28 ngày là tháng 2 năm 1918 của Nga, do đỗi lịch từ A sang lịch B. Khi đó, ngày liền kề sau ngày 31 tháng 1 năm 1918 là ngày 14 tháng 2 năm 1918.
  2. Lỗi từ hệ thống: không thể kết nối directory hiện tại: folder bị delete
  3. Trong C++: a = 2; b = 3 nhưng (a+b) không bằng 5: overloading operation
  4. Một tam giác có tổng góc trong khác 180 độ.
  5. Một phút không có 60 giây.
  6. (a + 1) <= a: tràn số

Decoupled

Luôn giữ tư tưởng, yêu cầu, thuật toán, cách implement luôn thay đổi. Do đó mình cần code làm sao để dễ đáp ứng với thay đổi. Thì Decoupled Code sẽ giúp dễ thay đổi.

Nhận diện vấn đề coupling:

  • Tồn tại dependency giữa 2 module không liên quan tới nhau.
  • "Thay đổi nhỏ" của một module yêu cầu thay đổi các module không liên quan khác, hoặc tạo ra lỗi ở module khác.
  • Developer ngại thay đổi code do không chắc thay đổi này có ảnh hưởng module hoặc phần nào khác không.
  • Yêu cầu họp với mọi người do không biết đoạn thay đổi này sẽ ảnh hưởng phần nào của khác của code.

Một số loại coupling:

  • Train wrecks: chuỗi các method call
  • Globalization: static và singleton
  • Inheritance: kế thừa và subclass nguy hiểm thế nào
Train wrecks
public void applyDiscount(customer, order_id, discount) 
{ 
	totals = customer.orders.find(order_id).getTotals(); 
	totals.grandTotal = totals.grandTotal - discount; 
	totals.discount = discount; 
}

Đoạn code trên là để apply discount vào đơn hàng của customer. Đầu tiên là cần tìm order của khách hàng theo order_id, tiếp đến là lấy total rồi apply discount theo logic vào grandTotaldiscount.
Như vậy là mình đang để hàm này phụ thuộc vào implement của totals object. Nếu grandTotal hoặc discount field thay đổi, mình cũng sẽ phải thay đổi ở đây. Việc apply discount đang được đưa ra ngoài scope của đơn hàng. Vậy thì làm sao? Nên đưa nó vào đúng scope của nó. Nó làm thay đổi totals của đơn hàng thì nên chuyển tạo một hàm applyDiscount() trong total object.

Ví dụ:

class Total:
	public void applyDiscount(discount)
	{
		this.discount = discount;
		grandTotal -= this.discount;
	}
	
public void applyDiscount(customer, order_id, discount) 
{ 
	customer.orders.find(order_id).getTotals().applyDiscount(discount);
}

Như vậy, mình có thể loại bỏ implement detail của apply discount ra khỏi hàm trên và đưa về đúng scope của nó.
Ngoài ra, còn có getTotals client (người sử dụng hàm applyDiscount) họ có orderID và muốn apply discount vào order đó, thì họ không nên biết về việc phải lấy totals ra rồi apply vào. Mà họ chỉ cần gọi order.applyDiscount(discount). Do đó có thể refactor lại thành:

class Total:
	public void applyDiscount(discount)
	{
		this.discount = discount;
		grandTotal -= this.discount;
	}

class Order:
	public void applyDiscount(discount)
	{
		getTotals().applyDiscount(discount);
	}
public void applyDiscount(customer, order_id, discount) 
{ 
	customer.orders.find(order_id).applyDiscount(discount);
}

Tới bước này có thể dừng được rồi, do customer và orders là hai thực thể lớn cố định, nên sẽ không thường xuyên thay đổi. Trong customer luôn có orders do đó không cần thiết phải thay orders.find ở bước này. Đây cũng là một phần trong việc biết thế nào là đủ.
Tuân thủ quá strict theo rule mà không nghĩ về context sẽ khiến nó bị over.

Globalization

Biến global hay static là một loại coupling mà mình thường bỏ sót. Do nó toàn cục, nên nó có thể được thay đổi ở bất kì đâu, và có thể là dependency của nhiều module khác, thay đổi nó sẽ khiến mình phải sửa ở nhiều nơi.
Một khó chịu của việc sử dụng global nữa là khi viết test, do module có sử dụng biến/data global thì khi setup test mình cũng phải initialize biến/data global đó, dẫn đến code phức tạp lên, tăng nổi khổ cho việc viết test.

Inheritance

Kế thừa cùng là một dạng coupling. Khi based class phải thay đổi, nó sẽ khiến toàn bộ các class con cũng bị thay đổi.
Và chuỗi kế thừa class A kế thừa từ class B và class C, đồng nghĩa với việc A đang phụ thuộc vào B, và C.
Nên có thể dùng virtual class hoặc interface thay vì dùng kế thừa trực tiếp. Việc dùng virtual hoặc interface bắt buộc class con phải implement lại method của nó, giúp giảm việc coupling với class cha.

[!INFO] Tóm lại dù là phương pháp nào, best practice nào, mục tiêu vẫn là làm sao code có thể dễ dàng thay đổi, và đảm bảo không phát sinh lỗi khi thay đổi.