Làm thế nào để cấu trúc lại mã thành một số mã phổ biến?


16

Lý lịch

Tôi đang làm việc trên một dự án C # đang diễn ra. Tôi không phải là lập trình viên C #, chủ yếu là lập trình viên C ++. Vì vậy, tôi đã được giao cơ bản dễ dàng và tái cấu trúc các nhiệm vụ.

Mã là một mớ hỗn độn. Đó là một dự án lớn. Vì khách hàng của chúng tôi yêu cầu phát hành thường xuyên với các tính năng mới và sửa lỗi, tất cả các nhà phát triển khác đã buộc phải áp dụng phương pháp vũ phu trong khi mã hóa. Mã này rất không thể nhầm lẫn và tất cả các nhà phát triển khác đồng ý với nó.

Tôi không ở đây để tranh luận liệu họ đã làm đúng. Khi tôi tái cấu trúc, tôi tự hỏi liệu tôi có đang thực hiện đúng cách không vì mã được cấu trúc lại của tôi có vẻ phức tạp! Đây là nhiệm vụ của tôi là ví dụ đơn giản.

Vấn đề

Có sáu loại: A, B, C, D, EF. Tất cả các lớp có một chức năng ExecJob(). Tất cả sáu triển khai đều rất giống nhau. Về cơ bản, lúc đầu A::ExecJob()đã được viết. Sau đó, một phiên bản hơi khác được yêu cầu được thực hiện B::ExecJob()bằng cách sao chép-dán-sửa đổi A::ExecJob(). Khi một phiên bản hơi khác được yêu cầu, C::ExecJob()đã được viết và như vậy. Tất cả sáu triển khai có một số mã chung, sau đó một số dòng mã khác nhau, sau đó lại một số mã phổ biến, v.v. Đây là một ví dụ đơn giản về việc triển khai:

A::ExecJob()
{
    S1;
    S2;
    S3;
    S4;
    S5;
}

B::ExecJob()
{
    S1;
    S3;
    S4;
    S5;
}

C::ExecJob()
{
    S1;
    S3;
    S4;
}

Trường hợp SNmột nhóm các tuyên bố chính xác giống nhau.

Để làm cho chúng trở nên phổ biến, tôi đã tạo một lớp khác và di chuyển mã chung trong một hàm. Sử dụng tham số để kiểm soát nhóm câu lệnh nào sẽ được thực thi:

Base::CommonTask(param)
{
    S1;
    if (param.s2) S2;
    S3;
    S4;
    if (param.s5) S5;
}

A::ExecJob() // A inherits Base
{
    param.s2 = true;
    param.s5 = true;
    CommonTask(param);
}

B::ExecJob() // B inherits Base
{
    param.s2 = false;
    param.s5 = true;
    CommonTask(param);
}

C::ExecJob() // C inherits Base
{
    param.s2 = false;
    param.s5 = false;
    CommonTask(param);
}

Lưu ý rằng, ví dụ này chỉ sử dụng ba lớp và các câu lệnh đơn giản hóa. Trong thực tế, CommonTask()hàm trông rất phức tạp với tất cả những kiểm tra tham số đó và còn nhiều câu lệnh nữa. Ngoài ra, trong mã thực, có một số CommonTask()chức năng trông.

Mặc dù tất cả các triển khai đang chia sẻ mã chung và các ExecJob()chức năng trông có vẻ dễ thương hơn, nhưng vẫn tồn tại hai vấn đề đang làm phiền tôi:

  • Đối với bất kỳ thay đổi nào CommonTask(), tất cả sáu tính năng (và có thể nhiều hơn trong tương lai) đều cần được kiểm tra.
  • CommonTask()đã phức tạp rồi Nó sẽ trở nên phức tạp hơn theo thời gian.

Tôi đang làm điều đó đúng cách?


Cuốn sách Tái cấu trúc của Martin Fowler có rất nhiều kỹ thuật cụ thể để tái cấu trúc mã mà bạn có thể thấy hữu ích.
Allan

Câu trả lời:


14

Vâng, bạn hoàn toàn đi đúng hướng!

Theo kinh nghiệm của tôi, tôi nhận thấy rằng khi mọi thứ phức tạp, những thay đổi xảy ra theo từng bước nhỏ. Những gì bạn đã làm là bước 1 trong quy trình tiến hóa (hoặc quá trình tái cấu trúc). Đây là bước 2 và bước 3:

Bước 2

class Base {
  method ExecJob() {
    S1();
    S2();
    S3();
    S4();
    S5();
  }
  method S1() { //concrete implementation }
  method S3() { //concrete implementation }
  method S4() { //concrete implementation}
  abstract method S2();
  abstract method S5();
}

class A::Base {
  method S2() {//concrete implementation}
  method S5() {//concrete implementation}
}

class B::Base {
  method S2() { // empty implementation}
  method S5() {//concrete implementation}
}

class C::Base {
  method S2() { // empty implementation}
  method S5() { // empty implementation}
}

Đây là 'Mẫu thiết kế mẫu' và nó đi trước một bước trong quy trình tái cấu trúc. Nếu lớp cơ sở thay đổi, các lớp con (A, B, C) không cần bị ảnh hưởng. Bạn có thể thêm các lớp con mới tương đối dễ dàng. Tuy nhiên, ngay từ bức ảnh trên bạn có thể thấy sự trừu tượng bị phá vỡ. Nhu cầu 'thực hiện trống rỗng' là một chỉ số tốt; nó cho thấy có điều gì đó không ổn với sự trừu tượng của bạn. Nó có thể là một giải pháp chấp nhận được trong thời gian ngắn, nhưng dường như có một giải pháp tốt hơn.

Bước 3

interface JobExecuter {
  void executeJob();
}
class A::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S2();
     helper->S3();
     helper->S4();
     helper->S5();
  }
}

class B::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S3();
     helper->S4();
     helper->S5();
  }
}

class C::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S3();
     helper->S4();
  }
}

class Base{
   void ExecJob(JobExecuter executer){
       executer->executeJob();
   }
}

class Helper{
    void S1(){//Implementation} 
    void S2(){//Implementation}
    void S3(){//Implementation}
    void S4(){//Implementation} 
    void S5(){//Implementation}
}

Đây là 'Mẫu thiết kế chiến lược' và có vẻ phù hợp với trường hợp của bạn. Có các chiến lược khác nhau để thực hiện công việc và mỗi lớp (A, B, C) thực hiện nó khác nhau.

Tôi chắc chắn có một bước 4 hoặc bước 5 trong quy trình này hoặc các phương pháp tái cấu trúc tốt hơn rất nhiều. Tuy nhiên, điều này sẽ cho phép bạn loại bỏ mã trùng lặp và đảm bảo rằng các thay đổi được bản địa hóa.


Vấn đề chính tôi thấy với giải pháp được nêu trong "Bước 2" là việc triển khai cụ thể của S5 tồn tại hai lần.
user281377

1
Có, việc sao chép mã không bị loại bỏ! Và đó là một chỉ số khác của sự trừu tượng không hoạt động. Tôi chỉ muốn đưa bước 2 ra khỏi đó để cho thấy cách tôi nghĩ về quy trình; một cách tiếp cận từng bước để tìm kiếm một cái gì đó tốt hơn.
Guven

1
+1 Chiến lược rất tốt (và tôi không nói về mô hình )!
Jordão

7

Bạn đang thực sự làm điều đúng đắn. Tôi nói điều này bởi vì:

  1. Nếu bạn cần thay đổi mã cho một chức năng nhiệm vụ chung, bạn không cần thay đổi mã trong tất cả 6 lớp sẽ chứa mã nếu bạn không viết mã trong một lớp chung.
  2. Số dòng mã sẽ được giảm.

3

Bạn thấy loại mã này chia sẻ rất nhiều với thiết kế hướng sự kiện (đặc biệt là .NET). Cách dễ duy trì nhất là giữ cho hành vi được chia sẻ của bạn thành nhiều phần nhỏ nhất có thể.

Hãy để mã cấp cao sử dụng lại một loạt các phương thức nhỏ, bỏ mã cấp cao ra khỏi cơ sở được chia sẻ.

Bạn sẽ có rất nhiều tấm nồi hơi trong việc thực hiện lá / bê tông của bạn. Đừng hoảng sợ, không sao đâu. Tất cả các mã đó là trực tiếp, dễ hiểu. Bạn sẽ phải sắp xếp lại nó đôi khi khi công cụ bị hỏng, nhưng nó sẽ dễ dàng thay đổi.

Bạn sẽ thấy rất nhiều mẫu trong mã cấp cao. Đôi khi chúng là thật, hầu hết thời gian chúng không có. "Cấu hình" của năm tham số trên đó trông giống nhau, nhưng chúng không giống nhau. Chúng là ba chiến lược hoàn toàn khác nhau.

Cũng muốn lưu ý rằng bạn có thể làm tất cả điều này với thành phần và không bao giờ lo lắng về sự kế thừa. Bạn sẽ có ít khớp nối hơn.


3

Nếu tôi là bạn, có lẽ tôi sẽ thêm 1 bước nữa vào lúc đầu: một nghiên cứu dựa trên UML.

Tái cấu trúc mã hợp nhất tất cả các phần chung lại với nhau không phải là cách tốt nhất, nghe có vẻ giống như một giải pháp tạm thời hơn là một cách tiếp cận tốt.

Vẽ sơ đồ UML, giữ cho mọi thứ đơn giản nhưng hiệu quả, hãy ghi nhớ một số khái niệm cơ bản về dự án của bạn như "những gì được cho là làm phần mềm này?" "cách tốt nhất để giữ cho phần mềm này trừu tượng, mô-đun, mở rộng, ... vv vv là gì?" "làm thế nào tôi có thể thực hiện đóng gói ở mức tốt nhất?"

Tôi chỉ nói điều này: không quan tâm đến mã ngay bây giờ, bạn chỉ cần quan tâm đến logic, khi bạn có logic rõ ràng, tất cả phần còn lại có thể trở thành một nhiệm vụ thực sự dễ dàng, cuối cùng tất cả các loại này các vấn đề mà bạn đang đối mặt chỉ đơn thuần là do logic xấu.


Đây phải là bước đầu tiên, trước khi bất kỳ tái cấu trúc nào được thực hiện. Cho đến khi mã được hiểu đủ để được ánh xạ (uml hoặc một số bản đồ khác của vùng hoang dã), tái cấu trúc sẽ được kiến ​​trúc trong bóng tối.
Kzqai

3

Bước đầu tiên, bất kể việc này đang diễn ra ở đâu, nên phá vỡ phương thức rõ ràng lớn A::ExecJob thành nhiều phần nhỏ hơn.

Do đó, thay vì

A::ExecJob()
{
    S1; // many lines of code
    S2; // many lines of code
    S3; // many lines of code
    S4; // many lines of code
    S5; // many lines of code
}

bạn lấy

A::ExecJob()
{
    S1();
    S2();
    S3();
    S4();
    S5();
}

A:S1()
{
   // many lines of code
}

A:S2()
{
   // many lines of code
}

A:S3()
{
   // many lines of code
}

A:S4()
{
   // many lines of code
}

A:S5()
{
   // many lines of code
}

Từ đây trở đi, có nhiều cách để đi. Tôi chấp nhận điều đó: Biến A thành lớp cơ sở của lớp hierachy và ExecJob ảo của bạn và việc tạo B, C, ... trở nên dễ dàng mà không cần sao chép quá nhiều - chỉ cần thay thế ExecJob (giờ là năm lớp) bằng một sửa đổi phiên bản.

B::ExecJob()
{
    S1();
    S3();
    S4();
    S5();
}

Nhưng tại sao có rất nhiều lớp học? Có lẽ bạn có thể thay thế tất cả chúng bằng một lớp duy nhất có hàm tạo có thể được cho biết hành động nào là cần thiết ExecJob.


2

Tôi đồng ý với các câu trả lời khác rằng cách tiếp cận của bạn là đúng hướng mặc dù tôi không nghĩ rằng kế thừa là cách tốt nhất để triển khai mã chung - tôi thích sáng tác hơn. Từ Câu hỏi thường gặp về C ++ có thể giải thích nó tốt hơn bao giờ hết: http://www.parashift.com/c++-faq/priv-inherit-vs-compos.html


1

Trước tiên, bạn nên chắc chắn rằng thừa kế thực sự là công cụ ngay tại đây cho công việc - chỉ bởi vì bạn cần một nơi phổ biến cho các chức năng được sử dụng bởi các lớp học của bạn Ađể Fkhông có nghĩa là một lớp cơ sở chung là điều đúng đắn ở đây - đôi khi một helper riêng Lớp học làm công việc tốt hơn. Nó có thể, nó có thể không. Điều đó phụ thuộc vào việc có mối quan hệ "is-a" giữa A đến F và lớp cơ sở chung của bạn, không thể nói từ tên nhân tạo AF. Đây bạn tìm thấy một bài viết blog liên quan đến chủ đề này.

Giả sử bạn quyết định rằng lớp cơ sở chung là điều đúng trong trường hợp của bạn. Sau đó, điều thứ hai tôi sẽ làm là đảm bảo các đoạn mã từ S1 đến S5 được thực hiện theo từng phương thức riêng biệt S1()cho S5()lớp cơ sở của bạn. Sau đó, các hàm "ExecJob" sẽ trông như thế này:

A::ExecJob()
{
    S1();
    S2();
    S3();
    S4();
    S5();
}

B::ExecJob()
{
    S1();
    S3();
    S4();
    S5();
}

C::ExecJob()
{
    S1();
    S3();
    S4();
}

Như bạn thấy bây giờ, vì S1 đến S5 chỉ là các cuộc gọi phương thức, không còn khối mã nào nữa, việc sao chép mã đã được loại bỏ gần như hoàn toàn và bạn không cần kiểm tra tham số nào nữa, tránh vấn đề tăng độ phức tạp bạn có thể nhận được nếu không thì.

Cuối cùng, nhưng chỉ là bước thứ ba (!), Bạn có thể nghĩ đến việc kết hợp tất cả các phương thức ExecJob đó vào một trong các lớp cơ sở của bạn, nơi việc thực thi các phần đó có thể được kiểm soát bởi các tham số, giống như cách bạn đề xuất hoặc bằng cách sử dụng mẫu phương pháp mẫu. Bạn phải tự quyết định xem điều đó có xứng đáng với nỗ lực trong trường hợp của bạn hay không, dựa trên mã thực.

Nhưng IMHO kỹ thuật cơ bản để chia nhỏ các phương thức lớn thành các phương thức nhỏ là rất nhiều, quan trọng hơn nhiều để tránh trùng lặp mã so với áp dụng các mẫu.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.