Tái cấu trúc cho mọi người

Làm thế nào và tại sao lại sử dụng các tính năng tái cấu trúc được tự động hóa của Eclipse

Eclipse cung cấp tập các phép tái cấu trúc (refactoring) tự động mạnh mẽ, so với những thứ khác, cho phép bạn đổi tên các phần tử Java™, di chuyển các lớp và các gói, tạo các giao diện từ các lớp cụ thể, chuyển các lớp lồng nhau vào các lớp mức cao nhất và lấy ra một phương thức mới từ các đoạn mã trong một phương thức cũ. Việc trở nên quen thuộc với các công cụ tái cấu trúc của Eclipse là một cách hay để cải thiện năng suất của bạn. Tổng quan về tính năng tái cấu trúc của Eclipse này, kèm với các ví dụ, giải thích cách thức và lí do sử dụng từng tính năng.

David Gallardo, Tư vấn phần mềm

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.



09 01 2010

Lí do cần tái cấu trúc?

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):

  1. 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.
  2. 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.
  3. 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
Đổ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
Di chuyển một lớp

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()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 Vehiclemotor (độ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
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()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 đó AutomobileBus 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
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
Siêu kiểu

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.

Lấy ra và nội tuyến

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 propStrings. 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
Lấy ra biế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.

Bao bọc các trườ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()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
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()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";
      // ...
   }
}

Thay đổi Chữ kí phương thức

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
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


Kết luận

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.

Tài nguyê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

Các bài viết và các hướng dẫn trên developerWorks

Bình luận

developerWorks: Đăng nhập

Các trường được đánh dấu hoa thị là bắt buộc (*).


Bạn cần một ID của IBM?
Bạn quên định danh?


Bạn quên mật khẩu?
Đổi mật khẩu

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Ở lần bạn đăng nhập đầu tiên vào trang developerWorks, một hồ sơ cá nhân của bạn được tạo ra. Thông tin trong bản hồ sơ này (tên bạn, nước/vùng lãnh thổ, và tên cơ quan) sẽ được trưng ra cho mọi người và sẽ đi cùng các nội dung mà bạn đăng, trừ khi bạn chọn việc ẩn tên cơ quan của bạn. Bạn có thể cập nhật tài khoản trên trang IBM bất cứ khi nào.

Thông tin gửi đi được đảm bảo an toàn.

Chọn tên hiển thị của bạn



Lần đầu tiên bạn đăng nhập vào trang developerWorks, một bản trích ngang được tạo ra cho bạn, bạn cần phải chọn một tên để hiển thị. Tên hiển thị của bạn sẽ đi kèm theo các nội dung mà bạn đăng tải trên developerWorks.

Tên hiển thị cần có từ 3 đến 30 ký tự. Tên xuất hiện của bạn phải là duy nhất trên trang Cộng đồng developerWorks và vì lí do an ninh nó không phải là địa chỉ email của bạn.

Các trường được đánh dấu hoa thị là bắt buộc (*).

(Tên hiển thị cần có từ 3 đến 30 ký tự)

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Thông tin gửi đi được đảm bảo an toàn.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=70
Zone=Nguồn mở, Công nghệ Java
ArticleID=459870
ArticleTitle=Tái cấu trúc cho mọi người
publish-date=01092010