Làm thế nào để cấu trúc lại một ứng dụng với nhiều trường hợp chuyển đổi?


10

Tôi có một ứng dụng lấy một số nguyên làm đầu vào và dựa trên các đầu vào gọi các phương thức tĩnh của các lớp khác nhau. Mỗi khi một số mới được thêm vào, chúng ta cần thêm một trường hợp khác và gọi một phương thức tĩnh khác của một lớp khác. Hiện tại có 50 trường hợp trong chuyển đổi và mỗi lần tôi cần thêm một trường hợp khác, tôi lại rùng mình. Có cách nào tốt hơn để làm điều này.

Tôi đã làm một số suy nghĩ và đưa ra ý tưởng này. Tôi sử dụng mô hình chiến lược. Thay vì có trường hợp chuyển đổi, tôi có một bản đồ các đối tượng chiến lược với khóa là số nguyên đầu vào. Khi phương thức được gọi, nó sẽ tra cứu đối tượng và gọi phương thức chung cho đối tượng. Bằng cách này tôi có thể tránh sử dụng cấu trúc trường hợp chuyển đổi.

Bạn nghĩ sao?


2
Vấn đề thực tế với mã hiện tại là gì?
Philip Kendall

Điều gì xảy ra khi bạn phải thực hiện một trong những thay đổi này? Bạn có phải thêm một switchtrường hợp và gọi một phương thức có sẵn trong hệ thống phức tạp của bạn, hoặc bạn phải phát minh ra cả phương thức và cuộc gọi của nó?
Kilian Foth

@KilianFoth Tôi đã kế thừa dự án này với tư cách là nhà phát triển bảo trì và chưa phải thực hiện bất kỳ thay đổi nào. Tuy nhiên tôi sẽ thực hiện thay đổi sớm, vì vậy tôi muốn cấu trúc lại ngay bây giờ. Nhưng để trả lời câu hỏi của bạn, có cho cái sau.
Kaushik Chakraborty

2
Tôi nghĩ bạn cần đưa ra một ví dụ cô đọng về những gì đang diễn ra.
whatsisname

1
@KaushikChakraborty: sau đó tạo một ví dụ từ bộ nhớ. Có những tình huống trong đó hơn 250 trường hợp uber-switch là phù hợp, và có những trường hợp chuyển đổi là xấu cho dù có bao nhiêu trường hợp. Ma quỷ là trong các chi tiết và chúng tôi không có chi tiết.
whatsisname

Câu trả lời:


13

Hiện tại có 50 trường hợp trong chuyển đổi và mỗi lần tôi cần thêm một trường hợp khác, tôi lại rùng mình.

Tôi yêu đa hình. Tôi yêu RẮN. Tôi yêu lập trình hướng đối tượng thuần túy. Tôi ghét phải xem những người được cho là một đại diện xấu bởi vì họ được áp dụng giáo điều.

Bạn đã không làm một trường hợp tốt để tái cấu trúc chiến lược. Nhân tiện tái cấu trúc có một tên. Nó được gọi là Thay thế có điều kiện bằng Đa hình .

Tôi đã tìm thấy một số lời khuyên thích hợp cho bạn từ c2.com :

Nó thực sự chỉ có ý nghĩa nếu các bài kiểm tra có điều kiện giống hoặc rất giống nhau được lặp lại thường xuyên. Đối với các thử nghiệm đơn giản, ít khi lặp đi lặp lại, thay thế một điều kiện đơn giản bằng tính dài dòng của nhiều định nghĩa lớp và có khả năng di chuyển tất cả những thứ này ra khỏi mã thực sự đòi hỏi hoạt động có điều kiện, sẽ dẫn đến một ví dụ trong sách giáo khoa về mã ẩn. Thích sự rõ ràng hơn độ tinh khiết giáo điều. - DanMuller

Bạn có một công tắc với 50 trường hợp và thay thế của bạn là sản xuất 50 đối tượng. Oh và 50 dòng mã xây dựng đối tượng. Đây không phải là tiến bộ. Tại sao không? Bởi vì phép tái cấu trúc này không làm giảm số lượng từ 50. Bạn sử dụng phép tái cấu trúc này khi bạn thấy bạn cần tạo một câu lệnh chuyển đổi khác trên cùng một đầu vào ở một nơi khác. Đó là khi việc tái cấu trúc này giúp vì nó biến 100 trở lại thành 50.

Chừng nào bạn đang đề cập đến "công tắc" giống như đó là người duy nhất bạn có, tôi không khuyên bạn điều này. Lợi thế duy nhất đến từ tái cấu trúc bây giờ là nó làm giảm khả năng một số quả bóng ném sẽ sao chép và dán 50 trường hợp chuyển đổi của bạn.

Những gì tôi khuyên bạn nên xem xét kỹ 50 trường hợp này để tìm ra những điểm tương đồng có thể được nêu ra. Ý tôi là 50? Có thật không? Bạn có chắc rằng bạn cần nhiều trường hợp? Bạn có thể đang cố gắng làm nhiều ở đây.


Tôi đồng ý với những gì bạn đang nói. Mã này có rất nhiều dư thừa, có thể là rất nhiều trường hợp thậm chí không cần thiết nhưng từ cái nhìn lướt qua thì có vẻ như không phải vậy. Mỗi trường hợp gọi một phương thức gọi nhiều hệ thống và tổng hợp kết quả và trả về mã gọi. Mỗi lớp đều khép kín, làm một công việc và tôi sợ rằng tôi sẽ vi phạm nguyên tắc gắn kết cao, là tôi để giảm số lượng các trường hợp.
Kaushik Chakraborty

2
Tôi có thể nhận được 50 mà không vi phạm sự gắn kết cao và giữ mọi thứ khép kín. Tôi chỉ không thể làm điều đó với một số. Tôi cần 2, 5 và 5 khác. Đó là lý do tại sao nó được gọi là bao thanh toán. Nghiêm túc, nhìn vào toàn bộ kiến ​​trúc của bạn và xem liệu bạn không thể tìm ra lối thoát trong 50 trường hợp này bạn đang ở đâu. Tái cấu trúc là về việc hoàn tác các quyết định tồi tệ. Không duy trì chúng trong các hình thức mới.
candied_orange

Bây giờ, nếu bạn có thể thấy một cách để giảm 50 bằng cách sử dụng phép tái cấu trúc này, hãy tìm nó. Để tận dụng ý tưởng của Doc Browns: Một bản đồ bản đồ có thể mất hai phím. Đôi điều suy nghĩ.
candied_orange

1
Tôi đồng ý với nhận xét của Candied. Vấn đề không phải là 50 trường hợp trong câu lệnh chuyển đổi, vấn đề là thiết kế kiến ​​trúc cấp cao hơn khiến bạn phải gọi một hàm cần quyết định giữa 50 tùy chọn. Tôi đã thiết kế một số hệ thống rất lớn và phức tạp và chưa bao giờ bị ép vào tình huống như thế.
Dunk

@Candied "Bạn sử dụng phép tái cấu trúc này khi bạn thấy bạn cần tạo một câu lệnh chuyển đổi khác trên cùng một đầu vào ở nơi khác." Bạn có thể giải thích điều này không? Dự án ủy quyền đầu tiên, xác nhận, thủ tục CRUD sau đó dao. Vì vậy, trong mỗi lớp có các trường hợp chuyển đổi trên cùng một đầu vào tức là một số nguyên, nhưng thực hiện các chức năng khác nhau như auth, hợp lệ. Vì vậy, chúng ta nên tạo một lớp linh hoạt mỗi loại có các phương thức khác nhau? Liệu trường hợp của chúng tôi có phù hợp với những gì bạn đang cố gắng nói bằng cách "lặp lại cùng một công tắc trên cùng một đầu vào" không?
Siddharth Trikha

9

Bản đồ chỉ các đối tượng chiến lược, được khởi tạo trong một số chức năng của mã của bạn, nơi bạn có một vài dòng mã trông giống như

     myMap.Add(1,new Strategy1());
     myMap.Add(2,new Strategy2());
     myMap.Add(3,new Strategy3());

yêu cầu bạn và đồng nghiệp của bạn thực hiện các chức năng / chiến lược được gọi trong các lớp riêng biệt, theo cách thức thống nhất hơn (vì tất cả các đối tượng chiến lược của bạn sẽ phải thực hiện cùng một giao diện). Mã như vậy thường toàn diện hơn một chút so với

     case 1:
          MyClass1.Doit1(someParameters);
          break;
     case 2:
          MyClass2.Doit2(someParameters);
          break;
     case 3:
          MyClass3.Doit3(someParameters);
          break;

Tuy nhiên, nó vẫn không giải phóng bạn khỏi gánh nặng chỉnh sửa tệp mã này bất cứ khi nào cần thêm số mới. Những lợi ích thực sự của phương pháp này là một cách khác:

  • việc khởi tạo bản đồ bây giờ trở nên tách biệt với mã công văn thực sự gọi hàm liên quan đến một số cụ thể và sau này không chứa 50 lần lặp lại đó nữa, nó sẽ trông như thế myMap[number].DoIt(someParameters). Vì vậy, mã công văn này không cần phải chạm vào bất cứ khi nào một số mới đến và có thể được thực hiện theo nguyên tắc Đóng-Đóng. Ngoài ra, khi bạn nhận được yêu cầu khi cần mở rộng mã công văn, bạn sẽ không phải thay đổi 50 địa điểm nữa mà chỉ cần một.

  • nội dung của bản đồ được xác định vào thời gian chạy (trong khi nội dung của cấu trúc chuyển đổi được xác định trước thời gian biên dịch), do đó, điều này mang đến cho bạn cơ hội để làm cho logic khởi tạo linh hoạt hơn hoặc có thể mở rộng.

Vì vậy, có, có một số lợi ích và đây chắc chắn là một bước tiến tới mã RẮN hơn. Tuy nhiên, nếu nó trả hết cho tái cấu trúc, thì đó là điều mà bạn hoặc nhóm của bạn sẽ phải tự quyết định. Nếu bạn không mong đợi mã công văn bị thay đổi, logic khởi tạo sẽ bị thay đổi và khả năng đọc của switchnó không phải là vấn đề thực sự, thì việc tái cấu trúc của bạn có thể không quá quan trọng.


Mặc dù tôi miễn cưỡng thay thế một cách mù quáng mọi công tắc bằng đa hình, tôi sẽ nói rằng sử dụng bản đồ theo cách mà Doc Brown gợi ý ở đây đã giúp ích rất nhiều cho tôi trong quá khứ. Khi bạn thực hiện cùng một giao diện hãy thay thế Doit1, Doit2vv với một Doitphương pháp mà có nhiều triển khai khác nhau.
candied_orange

Và nếu bạn có quyền kiểm soát loại ký hiệu đầu vào được sử dụng làm khóa, bạn có thể tiến thêm một bước bằng cách thực hiện doTheThing()một phương thức của ký hiệu đầu vào. Sau đó, bạn không cần phải duy trì bản đồ.
Kevin Krumwiede

1
@KevinKrumwiede: những gì bạn đề xuất có nghĩa là chỉ cần truyền các đối tượng chiến lược xung quanh trong chương trình, để thay thế cho các số nguyên. Tuy nhiên, khi chương trình lấy một số nguyên làm đầu vào từ một số nguồn dữ liệu ngoài, thì phải có một ánh xạ từ số nguyên sang chiến lược liên quan ít nhất là ở một nơi của hệ thống.
Doc Brown

Mở rộng theo đề xuất của Doc Brown: bạn cũng có thể tạo một nhà máy có chứa logic để tạo các đối tượng chiến lược, nếu bạn quyết định đi theo cách này. Điều đó nói rằng, câu trả lời được cung cấp bởi CandiedOrange có ý nghĩa nhất đối với tôi.
Vladimir Stokic

@DocBrown Đó là những gì tôi đã nhận được với "nếu bạn có quyền kiểm soát loại biểu tượng đầu vào."
Kevin Krumwiede

0

Tôi rất ủng hộ chiến lược được nêu trong câu trả lời của @DocBrown .

Tôi sẽ đề nghị một cải tiến cho câu trả lời.

Các cuộc gọi

 myMap.Add(1,new Strategy1());
 myMap.Add(2,new Strategy2());
 myMap.Add(3,new Strategy3());

có thể được phân phối. Bạn không cần phải quay lại cùng một tệp để thêm một chiến lược khác, tuân thủ nguyên tắc Mở-Đóng thậm chí còn tốt hơn.

Giả sử bạn thực hiện Strategy1trong tệp Strateg1.cpp. Bạn có thể có khối mã sau đây.

namespace Strategy1_Impl
{
   struct Initializer
   {
      Initializer()
      {
         getMap().Add(1, new Strategy1());
      }
   };
}
using namespace Strategy1_Impl;

static Initializer initializer;

Bạn có thể lặp lại cùng một mã trong mọi tệp StargetyN.cpp. Như bạn có thể thấy, đó sẽ là rất nhiều mã lặp đi lặp lại. Để giảm trùng lặp mã, bạn có thể sử dụng một mẫu có thể được đặt trong một tệp có thể truy cập được cho tất cả các Strategylớp.

namespace StrategyHelper
{
   template <int N, typename StrategyType> struct Initializer
   {
      Initializer()
      {
         getMap().Add(N, new StrategyType());
      }
   };
}

Sau đó, điều duy nhất bạn phải sử dụng trong Strateg1.cpp là:

static StrategyHelper::Initializer<1, Strategy1> initializer;

Dòng tương ứng trong StrategN.cpp là:

static StrategyHelper::Initializer<N, StrategyN> initializer;

Bạn có thể đưa việc sử dụng các mẫu lên một cấp độ khác bằng cách sử dụng một mẫu lớp cho các lớp Chiến lược cụ thể.

class Strategy { ... };

template <int N> class ConcreteStrategy;

Và sau đó, thay vì Strategy1, sử dụng ConcreteStrategy<1>.

template <> class ConcreteStrategy<1> : public Strategy { ... };

Thay đổi lớp người trợ giúp để đăng ký Strategys thành:

namespace StrategyHelper
{
   template <int N> struct Initializer
   {
      Initializer()
      {
         getMap().Add(N, new ConcreteStrategy<N>());
      }
   };
}

Thay đổi mã trong Strateg1.cpp thành:

static StrategyHelper::Initializer<1> initializer;

Thay đổi mã trong StrategN.cpp thành:

static StrategyHelper::Initializer<N> initializer;
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.