Mã xấu (Code Smells) và Refactor hướng đến người lập trình chuyên nghiệp
Bài đăng này đã không được cập nhật trong 6 năm
Khi mới bắt đầu làm quen về lập trình việc đầu tiên tôi hướng đến là việc thực thi của đoạn lệnh mình vừa tạo ra cùng kết quả mình mong muốn, mà quên đi việc làm sao cho đoạn lệnh đó được sử dụng sau này, và tôi luôn phải tự mình mò mẩn trong đống code mình vừa tạo ra và tự hỏi bản thân "method này dùng để làm gi? ý nghĩa đoạn code này như thế nào...". Thói quen code vội, code cẩu thả của mình trong thời gian học tập đã tạo nên một người lập trình thiếu chuyên nghiệp, và luôn nhận được nhiều comment về nội dung cách trình bày source code. Trong thời gian làm việc ở môi trường chuyên nghiệp mình đã học hỏi được nhiều điều và tự nhận thấy việc REFACTOR code sẽ hướng đến một người lập trình chuyên nghiệp. Viết code là một công việc phức tạp. Để cho ra đời một đoạn code tốt đòi hỏi lập trình viên phải tốn khá nhiều thời gian và công sức. Hầu hết những người mới vào nghề thường viết code theo kiểu “miễn sao chạy là được”. Đây là một thói quen xấu mà nếu không thay đổi ngay từ đầu, sẽ rất khó sửa về sau.
Mã xấu là gì?
Mã xấu là từ được dùng để chỉ phần code mà ta cảm thấy không ổn. Đây thường là đoạn code vi phạm những quy tắc trong lập trình. Hãy xem qua ví dụ sau: bạn đang đọc một bài viết và bắt gặp một lỗi chính tả. Ngay lập tức, bạn có một cảm giác khó chịu trong người. Khi xem code, ta có những phản ứng tương tự và biết ngay chỗ đó không ổn. Đó chính là mã xấu.
Refactor là gì?
Để nhận biết mã xấu đòi hỏi ta phải có nhiều kinh nghiệm. Tuy nhiên, nhận biết được chúng vẫn chưa đủ, ta còn phải biết cách tùy biến sao cho tối ưu nhất thông qua các kỹ thuật refactor. Khi sử dụng các kỹ thuật này, ta phải hết sức cẩn trọng vì nếu dùng sai mục đích, chúng có thể gây hại. Vậy refactor là gì và tại sao nó lại quan trọng như thế?
Refactor là các thao tác tùy chỉnh code nhằm cải thiện nó mà không thay đổi chức năng ban đầu.
Các kỹ thuật thường dùng
Tách method
Chúng ta thường có thói quen code theo logic, suy nghĩ theo luồng xử lý, dẫn đến khi thực hiện code chúng ta thường tạo những đoạn code dài với nhiều xử lý bên trong. Kỹ thuật refactor thường thấy nhất đó là “Tách method” (Extract method). Kỹ thuật này đơn giản chỉ là tìm một đoạn code dùng nhiều lần ở nhiều nơi, tách nó ra và cho vào một method riêng. Sau đó, tại vị trí cũ, ta gọi method vừa mới tạo. Kỹ thuật này giúp ích cho người lập trình tận dụng code cho những xử lý giống nhau.
Kỹ thuật này khá phổ biến nên hầu hết IDE đều hỗ trợ. Trong Visual Studio hay Eclipse, bạn nhấp phải chuột vào code muốn tách, chọn Refactor > Extract Method hoặc dùng tổ hợp phím Ctrl + R + M, một hộp thoại hiện ra để ta nhập tên cho method mới.
Tách class
Tách class là kỹ thuật refactor được áp dụng cho những class lớn. Trong lập trình hướng đối tượng, dữ liệu và phương thức có liên quan sẽ được gom thành một class. Tuy nhiên, khi thiết kế, đôi lúc ta thêm nhiều chức năng không thuộc class đó. Đây là lúc nên áp dụng kỹ thuật tách class. Để tách class, ta phải xem trong class hiện tại có những thành phần nào liên quan với nhau. Đây là một quan điểm quang trọng trong lập trình hướng đối tượng, thể hiện tư duy và suy nghĩ của một người lập trình chuyên nghiệp
Sử dụng phương pháp này, ta chỉ cần tìm những class có kích thước lớn, sau đó tách những thành phần có liên quan và gom chúng lại vào một class mới.
Đơn giản hóa biểu thức điều kiện
Nếu xem code của lập trình viên mới vào nghề, ta thường gặp những biểu thức điều kiện như thế này:
public void Discount(Order order)
{
if (order.Items.Count > 100 && order.Items.Count <= 200 && (DateTime.Now - order.OrderDate).Days < 10)
{
order.Total -= order.Total * 10 / 100;
}
}
Biểu thức điều kiện trong câu lệnh if quá phức tạp do phải xác thực nhiều dữ kiện khác nhau. Do đó, để dễ dàng cho việc bảo trì, ta nên đơn giản hóa bằng cách tách nó ra thành một method riêng.
public void Discount(Order order)
{
if (isDiscountable(order))
{
order.Total -= order.Total * 10 / 100;
}
}
bool isDiscountable(Order order)
{
return order.Items.Count > 100 && order.Items.Count <= 200 && (DateTime.Now - order.OrderDate).Days < 10;
}
Sau khi tách thành một method riêng, ta thấy biểu thức điều kiện trở nên dễ đọc và dễ hiểu hơn.
Di chuyển dữ liệu
Thông thường, ta sẽ thực hiện di chuyển method nhiều hơn là di chuyển dữ liệu. Tuy nhiên, có những trường hợp mà một class liên tục truy xuất dữ liệu trong một class khác. Lúc này, ta nên xem xét việc mang dữ liệu của class kia qua class chứa method đó.
Để hiểu rõ hơn vấn đề này, ta hãy xem qua ví dụ sau:
class A
{
public void MethodA() { var result = B.PropertyB; }
public void MethodB() { var result = B.PropertyB; }
public void MethodC() { var result = B.PropertyB; }
}
class B
{
public static string PropertyB { get; set; }
...
}
Ta thấy các method trong class A đều truy xuất PropertyB trong class B. Do đó, ta cân nhắc xem có nên chuyển PropertyB sang class A hay không.
class A { public string PropertyB { get; set; }
public void MethodA() { ... }
public void MethodB() { ... }
public void MethodC() { ... }
}
class B {
...
}
Giờ đây class A chứa các method và dữ liệu cùng một chỗ. Điều này giúp cho việc truy xuất dễ dàng và thuận tiện hơn.
Bảo toàn đối tượng
Danh sách tham số quá dài trong một method là dấu hiệu mã xấu. Thông thường, danh sách tham số nên dừng lại ở 3 đến 4 tham số, không nên nhiều hơn.
Tạo đối tượng cho danh sách tham số
Có những trường hợp, ta liên tục sử dụng một danh sách tham số lặp đi lặp lại trong rất nhiều method khác nhau. Lúc này, ta nên xem xét việc lấy tất cả các tham số để tạo thành một class mới.
Tham số hóa method
Đây là kỹ thuật refactor thường được dùng để biến nhiều method tương tự nhau thành một method duy nhất. Ta hãy xem qua ví dụ sau:
class Order
{
public void ChangeStatusToNew() { ... }
public void ChangeStatusToDelivering() { ... }
public void ChangeStatusToClosed() { ... }
public void ChangeStatusToCanceled() { ... }
}
Các method trong class Order rất giống nhau về chức năng, tất cả đều chuyển trạng thái status. Ta sẽ gom chúng lại thành một method như sau:
class Order
{
public void ChangeStatus(string status) { ... }
}
Thay vì dùng quá nhiều method, ta chỉ cần một method duy nhất. Tuy nhiên, chuyển qua dùng tham số kiểu string có thể nảy sinh bug vì người dùng được phép nhập giá trị không phù hợp cho status.
Kéo method và dữ liệu lên lớp cha
Việc kéo method và dữ liệu lên lớp cha có liên quan tới tính chất kế thừa trong lập trình hướng đối tượng. Để biết nên kéo thành phần nào lên lớp cha, ta xem trong các lớp con có thành phần nào giống nhau hay không. Nếu có thì nó chính là thứ mà ta cần phải kéo lên lớp cha.
class Employee
{
public string Name { get; set; }
public string ID { get; set; }
}
class Coder : Employee
{
public void DoWork() { ... }
}
class Manager : Employee
{
public void DoWork() { ... }
}
Ta thấy class Coder và Manager đều thực hiện DoWork. Do vậy, ta nên kéo method này lên lớp cha Employee để các lớp con có thể kế thừa. Đồng thời, nếu cần thiết, ta cũng có thể ghi đè (override) method DoWork cho từng class con.
Đẩy method và dữ liệu xuống lớp con
Cũng giống như phương pháp ở trên, nhưng chỉ khác chiều di chuyển. Thay vì kéo chúng lên, ta sẽ đẩy chúng từ lớp cha xuống lớp con. Vậy làm sao để biết những thành phần nào nên đẩy xuống? Rất đơn giản, bạn hãy tìm những thành phần mà chỉ hữu ích cho một lớp con mà không được dùng trong những lớp con khác. Hãy xem ví dụ sau:
class Bird
{
public void Eat() { ... }
public void Drink() { ... }
public void Sleep() { ... }
public void Fly() { ... }
}
class Parrot : Bird { ... }
class Penguin : Bird { ... }
class Ostrich : Bird { ... }
Trong các loài chim, chỉ có Parrot là có thể bay, hai loài còn lại thì không. Do đó, ta nên đẩy method Fly từ lớp cha Bird xuống lớp Parrot vì method này chỉ hữu ích cho lớp Parrot mà thôi.
Chuỗi gọi method
Khi viết code, ta thường bắt gặp tình huống như sau:
Coder coder = coderList.getCurrentCoder();
Work work = coder.getCurrentWork();
Status status = work.getCurrentStatus();
Hoặc ta thấy nó ở dưới dạng gọn hơn:
Status status = coders.getCurrentCoder().getCurrentWork().getCurrentStatus();
Việc gọi liên tục từ method này đến method khác tạo thành một chuỗi gọi method. Các chuỗi gọi method gây phiền toái do phải truy xuất nhiều cấp để có được thông tin cần thiết. Trong trường hợp này, nếu cần truy xuất trạng thái công việc thường xuyên, tốt nhất là nên tạo một method để lấy thông tin ngay tại class CoderList. Khi cần, ta chỉ gọi dòng lệnh đơn giản sau thay vì phải ngụp lặn trong đống method.
Status status = coderList.getCurrentCoderWorkStatus();
Một chức năng, một method
Trong lập trình, nguyên tắc separation of concerns luôn là một thói quen tốt mà ta nên tuân theo. Do đó, những method như sau nên được viết lại:
bool SubmitInfoAndSendMail() { ... }
Nhìn vào tên method, ta thấy nó đảm nhận 2 chức năng: một là gửi thông tin (SubmitInfo), hai là gửi email (SendMail) và nó sẽ trả về giá trị boolean để thông báo trạng thái. Giả sử có lỗi xảy ra và method này trả về false, vậy làm sao biết được lỗi đó do phần SubmitInfo hay SendMail gây ra? Giải pháp hiệu quả nhất là nên tách nó thành 2 method nhỏ. Mỗi method đảm nhận một chức năng:
bool SubmitInfo() { ... }
bool SendMail() { ... }
Giờ đây, nếu method nào trả về false, ta biết ngay method đó có vấn đề.
Phần lớn những kỹ thuật refactor chỉ là những thay đổi nhỏ trong code, tuy nhiên, giá trị nó mang về lại rất lớn. Các phương pháp refactor đã trình bày trong bài này chỉ là một phần rất nhỏ trong thế giới refactor. Để nhận ra khi nào nên dùng kỹ thuật nào đòi hỏi nhiều kinh nghiệm. Do vậy, ngay từ bây giờ, hãy tích lũy kinh nghiệm bằng cách áp dụng refactor vào quá trình viết code của bạn.
Với những chia sẻ trên, ngay từ bây giờ, hãy tích lũy kinh nghiệm bằng cách áp dụng refactor vào quá trình viết code của bạn.
All rights reserved