Nên nếu thống kê là trong phương pháp bên trong hay bên ngoài?


17

Những thiết kế nào là tốt hơn? Những ưu và nhược điểm của mỗi là gì? Bạn sẽ sử dụng cái nào? Bất kỳ đề xuất nào khác về cách đối phó với các phương pháp như được đánh giá cao.

Thật hợp lý khi giả định rằng Draw () là nơi duy nhất mà các phương thức vẽ khác được gọi từ đó. Điều này cần mở rộng ra nhiều thuộc tính Draw * và Show * hơn, không chỉ ba thuộc tính được hiển thị ở đây.

public void Draw()
{
    if (ShowAxis)
    {
        DrawAxis();
    }

    if (ShowLegend)
    {
        DrawLegend();
    }

    if (ShowPoints && Points.Count > 0)
    {
        DrawPoints();
    }
}

private void DrawAxis()
{
    // Draw things.
}

private void DrawLegend()
{
    // Draw things.
}

private void DrawPoints()
{
    // Draw things.
}

Hoặc là

public void Draw()
{
    DrawAxis();
    DrawLegend();
    DrawPoints();
}

private void DrawAxis()
{
    if (!ShowAxis)
    {
        return;
    }

    // Draw things.
}

private void DrawLegend()
{
    if (!ShowLegend)
    {
        return;
    }

    // Draw things.
}

private void DrawPoints()
{
    if (!ShowPoints ||  Points.Count <= 0))
    {
        return;
    }

    // Draw things.
}

Để tham khảo thêm, tôi đã hỏi điều này trên SO - stackoverflow.com/questions/2966216/
Kẻ

1
Bất cứ khi nào tôi thấy một cụm từ như "Điều này cần mở rộng ra nhiều hơn nữa ...", tôi ngay lập tức nghĩ rằng "Đây nên là một vòng lặp".
Paul Butcher

Câu trả lời:


28

Tôi không nghĩ rằng bạn có thể có một quy tắc chăn cho loại điều này, nó phụ thuộc vào tình huống.

Trong trường hợp này, tôi khuyên bạn nên có các mệnh đề if bên ngoài các phương thức vì tên của các phương thức Draw ngụ ý rằng chúng chỉ vẽ những thứ mà không có bất kỳ điều kiện đặc biệt nào.

Nếu bạn thấy rằng bạn phải thực hiện kiểm tra trước khi gọi phương thức ở nhiều nơi, thì bạn có thể muốn đặt kiểm tra bên trong các phương thức và đổi tên chúng để làm rõ rằng điều này đang xảy ra


7

Tôi nói thứ hai.

Các phương pháp nên có cùng mức độ trừu tượng.

Trong ví dụ thứ hai, trách nhiệm của Draw()phương thức là hoạt động như một bộ điều khiển, gọi từng phương thức vẽ riêng lẻ. Tất cả các mã trong Draw()phương thức này đều ở cùng một mức độ trừu tượng. Hướng dẫn này đi vào chơi trong các kịch bản sử dụng lại. Ví dụ: nếu bạn muốn sử dụng lại DrawPoints()phương thức này trong một phương thức công khai khác (hãy gọi nó Sketch()), bạn sẽ không cần lặp lại mệnh đề bảo vệ (câu lệnh if quyết định có vẽ điểm không).

Tuy nhiên, trong ví dụ đầu tiên, Draw()phương thức chịu trách nhiệm xác định xem có nên gọi từng phương thức riêng lẻ hay không, sau đó gọi các phương thức đó. Draw()có một số phương thức cấp thấp hoạt động, nhưng nó ủy thác công việc cấp thấp khác cho các phương thức khác và do đó Draw()có mã ở các mức độ trừu tượng khác nhau. Để tái sử dụng DrawPoints()trong Sketch(), bạn sẽ cần phải sao chép các khoản bảo vệ trong Sketch()là tốt.

Ý tưởng này được thảo luận trong cuốn sách "Clean Code" của Robert C. Martin, mà tôi rất khuyến khích.


1
Đồ tốt. Để giải quyết một điểm được đưa ra trong một câu trả lời khác, tôi sẽ đề nghị thay đổi tên của DrawAxis()et. al. để chỉ ra rằng việc thực hiện của họ là có điều kiện, có thể TryDrawAxis(). Nếu không, bạn cần điều hướng từ Draw()phương thức đến từng phân nhóm riêng lẻ để xác nhận hành vi của nó.
Josh Earl

6

Ý kiến ​​của tôi về chủ đề này khá gây tranh cãi, nhưng hãy đồng ý với tôi, vì tôi tin rằng khá nhiều người đồng ý về kết quả cuối cùng. Tôi chỉ có một cách tiếp cận khác nhau để đạt được điều đó.

Trong bài viết của tôi Chức năng địa ngục , tôi giải thích lý do tại sao tôi không thích chia tách các phương thức chỉ vì mục đích tạo ra các phương thức nhỏ hơn. Tôi chỉ chia chúng khi tôi biết , chúng sẽ được sử dụng lại, hoặc tất nhiên, khi tôi có thể tái sử dụng chúng.

OP tuyên bố:

Thật hợp lý khi giả định rằng Draw () là nơi duy nhất mà các phương thức vẽ khác được gọi từ đó.

Điều này dẫn tôi đến một lựa chọn thứ ba (trung gian) không được đề cập. Tôi tạo 'khối mã' hoặc 'đoạn mã' mà người khác sẽ tạo chức năng cho.

public void Draw()
{
    // Draw axis.
    if (ShowAxis)
    {
        // Drawing code ...
    }

    // Draw legend.
    if (ShowLegend)
    {
        // Drawing code ...
    }

    // Draw points.
    if (ShowPoints && Points.Count > 0)
    {
        // Drawing code ...
    }
}

OP cũng tuyên bố:

Điều này cần mở rộng ra nhiều thuộc tính Draw * và Show * hơn, không chỉ ba thuộc tính được hiển thị ở đây.

... vì vậy phương pháp này sẽ phát triển rất lớn một cách nhanh chóng. Hầu như tất cả mọi người đồng ý điều này làm giảm khả năng đọc. Theo tôi, giải pháp thích hợp không chỉ là chia thành nhiều phương thức, mà là chia mã thành các lớp có thể sử dụng lại. Giải pháp của tôi có thể sẽ trông giống như thế này.

private void Initialize()
{               
    // Create axis.
    Axe xAxe = new Axe();
    Axe yAxe = new Axe();

    _drawObjects = new List<Drawable>
    {
        xAxe,
        yAxe,
        new Legend(),
        ...
    }
}

public void Draw()
{
    foreach ( Drawable d in _drawObjects )
    {
        d.Draw();
    }
}

Đối số tất nhiên vẫn cần phải được thông qua và như vậy.


Mặc dù tôi không đồng ý với bạn về chức năng địa ngục, nhưng giải pháp của bạn là (IMHO) là giải pháp chính xác và là giải pháp phát sinh từ việc áp dụng đúng Clean Code & SRP.
Paul Butcher

4

Đối với các trường hợp bạn đang kiểm tra trường kiểm soát xem có vẽ hay không, sẽ có ý nghĩa khi có bên trong đó Draw(). Khi quyết định đó trở nên phức tạp hơn, tôi có xu hướng thích cái sau (hoặc phân chia nó). Nếu bạn cần sự linh hoạt hơn, bạn luôn có thể mở rộng nó.

private void DrawPoints()
{
    if (ShouldDrawPoints())
    {
        DoDrawPoints();
    }
}

protected void ShouldDrawPoints()
{
    return ShowPoints && Points.Count > 0;
}

protected void DoDrawPoints()
{
    // Draw things.
}

Lưu ý các phương thức bổ sung được bảo vệ cho phép các lớp con mở rộng những gì chúng cần. Điều này cũng cho phép bạn buộc vẽ để thử nghiệm hoặc bất kỳ lý do nào khác mà bạn có thể có.


Điều này là tốt đẹp: tất cả mọi thứ được lặp đi lặp lại, là trong phương pháp riêng của mình. Bây giờ bạn thậm chí có thể thêm một DrawPointsIfItShould()phương thức tắt các phím tắt if(should){do}:)
Konerak

4

Trong hai lựa chọn thay thế, tôi thích phiên bản đầu tiên. Lý do của tôi là tôi muốn một phương thức để làm những gì mà tên ngụ ý, mà không có bất kỳ phụ thuộc ẩn nào. DrawLegend nên vẽ một huyền thoại, có lẽ không vẽ một huyền thoại.

Tuy nhiên, câu trả lời của Steven Jeuris tốt hơn hai phiên bản trong câu hỏi.


3

Thay vì có các Show___thuộc tính riêng cho từng phần, có lẽ tôi sẽ xác định một trường bit. Điều đó sẽ đơn giản hóa nó một chút để:

[Flags]
public enum DrawParts
{
    Axis = 1,
    Legend = 2,
    Points = 4,
    // More...
}

public class MyClass
{
    public void Draw() {
        if (VisibleParts.HasFlag(DrawPart.Axis))   DrawAxis();
        if (VisibleParts.HasFlag(DrawPart.Legend)) DrawLegend();
        if (VisibleParts.HasFlag(DrawPart.Points)) DrawPoints();
        // More...
    }

    public DrawParts VisibleParts { get; set; }
}

Bên cạnh đó, tôi sẽ nghiêng về việc kiểm tra khả năng hiển thị trong phương pháp bên ngoài, chứ không phải bên trong. Đó là, sau tất cả, DrawLegendvà không DrawLegendIfShown. Nhưng nó phụ thuộc vào phần còn lại của lớp học. Nếu có những nơi khác gọi DrawLegendphương thức đó và họ cũng cần kiểm tra ShowLegend, có lẽ tôi chỉ cần chuyển séc vào DrawLegend.


2

Tôi sẽ đi với tùy chọn đầu tiên - với "nếu" bên ngoài phương thức. Nó mô tả tốt hơn logic được theo dõi, cộng với cung cấp cho bạn tùy chọn để thực sự vẽ một trục, ví dụ, trong trường hợp khi bạn muốn vẽ một trục bất kể cài đặt. Thêm vào đó, nó loại bỏ chi phí của một cuộc gọi chức năng bổ sung (giả sử nó không được nội tuyến), có thể tích lũy nếu bạn đang đi tốc độ (ví dụ của bạn có vẻ như nó chỉ là vẽ một biểu đồ trong đó có thể không phải là một yếu tố, nhưng trong một hình ảnh động hoặc trò chơi nó có thể được).


1

Nói chung, tôi thích có các mức mã thấp hơn giả định rằng các điều kiện tiên quyết được đáp ứng và thực hiện bất kỳ công việc nào mà chúng phải làm, với việc kiểm tra được thực hiện cao hơn trong ngăn xếp cuộc gọi. Điều này có lợi ích phụ là tiết kiệm chu kỳ bằng cách không thực hiện kiểm tra dự phòng, nhưng nó cũng có nghĩa là bạn có thể viết mã đẹp như thế này:

int do_something()
{
    sometype* x;
    if(!(x = sometype_new())) return -1;
    foo(x);
    bar(x);
    baz(x);
    return 0;
}

thay vì:

int do_something()
{
    sometype x* = sometype_new();
    if(ERROR == foo(x)) return -1;
    if(ERROR == bar(x)) return -1;
    if(ERROR == baz(x)) return -1;
    return 0;
}

Và tất nhiên điều này thậm chí còn khó khăn hơn nếu bạn thực thi thoát đơn, có bộ nhớ bạn cần giải phóng, v.v.


0

Tất cả phụ thuộc vào bối cảnh:

void someThing(var1, var2)
{
    // If input fails validation then return quickly.
    if (!isValid(var1) || !isValid(var2))
    {   return;
    }


    // Otherwise do what is logical and makes the code easy to read.
    if (doTaskConditionOK())
    {   doTask();
    }

    // Return early if it is logical
    // This is OK in C++ but not C like languages.
    // You have to be careful of cleanup in C like languages while RIAA will do
    // that auto-magically in C++. 
    if (allFinished)
    {   return;
    }

    doAlternativeIfNotFinished();

    // -- Alternative to the above for C
    if (!allFinished)
    {   
        doAlternativeIfNotFinished();
    }

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