Lập trình là một lĩnh vực phức tạp và đầy thử thách, đòi hỏi sự am hiểu sâu sắc về các nguyên tắc, kỹ thuật và công cụ khác nhau. Tuy nhiên, bên cạnh những kiến thức chuyên môn, còn có những quy tắc mà mỗi lập trình viên cần nắm vững để trở nên thành công trong lĩnh vực này. 

Bài viết này VietnamWorks inTECH sẽ giới thiệu 24 quy tắc trong lập trình không phải ai cũng biết, giúp bạn nâng cao kỹ năng và hiệu quả công việc.

1. Không sử dụng thuộc tính kiểu dữ liệu làm tên biến

Đôi khi, chúng ta muốn chỉ rõ rằng một biến cụ thể là chuỗi ký tự (string) hoặc số nguyên (int) nên thường có xu hướng đặt tên biến như sau: ten_bien_str hoặc ten_bien_int.

Tuy nhiên, cách này khá dư thừa, đặc biệt là đối với các biến có tên đã thể hiện rõ ràng kiểu dữ liệu. Ví dụ, biến ten thì rõ ràng không thể là kiểu số nguyên.

Thay vào đó, khi kiểu dữ liệu của biến không trực quan, cách tốt nhất để khai báo là sử dụng chú thích kiểu dữ liệu.

ten_bien: str = gia_tri thay vì ten_bien_str = gia_tri.

Với cách này, mọi người đều có thể biết ten_bien là kiểu chuỗi trong khi vẫn giữ code gọn gàng và chính xác.

2. Tên class phải là danh từ

Trong lập trình, việc đặt tên class (lớp) theo danh từ là một quy tắc hay. Chủ yếu là vì một object (đối tượng) của class được dùng để xác định hoặc biểu diễn một nhóm các thuộc tính và hành vi. 

Ngoài ra, đặt tên class theo danh từ còn giúp code dễ đọc và ít dài dòng. Ví dụ, Goat.get_horn_length() dễ hiểu hơn so với GetGoat.get_horn_length().

3. Một hàm chỉ nên có một nhiệm vụ duy nhất

Các lập trình viên mới vào nghề thường hay phá vỡ quy tắc này. Việc để một hàm thực hiện một chức năng duy nhất là rất quan trọng vì nó giúp phát hiện lỗi dễ dàng, cho phép tái sử dụng, và thực hiện đúng như tên của hàm đã chỉ ra.

Ví dụ:

Đoạn code này kiểm tra xem địa chỉ có hợp lệ hay không và sau khi kiểm tra sẽ trả về vĩ độ và kinh độ. Hàm này thực hiện hai việc: kiểm tra địa chỉ có hợp lệ không và trả về vị trí địa lý của địa chỉ đó.

Dưới đây là một cách làm tốt hơn:

Các hàm trên chỉ thực hiện một chức năng duy nhất và không hơn. Mặc dù có vẻ dài dòng, nhưng chúng lại súc tích và dễ đọc hơn rất nhiều.

Nếu bạn có thể tách ra hoặc nhóm một số hành động trong một hàm thành một hàm khác, thì hàm đó đang thực hiện nhiều hơn một chức năng. Hãy cân nhắc tách chúng ra để có thể dễ dàng maintain sau này.

4. Các hàm phải có cùng mức độ trừu tượng

Khi chúng ta nói về các hàm ở cùng một mức độ trừu tượng, chúng ta đang nói về việc một hàm nên thực hiện một nhiệm vụ duy nhất, được định nghĩa rõ ràng. Nhiệm vụ đó nên ở một mức độ trừu tượng nhất quán trong suốt hàm.

Nói cách khác, hàm nên tập trung vào một mức độ chi tiết hoặc phức tạp cụ thể, và tất cả các hoạt động của hàm nên hoạt động ở cùng một mức độ đó.

5. Nguyên tắc đóng mở (Open Closed Principles)

Nguyên tắc đóng mở (OCP) khẳng định rằng một lớp, phương thức hoặc hàm phải khả năng mở rộng nhưng không được sửa đổi. Điều này có nghĩa là bất kỳ lớp, phương thức hoặc hàm nào được định nghĩa đều có thể dễ dàng sử dụng lại hoặc mở rộng cho nhiều trường hợp mà không cần thay đổi mã nguồn.

Chẳng hạn, chúng ta có một lớp gọi là address.

Ví dụ trên không tuân thủ theo nguyên tắc OCP vì mỗi khi có một quốc gia mới, chúng ta sẽ cần phải viết một câu lệnh if mới để bổ sung cho nó. Điều này có vẻ đơn giản ở hiện tại nhưng hãy tưởng tượng nếu chúng ta có 100 quốc gia hoặc nhiều hơn để xem xét. Điều đó sẽ như thế nào?

Đây là lúc OCP phát huy tác dụng:

Đây là một giải pháp mạnh mẽ hơn vì bây giờ chúng ta không cần phải sửa đổi cả lớp hoặc các hàm của nó. Nếu bạn muốn xem xét một quốc gia và thủ đô, bạn có thể dễ dàng điều chỉnh từ điển thủ đô.

Một ví dụ phổ biến khác mà bạn thường thấy là sử dụng lớp kế thừa.

Ví dụ:

Đây là một phương pháp không hề hay vì mỗi khi chúng ta muốn thêm một phương thức thanh toán mới, chúng ta luôn cần phải sửa đổi lớp PaymentProcessor.

Một cách tốt hơn để làm điều này là:

Như vậy, mỗi khi chúng ta cần thêm một phương thức thanh toán mới, chẳng hạn như tiền điện tử hoặc paypal, chúng ta không cần phải chỉnh sửa hoặc sửa đổi bất kỳ lớp nào để đạt được điều này. Chúng ta có thể đơn giản làm như sau:

6. Thời điểm nên sử dụng comment

Chúng ta thường có xu hướng ngại comment vì điều này chứng tỏ code bản thân viết ra không rõ ràng. Tuy nhiên, có một số trường hợp mà việc sử dụng comment thực sự có thể giúp diễn đạt cách hoạt động bên trong của code tốt hơn so với chính code đó. Dưới đây là 5 ví dụ điển hình về những comment "tốt".

  • Comment cung cấp thông tin: Viết comment cung cấp thông tin luôn hữu ích để giải thích code cho người đọc. Ví dụ, một comment làm nổi bật giá trị trả về của một hàm sẽ giúp code rõ ràng hơn. Nhưng những comment kiểu này có thể trở nên thừa nếu bạn đã sử dụng tên hàm hoặc biến mô tả rõ ràng.

  • Comment TODO: Những comment như thế này giúp các lập trình viên khác biết đây là một hàm/nhiệm vụ chưa hoàn thành hoặc cần sửa đổi. Có thể có cách triển khai hàm hiệu quả hơn chưa được tận dụng. Hoặc code có thể thỉnh thoảng bị lỗi. Bất kể là lý do nào, comment kiểu này mang lại nhiều giá trị. 

  • Cảnh báo về hậu quả: Đôi khi, chúng ta muốn thông báo cho các developer khác về những "bom mìn" tiềm ẩn. Vấp phải chúng có thể dẫn đến những hậu quả không lường trước. Comment có thể cứu nguy trong tình huống này. Giả sử một đoạn code tốn thời gian hoặc có khả năng làm quá tải một số hệ thống, thì một cảnh báo như "# TIÊU TỐN RẤT NHIỀU NGUỒN LỰC " sẽ có lợi cho người đọc hoặc các lập trình viên khác.

7. Giữ file nguồn ngắn gọn

Một file nguồn nên có độ dài từ 100-200 dòng, tối đa là 500 dòng. Trừ khi bạn có lý do rất chính đáng để khiến file dài hơn thế. Giữ cho file nguồn ngắn gọn sẽ giúp code của bạn có khả năng tái sử dụng, dễ đọc, dễ bảo trì và cập nhật hơn vì chúng ta tốn ít thời gian cuộn trang và kết nối các phần của code.

8. Luôn luôn sử dụng câu lệnh Try-Catch-Finally

Bạn nên sử dụng try-catch khi code của bạn có khả năng gặp lỗi. Các tình huống như yêu cầu API, xử lý tệp, v.v. thường gặp vấn đề hoặc gây ra lỗi do nhiều nguyên nhân. Try-catch sẽ giúp tăng tốc quá trình gỡ lỗi và nâng cao chất lượng của mã, đồng thời giữ cho code gọn gàng và dễ quản lý.

Tuy nhiên, áp dụng phương pháp này cho các phép nhân hoặc chia là không cần thiết và có thể gây ra nhiều vấn đề.

9. Một hàm chỉ nên thay đổi dữ liệu hoặc trả về giá trị, không nên cả hai

Trong lập trình, khi tạo một hàm, bạn cần xác định rõ ràng chức năng của hàm đó. Một hàm có thể:

  • Thay đổi dữ liệu của các đối số đầu vào (mutate the arguments passed): Hàm này trực tiếp chỉnh sửa giá trị của các biến (đối số) được truyền vào hàm.

  • Trả về một giá trị (return something): Hàm thực hiện một tác vụ nào đó và trả về kết quả dưới dạng một giá trị.

Lưu ý: Một hàm không nên thực hiện cả hai việc cùng một lúc (vừa thay đổi dữ liệu của đối số vừa trả về giá trị).

Tại sao không nên làm cả hai?

  • Khó hiểu và dễ gây lỗi: Khi một hàm vừa thay đổi dữ liệu bên ngoài (qua đối số) vừa trả về giá trị, logic của hàm sẽ trở nên khó hiểu và dễ dẫn đến lỗi. Các lập trình viên khác khi đọc code có thể không nắm bắt được hành vi của hàm, dẫn đến việc sử dụng sai.

  • Giảm tính tái sử dụng: Hàm thay đổi dữ liệu bên ngoài khó có thể tái sử dụng trong các ngữ cảnh khác nhau. Vì việc tái sử dụng phụ thuộc vào trạng thái ban đầu của dữ liệu được nhập vào hàm.

10. Các lớp (class) phải nhỏ

Đúng vậy! Các lớp nên được giữ ngắn gọn nhất có thể. Giống như các hàm.

Sự khác biệt duy nhất là trong các hàm, kích thước được xác định bởi số dòng trong khi các lớp được xác định bởi số lượng trách nhiệm trong lớp đó.

Thường, tên của một lớp đại diện cho các loại trách nhiệm mà nó có thể sở hữu nhưng khi tên mơ hồ hoặc quá chung chung, có khả năng lớp đó đang mang quá nhiều trách nhiệm.

Điều này đưa chúng ta trở lại với nguyên lý SRP (single responsibility principle - nguyên tắc đơn trách nhiệm), khẳng định rằng một lớp chỉ nên có một trách nhiệm để thay đổi.

11. Các lớp nên có một số lượng biến thể hiện nhỏ

Lớp nên có một số lượng biến thể hiện nhỏ. Biến thể hiện (instance variables), trong trường hợp bạn chưa biết, là các biến được định nghĩa khi lớp được định nghĩa hoặc được khởi tạo.

Ví dụ, trong lớp Animal dưới đây, self.name là một biến thể hiện:

class Animal:
def __init__(self, name):
self.name = name #instance variable

Nếu tất cả các hàm của chúng ta liên quan đến trách nhiệm của lớp, không có lý do gì để có nhiều biến thể hiện. Khi chúng ta có quá nhiều biến thể hiện, đó là khi một số hàm trong lớp bắt đầu lệch khỏi trách nhiệm cốt lõi của nó. Những hàm này thường đi kèm với các biến riêng của chúng mà các hàm khác trong lớp không cần.

12. Tránh các biểu thức ba ngôi phức tạp

Tránh sử dụng các biểu thức bậc ba quá phức tạp; ưu tiên khả năng đọc hơn là sự ngắn gọn để làm cho code dễ hiểu hơn.

# Bad
result = "even" if number % 2 == 0 else "odd" if number % 3 == 0 else "neither"

# Good
if number % 2 == 0:
result = "even"
elif number % 3 == 0:
result = "odd"
else:
result = "neither"

13. Sử dụng câu lệnh with để quản lý tài nguyên

Sử dụng câu lệnh "with" để tự động quản lý các tài nguyên như tệp tin hoặc kết nối cơ sở dữ liệu, đảm bảo chúng được đóng hoặc release đúng cách.

# Bad
file = open("example.txt", "r")
data = file.read()
file.close()

# Good
with open("example.txt", "r") as file:
data = file.read()

14. Dependency inversion principle (DIP)

Nguyên tắc đảo ngược phụ thuộc (Dependency Inversion Principle - DIP) là một trong những nguyên tắc thiết kế phần mềm hướng đối tượng (SOLID) quan trọng, giúp tạo ra các hệ thống linh hoạt và dễ bảo trì hơn. Nó tập trung vào việc tách biệt các lớp (class) thành các thành phần cấp cao (high-level) và cấp thấp (low-level), đồng thời thiết lập mối quan hệ phụ thuộc theo hướng đảo ngược.

# Bad
class Logger:
def log(self, message):
with open('log.txt', 'a') as f:
f.write(message + '\n')

class Calculator:
def __init__(self):
self.logger = Logger()

def add(self, x, y):
result = x + y
self.logger.log(f"Added {x} and {y}, result = {result}")
return result

Trong ví dụ trên, chúng ta định nghĩa lớp Logger và trực tiếp tạo một lớp thể hiện mới của nó trong lớp Calculator. Có thể thấy, Calculator hiện tại đang phụ thuộc vào lớp Logger, và nếu vì bất kỳ lý do gì mà chúng ta thay đổi lớp Logger, chúng ta cũng phải sửa đổi lớp Calculator.

Và như vậy, điều này cũng không tuân theo nguyên tắc mở đóng (open for extension, closed for modification).

Sự ràng buộc chặt chẽ này làm cho mã nguồn khó kiểm tra hơn, vì chúng ta không thể đơn giản sử dụng một lớp logger giả (fake logger).

Thay vào đó hãy viết như sau:

# good
from abc import ABC, abstractmethod

class LoggerInterface(ABC):
@abstractmethod
def log(self, message):
pass

class Logger(LoggerInterface):
def log(self, message):
with open('log.txt', 'a') as f:
f.write(message + '\n')

class Calculator:
def __init__(self, logger: LoggerInterface):
self.logger = logger

def add(self, x, y):
result = x + y
self.logger.log(f"Added {x} and {y}, result = {result}")
return result

Cách này thúc đẩy tính mô-đun vì khi một thành phần thay đổi, các thành phần khác không nhất thiết phải thay đổi theo, miễn là giao diện giữa chúng vẫn giữ nguyên.

Tính mô-đun này giúp dễ hiểu, sửa đổi và mở rộng mã nguồn.

15. Tránh sử dụng 'assert' để xác thực dữ liệu

Bạn chỉ nên sử dụng các câu lệnh assert cho mục đích gỡ lỗi và phát triển; tránh sử dụng chúng cho việc xác thực dữ liệu trong mã sản xuất.

# Bad
assert x > 0, "x should be positive"

# Good
if x <= 0:
raise ValueError("x should be positive")

16. Tránh sử dụng các số Hard-Coded

Trong lập trình, thay vì sử dụng các con số được mã hóa cứng (hard-coded values), bạn nên dùng hằng số có tên (named constants).

  • Hằng số có tên là những giá trị cố định được đặt tên rõ ràng, giúp code dễ hiểu và dễ bảo trì hơn.

  • Tên của hằng số nên phản ánh ý nghĩa của giá trị, giúp người đọc dễ dàng nắm bắt mục đích sử dụng của nó.

def calculate_discount(price):
discount = price * 0.1 # 10% discount
return price - discount

Ví dụ trên đây sử dụng số cố định 0.1 để biểu thị giảm giá 10%.

Điều này khiến cho việc hiểu ý nghĩa của con số trở nên khó khăn (nếu không có comment) và việc điều chỉnh tỷ lệ giảm giá nếu cần thiết ở các phần khác của hàm trở nên phức tạp hơn.

def calculate_discount(price):
TEN_PERCENT_DISCOUNT = 0.1
discount = price * TEN_PERCENT_DISCOUNT
return price - discount

Thay vào đó chúng ta thay thế số cố định bằng một hằng số tên là TEN_PERCENT_DISCOUNT. Tên này ngay lập tức cho thấy ý nghĩa của giá trị, làm cho code dễ hiểu hơn.

17. Tuân thủ nguyên tắc DRY

Tránh viết cùng một đoạn code nhiều lần. Thay vào đó, hãy tái sử dụng code của bạn bằng cách sử dụng hàm, lớp, module, thư viện, hoặc các trừu tượng khác. Điều này làm cho code của bạn hiệu quả, nhất quán và dễ bảo trì hơn.

Nó cũng giảm nguy cơ lỗi và bug vì bạn chỉ cần sửa đổi code ở một chỗ nếu cần thay đổi hoặc cập nhật.

# Bad

def calculate_book_price(quantity, price):
return quantity * price
def calculate_laptop_price(quantity, price):
return quantity * price

# Good

def calculate_product_price(product_quantity, product_price):
return product_quantity * product_price

18. Tuân theo các tiêu chuẩn viết code đã được thiết lập

Điều quan trọng là tuân theo các quy tắc phổ biến trong lập trình như về khoảng cách, ghi chú và cách đặt tên. Hầu hết các ngôn ngữ lập trình đều có các tiêu chuẩn mã hóa và hướng dẫn về kiểu được cộng đồng chấp nhận, ví dụ như PEP 8 cho Python có một số quy tắc phổ biến là:

  • Sử dụng snake_case cho tên biến, hàm và lớp.

  • Sử dụng dấu cách thay vì tab để thụt lề.

  • Dùng 4 dấu cách cho mỗi mức thụt lề.

  • Giới hạn độ dài mỗi dòng tối đa 79 ký tự.

  • Xuống dòng trước toán tử nhị phân.

19. Luật Demeter

Luật Demeter, còn được gọi là nguyên tắc Ceres hoặc nguyên tắc "biết ít càng tốt", là một nguyên tắc thiết kế phần mềm hướng đối tượng (OOP) nhằm mục đích tăng cường sự gắn kết lỏng lẻo (loose coupling) và cải thiện tính dễ hiểu, bảo trì của code. 

Nghĩa là mỗi phương thức chỉ nên thực hiện một nhiệm vụ cụ thể và chỉ nên tương tác với các dữ liệu liên quan trực tiếp đến nhiệm vụ đó. Các lớp không nên phụ thuộc trực tiếp vào cấu trúc bên trong của nhau. Thay vào đó, chúng nên giao tiếp thông qua các giao diện (interface) được định nghĩa rõ ràng.

Để dễ hiểu hơn bạn có thể theo dõi ví dụ ở bên dưới:

class Order:
def __init__(self, customer):
self.customer = customer

def get_customer_name(self):
# Violation: Order knows too much about the customer's structure
return self.customer.get_profile().get_name()

Trong ví dụ này, lớp Order truy cập trực tiếp vào hồ sơ của khách hàng để lấy tên. Điều này vi phạm Luật của Demeter vì Order đang truy cập sâu vào cấu trúc bên trong của đối tượng Customer để lấy hồ sơ và tên.

class Order:
def __init__(self, customer):
self.customer = customer

def get_customer_name(self):
# Adherence: Order only interacts with its immediate collaborator
return self.customer.get_name()

Thay vào đó các bạn có thể viết như trên, lớp Order chỉ tương tác với thành phần lân cận của nó, đối tượng Customer, và gọi một phương thức trực tiếp trên nó để lấy tên của khách hàng.

20. Ưu tiên sự dễ đọc hơn sự ngắn gọn

Mặc dù code cần phải chạy và được máy tính hiểu, nhưng điều quan trọng là các lập trình viên khác cũng cần hiểu được code đó, đặc biệt là khi bạn đang làm việc trên một dự án với nhiều người.

Do đó, tính dễ đọc của code quan trọng hơn sự ngắn gọn trong phát triển phần mềm. Viết code ngắn gọn nhưng khó hiểu cho người khác thì không có ích lợi gì.

21. Giữ phần Import gọn gàng

Trong lập trình, bạn chỉ nên import các module và symbol cần thiết để giữ cho phần import sạch sẽ và cải thiện khả năng đọc hiểu code. Vì khi bạn import tất cả mọi thứ từ một module, tất cả các biến, hàm và lớp trong module đó cũng được import, điều này có thể gây ra khó khăn trong việc xác định nguồn gốc của một hàm hoặc lớp cụ thể.

Hãy tưởng tượng bạn muốn viết một hàm có tên là get_file. Bạn nhấn g và IDE của bạn đề xuất cho bạn một danh sách các hàm/lớp/biến bắt đầu bằng chữ g. Việc này sẽ gây rối rắm cho quá trình tìm kiếm của bạn. Đặc biệt khi bạn muốn gọi hàm đó, tên hàm của bạn có thể bị mất trong số các đề xuất và bây giờ IDE của bạn trở thành vấn đề hơn là một giải pháp hiệu quả.

# Bad
from module import *

# Good
from module import symbol1, symbol2

22. Không Nên Trả Về Null/None

Trong lập trình, nhiều ngôn ngữ mặc định trả về None (hoặc null) cho hàm nếu không có giá trị trả về nào được xác định. Tuy nhiên, việc trả về None một cách rõ ràng trong hàm có thể gây hiểu lầm.

Bởi vì, khi bạn trả về None một cách rõ ràng, bạn đang ngầm báo hiệu cho người đọc rằng hàm có thể trả về nhiều loại giá trị khác nhau, bao gồm cả None. Tuy nhiên, trong nhiều trường hợp, hàm chỉ trả về None trong một số tình huống nhất định, và các trường hợp khác sẽ trả về giá trị cụ thể.

23. Tránh lồng các khối try-except quá nhiều

Việc lồng các khối try-except quá nhiều có thể khiến logic xử lý lỗi trở nên phức tạp và khó hiểu. Điều này có thể gây ra khó khăn trong việc gỡ rối lỗi và bảo trì code về lâu dài.

Thay vào đó, các bạn hãy cố gắng đơn giản hóa logic xử lý lỗi. Nếu có thể, hãy tách các khối try-except ra thành các hàm riêng biệt để xử lý các loại lỗi cụ thể. Việc này sẽ giúp code dễ đọc, dễ hiểu và dễ maintain hơn.

# Bad
try:
try:
# Code that might raise errors
pass
except ValueError:
# Handle ValueError
pass
except Exception as e:
# Handle any other unexpected errors
pass

# Good
try:
# Code that might raise errors
pass
except ValueError:
# Handle ValueError
pass
except Exception as e:
# Handle any other unexpected errors
pass

24. Sử dụng lập trình đồng thời (concurrency) chỉ khi cần thiết

Lập trình đồng thời là một kỹ thuật lập trình cho phép nhiều tác vụ chạy đồng thời trên cùng một hệ thống. Mặc dù nó có thể cải thiện hiệu suất, nhưng việc sử dụng không đúng cách có thể dẫn đến code trở nên phức tạp và khó gỡ lỗi.

Tại sao nên cẩn trọng khi sử dụng lập trình đồng thời?

  • Dễ viết code sai: Việc triển khai tính năng đồng thời dễ dẫn đến code rối và khó bảo trì.

  • Lỗi tiềm ẩn: Ngay cả khi code có vẻ hoạt động bình thường, lỗi liên quan đến đồng thời có thể ẩn náu và chỉ phát sinh khi hệ thống chịu áp lực cao.

Một số vấn đề thường gặp trong lập trình đồng thời:

  • Đói tài nguyên (Starvation): Xảy ra khi một tiến trình (thread) không thể truy cập vào một tài nguyên được chia sẻ trong một khoảng thời gian dài, mặc dù nó đã nhiều lần yêu cầu. Nguyên nhân có thể do các tiến trình khác liên tục chiếm giữ tài nguyên, khiến tiến trình còn lại rơi vào tình trạng "đói" không thể thực hiện được.

  • Bế tắc (Deadlock): Xảy ra khi hai hoặc nhiều tiến trình bị chặn lại vô thời hạn, mỗi tiến trình chờ tiến trình khác giải phóng tài nguyên mà nó cần. Ví dụ, tiến trình A giữ tài nguyên X và chờ tài nguyên Y do tiến trình B nắm giữ, trong khi tiến trình B lại giữ tài nguyên Y và chờ tài nguyên X do tiến trình A nắm giữ, dẫn đến tình trạng phụ thuộc lẫn nhau.

Lời kết

Như vậy, VietnamWorks inTECH đã vừa đi qua 24 quy tắc quan trọng trong lập trình, từ việc giữ file nguồn ngắn gọn cho đến việc không nên trả về giá trị Null/None.

Việc tuân thủ các luật này không chỉ giúp code của bạn trở nên sạch sẽ và dễ đọc hơn mà còn giúp bạn tránh được nhiều lỗi và khó khăn trong quá trình phát triển và bảo trì mã nguồn. Hãy luôn nhớ rằng việc áp dụng các nguyên tắc này không chỉ là việc của một lập trình viên giỏi mà còn là cách tạo ra phần mềm chất lượng cao.

VietnamWorks inTECH