Tái cấu trúc (Refactoring) là thay đổi cấu trúc của một chương trình mà không làm thay đổi chức năng của nó. Tái cấu trúc là một kỹ thuật mạnh, nhưng nó cần được thực hiện cẩn thận. Mối nguy hiểm chính là các lỗi vô ý có thể được đưa vào, đặc biệt là khi tái cấu trúc được thực hiện bằng tay. Mối nguy hiểm này dẫn đến một sự chỉ trích thường xuyên về tái cấu trúc: tại sao lại sửa chữa mã nếu nó không bị hỏng?
Có một vài lý do để bạn có thể muốn tái cấu trúc mã. Đầu tiên là bắt nguồn của câu chuyện cổ tích: cơ sở mã rất cổ của sản phẩm đáng kính được kế thừa hoặc nếu không thì xuất hiện bí ẩn. Nhóm phát triển ban đầu đã biến mất. Một phiên bản mới, với các tính năng mới, phải được tạo ra, nhưng mã không còn hiểu được nữa. Nhóm phát triển mới, làm việc cả đêm lẫn ngày, giải mã nó, vẽ bản đồ nó và sau nhiều kế hoạch và thiết kế, phá hỏng mã hoàn toàn. Cuối cùng, cẩn thận, họ đặt nó tất cả trở lại với nhau theo tầm nhìn mới. Đây là tái cấu trúc trên quy mô khác thường và một ít vẫn còn hoạt động để nói về chuyện này.
Một kịch bản thực tế hơn là một yêu cầu mới được đưa vào cho dự án đòi hỏi thay đổi thiết kế. Thật là vụn vặt cho dù yêu cầu này đã được đưa vào do sơ xuất trong kế hoạch ban đầu hoặc do cách tiếp cận lặp lại (chẳng hạn như phát triển nhanh nhẹn hoặc phát triển dựa vào thử nghiệm) đang được sử dụng để thận trọng đưa vào các yêu cầu trong suốt quá trình phát triển. Đây là tái cấu trúc trên một quy mô nhỏ hơn nhiều và nó thường yêu cầu thay đổi hệ thống phân cấp lớp, có lẽ do đưa vào các giao diện hoặc lớp trừu tượng, chia tách các lớp, sắp xếp lại các lớp và v.v.
Một lý do cuối cùng để tái cấu trúc, khi các công cụ tái cấu trúc tự động có sẵn, chỉ đơn giản là một phím tắt để tạo mã ở vị trí đầu tiên -- một cái gì đó giống như cách sử dụng một chương trình kiểm tra lỗi chính tả (spellchecker) để phân loại một từ khi bạn không chắc cách đánh vần nó. Việc sử dụng tái cấu trúc nhàm chán này -- chẳng hạn để tạo ra các phương thức getter và setter -- có thể là một bộ tiết kiệm thời gian hiệu quả một khi bạn đã quen thuộc với các công cụ này.
Các công cụ tái cấu trúc của Eclipse không được dự kiến để sử dụng cho phép tái cấu trúc tại một quy mô khác thường -- một vài công cụ có -- nhưng chúng là vô giá để làm thay đổi mã trong quá trình diễn biến của một ngày làm việc của lập trình viên trung bình, cho dù điều đó liên quan đến các kỹ thuật phát triển nhanh nhẹn hay không. Cuối cùng, bất kỳ hoạt động phức tạp nào có thể được tự động hóa đều là nhàm chán, cần tránh. Việc biết các công cụ tái cấu trúc Eclipse có sẵn những gì và cách sử dụng đã dự kiến của chúng, sẽ cải thiện rất nhiều năng suất của bạn.
Có hai cách quan trọng để bạn có thể làm giảm nguy cơ làm hỏng mã. Một cách là phải có một bộ đầy đủ các bài thử nghiệm bộ phận cho mã đó: mã phải vượt qua các bài thử nghiệm cả trước và sau khi tái cấu trúc. Cách thứ hai là sử dụng một công cụ tự động hoá, chẳng hạn như các tính năng tái cấu trúc của Eclipse, để thực hiện phép tái cấu trúc này.
Cách kết hợp thử nghiệm kỹ lưỡng và tái cấu trúc tự động đặc biệt mạnh mẽ và đã chuyển nghệ thuật bí ẩn này thành một công cụ thường ngày, có ích. Khả năng thay đổi cấu trúc mã của bạn mà không cần thay đổi chức năng của nó, theo cách nhanh chóng và an toàn, thêm chức năng hoặc cải thiện việc bảo trì của nó có thể ảnh hưởng đáng kể đến cách bạn thiết kế và phát triển mã, cho dù bạn kết hợp nó vào một phương thức nhanh nhẹn chính thức hay không.
Các kiểu tái cấu trúc trong Eclipse
Các công cụ tái cấu trúc của Eclipse có thể được nhóm lại thành ba thể loại rõ ràng (và đây là thứ tự mà chúng xuất hiện trong trình đơn Refactoring):
- Thay đổi tên và tổ chức vật lý của mã, bao gồm đổi tên các trường, các biến, các lớp và các giao diện và di chuyển các gói và các lớp.
- Thay đổi tổ chức logic của mã ở mức lớp, gồm việc chuyển các lớp ẩn danh thành các lớp lồng nhau, chuyển các lớp lồng nhau thành các lớp mức cao nhất, tạo ra các giao diện từ các lớp cụ thể và di chuyển các phương thức hoặc các trường từ một lớp đến lớp con hoặc siêu lớp.
- Thay đổi mã trong một lớp, gồm chuyển các biến chuyển địa phương thành các trường lớp, chuyển mã chọn trong phương thức thành một phương thức tách biệt và tạo ra các phương thức getter và setter cho các trường.
Một số phép tái cấu trúc gần như không khớp với ba thể loại này, đặc biệt là Thay đổi chữ kí phương thức (Change Method Signature), có trong thể loại thứ ba ở đây. Ngoài những trường hợp ngoại lệ này, các phần theo sau sẽ thảo luận về các công cụ tái cấu trúc của Eclipse theo thứ tự này.
Tổ chức lại và đổi tên lại vật lý
Bạn rõ ràng có thể đổi tên hoặc
di chuyển các tệp xung quanh trong hệ thống tệp mà không cần một công cụ
đặc biệt, nhưng làm như vậy với các tệp mã nguồn Java có thể đòi hỏi bạn
phải chỉnh sửa nhiều tệp để cập nhật các câu lệnh import (nhập khẩu) hoặc package
(gói). Tương tự như vậy, bạn có thể dễ dàng đổi tên các lớp, các phương
thức và các biến bằng cách sử dụng một trình soạn thảo văn bản để tìm kiếm
và thay thế chức năng, nhưng bạn cần phải làm điều này cẩn thận, vì các
lớp khác nhau có thể có các phương thức hoặc các biến cùng tên; có thể rất
nhàm chán để duyệt qua tất cả các tệp trong một dự án để đảm bảo chắc chắn
rằng mọi cá thể được xác định và được thay đổi chính xác.
Di chuyển và Đổi tên (Rename and Move) của Eclipse có thể thực hiện các thay đổi này một cách thông minh, trong suốt toàn bộ dự án, mà không có sự can thiệp của người dùng, vì Eclipse hiểu mã theo ngữ nghĩa và có thể xác định các tham chiếu đến một phương thức, biến cụ thể, hoặc các tên lớp. Việc thực hiện nhiệm vụ này dễ dàng giúp đảm bảo rằng phương thức, biến và các tên lớp thể hiện rõ ràng ý định của chúng.
Thật dễ dàng tìm ra mã có các
tên không phù hợp hoặc gây hiểu nhầm vì mã đã được thay đổi để thực hiện
khác so với kế hoạch ban đầu đã lập. Ví dụ, một chương trình tìm kiếm các
từ cụ thể trong một tệp có thể được mở rộng để làm việc với các trang Web
bằng cách sử dụng lớp URL để có được một InputStream (luồng đầu vào). Nếu trước tiên người
ta đã gọi file (tệp) cho luồng đầu vào này, thì
nó cần được thay đổi để phản ánh tính chất tổng quát mới hơn của nó, có lẽ
là sourceStream (luồng nguồn). Các nhà phát
triển thường không tạo ra các thay đổi như thế này vì nó có thể là quá
trình lộn xộn và nhàm chán. Tất nhiên, điều này làm cho mã khó hiểu với
nhà phát triển tiếp theo, tức người phải tiếp tục làm việc với
nó.
Để đổi tên một phần tử Java, chỉ cần nhấn vào nó trong khung nhìn Package Explorer (Trình thám hiểm gói) hoặc chọn nó trong một tệp nguồn Java, sau đó chọn Refactor > Rename. Trong hộp thoại, chọn tên mới và chọn xem Eclipse có cần thay đổi các tham chiếu đến tên không. Các trường chính xác được hiển thị tùy thuộc vào kiểu phần tử mà bạn chọn. Ví dụ, nếu bạn chọn một trường có phương thức getter và setter, bạn cũng có thể cập nhật các tên của các phương thức này để phản ánh trường mới. Hình 1 chỉ ra một ví dụ đơn giản.
Hình 1. Đổi tên một biến địa phương
Giống như tất cả các phép tái cấu trúc Eclipse, sau khi bạn đã xác định mọi thứ cần thiết để thực hiện tái cấu trúc, bạn có thể nhấn Preview (Xem trước) để xem sự thay đổi mà Eclipse đề xuất thực hiện, trong một hộp thoại so sánh, nó cho phép bạn bác bỏ hoặc chấp nhận từng thay đổi trong mỗi tệp bị tác động tới. Nếu bạn tin vào khả năng của Eclipse để thay đổi đúng, bạn có thể chỉ cần nhấn OK. Tất nhiên, nếu bạn không chắc chắn phép tái cấu trúc sẽ làm gì, trước tiên bạn sẽ muốn xem trước, nhưng với các phép tái cấu trúc đơn giản như Đổi tên và Di chuyển thì điều này thường không cần thiết.
Di chuyển thực hiện rất nhiều việc giống như với Đổi tên: Bạn chọn một phần tử Java (thường là một lớp), xác định vị trí mới của nó và xác định liệu các tham khảo có nên được cập nhật không. Sau đó bạn có thể chọn Preview để kiểm tra các thay đổi hoặc nhấn OK để thực hiện ngay lập tức phép tái cấu trúc như trong Hình 2.
Hình 2. Di chuyển một lớp từ một gói này sang một gói khác
Trên một số nền tảng (đặc biệt là Windows), bạn cũng có thể di chuyển các lớp từ một gói hoặc thư mục đến gói hoặc thư mục khác bằng cách kéo và thả chúng vào khung nhìn Package Explorer. Tất cả các tài liệu tham khảo sẽ được cập nhật tự động.
Định nghĩa lại các mối quan hệ lớp
Một tập nhiều phép tái cấu trúc của Eclipse cho phép bạn thay đổi các mối quan hệ lớp của bạn tự động. Các phép tái cấu trúc này không có ích lợi chung như các kiểu tái cấu trúc mà Eclipse phải cung cấp, nhưng có giá trị vì chúng thực hiện nhiệm vụ khá phức tạp. Khi chúng được dùng, chúng rất có ích.
Tăng cường lớp ẩn danh và lồng nhau
Hai phép tái cấu trúc, Chuyển đổi lớp ẩn danh thành lớp lồng nhau (Convert Anonymous Class to Nested) và Chuyển đổi lớp lồng nhau tới lớp mức cao nhất (Convert Nested Type to Top Level), là như nhau trong đó chúng di chuyển một lớp ngoài hướng của nó tới nơi có cơ hội để bao bọc.
Lớp ẩn danh là loại viết nhanh cú
pháp, cho phép bạn thuyết minh một lớp thực hiện một lớp hay giao diện
trừu tượng ở nơi bạn cần đến nó, không cần phải cho nó một tên lớp rõ
ràng. Điều này thường được sử dụng khi tạo các người nghe trong giao diện
người sử dụng chẳng hạn. Trong Liệt kê 1, giả định rằng Bag là một giao
diện được định nghĩa ở nơi khác để khai báo hai phương thức, get() và set().
Liệt kê 1. Lớp Bag
public class BagExample
{
void processMessage(String msg)
{
Bag bag = new Bag()
{
Object o;
public Object get()
{
return o;
}
public void set(Object o)
{
this.o = o;
}
};
bag.set(msg);
MessagePipe pipe = new MessagePipe();
pipe.send(bag);
}
}
|
Khi
lớp ẩn danh trở nên lớn đến mức mã trở nên khó đọc, bạn nên nghĩ đến việc
tạo cho lớp ẩn danh một lớp thích hợp; để giữ gìn sự bao bọc (nói cách
khác, để ẩn giấu nó khỏi các lớp bên ngoài không cần biết về nó), bạn nên
tạo cho lớp này một lớp lồng nhau chứ không phải là một lớp cao nhất. Bạn
có thể làm điều này bằng cách nhấn vào bên trong lớp ẩn danh và chọn
Refactor > Convert Anonymous Class to Nested. Nhập tên cho
lớp này, chẳng hạn như BagImpl, khi được nhắc
và sau đó chọn Preview hoặc OK. Việc này sẽ thay đổi mã như
trong Liệt kê
2.
Liệt kê 2. Lớp Bag được tái cấu trúc
public class BagExample
{
private final class BagImpl implements Bag
{
Object o;
public Object get()
{
return o;
}
public void set(Object o)
{
this.o = o;
}
}
void processMessage(String msg)
{
Bag bag = new BagImpl();
bag.set(msg);
MessagePipe pipe = new MessagePipe();
pipe.send(bag);
}
} |
Chuyển
đổi lớp lồng nhau tới lớp mức cao nhất là có ích khi bạn muốn tạo một lớp
lồng nhau có sẵn cho các lớp khác. Bạn có thể, ví dụ, đang sử dụng một đối
tượng giá trị bên trong một lớp -- chẳng hạn như lớp BagImpl ở trên. Nếu sau này bạn quyết định rằng dữ liệu này
nên được dùng chung giữa các lớp, thì phép tái cấu trúc này sẽ tạo một tệp
lớp mới từ lớp lồng nhau. Bạn có thể làm điều này bằng cách làm nổi bật
tên lớp trong tệp nguồn (hoặc nhấn vào tên lớp trong khung nhìn Outline)
và chọn Refactor > Convert Nested Type to Top Level.
Phép
tái cấu trúc này sẽ yêu cầu bạn cung cấp một tên cho cá thể kèm theo. Nó
có thể đưa ra đề nghị, như example (ví dụ), mà
bạn có thể chấp nhận. Ý nghĩa của việc này sẽ được làm rõ trong giây lát.
Sau khi nhấn OK, mã cho lớp BagExample
kèm theo sẽ được thay đổi như thể hiện trong Liệt kê 3.
Liệt kê 3. Lớp Bag được tái cấu trúc
public class BagExample
{
void processMessage(String msg)
{
Bag bag = new BagImpl(this);
bag.set(msg);
MessagePipe pipe = new MessagePipe();
pipe.send(bag);
}
}
|
Lưu ý rằng khi một lớp được lồng nhau, nó có quyền truy cập tới các thành viên của lớp bên ngoài. Để duy trì chức năng này, phép tái cấu trúc sẽ bổ sung một cá thể của lớp BagExample kèm theo vào lớp đã lồng nhau trước đây. Đây là biến của cá thể mà trước đây bạn đã được yêu cầu cung cấp một tên cho nó. Nó cũng tạo ra một hàm tạo để thiết lập biến của cá thể này. Lớp mới BagImpl do phép tái cấu trúc tạo ra được hiển thị trong Liệt kê 4.
Liệt kê 4. Lớp BagImpl
final class BagImpl implements Bag
{
private final BagExample example;
/**
* @paramBagExample
*/
BagImpl(BagExample example)
{
this.example = example;
// TODO Auto-generated constructor stub
}
Object o;
public Object get()
{
return o;
}
public void set(Object o)
{
this.o = o;
}
}
|
Nếu
bạn không cần phải duy trì quyền truy cập vào lớp BagExample như trường hợp ở đây, bạn có thể an toàn loại bỏ
biến của cá thể và hàm tạo và thay đổi mã trong lớp BagExample tới hàm tạo không có đối số (no-arg constructor)
mặc định.
Di chuyển thành viên trong hệ thống phân cấp lớp
Hai phép
tái cấu trúc khác, Đẩy xuống (Push Down) và Kéo lên (Pull Up), di chuyển
các phương thức lớp hoặc các trường từ một lớp đến lớp con hoặc siêu lớp
của nó, tương ứng. Giả sử bạn có một lớp trừu tượng Vehicle (xe cộ), được xác định như sau trong Liệt kê 5.
Liệt kê 5. Lớp trừu tượng Vehicle
public abstract class Vehicle
{
protected int passengers;
protected String motor;
public int getPassengers()
{
return passengers;
}
public void setPassengers(int i)
{
passengers = i;
}
public String getMotor()
{
return motor;
}
public void setMotor(String string)
{
motor = string;
}
}
|
Bạn
cũng có một lớp con của lớp Vehicle gọi là
Automobile (xe ô tô) như trong Liệt kê
6.
Liệt kê 6. Lớp Automobile
public class Automobile extends Vehicle
{
private String make;
private String model;
public String getMake()
{
return make;
}
public String getModel()
{
return model;
}
public void setMake(String string)
{
make = string;
}
public void setModel(String string)
{
model = string;
}
}
|
Chú
ý rằng một thuộc tính của Vehicle là motor (động cơ). Thật tốt nếu bạn biết rằng bạn
sẽ chỉ luôn đề cập đến các loại xe cơ giới có động cơ, nhưng nếu bạn muốn
cho phép những thứ như các thuyền có mái chèo (rowboat), bạn có thể muốn
đẩy thuộc tính motor xuống từ lớp Vehicle vào trong lớp Automobile. Để làm điều
này, chọn motor trong khung nhìn Outline, rồi
chọn Refactor > Push Down.
Eclipse đủ thông minh để nhận
ra rằng bạn không thể luôn luôn di chuyển một trường bằng chính nó và cung
cấp một nút Add Required, nhưng điều này không luôn hoạt động đúng
trong Eclipse 2.1. Bạn cần phải xác minh rằng các phương thức bất kỳ phụ
thuộc vào trường này cũng bị đẩy xuống. Trong trường hợp này, có hai,
phương thức getter và setter đi cùng trường motor, như trong Hình 3.
Hình 3. Thêm các thành viên cần thiết
Sau khi nhấn OK, trường motor và các
phương thức getMotor() và setMotor() sẽ được chuyển đến lớp Automobile. Liệt kê 7 cho thấy hình dạng của lớp Automobile sau phép tái cấu trúc
này.
Liệt kê 7. Lớp Automobile được tái cấu trúc
public class Automobile extends Vehicle
{
private String make;
private String model;
protected String motor;
public String getMake()
{
return make;
}
public String getModel()
{
return model;
}
public void setMake(String string)
{
make = string;
}
public void setModel(String string)
{
model = string;
}
public String getMotor()
{
return motor;
}
public void setMotor(String string)
{
motor = string;
}
}
|
Phép
tái cấu trúc Kéo lên gần giống với Đẩy xuống, tất nhiên trừ việc nó di
chuyển các thành viên lớp từ một lớp đến các siêu lớp của nó thay vì lớp
con. Bạn có thể sử dụng điều này nếu sau đó bạn đổi ý của mình và quyết
định chuyển motor quay trở lại lớp Vehicle. Những cảnh báo tương tự đảm bảo rằng bạn
chọn áp dụng tất cả các thành viên cần thiết.
Có motor trong lớp
Automobile có nghĩa nếu bạn tạo lớp con
khác của Vehicle, như Bus, bạn sẽ cần bổ sung motor (và
phương thức có liên quan của nó) cũng vào lớp Bus. Một cách thể hiện mối quan hệ như vậy là tạo ra một giao
diện, Motorized, trong đó Automobile và Bus sẽ thực hiện,
nhưng RowBoat thì không.
Cách dễ nhất để
tạo ra giao diện Motorized là sử dụng tái cấu
trúc Lấy ra giao diện (Extract Interface) trên Automobile. Để làm điều này, chọn lớp Automobile trong khung nhìn Outline và sau đó chọn Refactor
> Extract Interface từ trình đơn. Hộp thoại sẽ cho phép bạn
chọn những phương thức nào bạn muốn đưa vào trong giao diện như trong Hình
4.
Hình 4. Lấy ra giao diện Motorized
Sau khi chọn OK, một giao diện được tạo ra, như được hiển thị trong Liệt kê 8.
Liệt kê 8. Giao diện Motorized
public interface Motorized
{
public abstract String getMotor();
public abstract void setMotor(String string);
}
|
Và
việc khai báo lớp cho Automobile được thay đổi
như
sau:
public class Automobile extends Vehicle implements Motorized |
Sử dụng một siêu kiểu (supertype)
Phép tái cấu trúc cuối cùng có trong thể loại này là Sử dụng
siêu kiểu ở nơi có thể (Use Supertype Where Possible). Hãy xem xét một ứng
dụng quản lý hàng tồn kho ô tô. Từ đầu đến cuối, nó sử dụng các đối tượng
của kiểu Automobile. Nếu bạn muốn có thể xử lý
tất cả các kiểu xe cộ, bạn có thể sử dụng phép tái cấu trúc này để thay
đổi các tham chiếu đến Automobile thành các
tham chiếu đến Vehicle (xem Hình 5). Nếu bạn thực hiện bất kỳ việc kiểm
tra kiểu nào trong mã của bạn bằng cách sử dụng toán tử instanceof, bạn sẽ cần phải xác định xem nó có
phù hợp để sử dụng các kiểu cụ thể hoặc siêu kiểu không và kiểm tra tùy
chọn đầu tiên. Sử dụng siêu kiểu đã chọn trong các biểu thức 'instanceof',
cho phù hợp.
Hình 5. Thay đổi Automobile sang siêu kiểu của nó, Vehicle
Nhu cầu sử dụng một siêu kiểu phát sinh thường xuyên trong ngôn ngữ Java,
đặc biệt là khi mẫu Factory Method (Phương thức nhà máy) được sử dụng.
Điều này được thực hiện tiêu biểu bởi có một lớp trừu tượng với phương
thức tĩnh create() trả về một đối tượng cụ thể
đang triển khai thực hiện lớp trừu tượng đó. Điều này có thể có ích nếu
kiểu đối tượng cụ thể phải được tạo ra phụ thuộc vào chi tiết triển khai
thực hiện mà không quan tâm đến các lớp khách.
Việc thay đổi mã trong một lớp
Sự đa dạng lớn nhất của các phép tái cấu trúc là việc tổ chức lại mã trong một lớp. Trong số các việc khác, các việc này cho phép bạn đưa vào (hoặc loại bỏ) các biến trung gian, tạo ra một phương thức mới từ một phần của phương thức cũ, và tạo ra các phương thức getter và setter cho một trường.
Có một số phép tái cấu trúc bắt đầu bằng từ Lấy ra (Extract):
Lấy ra phương thức (Extract Method), Lấy ra biến địa phương (Extract Local
Variable) và Lấy ra các hằng số (Extract Constants). Tái cấu trúc thứ
nhất, Extract Method, như bạn có thể mong đợi, sẽ tạo ra một phương thức
mới từ mã bạn đã chọn. Ví dụ, chọn phương thức main() tại lớp đó trong Liệt kê 8. Nó đánh giá các tùy chọn
dòng lệnh và nếu nó tìm thấy bất kỳ thứ gì bắt đầu với -D, lưu chúng như là các cặp giá trị tên trong một đối tượng
Properties.
Liệt kê 8. main()
import java.util.Properties;
import java.util.StringTokenizer;
public class StartApp
{
public static void main(String[] args)
{
Properties props = new Properties();
for (int i= 0; i < args.length; i++)
{
if(args[i].startsWith("-D"))
{
String s = args[i].substring(2);
StringTokenizer st = new StringTokenizer(s, "=");
if(st.countTokens() == 2)
{
props.setProperty(st.nextToken(), st.nextToken());
}
}
}
//continue...
}
}
|
Có
hai trường hợp chính trong đó bạn có thể muốn đưa ra một số mã của một
phương thức và đặt nó trong phương thức khác. Trường hợp đầu tiên là nếu
phương thức này quá dài và thực hiện hai hay nhiều hoạt động khác nhau về
logic. (Chúng ta không biết cái gì khác mà phương thức main() này thực hiện, nhưng từ chứng cứ mà chúng ta thấy ở
đây, đó không phải là một lý do để lấy ra một phương thức ở đây). Trường
hợp thứ hai là nếu có một đoạn mã riêng biệt theo logic có thể được tái sử
dụng bởi các phương thức khác. Ví dụ, đôi khi bạn thấy mình đang lặp lại
một vài dòng mã của một số phương thức khác. Đó là một khả năng trong
trường hợp này, nhưng có thể bạn sẽ không thực hiện phép tái cấu trúc này
cho đến khi bạn thực sự cần tái sử dụng mã này.
Giả sử có một nơi
khác mà bạn cần phải phân tích cú pháp các cặp giá trị tên và thêm chúng
vào một đối tượng Properties, bạn có thể lấy ra
đoạn mã trong đó có khai báo StringTokenizer và
sau đó là mệnh đề if. Để làm điều này, làm nổi
bật mã này và sau đó chọn Refactor > Extract Method từ trình
đơn. Bạn sẽ được nhắc nhở cho một tên phương pháp; nhập addProperty, và sau đó xác minh xem phương thức
này có hai tham số, Properties prop và Strings. Liệt kê lớp 9 cho thấy lớp đó sau khi
Eclipse lấy ra phương thức addProp().
Liệt kê 9. addProp () được lấy ra
import java.util.Properties;
import java.util.StringTokenizer;
public class Extract
{
public static void main(String[] args)
{
Properties props = new Properties();
for (int i = 0; i < args.length; i++)
{
if (args[i].startsWith("-D"))
{
String s = args[i].substring(2);
addProp(props, s);
}
}
}
private static void addProp(Properties props, String s)
{
StringTokenizer st = new StringTokenizer(s, "=");
if (st.countTokens() == 2)
{
props.setProperty(st.nextToken(), st.nextToken());
}
}
}
|
Tái
cấu trúc Lấy ra biến địa phương chọn một biểu thức đang được sử dụng trực
tiếp và trước hết, gán nó vào một biến địa phương. Rồi sử dụng biến này
được sử dụng ở nơi biểu thức dùng nó. Ví dụ, trong phương thức addProp() ở trên, bạn có thể làm nổi bật cuộc gọi
đầu tiên st.nextToken() và chọn Refactor
> Extract Local Variable. Bạn sẽ được nhắc để tạo ra một biến;
nhập vào key. Chú ý rằng có một tùy chọn để
thay thế tất cả các lần xuất hiện của biểu thức được chọn bằng các tham
chiếu tới biến mới. Điều này thường thích hợp, nhưng không phải trong
trường hợp của phương thức nextToken(), (rõ
ràng) nó trả về một giá trị khác nhau mỗi khi nó được gọi. Hãy chắc chắn
rằng tùy chọn này không được chọn, xem Hình 6.
Hình 6. Không thay thế tất cả các thể hiện của biểu thức được chọn
Tiếp theo, lặp lại phép tái cấu trúc này cho cuộc gọi thứ hai tới st.nextToken(), lúc này gọi biến địa phương mới
value. Liệt kê 10 cho thấy mã đó sau hai
phép tái cấu trúc
này.
Listing 10. Mã được tái cấu trúc
private static void addProp(Properties props, String s)
{
StringTokenizer st = new StringTokenizer(s, "=");
if(st.countTokens() == 2)
{
String key = st.nextToken();
String value = st.nextToken();
props.setProperty(key, value);
}
}
|
Việc đưa vào các biến theo cách này đảm bảo một số lợi ích. Thứ nhất, bằng cách cung cấp các tên có ý nghĩa cho các biểu thức, việc đoạn mã thực hiện sẽ rõ ràng. Thứ hai, dễ dàng gỡ rối mã hơn, bởi vì chúng ta có thể dễ dàng kiểm tra các giá trị được biểu thức trả về. Cuối cùng, trong các trường hợp mà nhiều cá thể của một biểu thức có thể được thay thế bằng một biến đơn, điều này có thể hiệu quả hơn.
Lấy ra các hằng số (Extract
Constant) cũng tương tự như Lấy ra biến địa phương, nhưng bạn phải chọn
một biểu thức hằng số, tĩnh, phép tái cấu trúc sẽ chuyển đổi nó sang một
hằng số tĩnh cuối cùng. Điều này có ích để loại bỏ các số và các chuỗi mã
hoá cứng khỏi mã của bạn. Ví dụ, trong mã ở trên, chúng ta đã sử dụng
-D" cho các tùy chọn dòng lệnh đang xác
định một cặp giá trị tên. Hãy làm nổi bật -D"
trong mã đó, chọn Refactor > Extract Constant, và nhập DEFINE làm tên của hằng số đó. Phép tái cấu trúc
này sẽ thay đổi mã như trong Liệt kê
11.
Liệt kê 11. Mã được tái cấu trúc
public class Extract
{
private static final String DEFINE = "-D";
public static void main(String[] args)
{
Properties props = new Properties();
for (int i = 0; i < args.length; i++)
{
if (args[i].startsWith(DEFINE))
{
String s = args[i].substring(2);
addProp(props, s);
}
}
}
// ...
|
Đối
với mỗi tái cấu trúc Lấy ra... (Extract...), có một tái cấu trúc Nội
tuyến... (Inline...) tương ứng để thực hiện các hoạt động đảo ngược. Ví
dụ, nếu bạn làm nổi bật biến s trong đoạn mã trên, chọn Refactor >
Inline..., sau đó nhấn OK, Eclipse sử dụng trực tiếp biểu
thức args[i].substring(2) trong cuộc gọi đến
addProp() như
sau:
if(args[i].startsWith(DEFINE))
{
addProp(props,args[i].substring(2));
} |
Điều này có thể đủ hiệu quả hơn khi sử dụng một biến tạm thời và, bằng cách tạo ra bộ chuyển (terser) mã, làm cho nó hoặc là dễ dàng đọc hơn hoặc khó hiểu hơn, tùy thuộc vào quan điểm của bạn. Nói chung, tuy nhiên, việc nội tuyến như thế này không có nhiều thứ để bình luận.
Theo cùng cách mà bạn có thể thay thế một biến bằng một biểu thức nội tuyến, bạn cũng có thể làm nổi bật một tên phương thức hoặc một hằng số tĩnh. Chọn Refactor > Inline... từ trình đơn, và Eclipse sẽ thay thế các cuộc gọi phương thức bằng mã phương thức, hoặc các tham chiếu đến hằng số bằng giá trị hằng số, tương ứng.
Trưng ra cấu trúc trong của các đối tượng của bạn
thường không được coi là cách thực hành tốt. Đó là lý do tại sao lớp Vehicle và các lớp con của nó, có các trường
riêng hay có bảo vệ, và các phương thức công cộng setter và getter để cung
cấp quyền truy cập. Những phương thức này có thể được tạo tự động theo hai
cách khác nhau.
Một cách để tạo ra những phương thức này là sử dụng Source > Generate Getter and Setter. Việc này sẽ hiển thị hộp thoại với các phương thức getter và setter được đề xuất cho từng trường, mà đã không chỉ có một trường. Đây không phải là tái cấu trúc, tuy nhiên, bởi vì nó không cập nhật các tham khảo tới các trường để sử dụng các phương thức mới, bạn sẽ cần phải có trường đó cho mình nếu cần thiết. Tùy chọn này là một bộ tiết kiệm thời gian quan trọng, nhưng tốt nhất nó thường được sử dụng khi tạo ra một lớp ban đầu hoặc khi thêm các trường mới vào một lớp, vì không có các tài liệu tham khảo mã khác mà các trường này chưa có; vậy không có mã nào khác để thay đổi.
Cách thứ hai để tạo ra các phương thức getter và setter là chọn trường và sau đó chọn Refactor > Encapsulate Field từ trình đơn. Phương thức này chỉ tạo ra getters và setters cho một trường duy nhất tại một thời gian, nhưng ngược với Source > Generate Getter and Setter, nó cũng thay đổi các tham chiếu đến trường đó thành các cuộc gọi đến các phương thức mới.
Ví dụ, khởi đầu mới với một phiên bản mới, đơn giản của
lớp Automobile, như thể hiện trong Liệt kê 12.
Liệt kê 12. Lớp Automobile đơn giản
public class Automobile extends Vehicle
{
public String make;
public String model;
}
|
Tiếp
theo, nên tạo một lớp để thuyết minh Automobile
và truy cập trực tiếp trường make như thể hiện
trong Liệt kê
13.
Liệt kê 13. Thuyết minh Automobile
public class AutomobileTest
{
public void race()
{
Automobilecar1 = new Automobile();
car1.make= "Austin Healy";
car1.model= "Sprite";
// ...
}
} |
Bây
giờ bao bọc trường make bằng cách làm nổi bật
tên trường và chọn Refactor > Encapsulate Field. Trong hộp
thoại, nhập vào các tên cho các phương thức getter và setter -- như bạn có
thể mong muốn, theo mặc định chúng là getMake()
và setMake(). Bạn cũng có thể chọn xem các
phương thức ở trong cùng lớp như trường đó sẽ tiếp tục truy cập trường đó
trực tiếp không hoặc xem các tài liệu tham khảo này sẽ được thay đổi để sử
dụng các phương thức truy cập như tất cả các lớp khác không. (Một số người
có rất thích theo cách này hay cách khác, nhưng khi nó xảy ra, bạn chọn
trường hợp nào cũng vậy, vì không có tài liệu tham khảo tới trường make trong Automobile). Xem Hình 7.
Hình 7. Bao bọc một trường
Sau khi nhấn OK, trường make trong lớp
Automobile sẽ là riêng biệt và sẽ có các phương thức getMake() và setMake()
thể hiện trong Liệt kê
14.
Liệt kê 14. Lớp Automobile được cấu trúc lại
public class Automobile extends Vehicle
{
private String make;
public String model;
public void setMake(String make)
{
this.make = make;
}
public String getMake()
{
return make;
}
}
|
Lớp
AutomobileTest cũng sẽ được cập nhật để sử
dụng các phương thức truy cập mới, như thể hiện trong Liệt kê 15.
Liệt kê 15. Lớp AutomobileTest
public class AutomobileTest
{
public void race()
{
Automobilecar1 = new Automobile();
car1.setMake("Austin Healy");
car1.model= "Sprite";
// ...
}
} |
Phép tái cấu trúc cuối cùng được xem xét ở đây là khó sử dụng nhất: Thay đổi Chữ kí phương thức (Method Signature). Điều làm được là hoàn toàn rõ -- thay đổi các tham số, khả năng hiển thị và kiểu trả về của một phương thức. Điều không rõ ràng là ảnh hưởng mà những thay đổi này có trên phương thức đó hoặc trên mã gọi phương pháp đó. Không có ma thuật nào ở đây cả. Nếu những thay đổi gây ra các vấn đề trong các phương thức đang được tái cấu trúc -- bởi vì nó để lại các biến chưa xác định hay các kiểu không khớp -- thì các hoạt động tái cấu trúc sẽ gắn cờ những điều này. Bạn có tuỳ chọn để chấp nhận phép tái cấu trúc ở bất kỳ đâu và chỉnh sửa các vấn đề sau đó, hoặc huỷ bỏ tái cấu trúc. Nếu tái cấu trúc gây ra các vấn đề trong các phương thức khác, thì hãy bỏ qua chúng và bạn phải tự mình sửa chữa chúng sau khi tái cấu trúc.
Để làm rõ điều này, hãy xem lớp và phương thức sau đây trong Liệt kê 16.
Liệt kê 16. Lớp MethodSigExample
public class MethodSigExample
{
public int test(String s, int i)
{
int x = i + s.length();
return x;
}
}
|
Phương
thức test() trong lớp ở trên được gọi bằng một
phương thức trong lớp khác, như thể hiện trong Liệt kê 17.
Liệt kê 17. Phương thức callTest
public void callTest()
{
MethodSigExample eg = new MethodSigExample();
int r = eg.test("hello", 10);
} |
Làm
nổi bật test trong lớp đầu tiên và chọn
Refactor > Change Method Signature. Hộp thoại trong Hình 8
sẽ xuất hiện.
Hình 8. Các tùy chọn thay đổi Chữ kí phương thức
Tùy chọn đầu tiên là thay đổi khả năng hiển thị của phương thức. Trong ví
dụ này, việc thay đổi nó thành được bảo vệ hay riêng biệt sẽ ngăn cản
phương thức callTest() trong lớp thứ hai truy
cập. (Nếu chúng đã có trong các gói riêng, thì việc thay đổi quyền truy
cập theo mặc định cũng sẽ gây ra vấn đề này). Eclipse sẽ không gắn cờ lỗi
này khi thực hiện tái cấu trúc; nó cho bạn chọn một giá trị thích
hợp.
Tùy chọn tiếp theo là để thay đổi kiểu trả về. Ví dụ, việc thay
đổi kiểu trả về tới float, không được gắn cờ
như là một lỗi bởi vì một int trong câu lệnh
trả về của phương thức test() được tự động đưa
đến float. Tuy nhiên, điều này sẽ gây ra một
vấn đề trong callTest() trong lớp thứ hai, bởi
vì một float không thể được chuyển đổi sang
int. Bạn sẽ cần hoặc tạo khuôn mẫu giá trị
trả về được test() trả về tới int hoặc thay đổi kiểu r trong callTest() tới float.
Các xem xét tương tự áp dụng khi
chúng ta thay đổi kiểu của tham số đầu tiên từ String sang int. Điều này sẽ được
gắn cờ trong tái cấu trúc vì nó gây ra vấn đề trong phương thức đang được
tái cấu trúc: int không có phương thức length(). Thay đổi nó thành StringBuffer, tuy nhiên, sẽ không được gắn cờ có vấn đề, bởi
vì nó không có phương thức length(). Điều này
sẽ, tất nhiên, gây ra một vấn đề trong phương thức callTest(), vì nó vẫn còn chuyển qua một String khi nó gọi test().
Như
đã đề cập trước đó, trong trường hợp ở nơi tái cấu trúc này dẫn đến lỗi,
cho dù đã gắn cờ hay chưa, bạn có thể tiếp tục bằng cách sửa chữa đơn giản
các lỗi trên cơ sở từng trường hợp một. Cách tiếp cận khác là ngăn chặn
các lỗi. Nếu bạn muốn loại bỏ tham số i, vì nó
không cần thiết, bạn có thể bắt đầu bằng cách loại bỏ các tham chiếu đến
nó trong phương thức đang được tái cấu trúc. Việc loại bỏ tham số sau đó
sẽ diễn ra thuận lợi hơn nhiều.
Một điều cuối cùng được giải thích
là tùy chọn Giá trị mặc định (Default Value). Điều này chỉ được sử dụng
khi một tham số đang được bổ sung vào chữ ký phương thức. Nó được sử dụng
để cung cấp một giá trị khi tham số này được thêm vào những người gọi. Ví
dụ, nếu chúng ta thêm một tham số của kiểu String, với tên n, và một giá trị
mặc định là world, lời gọi tới test() trong phương thức callTest() sẽ được thay đổi như
sau:
public void callTest()
{
MethodSigExample eg = new MethodSigExample();
int r = eg.test("hello", 10, "world");
}
|
Điểm làm giảm giá trị cuộc thảo luận có vẻ đáng sợ này về phép tái cấu trúc Thay đổi Chữ ký phương thức (Change Method Signature) không phải là nó khó giải quyết, mà nó là một phép tái cấu trúc mạnh, tiết kiệm thời gian, thường đòi hỏi phải lập kế hoạch chu đáo để sử dụng thành công
Các công cụ của Eclipse làm cho phép tái cấu trúc dễ dàng và việc hiểu rõ chúng có thể giúp bạn cải thiện năng suất của bạn. Các phương thức phát triển nhanh nhẹn, bổ sung thêm các tính năng chương trình theo cách lặp lại, phụ thuộc vào phép tái cấu trúc như là một kỹ thuật để thay đổi và mở rộng một thiết kế của chương trình. Nhưng ngay cả khi bạn không sử dụng một phương thức chính thức để yêu cầu tái cấu trúc, các công cụ tái cấu trúc Eclipse cung cấp một cách tiết kiệm thời gian để tạo ra các kiểu thay đổi mã phổ biến. Phải mất một số thời gian để hiểu rõ chúng sao cho bạn có thể nhận ra các tình huống ở đó chúng có thể được áp dụng là một sự đầu tư đáng giá về thời gian của bạn.
Sách
- Tài liệu quan trọng về tái cấu trúc là Tái cấu trúc: Cải thiện việc
thiết kế mã hiện có của Martin Fowler, Kent Beck, John Brant,
William Opdyke và Don Roberts (Addison-Wesley, 1999).
- Tái cấu trúc, như là một quá trình liên tục, được tác giả thảo luận
trong ngữ cảnh thiết kế và phát triển một dự án trong Eclipse trong
Eclipse đang hoạt động: Một Hướng dẫn cho các nhà phát triển
Java, của David Gallardo, Ed Burnette, và Robert McGovern
(Manning, 2003).
- Các mẫu (như Phương thức Factory (Nhà máy) đã đề cập trong bài viết
này) là một công cụ quan trọng để tìm hiểu và thảo luận về thiết kế
hướng đối tượng. Tài liệu cổ điển là Các mẫu thiết kế: Các phần tử
của phần mềm hướng dịch vụ tái sử dụng được của Erich Gamma,
Richard Helm, Ralph Johnson, John Vlissides (Addison-Wesley, 1995).
- Một hạn chế với lập trình viên Java là các ví dụ trong Các mẫu
thiết kế sử dụng C++; với một cuốn sách dịch các mẫu sang ngôn
ngữ Java, xem Các mẫu trong Java, Tập 1: Một danh mục về Các mẫu
thiết kế tái sử dụng được minh họa bằng UML, Mark Grand
(Wiley, 1998).
- Đối với một sự giới thiệu về một loạt lập trình nhanh nhẹn, xem Lập
trình cực đoan đã giải thích: Chấp nhận thay đổi, của Kent
Beck (Addison-Wesley, 1999).
Các trang Web
-
Trang Web của Martin Fowler
về tái cấu trúc trung tâm trên trang Web.
- Để biết thêm thông tin về việc thử nghiệm đơn vị với JUnit, hãy truy
cập vào JUnit Web site.
Các bài viết và các hướng dẫn trên developerWorks
- "Tái cấu trúc với Eclipse," của Daniel H. Steinberg, nhìn vào
những gì diễn ra ở hậu trường (developerWorks, 11.2001).
- "Các mẫu thiết kế Java 101" là hướng dẫn mở đầu của David về
các mẫu (developerWorks, 01.2002).
- "Các mẫu thiết kế Java 201" là một hướng dẫn nâng cao hơn về
các mẫu của Paul Monday (developerWorks, 04.2002).
- Trong "Mở rộng các công cụ phát triển Java của Eclipse," Dan Kehn
thảo luận cách mở rộng Eclipse với các phép tái cấu trúc riêng của bạn
(developerWorks, 07.2003).
- Trong "Bắt đầu với Nền tảng Eclipse," David cung cấp một điểm khởi
đầu cho việc học thêm về Eclipse (developerWorks, November
11.2002).
- Trong "Làm rõ lập trình cực đoan," Roy W. Miller thảo luận về cách
sử dụng JUnit trong Eclipse (developerWorks, 05. 2003).
- Tìm thêm các bài viết cho người sử dụng Eclipse trong vùng dự án mã nguồn mở trên developerWorks. Cũng xem
các bản tải công
nghệ Eclipse mới nhất trên alphaWorks.
David Gallardo là một nhà tư vấn phần mềm độc lập và tác giả chuyên về quốc tế hóa phần mềm, các ứng dụng Web Java và phát triển cơ sở dữ liệu. Ông là một kỹ sư phần mềm chuyên nghiệp trong hơn 15 năm qua và có kinh nghiệm với nhiều hệ điều hành, các ngôn ngữ lập trình và các giao thức mạng. Kinh nghiệm gần đây của ông bao gồm lãnh đạo việc phát triển cơ sở dữ liệu và quốc tế hóa tại một công ty thương mại điện tử doanh nghiệp tới doanh nghiệp, TradeAccess, Inc. Trước đó, ông đã là một kỹ sư cao cấp trong nhóm Phát triển sản phẩm quốc tế tại Tập đoàn phát triển Lotus (Lotus Development Corporation), nơi ông đã đóng góp vào sự phát triển của của một thư viện cho nhiều nền tảng khác nhau cung cấp sự hỗ trợ Unicode và ngôn ngữ quốc tế cho các sản phẩm Lotus bao gồm Domino.