Nội dung bài viết
La Kiến Vinh Coding Convention là một chuẩn mực và quy tắc để khi code ta sẽ phải tuân theo khi code, trong bài viết có nhiều ví dụ cụ thể giúp bạn tiếp cận việc code có khuôn mẫu, giúp cho codes bạn đẹp, trong sáng và dễ bảo trì hơn.

Giới thiệu

Coding Convention là một chuẩn mực và quy tắc để khi code ta sẽ phải tuân theo. Việc áp dụng các quy tắc khi code tối thiểu sẽ giúp cho chương trình chúng ta dễ đọc, có quy tắc hơn, đẹp hơn (nó cũng sẽ tương tự như quy tắc soạn văn bản cần có quy tắc canh lề hoặc quy tắc chữ đậm, chữ nhạt).

Xa hơn nữa, với các dự án lớn, việc đòi hỏi đồng bộ giữa các thành viên với nhau là cần thiết, các chuẩn mực sẽ giúp cho các lập trình viên cùng có cách hiểu, cùng nhìn về một hướng, tối ưu thời gian đọc code.

Mọi kiến thức đều sinh ra do quan điểm, cách nhìn nhận của cá nhân, của tổ chức, STDIO Coding Convention cũng vậy, tùy quan điểm, mục tiêu mà bạn có thể sinh ra một chuẩn riêng cho bản thân hoặc áp dụng thử phương pháp này.

STDIO Coding Convention dành cho một số ngôn ngữ quen thuộc như: C, C++, C#, Java, JavaScript.

Tiền đề bài viết

Trong quá trình training các bạn tại STDIO Training, các trainees ban đầu code không có 1 chuẩn mực nào, tôi phải làm lại việc là hướng dẫn họ code đẹp hơn và công việc đó lặp lại liên tục, bài viết này cũng dùng vào mục đích training cá nhân.

Đối tượng hướng đến

STDIO Coding Convetion – Level 1 hướng đến người đọc là các bạn chưa có 1 chuẩn mực coding nào. Vấn đề là bài viết này tập trung vào đối tượng không biết qua Coding Convention trước đó và bao gồm các học trò của tôi.

Các vấn đề khi khai báo biến

Cách đặt tên biến phải mang nghĩa nhất định

Ta xét ví dụ

ConvergedSins* ConvergedSins::createConvergedSins(Color a, int b)
{
	ConvergedSins* ret = new ConvergedSins();

	if(ret && ret->init(a, b))
	{
		ret->autorelease();
		return ret;
	}

	SAFE_DEL(ret);
	return NULL;
}

2 biến a và b được truyền vào hàm createConvergedSins không mang ý nghĩa. Sẽ tốt hơn nếu thay a là color và b trở thành level do trong ngữ cảnh này a và b mang 2 ý nghĩa đó, thay xong ta sẽ được code như bên dưới.

ConvergedSins* ConvergedSins::createConvergedSins(Color color, int level)
{
	ConvergedSins* ret = new ConvergedSins();

	if(ret && ret->init(color, level))
	{
		ret->autorelease();
		return ret;
	}

	SAFE_DEL(ret);
	return NULL;
}

Sau khi thay đổi được đoạn code như trên lúc xem xét hoặc bảo trì chúng ta sẽ tránh mất thời gian khi phải tra lại ý nghĩa của một biến.

Tuy nhiên, cũng không nhất thiết phải tuân theo quy tắc 100% vì đôi lúc hàm ta triển khai khá đơn giản, ta khảo sát hàm cộng 2 số sau

int Add(int a, int b)
{
	return a + b;
}

Bảng liệt kê một số loại biến và cách đặt tên

LOẠI BIẾN CÁCH ĐẶT BIẾN VÍ DỤ

Biến cục bộ
(local)

Chữ thường toàn bộ cho các ký tự, ngăn cách mỗi từ trong biến bởi dấu gạch dưới.

int age_of_person

char* name
Biến toàn cục
(global)
Chữ thường toàn bộ cho các ký tự, tiền tố g_, ngăn cách mỗi từ trong biến bởi gạch dưới.

int g_age_of_person

char* g_name
Biến dạng static Chữ thường toàn bộ cho các ký tự, tiền tố s_, ngăn cách mỗi từ trong biến bởi gạch dưới.

int s_age_of_person

char* s_name
Biến dạng hằng
(constant)
Viết hoa toàn bộ các ký tự, tiền tố C_, ngăn cách mỗi từ trong biến bởi gạch dưới.

const int C_AGE_OF_PERSON

const char* C_NAME
Các trường
của struct và class
Chữ thường toàn bộ cho các ký tự, tiền tố m_, ngăn cách mỗi từ trong trường này bởi gạch dưới.

int m_age_of_person

char* m_name
Các trường static của struct và class Chữ thường toàn bộ cho các ký tự, tiền tố s_, ngăn cách các từ trong trường bởi gạch dưới.

int s_age_of_person

char* s_name

Các quy tắc khác

Biến truyền vào hàm sẽ viết thường toàn bộ các ký tự, ngăn cách các từ trong biến bởi dấu gạch dưới.

Cách khai báo một struct cần viết hoa các ký tự đầu của mỗi từ tên struct và bắt đầu với tiền tố S, ví dụ struct STesla.

Cách khai báo một class cần viết hoa các ký tự đầu của mỗi từ trong tên class và bắt đầu với tiền tố C, ví dụ class CHumanBeing.

Các vấn đề khi khai báo hàm

*Lưu ý: tôi gọi phương thức của class hay hàm vắn tắt là hàm cho ngắn gọn mặc dù chúng ta cần phân biệt chúng.

Với Java, có nhiều bạn tiếp xúc với Java Coding Convention nhận thấy đa phần các lập trình viên bắt đầu tên phương thức bằng từ viết thường, nếu phương thức được tạo thành từ nhiều từ, thì từ thứ 2 trở đi sẽ viết hoa chữ cái đầu tiên, ta có ví dụ: createBasicSinsWithData(), playEffectDestroySins().

Với STDIO Coding Convention ở mức 1 thì ta có thể chọn 1 trong 2 cách sau, viết theo cách của Java Coding Convention hoặc viết hoa chữ cái đầu tiên của các từ trong tên hàm, ta có ví dụ cho cách thứ 2 này: CreateBasicSinsWithData(), PlayEffectDestroySins().

Cách đặt tên hàm phải mang nghĩa nhất định

Việc đặt tên hàm mang ý nghĩa cũng như đặt tên biến có ý nghĩa như đã trình bày ở trên, ở đây ta sẽ nói sâu hơn về các cấp độ của ý nghĩa. Đặt tên hàm hay biến càng mang đầy đủ ý nghĩa sẽ càng tốt.

Mức độ 1: ta xét ví dụ với hàm có ý nghĩa thấp sau, hàm create(...)

LightSins* LightSins::create(eBasicSinsColor color)
{
	LightSins* ret = new LightSins();

	if(ret && ret->init(color))
	{
		ret->autorelease();
		return ret;
	}

	SAFE_DEL(ret);
	return NULL;
}

Mức độ 2: gia tăng ý nghĩa hơn cho hàm create(...), ta đặt lại tên hàm với nhiều từ hơn, sửa create(...) thành createLightSins(...)

LightSins* LightSins::createLightSins(eBasicSinsColor color)
{
	LightSins* ret = new LightSins();

	if(ret && ret->init(color))
	{
		ret->autorelease();
		return ret;
	}

	SAFE_DEL(ret);
	return NULL;
}

Mức độ 3: làm rõ nghĩa hơn nữa, ta điều chỉnh thành createLightSinsWithColor(...)

LightSins* LightSins::createLightSinsWithColor(eBasicSinsColor color)
{
	LightSins* ret = new LightSins();

	if(ret && ret->init(color))
	{
		ret->autorelease();
		return ret;
	}

	SAFE_DEL(ret);
	return NULL;
}

Khi sử dụng hàm trên, ta sẽ tránh mất thời gian hơn trong việc xét xem cần truyền tham số thuộc loại nào vào hàm, ngoài ra ta sẽ thấy rõ ràng hơn ý nghĩa của hàm, đôi lúc không cần xem phần hiện thực cũng có thể đoán được ý nghĩa của hàm.

Các vấn đề khác về soạn thảo văn bản

Bên dưới là phong cách soạn thảo code trong game Sins của STDIO. Với việc trình bày “văn bản” một cách trong sáng hơn và có "ý nghĩa" trong việc viết code sẽ làm cho chương trình dễ đọc và dễ bảo trì. Tuy nhiên, đó là quan điểm phù hợp với dự án Sins, tùy vào mỗi cá nhân mà nó sẽ có các biến thể. Các bạn có thể xem tham khảo.

Level của 1 khối code

Việc đặt level đúng của 1 khối code là rất quan trọng, nó giúp ta xác định nhanh hơn "tầm vực" của 1 dòng code và giúp ta xác định vấn đề nhanh hơn.

Xét ví dụ sau, code không được trình bày đúng level

if(m_fAdd1ScoreTimer == 0.0f)
{
	if(m_iScorePool > 0)
	{
	int addScore = (int)(m_iScorePool * 0.2f);
	if(addScore < 1) 
	addScore = 1;

m_iScorePool -= addScore;
m_iCurrentScore += addScore;
m_fAdd1ScoreTimer = DELAY_ADD_1_SCORE;

		if(m_iScorePool == 0)
		{
m_pScoreLabel->runAction(
CCSequence::create(
CCScaleTo::create(0.05f, 2.0f*m_ScaleFactorScore),
CCScaleTo::create(0.1f, 1.0f*m_ScaleFactorScore), NULL
));
	}
	}
}

Với code như trên, ta có thể hiểu lầm rằng m_iCurrentScore += addScore; (dòng 9) nằm cùng cấp với dòng if(m_fAdd1ScoreTimer == 0.0f) (dòng 1), tức là nó không nằm trong if.

Thay vì vậy, ta nên đưa nó vào đúng level của nó, bạn có thể tự cảm nhận được sự khác biệt này.

if(m_fAdd1ScoreTimer == 0.0f)
{
	if(m_iScorePool > 0)
	{
		int addScore = (int)(m_iScorePool * 0.2f);
		if(addScore < 1) 
			addScore = 1;

		m_iScorePool -= addScore;
		m_iCurrentScore += addScore;
		m_fAdd1ScoreTimer = DELAY_ADD_1_SCORE;

		if(m_iScorePool == 0)
		{
			m_pScoreLabel->runAction(CCSequence::create(
				CCScaleTo::create(0.05f, 2.0f*m_scaleFactorPts),
				CCScaleTo::create(0.1f, 1.0f*m_scaleFactorPts),
				NULL
			));
		}
	}
}

Khi làm việc với Taco Nguyen anh ta sử dụng Python khá nhiều cho công việc, tôi mới biết thêm về  ngôn ngữ này, anh ta cho tôi biết rằng chỉ cần dư hoặc thiếu 1 khoảng trắng hoặc tab, thì chương trình sẽ chạy sai mục đích ngay, vì cách định ra 1 level của 1 dòng code trong Python là dựa vào số lượng tab hoặc khoảng trắng, khác với nhiều ngôn ngữ khác là “đống khối” 1 khối code dựa vào cặp ngoặc nhọn {} như trong ngôn ngữ họ C.

Đặt dấu (chấm, phẩy, hai chấm, chấm phẩy, hỏi, chấm than)

Theo phương pháp đặt dấu trong văn bản thì "dấu câu" trong code cũng có vài nét tương đồng. Nguyên tắc là dấu sẽ nằm ngay phía sau ký tự cuối và sau đó là đến khoảng trắng rồi mới đến ký tự kế tiếp.

Ví dụ đặt dấu sai, các bạn khảo sát tại dấu phẩy (,) và dấu hỏi (?)

Tôi sẽ code tốt hơn sau khi đọc bài viết này ,còn bạn thì sao? (sai chỗ dấu phẩy, đúng chỗ dấu hỏi)
Tôi sẽ code tốt hơn sau khi đọc bài viết này,còn bạn thì sao ? (sai chỗ dấu phẩy, sai chỗ dấu hỏi)
Tôi sẽ code tốt hơn sau khi đọc bài viết này, còn bạn thì sao ? (đúng chỗ dấu phẩy, sai chỗ dấu hỏi)

Cách đặt dấu đúng dựa theo nguyên tắc đã đề cập phía trên:

Tôi sẽ code tốt hơn sau khi đọc bài viết này, còn bạn thì sao?

Từ cách trình bày văn bản như trên, ta có thể tận dụng vào việc trình bày code, xét ví dụ code về việc đặt dấu sau

playActionFollowDelete(1,this,callfunc_selector(SinsDestroy::playDone)) ;

Các dấu phẩy hay chấm phẩy như trên đặt không tốt, ta có thể trình bày lại như sau

playActionFollowDelete(1, this, callfunc_selector(SinsDestroy::playDone));

Cách dòng trong code

Khảo sát đoạn codes sau, đây là đoạn codes đã cách dòng tốt

void CLoadingData::releaseAllResource()
{
	m_iNumBackgroundAudios = 0;

	// RELEASE SPRITE
	for(u8 i = 0; i < m_tSpritePacks.size(); i++)
	{
		GetSpriteManager()->unloadSprite(m_tSpritePacks.at(i));
	}
	m_tSpritePacks.clear();
	
	// RELEASE AFX
	for(u8 i = 0; i < m_tAudioPacks.size(); i++)
	{
		unloadEffect(m_tAudioPacks.at(i));			
	}
	m_tAudioPacks.clear();
}

Xem xét đoạn từ comment // RELEASE SPRITE (dòng 5) và tới đoạn // RELEASE AFX (dòng 12), giữa 2 đoạn đó cách nhau bởi 1 dòng trống (blank) (dòng 11). Ý nghĩa của nó nhằm ngăn cách 2 khối codes có 2 chức năng khác nhau. Như vậy, khi bảo trì ta có thể nhanh chóng xác định được đoạn codes nào làm việc gì thay vì để liên tục từ trên xuống như đoạn codes bên dưới.

void CLoadingData::releaseAllResource()
{
	m_iNumBackgroundAudios = 0;
	// RELEASE SPRITE
	for(u8 i = 0; i < m_tSpritePacks.size(); i++)
	{
		GetSpriteManager()->unloadSprite(m_tSpritePacks.at(i));
	}
	m_tSpritePacks.clear();
	// RELEASE AFX
	for(u8 i = 0; i < m_tAudioPacks.size(); i++)
	{
		unloadEffect(m_tAudioPacks.at(i));			
	}
	m_tAudioPacks.clear();
}

Hướng phát triển

Chuẩn mực trên đã từng được sử dụng bởi STDIO trong dự án Sins, để ngày càng mạnh và chặt chẽ hơn, các chuẩn mực này đã tiến hóa hơn rất nhiều, do đó tôi đã cắt gọt lại và giữ bản STDIO Coding Convention đầu tiên cho các bạn dễ học, dễ nhớ.

Ngoài ra, từ đây các bạn có thể tự thiết kế nên chuẩn mực chung của nhóm làm việc, hoặc học cách tuân theo chuẩn mực cho các dự án của các bạn sau này.

Lời kết

Như một hành trình của máy bay đã được định tuyến bay thẳng từ Thành Phố Hồ Chí Minh đến Hà Nội, vì các yếu tố vật lý như gió, mây thì máy bay vẫn lệch khỏi đường bay đã vạch ra sẵn, nhưng nó sẽ mau chóng trở lại với định hướng ban đầu, đơn giản là vì nó đã có chuẩn mực về đường bay.

Nguyên tắc do con người đặt ra và nguyên tắc là chết, con người là sống và ta cần linh động, nhưng việc linh động đó chưa phải là cái lúc mà bạn chưa nắm được định hướng chính vì sẽ dễ đi lạc.

THẢO LUẬN
ĐÓNG