Search…

Sơ Lược về Phong Cách Lập Trình

20/07/202216 min read
Bài viết là một vài chia sẻ về cách hình thành phong cách lập trình để giúp hệ thống phần mềm dễ bảo trì và mở rộng.

Năm 2012, tôi và một số người bạn cùng 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. 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, 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. Thời điểm đó các nguyên tắc về coding convention đã tồn tại, các khái niệm như SOLID principles chỉ mới sơ khai nhưng nội tại các nhóm phát triển đều tự tìm tòi học hỏi xây dựng nên phong cách của riêng đội ngũ đó.

Phong cách lập trình là gì?

Phong cách lập trình cũng như một tư tưởng xuyên suốt quá trình phát triển chương trình. Giả sử 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, 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 đã lựa chọn, 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, AgeOfMonster 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 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 được gặp rất thường xuyên trong quá trình lập trình:

  • Khi khởi tạo dự án, dự án cần được đặt tên.
  • Khi thiết kế ứng dụng, phải đặt tên cho các thành phần (component) trong ứng dụng.
  • Khi lập trình, 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. 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 "tương lai" cho chính mình và đội 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 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 khó hiểu khi người đặt ra một cái tên mà không thể trả lời được những câu hỏi trên. Người đọc không thể hiểu được hơn những gì người khác nghĩ. Để người khác hiểu được tác phẩm, thì người viết cần cẩn thận, cân nhắc trước khi viết ra bất cứ điều gì.

Cần đặ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 không thể trả lời được các câu hỏi trên sau khi đặt tên biến thì nên đặt lại cho đến khi mọi thứ đều rõ ràng.

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

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 nhận 1 dự án 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ễ khi template nhận được chỉ có thẻ đọc được các keyword của C++, những phần còn lạ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, 1 số comment sử dụng chữ Hoa và chữ Nhật.

Nếu họ sử dụng tiếng Anh thì tôi đã nhanh chóng hiểu được những gì họ viết, tránh lãng phí thời gian. Nếu bạn mới bắt đầu, 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, ... điều đó sẽ rất có ích cho công việc của các bạn sau này.

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ư ttnn, larige. Bạn không thể hiểu được ý nghĩa của chúng vì có thể nó là tên viết tắt của 1 cá nhân. Chúng thật đơn giản với người viết ở hiện tại nhưng không đơn giản với người đọc và chính người đó trong tương lai khi đọc lại.

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”.

Việc gì sẽ xảy ra nếu cái tên abc quá thông dụng. Giả sử lỗi của bạn liên quan đến quá trình tính tổng số file đã bị xóa, nếu tên biến 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 là num thì khu vực tìm kiếm có thể rất rộng.

Cho đến khi bạn biết được cảm giác tìm 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 cũng là lập trình viên. Các lập trình viên 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 sẽ 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.

Đ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 trong 1 hàm. Điều đó 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ẻ 1 số kinh nghiệm để bạn có nguồn tham khảo:

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 thử tìm cách tối ưu hoặc phân nhỏ; 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ỏ 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)

Thuật ngữ hiệu ứng phụ được nghe nhiều trong ngành y tế, sử dụng thuốc. Nếu một loại thuốc ngoài công dụng chữa bệnh, còn gây ra một số biến đổi khác trong cơ thể người sử dụng thì 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 cam kết thực hiện ra, 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 hàm hoặc bảo trì hàm đó khó khăn trong quản lý. Xem xét thử đoạn mã sau:

char* str;
stringCopy(str, "text");

Để tiết kiệm thời gian, công sức, lập trình viên sẽ không đọc chi tiết cách hiện thực của getNextFoo và tin tưởng 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:

void stringCopy(char* des, char* src)
{
	// Get src length ...
	des = new char[length + 1];
	// Copy src to des
}

Hàm sao chép chuỗi trên thực hiện thêm việc cấp phát bên trong nó và có thể dẫn đến rò rỉ bộ nhớ.

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. Nghĩa của quy tắc này đúng với cái tên của nó: đừng lặp lại những gì đã 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 đã 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ế, để tiết kiệm thời gian tái tổ chức hàm và gọi hàm, 1 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, chỉ cần 1 update, phải lặp lại việc sao chép này thêm 20 lần và còn nguy cơ gây ra lỗi nếu cập nhật thiếu sót.

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

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ỳ client code nào 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?

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ô. 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, 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á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 1 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 đ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 để tâm đến những gì mình tạo ra.

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. 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
IO Stream

IO Stream Co., Ltd

30 Trinh Dinh Thao, Hoa Thanh ward, Tan Phu district, Ho Chi Minh city, Vietnam
+84 28 22 00 11 12
developer@iostream.co

383/1 Quang Trung, ward 10, Go Vap district, Ho Chi Minh city
Business license number: 0311563559 issued by the Department of Planning and Investment of Ho Chi Minh City on February 23, 2012

©IO Stream, 2013 - 2024