Bạn là một lập trình viên, bạn có nghĩ rằng một lập trình viên giỏi chỉ cần viết ra một chương trình vừa chạy nhanh vừa tiết kiệm được tài nguyên hệ thống là đủ. Bài viết là một vài chia sẻ của tác giả về phong cách lập trình để giúp lập trình tốt hơn.
Trải nghiệm La Kiến Vinh 2014-06-11 17:38:37

Giới thiệu

Năm 2012, tôi và một số người bạn cùng nhau phát triển một ứng dụng. Vai trò của tôi là quản lý kiến trúc đề xuất giải pháp cho ứng dụng và tôi không tham gia trực tiếp vào lập trình. Sau thời gian phát triển 2 tháng, ứng dụng của chúng tôi dường như tiến triển rất tốt, giao diện không đẹp nhưng cũng dễ nhìn, không chiếm nhiều tài nguyên của hệ thống, hiệu năng tốt. Tuy nhiên, khi cần thiết mở rộng, thay đổi hoặc bảo trì tốn kém nhiều thời gian hơn do mã nguồn không sạch và kiến trúc không tốt.

Trong phần còn lại của bài viết, tôi sẽ chia sẻ với bạn một vài điểm về phong cách lập trình tôi nghĩ sẽ giúp bạn lập trình tốt hơn. Ngoài ra, các bạn có thể đọc thêm bài STDIO Coding Convention - Level 1 để có thể hiểu cụ thể hơn 1 vài trường hợp cụ thể để lập trình tốt hơn.

Vì sao cần phong cách lập trình?

Phong cách lập trình cũng như một tư tưởng xuyên suốt chương trình. Ví dụ như bạn muốn đặt tên biến cục bộ theo phong cách sau:

  • Toàn bộ kí tự trong biến phải viết thường.
  • Ngăn cách mỗi từ trong biến là 1 gạch dưới.
  • Đầu tên biến có 1 gạch dưới.
  • Mỗi tên biến phải viết có nghĩa.

Giả sử ta cần đặt tên biến nhằm lưu trữ độ tuổi của 1 quái vật ta sẽ có:

int _age_of_monster;
... thay vì

int a;
... không chứa đựng ngữ nghĩa

Việc đặt tên biến phải xuyên suốt theo phong cách mà bạn đã quyết định lựa chọn và nếu có lý do đúng đắn để ra đời các quy tắc trên thì càng tốt. Ví dụ:

  • Toàn bộ kí tự trong biến phải viết thường (Để phân biệt với các biến toàn cục hay biến được truyền từ ngoài vào hàm có các phong cách khác).
  • Ngăn cách mỗi từ trong biến là 1 gạch dưới (Để dễ đọc hơn, thay vì ageofmonster).
  • Đầu tên biến có 1 gạch dưới (Để phân biệt nó với các loại biến có đặc trưng khác).
  • Mỗi tên biến phải viết có nghĩa (Để dễ hiểu tính chất của biến đó và dễ dàng bảo trì trong tương lai).

Cách đặt tên

Tên là một khái niệm bạn sẽ gặp rất thường xuyên trong quá trình lập trình. Khi bắt đầu khởi tạo dự án, bạn cần phải đặt tên cho dự án đó. Khi thiết kế ứng dụng, bạn phải đặt tên cho các thành phần (component) trong ứng dụng của mình. Khi lập trình, bạn phải đặt tên cho class, hàm, tham số hàm và các biến. Bởi vậy đặt tên như thế nào cho hiệu quả là một kỹ năng cực kỳ quan trọng trong quá trình phát triển phần mềm. Theo tôi, một cái tên hiệu quả là một cái tên chứa đầy đủ ý nghĩa mà nó muốn gởi đến người dùng. Trong lập trình, tối thiểu cái tên phải đáp ứng được các yếu tố sau:

Tên đặt ra cần phải biết tại sao nó tồn tại, dùng để làm gì và dùng như thế nào

Thật vô lý khi bạn là người đặt ra một cái tên mà bạn lại không thể trả lời được những câu hỏi trên. Và tôi chắc chắn một điều với bạn rằng người đọc không thể hiểu được hơn những gì bạn nghĩ vì chúng là “tác phẩm” của bạn. Bạn phải trân trọng tác phẩm của mình. Bạn muốn người khác hiểu được tác phẩm của bạn thì điều tối thiểu bạn cần phải làm là cẩn thận, cân nhắc trước khi viết ra bất cứ điều gì. Bạn phải đặt mình vào vị trí của người khác khi đọc những gì mình viết ra, nếu bạn cảm thấy mù mờ khi trả lời 3 câu hỏi trên thì tôi khuyên bạn nên đặt lại cho đến khi mọi thứ đều rõ ràng.

Tên phải phát âm được

Điều này nghe có vẻ lố bịch vì bạn nghĩ làm gì có chuyện một cái tên đặt ra lại không thể phát âm được. Với kinh nghiệm làm việc của mình, tôi dám khẳng định với bạn rằng bạn sẽ gặp rất nhiều những cái tên không dễ để phát âm. 2 trường hợp tôi hay gặp trong thực tế là: tên sử dụng ngôn ngữ “không chính thức” và tên viết tắt.

Để minh họa cho việc sử dụng ngôn ngữ “không chính thức”, tôi sẽ lấy 1 ví dụ mà tôi đã gặp cách đây hơn 4 năm. Đó là lúc tôi bắt đầu những bước đi đầu trong sự nghiệp của mình. Lúc đó tôi có nhận 1 dự án outsource game. Trong dự án này, nhiệm vụ của tôi là phải phát triển tiếp 1 game dựa trên template có sẵn. Việc này thật không dễ cho tôi khi template tôi nhận được tôi chỉ đọc được các keyword của C++, những phần còn lại đối với tôi hoàn toàn trống rỗng. Sau vài giờ với sự trợ giúp của một số công cụ tôi mới hiểu được mục đích của người viết và biết rằng người viết ra template sử dụng tiếng Tây Ban Nha. Điều này thật lãng phí thời gian vì nếu họ sử dụng tiếng Anh thì tôi đã nhanh chóng hiểu được những gì họ viết rồi. Lời khuyên của tôi cho những bạn mới bước chân vào lĩnh vực lập trình là bạn nên tập thói quen dùng tiếng Anh (có thể xem đây là ngôn ngữ chính thức trong lập trình vì hầu hết các lập trình viên đều sử dụng tiếng Anh) khi đặt tên biến, tên hàm, comment… ngay từ đầu. Điều đó sẽ rất có ích cho công việc của các bạn sau này. Một thông tin tôi muốn chia sẻ với các bạn là: Linus Torwalds (người viết ra nhân hệ điều hành Linux) là người Phần Lan, ngôn ngữ chính thức của ông ấy không phải là tiếng Anh nhưng toàn bộ mã nguồn nhân Linux đều sử dụng tiếng Anh.

Nói về tên viết tắt, sẽ không có vấn đề gì nếu bạn viết tắt những từ mà mọi người đều công nhận như PC, RAM, IP, MAC… Nhưng mọi chuyện sẽ khác khi bạn gặp những tên biến như cyutn, tnit. Chắc chắn là bạn không thể hiểu được ý nghĩa của chúng rồi vì đó là những cái tên tôi vừa nghĩ ra dùng để minh họa cho bài viết này. Đơn giản chúng chỉ tổ hợp những chữ cái đầu của “can you understand this name” và “this name is terrible”. Chúng thật đơn giản “đối với tôi” những không đơn giản “đối với bạn” chút nào.

Dùng tên có thể tìm kiếm được

Làm việc một thời gian với lập trình, bạn sẽ thường xuyên gặp những trường hợp như “Chương trình đang gặp lỗi X, tôi nghĩ lỗi này liên quan đến quá trình xử lý biến abc nào đó”. Việc gì sẽ xảy ra nếu cái tên abc quá thông dụng. Tôi lấy ví dụ lỗi của bạn liên quan đến quá trình tính tổng số file đã bị xóa chẳng hạn. Nếu tên biến bạn đặt là numOfDeletedFiles thì việc tìm kiếm vị trí bạn nghi ngờ bị lỗi sẽ không mấy khó khăn, nhưng nếu tên bạn đặt chỉ là num vì bạn nghĩ “khu vực” bạn đang xử lý không còn bất cứ đoạn mã nào liên quan đến tổng số thì đôi lúc bạn sẽ gặp vấn đề đấy. Cho đến khi bạn biết được cảm giác kiếm vị trí mình cần trong hàng trăm vị trí có thể kiếm thì bạn sẽ thích những cái tên “dài” hơn là những cái tên “súc tích”.

Dùng thuật ngữ chuyên ngành khi đặt tên

Bạn viết mã nguồn vì bạn là một lập trình viên, những người đọc mã nguồn của bạn họ “cũng là lập trình viên”. Tôi nghĩ các lập trình viên họ thích những cái tên đầy chất kỹ thuật hơn là những cái tên “thân thiện” với người dùng. Một cái tên như MACAddr chắc chắn dễ hiểu hơn NetworkAdapterId đối với những người lập trình mạng, cái tên như textureResolution sẽ dễ hiểu hơn imageSize đối với những người lập trình game…

Tên lớp (class), hàm (method)

Theo lẽ tự nhiên, tên lớp nên là danh từ hoặc cụm danh từ như Monster, Obstacle, Tree; tên hàm nên là động tự hoặc cụm động từ như attackOtherMonsters, jumpOverObstacle, destroy. Ngoài ra chúng ta cũng thường hay gặp các hàm để truy xuất hoặc gán biến thành viên (data member) của lớp; theo kinh nghiệm, tôi thấy đa số các lập trình viên đều sử dụng cách đặt tên theo JavaBean conventions. Theo JavaBean conventions, những hàm này sẽ đặt tên có tiền tố get, set hoặc is (hàm để truy xuất biến thành viên kiểu boolean).

Cách viết hàm

Hàm là một đoạn mã được đặt tên để thực hiện một tác vụ nhất định. Trong một số ngôn ngữ lập trình (tiêu biểu là Pascal), chúng phân biệt giữa hàm (function), có giá trị trả về, và thủ tục (procedure), không có giá trị trả về. Trong 1 chương trình, cách phân tích hàm rất quan trọng vì bạn không thể nào viết tất cả mã nguồn mình trong 1 hàm được. Điều đó sẽ gây khó khăn cho các lập trình viên khác khi họ muốn mở rộng hoặc bảo trì mã nguồn của bạn. Dưới đây tôi sẽ chia sẻ bạn 1 số kinh nghiệm để bạn có thể viết hàm tốt hơn:

Nhỏ

Một hàm được viết ra được xem là tốt ít nhất nó phải không quá lớn. 1 hàm quá dài đồng nghĩa với việc bạn đã thực hiện nhiều công việc trong đó. Cũng không dễ để có thể nói một hàm như thế nào là lớn, một hàm như thế nào là nhỏ. Theo kinh nghiệm, tôi thường viết một hàm dài không quá 100 dòng. Nếu bạn viết hàm dài hơn thế, bạn nên xem lại xem hàm của bạn có thể phân tích nhỏ hơn được nữa hay không; nếu không thì cũng không vấn đề gì vì tùy trường hợp cụ thể hàm của bạn có thể ngắn hoặc dài hơn.

Làm một việc

Một hàm của bạn tốt nhất nên đảm nhiệm một công việc duy nhất, điều đó sẽ làm cho mã nguồn của bạn trong sáng hơn. Một ví dụ tiêu biểu cho hàm được thiết kế đảm nhiệm nhiều công việc khác nhau là hàm realloc (một hàm trong bộ thư viện chuẩn của ngôn ngữ lập trình C). Công dụng chính của realloc là thay đổi kích thước của vùng nhớ được cấp phát bởi malloc, calloc hoặc chính realloc. Ngoài công dụng chính ra, nếu tham số con trỏ đến vùng nhớ được truyền vào là NULL thì công dụng của nó không khác gì malloc. Việc thiết kế như vậy sẽ gây không ít khó khăn cho các lập trình viên sử dụng chúng.

Không có hiệu ứng phụ (side effect)

Trong đời sống hằng ngày, chúng ta thường hay gặp thuật ngữ hiệu ứng phụ khi sử dụng các loại thuốc Tây. Nếu một loại thuốc ngoài công dụng chữa bệnh, nó còn có thể gây ra một số biến đổi khác trong cơ thể người sử dụng thì ta nói loại thuốc đó đã gây ra hiệu ứng phụ. Trong lập trình cũng vậy, một hàm ngoài công việc nó “cam kết” thực hiện ra, nó còn làm những công việc khác nữa gọi là hàm có hiệu ứng phụ. Viết hàm có hiệu ứng phụ có thể làm cho những người sử dụng hoặc bảo trì hàm đó không thể nào quản lý nổi. Tôi lấy ví dụ, tôi có đoạn mã sau được viết trong phương thức của 1 lớp nào đó:

printf("Before calling getNextFoo: _iFoo = %d\n", _iFoo);
int baz = getNextFoo();
printf("After calling getNextFoo: _iFoo = %d", _iFoo);	

Vì một số lý do nào đó như để tiết kiệm thời gian hoặc công sức, tôi thường sẽ không đọc chi tiết cách hiện thực của getNextFoo và tôi tin rằng kết quả in ra màn hình của đoạn code trên là 2 con số giống nhau. Điều gì sẽ xảy ra nếu một lập trình viên hiện thực hàm getNextFoo như sau:

int SomeClass::getNextFoo()
{
    return ++_iFoo;
}	

Tôi nghĩ đến đây đủ để bạn có thể biết hậu quả của việc viết hàm có hiệu ứng phụ.

Don't repeat yourself

Quy tắc này thường được biết với cái tên viết tắt DRY. Đây cũng là quy tắc tôi rất thường hay nhắc những học trò của mình trong quá trình giảng dạy. Nghĩa của quy tắc này đúng với cái tên của nó: ĐỪNG BAO GIỜ LẶP LẠI NHỮNG GÌ BẠN ĐÃ LÀM. Việc này có vẻ đơn giản nhưng nó là nguyên tắc cơ bản và cần thiết nếu bạn là lập trình viên. Một trong những ví dụ mà tôi hay gặp trong quá trình làm việc có dạng như sau:

float driveCSize = getDriveSize("C:");
float driveCSizeInGB = driveCSize / (1024.0f*1024.0f);
//…
float driveDSize = getDriveSize("D:");
float driveDSizeInGB = driveDSize / (1024.0f*1024.0f);

Rất dễ dàng để nhận ra rằng lập trình viên của chúng ta đã thực hiện một tác vụ (đổi đơn vị từ byte sang GB) 2 lần. Đó chỉ là một ví dụ nhỏ và đơn giản. Trong thực tế, tôi đã từng thấy một số lập trình viên copy đoạn mã dài hơn 20 dòng của họ ra 4-5 vị trí khác nhau; vì lý do gì họ làm như vậy thì tôi cũng không thể nào biết được.

Đối tượng và cấu trúc dữ liệu

Trước khi bàn về vấn đề này, tôi muốn bạn nghĩ thử xem: Tại sao bạn khai báo một biến thành viên trong lớp (class) có tầm vực private? Theo tôi lý do đơn giản chỉ là vì bạn không muốn bất kỳ ai từ bên ngoài có thể thao tác trên biến thành viên đó, nó sẽ làm bạn khó quản lý mã nguồn của mình hơn. Cá nhân tôi rất thích liên hệ những kiến thức mới với cuộc sống thường ngày trong quá trình chia sẻ, giảng dạy nên tôi sẽ lấy 1 ví dụ minh họa để các số bạn dễ hiểu hơn.

Giả sử bạn sở hữu một công ty sản xuất ô tô. Do hạn chế về kỹ thuật và tài chính, bạn bắt buộc phải nhập một số phụ kiện từ bên ngoài vào như bánh xe, vô lăng, gương chiếu hậu, sơn… thay vì tự sản xuất. Bạn cũng không muốn công ty bạn trở thành một công ty bán phế liệu (bánh xe, gương chiếu hậu cũ…) hay sơn đã qua pha chế… vì nó không thực sự mang lại nguồn thu lớn cho bạn. Bạn chỉ muốn tập trung vào việc sản xuất và bán ô tô mà thôi. Ngoài ra, thông số pha chế sơn của bạn rất đặc biệt, chắc chắn rằng bạn không muốn ai có thể thay đổi chúng. Bây giờ tưởng tượng rằng bạn muốn thiết kế phần mềm giả lập lại toàn bộ nhà máy trên. Bạn sẽ cần phải thiết kế một lớp (class) có tên dạng như CarFactory. Tôi dùng C++ để minh họa cho lớp này:

class CarFactory()
{
	public:
		Car produceCar();
		void importPaint(Paint p);
		void importWheel(Wheel w);
		void importSteeringWheel(SteeringWheel sw);
	private:
		destroyScrap();
	private:
		PaintColorSwatch	_internalColorSwatch;
};

Để lớp CarFactory đáp ứng được những mong muốn của bạn, bạn đã khai báo hàm destroyScrap và biến thành viên _internalColorSwatch có tầm vực private (hoặc protected tùy vào nhu cầu của bạn). Cách hiện thực này được gọi là ẩn thông tin (information hiding).

Tính trừu tượng hóa dữ liệu (Data abstraction)

Một ngày nọ, bạn muốn bán thông tin về bảng màu bạn sử dụng để sơn xuất ô tô cho những người cần nó và bạn cam kết rằng bảng màu này sử dụng cho tất cả những loại ô tô bạn sản xuất ra thị trường. Bởi vậy trong phần giả lập của mình (lớp CarFactory) bạn thêm một hàm PaintColorSwatch getPaintColorSwatch() có tầm vực public để những thành phần (component) khác có thể truy xuất được thông tin về bảng màu. Đến lúc này việc thay đổi phương thức sản xuất ô tô của bạn sẽ gặp một số trục trặc khi bạn không muốn dùng bảng màu trên để pha chế sơn nữa, bạn muốn nhập sơn trực tiếp từ công ty khác vì bạn nhận ra cách pha chế của bạn đã lỗi thời! Nói về góc nhìn kỹ thuật, tính trừu tượng hóa dữ liệu của bạn đã không thực sự tốt. Để trừu tượng hóa dữ liệu tốt, bạn chỉ nên cung cấp những thông tin cần thiết cho “thế giới bên ngoài” (trong ví dụ trên là sản phẩm ô tô sau khi được sản xuất) và giấu toàn bộ những thông tin chi tiết (trong trường hợp này là phương thức sử dụng sơn cho quá trình sản xuất ô tô).

Đối tượng truyền tải dữ liệu (Data Transfer Object)

Công ty sản xuất ô tô của bạn làm ăn ngày càng tốt, càng ngày càng có nhiều người muốn tìm hiểu thông tin của công ty bạn. Lúc này, bạn nghĩ ra ý tưởng là xây dựng một server để cung cấp thông tin công ty cho người dùng. Server sẽ gửi những thông tin người dùng thật sự cần và mỗi lần yêu cầu (request) họ chỉ lấy được một thông tin duy nhất như tên công ty, địa chỉ công ty, các loại ô tô công ty cung cấp… Sau một thời gian hoạt động, công ty được phản hồi từ người dùng là tốc độ truy xuất dữ liệu của công ty không đáp ứng được nhu cầu của họ. Bạn phân tích log của người dùng thì thấy rằng đa số người dùng đều muốn nhận hầu như toàn bộ thông tin của công ty, đồng nghĩa với việc họ phải truy xuất rất nhiều lần để có thể lấy được toàn bộ thông tin họ cần. Nhận thấy được vấn đề, bạn đã gom nhóm toàn bộ thông tin của công ty lại và gửi cho người dùng một lúc khi họ yêu cầu. Điều này đã làm giảm rất nhiều thời gian lấy dữ liệu của người dùng. Những đối tượng chứa toàn bộ thông tin như vậy gọi là đối tượng truyền tải dữ liệu. Đối tượng truyền tải dữ liệu được viết tắt là DTO (Data Transfer Object) là một đối tượng chứa dữ liệu dùng để truyền tải giữa các tiến trình (process). Khác với các đối tượng chúng ta thường gặp, DTO chỉ chứa dữ liệu và các phương thức để truy xuất dữ liệu đó. DTO thường gặp trong quá trình giao tiếp giữa client/server khi lập trình game online và trong một số framework hỗ trợ truy xuất cơ sở dữ liệu (database) như Rails, CodeIgniter. DTO dùng để ánh xạ trực tiếp từ một bảng trong cơ sở dữ liệu (database table) thường được biết dưới cái tên Active Record.

Tổng kết

Lập trình là một quá trình mang tính khoa học nhưng lập trình như thế nào để mã nguồn của bạn dễ hiểu, dễ mở rộng, dễ bảo trì thì đó là một nghệ thuật. Môn nghệ thuật này yêu cầu các lập trình viên phải để tâm đến những gì mình tạo ra và cũng như các môn nghệ thuật khác, nó cần phải được trau dồi thường xuyên mới có thể thành thạo được. Bài viết trên chỉ là những phần cơ bản nhất liên quan đến Phong cách lập trình mà tôi muốn chia sẻ cùng các bạn. Hy vọng những kiến thức này sẽ là bước dẫn dắt ban đầu để bạn có thể lập trình tốt hơn và không còn ai có thể phàn nàn về những gì bạn làm ra nữa. Chúc các bạn thành công trên con đường sự nghiệp của mình.

Tham khảo

  • Clean Code: A Handbook of Agile Software Craftsmanship by Robert C. Martin - 30/5/2014
  • http://docstore.mik.ua/orelly/java-ent/jnut/ch06_02.htm - 30/5/2014
  • http://en.wikipedia.org/wiki/Side_effect - 30/5/2014
  • http://en.wikipedia.org/wiki/Data_transfer_object - 30/5/2014
  • http://guides.rubyonrails.org/active_record_basics.html - 30/5/2014
  • http://ellislab.com/codeigniter/user-guide/database/active_record.html - 30/5/2014