Thêm độ phức tạp để loại bỏ mã trùng lặp


24

Tôi có một vài lớp mà tất cả đều kế thừa từ một lớp cơ sở chung. Lớp cơ sở chứa một tập hợp của một số đối tượng của loại T.

Mỗi lớp con cần có khả năng tính toán các giá trị nội suy từ bộ sưu tập các đối tượng, nhưng vì các lớp con sử dụng các loại khác nhau, phép tính thay đổi một chút từ lớp này sang lớp khác.

Cho đến nay tôi đã sao chép / dán mã của mình từ lớp này sang lớp khác và thực hiện các sửa đổi nhỏ cho từng mã. Nhưng bây giờ tôi đang cố gắng loại bỏ mã trùng lặp và thay thế nó bằng một phương thức nội suy chung trong lớp cơ sở của tôi. Tuy nhiên, điều đó đang tỏ ra rất khó khăn và tất cả các giải pháp tôi nghĩ có vẻ quá phức tạp.

Tôi bắt đầu nghĩ rằng nguyên tắc DRY không áp dụng nhiều trong tình huống này, nhưng điều đó nghe có vẻ như báng bổ. Bao nhiêu phức tạp là quá nhiều khi cố gắng loại bỏ sao chép mã?

CHỈNH SỬA:

Giải pháp tốt nhất tôi có thể đưa ra là một thứ như thế này:

Lớp cơ sở:

protected T GetInterpolated(int frame)
{
    var index = SortedFrames.BinarySearch(frame);
    if (index >= 0)
        return Data[index];

    index = ~index;

    if (index == 0)
        return Data[index];
    if (index >= Data.Count)
        return Data[Data.Count - 1];

    return GetInterpolatedItem(frame, Data[index - 1], Data[index]);
}

protected abstract T GetInterpolatedItem(int frame, T lower, T upper);

Trẻ lớp A:

public IGpsCoordinate GetInterpolatedCoord(int frame)
{
    ReadData();
    return GetInterpolated(frame);
}

protected override IGpsCoordinate GetInterpolatedItem(int frame, IGpsCoordinate lower, IGpsCoordinate upper)
{
    double ratio = GetInterpolationRatio(frame, lower.Frame, upper.Frame);

    var x = GetInterpolatedValue(lower.X, upper.X, ratio);
    var y = GetInterpolatedValue(lower.Y, upper.Y, ratio);
    var z = GetInterpolatedValue(lower.Z, upper.Z, ratio);

    return new GpsCoordinate(frame, x, y, z);
}

Lớp con B:

public double GetMph(int frame)
{
    ReadData();
    return GetInterpolated(frame).MilesPerHour;
}

protected override ISpeed GetInterpolatedItem(int frame, ISpeed lower, ISpeed upper)
{
    var ratio = GetInterpolationRatio(frame, lower.Frame, upper.Frame);
    var mph = GetInterpolatedValue(lower.MilesPerHour, upper.MilesPerHour, ratio);
    return new Speed(frame, mph);
}

9
Một ứng dụng quá nhỏ của các khái niệm như DRY và Tái sử dụng mã dẫn đến những tội lỗi lớn hơn nhiều.
Affe

1
Bạn đang nhận được một số câu trả lời chung tốt. Chỉnh sửa để bao gồm một chức năng ví dụ có thể giúp chúng tôi xác định xem bạn có đưa nó đi quá xa trong trường hợp cụ thể này không.
Karl Bielefeldt

Đây thực sự không phải là một câu trả lời, hơn nữa là một quan sát: Nếu bạn không thể dễ dàng giải thích những gì một lớp cơ sở bao gồm , thì tốt nhất là không nên có. Một cách nhìn khác là (tôi cho rằng bạn đã quen thuộc với RẮN?) 'Có bất kỳ người tiêu dùng nào có khả năng sử dụng chức năng này yêu cầu thay thế Liskov' không? Nếu không có trường hợp kinh doanh có khả năng cho một người tiêu dùng tổng quát về chức năng nội suy, một lớp cơ sở không có giá trị.
Tom W

1
Điều đầu tiên là thu thập bộ ba X, Y, Z thành một loại Vị trí và thêm phép nội suy vào loại đó với tư cách là thành viên hoặc có thể là phương thức tĩnh: Nội suy vị trí (Vị trí khác, tỷ lệ).
kevin cline

Câu trả lời:


30

Theo một cách nào đó, bạn đã trả lời câu hỏi của riêng bạn với nhận xét đó trong đoạn cuối:

Tôi bắt đầu nghĩ rằng nguyên tắc DRY không áp dụng nhiều trong tình huống này, nhưng điều đó nghe có vẻ như báng bổ .

Bất cứ khi nào bạn thấy một số thực hành không thực sự thiết thực để giải quyết vấn đề của mình, đừng cố gắng sử dụng thực hành đó một cách tôn giáo (từ báng bổ là một lời cảnh báo cho việc này). Hầu hết các thông lệ có họ whensWhys và thậm chí nếu họ bao gồm 99% của tất cả các trường hợp có thể, vẫn còn đó 1%, nơi bạn có thể cần một cách tiếp cận khác nhau.

Cụ thể, liên quan đến DRY , tôi cũng thấy rằng đôi khi thực sự tốt hơn là thậm chí có một vài đoạn mã trùng lặp nhưng đơn giản hơn một quái vật khổng lồ khiến bạn cảm thấy mệt mỏi khi nhìn vào nó.

Điều đó đang được nói, sự tồn tại của các trường hợp cạnh này không nên được sử dụng như một lý do cho việc sao chép và dán mã cẩu thả hoặc hoàn toàn thiếu các mô-đun có thể tái sử dụng. Đơn giản, nếu bạn không biết làm thế nào để viết một mã chung và có thể đọc được cho một số vấn đề trong một số ngôn ngữ, thì có lẽ sẽ ít tệ hơn khi có một số dư thừa. Hãy nghĩ về bất cứ ai phải duy trì mã. Họ sẽ dễ dàng sống với sự dư thừa hoặc che giấu?

Một lời khuyên cụ thể hơn về ví dụ cụ thể của bạn. Bạn nói rằng những tính toán này là tương tự nhưng hơi khác nhau . Bạn có thể muốn thử chia công thức tính toán của mình thành các biểu mẫu con nhỏ hơn và sau đó tất cả các phép tính hơi khác nhau của bạn gọi các hàm trợ giúp này để thực hiện các phép tính phụ. Bạn sẽ tránh được tình huống mọi phép tính phụ thuộc vào một số mã được khái quát hóa quá mức và bạn vẫn có một số mức độ sử dụng lại.


10
Một điểm khác về sự tương tự nhưng hơi khác một chút là mặc dù chúng trông giống mã, nhưng không có nghĩa là chúng phải giống nhau trong "kinh doanh". Tất nhiên phụ thuộc vào những gì nó là tất nhiên, nhưng đôi khi đó là một ý tưởng tốt để giữ mọi thứ riêng biệt bởi vì mặc dù chúng trông giống nhau, chúng có thể dựa trên các quyết định / yêu cầu kinh doanh khác nhau. Vì vậy, bạn có thể muốn xem chúng như những phép tính khác nhau rất lớn mặc dù chúng khôn ngoan có thể trông giống nhau. (Không phải là một quy tắc hay bất cứ điều gì, nhưng chỉ là một điều cần lưu ý khi quyết định liệu mọi thứ nên được kết hợp hoặc tái cấu trúc :)
Svish

@Svish Điểm thú vị. Không bao giờ nghĩ về nó theo cách đó.
Phil

17

các lớp con sử dụng các loại khác nhau, phép tính thay đổi một chút từ lớp này sang lớp khác.

Điều đầu tiên và quan trọng nhất là: Đầu tiên xác định rõ phần nào đang thay đổi và phần nào KHÔNG thay đổi giữa các lớp. Khi bạn đã xác định được điều đó, vấn đề của bạn đã được giải quyết.

Làm điều đó như bài tập đầu tiên trước khi bắt đầu bao thanh toán lại. Mọi thứ khác sẽ rơi vào vị trí tự động sau đó.


2
Vâng đặt. Đây có thể chỉ là một vấn đề của chức năng lặp đi lặp lại là quá lớn.
Karl Bielefeldt

8

Tôi tin rằng hầu như tất cả sự lặp lại của hơn một vài dòng mã có thể được thực hiện theo cách này hay cách khác, và hầu như luôn luôn phải như vậy.

Tuy nhiên, việc tái cấu trúc này dễ dàng hơn ở một số ngôn ngữ so với các ngôn ngữ khác. Nó khá dễ dàng trong các ngôn ngữ như LISP, Ruby, Python, Groovy, Javascript, Lua, v.v. Thông thường không quá khó trong C ++ khi sử dụng các mẫu. Đau đớn hơn ở C, trong đó công cụ duy nhất có thể là macro tiền xử lý. Thường đau đớn trong Java và đôi khi đơn giản là không thể, ví dụ như cố gắng viết mã chung để xử lý nhiều kiểu số tích hợp.

Trong các ngôn ngữ biểu cảm hơn, không có câu hỏi: tái cấu trúc bất cứ thứ gì nhiều hơn một vài dòng mã. Với các ngôn ngữ ít biểu cảm hơn, bạn phải cân bằng nỗi đau của việc tái cấu trúc so với độ dài và tính ổn định của mã lặp lại. Nếu mã lặp lại dài hoặc có thể thay đổi thường xuyên, tôi có xu hướng cấu trúc lại ngay cả khi mã kết quả hơi khó đọc.

Tôi sẽ chấp nhận mã lặp lại chỉ khi nó ngắn, ổn định và tái cấu trúc là quá xấu. Về cơ bản, tôi tính gần như tất cả các bản sao trừ khi tôi đang viết Java.

Không thể đưa ra khuyến nghị cụ thể cho trường hợp của bạn vì bạn chưa đăng mã hoặc thậm chí chỉ ra ngôn ngữ bạn đang sử dụng.


Lua không phải là từ viết tắt.
DeadMG

@DeadMG: lưu ý; cảm thấy tự do để chỉnh sửa. Đó là lý do tại sao chúng tôi đã mang đến cho bạn tất cả danh tiếng đó.
kevin cline

5

Khi bạn nói lớp cơ sở phải thực hiện một thuật toán, nhưng thuật toán thay đổi cho từng lớp phụ, điều này nghe có vẻ như là một ứng cử viên hoàn hảo cho Mẫu Mẫu .

Với điều này, lớp cơ sở thực hiện thuật toán và khi nói đến biến thể của mỗi lớp con, nó sẽ chuyển sang một phương thức trừu tượng mà nó là trách nhiệm của lớp con thực hiện. Hãy nghĩ về cách và trang ASP.NET trì hoãn mã của bạn để triển khai Page_Load chẳng hạn.


4

Âm thanh như "một phương thức nội suy chung" của bạn trong mỗi lớp đang làm quá nhiều và nên được thực hiện thành các phương thức nhỏ hơn.

Tùy thuộc vào mức độ phức tạp của phép tính của bạn, tại sao mỗi "phần" tính toán không thể là một phương thức ảo như thế này

Public Class Fraction
{
     public virtual Decimal GetNumerator(params?)
     public virtual Decimal GetDenominator(params?)
     //Some concrete method to actually compute GetNumerator / GetDenominator
}

Và ghi đè các phần riêng lẻ khi bạn cần thực hiện "biến đổi nhỏ" đó trong logic của phép tính.

(Đây là một ví dụ rất nhỏ và vô dụng về cách bạn có thể thêm nhiều chức năng trong khi ghi đè các phương thức nhỏ)


3

Theo tôi, bạn đã đúng, theo một nghĩa nào đó, DRY có thể được đưa đi quá xa. Nếu hai đoạn mã tương tự có khả năng phát triển theo các hướng rất khác nhau thì bạn có thể tự gây ra vấn đề bằng cách cố gắng không lặp lại chính mình ban đầu.

Tuy nhiên, bạn cũng hoàn toàn đúng khi cảnh giác với những suy nghĩ báng bổ như vậy. Cố gắng hết sức để suy nghĩ thông qua các lựa chọn của bạn trước khi bạn quyết định để nó một mình.

Chẳng hạn, sẽ tốt hơn nếu đặt mã lặp lại đó trong một lớp / phương thức tiện ích, thay vì trong lớp cơ sở? Xem Thành phần ưu tiên hơn kế thừa .


2

DRY là một hướng dẫn để tuân theo, không phải là một quy tắc không thể phá vỡ. Tại một số điểm, bạn cần phải quyết định rằng nó không có giá trị X mức kế thừa và mẫu Y trong mỗi lớp bạn đang sử dụng chỉ để nói rằng không có sự lặp lại trong mã này. Một vài câu hỏi hay được đặt ra là tôi sẽ mất nhiều thời gian hơn để trích xuất các phương thức tương tự này và thực hiện chúng như một, sau đó nó sẽ tìm kiếm thông qua tất cả chúng nếu cần thay đổi phát sinh hoặc là khả năng thay đổi của chúng sẽ xảy ra hoàn tác công việc của tôi trích xuất các phương pháp này ở nơi đầu tiên? Tôi có đang ở điểm mà sự trừu tượng hóa bổ sung đang bắt đầu để hiểu được nơi nào hoặc mã này làm gì là một thách thức không?

Nếu bạn có thể trả lời có cho một trong những câu hỏi đó thì bạn có một trường hợp mạnh mẽ để lại mã có khả năng trùng lặp


0

Bạn đã phải tự hỏi mình câu hỏi, "tại sao tôi nên cấu trúc lại nó"? Trong trường hợp của bạn khi bạn có mã "tương tự nhưng khác" nếu bạn thay đổi một thuật toán, bạn cần đảm bảo rằng bạn cũng phản ánh sự thay đổi đó ở các điểm khác. Đây thường là một công thức cho thảm họa, luôn luôn có người khác sẽ bỏ lỡ một vị trí và giới thiệu một lỗi khác.

Trong trường hợp này, việc tái cấu trúc các thuật toán thành một thuật toán khổng lồ sẽ khiến nó trở nên quá phức tạp, gây khó khăn cho việc bảo trì trong tương lai. Vì vậy, nếu bạn không thể tìm ra những thứ thông thường một cách hợp lý, thì đơn giản:

// this code is similar to class x function b

Bình luận sẽ đủ. Vấn đề được giải quyết.


0

Khi quyết định liệu nên có một phương thức lớn hơn hay hai phương thức nhỏ hơn với chức năng chồng chéo, câu hỏi 50.000 đô la đầu tiên là liệu phần chồng chéo của hành vi có thể thay đổi hay không và liệu có nên áp dụng bất kỳ thay đổi nào cho các phương thức nhỏ hơn không. Nếu câu trả lời cho câu hỏi đầu tiên là có nhưng câu trả lời cho câu hỏi thứ hai là không, thì các phương thức nên được tách riêng . Nếu câu trả lời cho cả hai câu hỏi là có, thì phải làm gì đó để đảm bảo rằng mọi phiên bản của mã vẫn được đồng bộ hóa; trong nhiều trường hợp, cách dễ nhất để làm điều đó là chỉ có một phiên bản.

Có một vài nơi mà Microsoft dường như đã đi ngược lại các nguyên tắc DRY. Ví dụ, Microsoft đã rõ ràng không khuyến khích việc các phương thức chấp nhận một tham số cho biết liệu một lỗi có nên đưa ra một ngoại lệ hay không. Mặc dù đúng là tham số "thất bại ném ngoại lệ" là xấu trong API "sử dụng chung" của phương thức, các tham số đó có thể rất hữu ích trong trường hợp phương thức Thử / Làm cần được tạo thành từ các phương thức Thử / Làm khác. Nếu một phương thức bên ngoài được cho là ném một ngoại lệ khi xảy ra lỗi, thì bất kỳ lệnh gọi phương thức bên trong nào bị lỗi sẽ ném một ngoại lệ mà phương thức bên ngoài có thể truyền đi. Nếu phương thức bên ngoài không được phép đưa ra một ngoại lệ, thì phương thức bên trong cũng không. Nếu một tham số được sử dụng để phân biệt giữa thử / làm, sau đó phương thức bên ngoài có thể truyền nó cho phương thức bên trong. Mặt khác, phương thức bên ngoài sẽ cần gọi các phương thức "thử" khi nó được coi là phương thức "thử" và "làm" khi nó được coi là "làm".

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.