NHẬN XÉT VÀ KẾT LUẬN

Một phần của tài liệu ỨNG DỤNG KỸ THUẬT TÁI CẤU TRÚC MÃ NGUỒN ĐỂ TRIỂN KHAI DÒ TÌM VÀ CẢI TIẾN CÁC ĐOẠN MÃ XẤU TRONG CHƢƠNG TRÌNH C# (Trang 26)

Với những nội dung đã đƣợc trình bày ở trên về cơ sở lý thuyết của kỹ thuật tái cấu trúc mã nguồn (refactoring), đó là một kỹ thuật làm thay đổi cấu trúc nội tại phần mềm, làm cho phần mềm dễ hiểu hơn và ít tốn chi phí để cập nhật mà không làm thay đổi ứng xử bên ngoài.

Hiện tại kỹ thuật mới này đang đƣợc áp dụng và triển khai ở các quốc gia có nền công nghiệp phần mềm phát triển (Mỹ, Nhật, Ấn Độ,..) và đang tiến đến một tiêu chuẩn trong qui trình phát triển phần mềm trong tƣờng lai gần. Trong một bài báo về tƣơng lai của ngành công nghệ phần mềm, một chuyên gia trong lãnh vực quản lý và tƣ vấn các chiến lƣợc phần mềm Alex Iskold đã đƣa ra một nhận định rằng nền công nghệ phần mềm trong tƣơng lai gần sẽ phát triển theo phƣơng pháp phần mềm phát triển linh hoạt thay cho phƣơng pháp mô hình thác nƣớc đã tồn tại [15]. Phƣơng pháp phát triển phần mềm linh hoạt (Agile Development Method) ngoài việc đáp ứng khả năng tạo ra các phần mềm có sự ổn định cao còn có khả năng thích nghi và tiến hóa để thích hợp với môi trƣờng hoạt động. Phƣơng pháp này dựa trên hai kỹ thuật chính đó là:

- Refactoring - Tái cấu trúc mã nguồn

- Developer Testing – Hoạt động kiểm thử do chính lập trình viên đảm nhận

Nhƣ vậy vấn đề nghiên cứu và ứng dụng kỹ thuật refactoring là một xu hƣớng tất yếu và cần thiết trong lãnh vực phát triển công nghệ phần mềm ngày nay.

CHƢƠNG II: LỖI CẤU TRÚC TRONG MÃ NGUỒN (BAD SMELLS IN CODE)

II.1 KHÁI NIỆM VỀ LỖI CẤU TRÚC (BAD SMELLS)

Trong khoa học máy tính, mã xấu hay lỗi cấu trúc (bad smells) là tất cả những dấu hiệu tồn tại trong mã nguồn của chƣơng trình mà nó tiềm ẩn khả năng xảy ra lỗi trong quá trình hoạt động. Các dấu hiệu đó có thể là: chƣơng trình đƣợc thiết kế không logic, các phân đoạn mã nguồn có cấu trúc không đồng nhất và khó hiểu, mã nguồn trùng lắp, tên hàm và biến khó kiểm soát, lớp và phƣơng thức phức tạp,..v.v..

Thông thƣờng các dấu hiệu này sẽ đƣợc các nhà phát triển và lập trình phát hiện và tinh chỉnh qua các bƣớc trong qui trình phát triển phần mềm dựa trên việc refactoring mã nguồn. Nhƣ vậy có thể xem mã xấu là điều kiện để thực thi việc refactoring mã nguồn của một chƣơng trình hay nói đúng hơn đó là cặp song hành: nếu một mã nguồn chƣơng trình có bad smells thì refactoring để làm cho chƣơng trình tốt hơn.

II.2 LỖI CẤU TRÚC VÀ GIẢI PHÁP CẢI TIẾN

Dựa trên kinh nghiệm nhiều năm lập trình và nghiên cứu về refactoring, hai chuyên gia Kent Beck và Marting Fowler đã đề xuất một tập các mã xấu thƣờng gặp[4] và giải pháp cải tiến dựa trên kỹ thuật refactoring

II.2.1 Duplicated Code - Trùng lặp mã

Nếu trong mã nguồn tồn tại những đoạn mã trùng lặp ở nhiều nơi:

- Sử dụng Extract Method để làm triệt tiêu các đoạn mã trùng lặp bên trong một lớp - Khi hai lớp đồng kế thừa từ một lớp cha (sibling classes) có các mã nguồn trùng

lặp, áp dụng Extract Method trong cả hai lớp này sau đó dùng Pull Up Method đến lớp cha

- Nếu tồn tại những đoạn mã tƣơng tự nhau thì sử dụng Extract Method trên những phần tƣơng tự nhau này. Và sau đó có thể áp dụng tiếp kỹ thuật Form Template Method.

- Nếu có các phƣơng thức cùng thực hiện một công việc với các thuật toán khác nhau, thì chọn ra một thuật toán tốt nhất và áp dụng Substitute Algorithm

- Nếu hai lớp không có quan hệ với nhau mà có các đoạn mã trùng lặp, sử dụng

Extract Class trên một lớp và sau đó dùng thành phần lớp mới đƣợc tạo ra cho lớp còn lại.

Ví dụ 1: Hai lớp con SalesmanEngneer đồng kế thừa từ lớp cha Employee có phƣơng thức getName bị trùng lặp => Sử dụng Pull Up Method

Ví dụ 2: Sử dụng Substitute Algorithm để thay mới thuật tóan trong thân phƣơng thức làm cho chƣơng trình ngắn gọn và dễ hiểu hơn.

String foundPerson(String[] people){ for (int i = 0; i < people.length; i++) {

if (people[i].equals ("Don")){ return "Don"; } if (people[i].equals ("John")){ return "John"; } if (people[i].equals ("Kent")){ return "Kent"; } } return ""; }

String foundPerson(String[] people){

List candidates = Arrays.asList(new String[] {"Don", "John", "Kent"});

for (int i=0; i<people.length; i++)

if (candidates.contains(people[i])) return people[i];

return ""; }

II.2.2 Long Method – Phƣơng thức phức tạp

Một trong những yêu cầu đối với một chƣơng trình nguồn là hạn chế các phƣơng thức đƣợc tổ chức với số lƣợng dòng mã quá nhiều. Điều này sẽ gây khó khăn cho việc kiểm soát và đọc hiểu mã lệnh trong quá trình cập nhật. Khi đó giải pháp ở đây là chia tách để làm cho các phƣơng thức nhỏ và tinh gọn hơn.

- Khi gặp các phƣơng thức có nhiều dòng mã, 99% là chúng ta sử dụng kỹ thuật (adsbygoogle = window.adsbygoogle || []).push({});

Extract Method để làm ngắn các phƣơng thức này bằng cách tạo ra một số phƣơng thức mới từ việc trích chọn một số đoạn mã từ phƣơng thức ban đầu.

- Nếu một phƣơng thức đi kèm với nhiều tham số và biến tạm, sử dụng Replace Temp with Query để triệt tiêu các biến tạm này. Với danh sách dài của các tham số, chúng ta sử dụng Introduce Parameter Object hoặc Preserve Whole Object để làm ít chúng lại. Nếu nhƣ lúc này vẫn còn nhiều biến tạm và tham số, một giải pháp triệt để hơn là dùng Replace Method with Method Object

- Khi trích xuất một khối mã lệnh ra làm phƣơng thức riêng, một giải pháp hiệu quả nhất trong việc xác định tên của phƣơng thức mới tạo này sao cho phù hợp là dựa vào thông tin chú giải về mục đích của khối mã lệnh đó.

- Khi gặp các điều kiện và vòng lặp, chúng ta nên sử dụng Decompose Conditional

để tách và tạo ra các phƣơng thức riêng

Ví dụ 1: Sử dụng kỹ thuật Extract Method để làm ngắn phƣơng thức bằng cách tạo ra một phƣơng thức mới từ việc trích chọn một số đoạn mã từ phƣơng thức ban đầu.

class Program {

static void Main(string[] args){

Console.WriteLine("*** Please enter your credentials ***");

// Get user name and password.

string userName; string passWord;

Console.Write("Enter User Name: "); userName = Console.ReadLine(); Console.Write("Enter Password: "); passWord = Console.ReadLine(); } } class Program {

static void Main(string[] args){

Console.WriteLine("*** Please enter your name ***");

GetCredentials();

Console.ReadLine(); }

private static void GetCredentials(){ string userName;

string passWord;

Console.Write("Enter User Name: "); userName = Console.ReadLine(); Console.Write("Enter Password: "); passWord = Console.ReadLine(); }

Ví dụ 2: Sử dụng Replace Temp with Query để chuyển đổi biểu thức thành phƣơng thức. Thay thế các tham chiếu của biến tạm nhận đƣợc từ biểu thức bị chuyển đổi bởi phƣơng thức vừa tạo mới.

double basePrice = _quantity * _itemPrice;

if (basePrice > 1000) return basePrice * 0.95; else return basePrice * 0.98; if (basePrice() > 1000) return basePrice() * 0.95; else return basePrice() * 0.98; ... double basePrice() {

return _quantity * _itemPrice; }

II.2.3 Large Class – Qui mô lớp lớn

- Một lớp có qui mô lớn thƣờng chứa nhiều biến số và tồn tại trùng lặp mã nguồn. Sử dụng Extract Class để đóng gói các biến thƣờng đi chung với nhau lại. Sử dụng Extract Subclass khi các phƣơng thức kết hợp với các biến mở rộng chức năng của lớp. Thông thƣờng không phải lúc nào tất cả các biến trong một lớp cũng luôn đƣợc sử dụng , vì vậy chúng ta cũng có thể sử dụng Extract Class hoặc

Extract Subclass để trích xuất chúng nhiều lần.

- Một thủ thuật hữu ích nữa là xem xét mục đích sử dụng của các lớp kết hợp với kỹ thuật Extract Interface để tổ chức và phân chia lớp cho hợp lý.

- Trong lập trình hƣớng đối tƣợng, khi gặp một lớp GUI có qui mô lớn cần chuyển dữ liệu và hoạt động xử lý đến một đối tƣợng có phạm vi riêng. Điều này yêu cầu có sự trùng lắp dữ liệu ở hai nơi cũng nhƣ sự đồng nhất trong quá trình động bộ. Lúc này Duplicate Observed Data trong refactoringsẽ đƣợc xem xét và sử dụng.

Ví dụ: Một lớp mà có một số chức năng chỉ đƣợc sử dụng cho một vài thực thể (instances) cá biệt thì nên sử dụng Extract Subclass để tạo một lớp con đi kèm các chức năng đó

II.2.4 Long Parameter List - Danh sách tham số quá dài (adsbygoogle = window.adsbygoogle || []).push({});

Trong lập trình hƣớng thủ tục, việc trao đổi mọi thứ dữ liệu giữa các thủ tục và hàm thông qua việc truyền tham số. Một giải pháp khác tốt hơn là sử dụng dữ liệu toàn cục nhƣng tiềm ẩn nhiều rủi ro trong việc kiểm soát. Với lập trình hƣớng đối tƣợng thì khác, chúng ta không cần phải chuyển mọi thứ thông qua một danh sách dài các tham số khó hiểu mà chỉ là một ít dữ liệu cơ sở vừa đủ cho việc truy suất tất cả các giá trị cần thiết khác luôn đƣợc cập nhật mới.

- Sử dụng Replace Parameter with Method để nhận dữ liệu từ một đối tƣợng đã biết. Đối tƣợng này có thể là một trƣờng dữ liệu hoặc một tham số khác. Sử dụng

Preserve Whole Object để thay thế một nhóm dữ liệu trong lớp bởi một thành phần dữ liệu thay thế chung của bản thân lớp đó.Với những mục dữ liệu riêng mà không gắn liền với một đối tƣợng nào cả, sử dụng Introduce Parameter Object

- Một ngoại lệ quan trọng trong việc thực hiện những thay đổi này, đó là khi chúng ta không muốn tạo ra một sự phụ thuộc từ đối tƣợng đƣợc gọi đến một đối tƣợng lớn hơn. Giải pháp hợp lý trong trƣờng hợp này là chuyển dữ liệu nhƣ các tham số, khi đó cần chú ý đến những rủi ro có liên quan. Nếu danh sách tham số quá dài hoặc thƣờng xuyên thay đổi, chúng ta cần xem xét lại cấu trúc phụ thuộc.

Ví dụ 1: Sử dụng Replace Parameter with Method để rút gọn tham số phƣơng thức public double getPrice() {

int basePrice = _quantity * _itemPrice;

int discountLevel;

if (_quantity > 100) discountLevel = 2; else discountLevel = 1;

double finalPrice = discountedPrice (basePrice, discountLevel);

return finalPrice; }

private double discountedPrice (int basePrice, int discountLevel) { if (discountLevel == 2) return basePrice * 0.1;

else return basePrice * 0.05; }

public double getPrice() {

int basePrice = _quantity * _itemPrice;

int discountLevel = getDiscountLevel();

double finalPrice = discountedPrice (basePrice);

return finalPrice; }

private int getDiscountLevel() { if (_quantity > 100) return 2; else return 1;

private double discountedPrice (int basePrice) {

if (getDiscountLevel() == 2) return basePrice * 0.1; else return basePrice * 0.05;

}

Ví dụ 2: Với những tham số là các mục dữ liệu riêng mà không gắn liền với một đối tƣợng nào cả, sử dụng Introduce Parameter Object

II.2.5 Divergent Change – Cấu trúc lớp ít có tính khả biến

Cấu trúc của phần mềm phải đƣợc thiết kế sao cho dễ dàng trong việc thay đổi và cập nhật. Nếu một lớp bị thay đổi theo nhiều cách khác nhau tùy thuộc vào các nguyên nhân khác nhau thì chúng ta nên chia lớp đó ra làm hai, trong đó mỗi lớp đƣợc tạo ra dựa trên yếu tố đặc trƣng thuận lợi với các cách thay đổi nêu trên. Trong trƣờng hợp chia tách này, giải pháp chính là sử dụng Extract Class.

Ví dụ: Sử dụng kỹ thuật Extract Class tạo một lớp mới và dịch chuyển các thuộc tính và phƣơng thức có liên quan từ lớp cũ sang

II.2.6 Shotgun Surgery – Lớp đƣợc thiết kế không hợp lý và bị phân rã

Trái ngƣợc với Divergent Change là Shotgun Surgery. Mỗi khi chúng ta thực hiện một sự thay đổi trong chƣơng trình, thông thƣờng việc thay đổi này kéo theo một chuỗi các thay đổi nhỏ ở nhiều lớp khác nhau và diễn ra ở khắp nơi. Lúc này việc phát hiện chính xác và thay đổi toàn bộ là một điều rất khó khăn không loại trừ có thể thiếu sót ở một nơi quan trọng nào đó.

- Sử dụng Move Method & Move Field để dịch chuyển (thâu gôm) các phƣơng thức và thuộc tính từ lớp này sang lớp khác để làm cho chƣơng trình có cấu trúc rõ hơn. - Nếu chƣa có lớp nào thích hợp để tích hợp vào thì tạo một lớp mới. Và trong trƣờng hợp này, chúng ta sử dụng giải pháp Inline Class để tích hợp các thành phần thay đổi này.

Ví dụ 1: Nếu một thuộc tính aField đƣợc sử dụng nhiều hơn trong lớp khác Class 2 thay vì lớp chứa nó Class 1 thì chúng ta nên sử dụng Move Fieldđể dịch chuyển phƣơng thức đó sang lớp kia.

Ví dụ 2: Nếu hai lớp có qui mô nhỏ (không làm gì nhiều) và có quan hệ với nhau thì nên sử dụng Inline Class để hợp nhất chúng lại một lớp. (adsbygoogle = window.adsbygoogle || []).push({});

II.2.7 Feature Envy – Phân bố phƣơng thức giữa các lớp không hợp lý

Trong lập trình hƣớng đối tƣợng, đôi lúc chúng ta gặp trƣờng hợp một phƣơng thức của một lớp mà nó sử dụng hoặc hỗ trợ nhiều chức năng (thuộc tính và phƣơng thức) cho một lớp khác hơn chính lớp chứa nó. Khi đó chúng ta sẽ sử dụng Extract MethodMove Method để trích xuất và dịch chuyển phƣơng thức vào một vị trị lớp thích hợp nhất.

Ví dụ: Một phƣơng thức của một lớp Class 1 mà nó sử dụng nhiều chức năng (thuộc tính và phƣơng thức) của một lớp khác Class 2 hơn chính nó thì sử dụng Move Method để dịch chuyển phƣơng thức đó sang lớp kia.

II.2.8 Data Clumps – Gôm cụm dữ liệu

Thông thƣờng trong mã nguồn, chúng ta cũng gặp trƣờng hợp cùng lúc ba hoặc bốn mục dữ liệu thƣờng đi chung với nhau (nhiều thuộc tính trong lớp, các tham số truyền cho phƣơng thức,…) và đƣợc sử dụng ở nhiều nơi. Một giải pháp tốt hơn là gôm cụm chúng lại và chuyển sang một lớp đối tƣợng:

- Nếu các mục dữ liệu đó là các thuộc tính -> sử dụng Extract Class để định nghĩa các lớp mới phù hợp với từng nhóm thuộc tính.

- Nếu các mục dữ liệu là các tham số -> sử dụng Introduce Parameter Object hoặc

Preserve Whole Object để làm gọn hơn biểu thức tham số truyền vào và thân chƣơng trình xử lý

Ví dụ: Thay vì truy xuất từng giá trị thuộc tính riêng lẻ của một đối tƣợng và truyền các giá trị này theo kiểu tham số cho một phƣơng thức. Chúng ta sẽ sử dụng kỹ thuật

Preserve Whole Object để truyền nguyên đối tƣợng đó.

int low = daysTempRange().getLow(); int high = daysTempRange().getHigh();

withinPlan = plan.withinRange(low, high);

withinPlan = plan.withinRange(daysTempRange());

II.2.9 Primitive Obsession – Khả năng thể hiện dữ liệu của lớp bị hạn chế

Một trong những mã xấu thƣờng gặp là sự gộp chung các lớp đối tƣợng có quan hệ với nhau thành một lớp đối tƣợng chung có cấu trúc phức tạp và khó hiểu. Trong trƣờng hợp này, chúng ta cần phải tùy biến cấu trúc ban đầu cho hợp lý hơn:

- Nếu một thuộc tính dữ liệu trong lớp cần đƣợc bổ sung thêm thộng tin hoặc hành vi, sử dụng Replace Data Value with Object để chuyển đổi thuộc tính dữ liệu đó sang một lớp đối tƣợng.

- Nếu giá trị dữ liệu là một mã kiểu (type code) thì sử dụng Replace Type Code with Class để chuyển sang một lớp mới

- Nếu mã kiểu (type code) là điều kiện rẻ hƣớng (làm ảnh hƣởng hành vi của một lớp) thì sử dụng Replace Type Code with Subclasses hoặc Replace Type Code with State/Strategy để chuyển đổi mã kiểu này sang một đối tƣợng tĩnh

- Nếu là các thuộc tính ban đầu thƣờng đi chung thì sử dụng Extract Class, và nếu đó là các danh sách tham số thì sử dụng Introduce Parameter Object hoặc Replace Array with Object trong trƣờng hợp một mảng dữ liệu để chuyển đổi sang một lớp đối tƣợng tƣơng ứng.

Ví dụ 1: Chúng ta có một lớp Order dùng để lƣu các dữ liệu về đơn hàng của đối tƣợng khách hàng trong đó customer là một thuộc tính chuỗi của lớp. Trên thực tế, thì đôi lúc ngƣời sử dụng cần biết thêm thông tin về khách hàng tƣơng trong đơn hàng (địa chỉ, số điện thoại,…). Lúc này chúng ta cần sử dụng kỹ thuật Replace Data Value with Object

để chuyển đổi dữ liệu customer sang một đối tƣợng Customer.

Ví dụ 2: Sử dụng Replace Type Code with Class để thay kiểu dữ liệu số bởi một lớp class Person {

public static final int O = 0; public static final int A = 1; public static final int B = 2; public static final int AB = 3; private int _bloodGroup;

public Person (int bloodGroup) { _bloodGroup = bloodGroup; }

public void setBloodGroup(int arg) { _bloodGroup = arg;

}

public int getBloodGroup() {

Một phần của tài liệu ỨNG DỤNG KỸ THUẬT TÁI CẤU TRÚC MÃ NGUỒN ĐỂ TRIỂN KHAI DÒ TÌM VÀ CẢI TIẾN CÁC ĐOẠN MÃ XẤU TRONG CHƢƠNG TRÌNH C# (Trang 26)