Dòng bổ sung trong khối so với tham số bổ sung trong Clean Code


33

Bối cảnh

Trong Clean Code , trang 35, nó nói

Điều này ngụ ý rằng các khối trong câu lệnh if, câu lệnh khác, trong khi câu lệnh, v.v. nên dài một dòng. Có lẽ dòng đó nên là một chức năng gọi. Điều này không chỉ giữ cho hàm kèm theo nhỏ mà còn thêm giá trị tài liệu vì hàm được gọi trong khối có thể có một tên mô tả độc đáo.

Tôi hoàn toàn đồng tình, điều đó rất có ý nghĩa.

Sau đó, trên trang 40, nó nói về các đối số chức năng

Số lượng đối số lý tưởng cho một hàm là 0 (niladic). Tiếp đến là một (monadic), theo sát bởi hai (dyadic). Nên tránh ba đối số (bộ ba) nếu có thể. Hơn ba (polyadic) đòi hỏi sự biện minh rất đặc biệt và sau đó không nên sử dụng. Luận cứ thì khó. Họ có rất nhiều sức mạnh khái niệm.

Tôi hoàn toàn đồng tình, điều đó rất có ý nghĩa.

Vấn đề

Tuy nhiên, thay vào đó, tôi thường thấy mình tạo ra một danh sách từ một danh sách khác và tôi sẽ phải sống với một trong hai tệ nạn.

Hoặc là tôi sử dụng hai dòng trong khối , một để tạo điều, một để thêm nó vào kết quả:

    public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
    {
        List<Flurp> flurps = new List<Flurp>();
        foreach (BadaBoom badaBoom in badaBooms)
        {
            Flurp flurp = CreateFlurp(badaBoom);
            flurps.Add(flurp);
        }
        return flurps;
    }

Hoặc tôi thêm một đối số vào hàm cho danh sách nơi điều sẽ được thêm vào, làm cho nó "một đối số tồi tệ hơn".

    public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
    {
        List<Flurp> flurps = new List<Flurp>();
        foreach (BadaBoom badaBoom in badaBooms)
        {
            CreateFlurpInList(badaBoom, flurps);
        }
        return flurps;
    }

Câu hỏi

Có (không) những lợi thế mà tôi không thấy, điều này làm cho một trong số chúng thích hợp hơn nói chung? Hoặc có những lợi thế như vậy trong các tình huống nhất định; trong trường hợp đó, tôi nên tìm kiếm gì khi đưa ra quyết định?


58
Có chuyện gì với bạn flurps.Add(CreateFlurp(badaBoom));vậy?
cmaster

47
Không, đó chỉ là một tuyên bố duy nhất. Nó chỉ là một biểu thức lồng nhau tầm thường (một mức lồng nhau). Và nếu đơn giản f(g(x))là trái với hướng dẫn phong cách của bạn, tốt, tôi không thể sửa hướng dẫn phong cách của bạn. Ý tôi là, bạn cũng không chia sqrt(x*x + y*y)thành bốn dòng, phải không? Và đó là ba (!) Các biểu hiện phụ lồng nhau ở hai cấp độ lồng trong (!) (Thở hổn hển!). Mục tiêu của bạn phải là khả năng đọc , không phải là câu lệnh đơn lẻ. Nếu bạn muốn sau này, tốt, tôi có ngôn ngữ hoàn hảo cho bạn: Trình biên dịch.
cmaster

6
@cmaster Ngay cả lắp ráp x86 không hoàn toàn có câu lệnh toán tử đơn. Các chế độ địa chỉ bộ nhớ bao gồm nhiều thao tác phức tạp và có thể được sử dụng cho số học - trên thực tế, bạn có thể tạo một máy tính hoàn chỉnh Turing chỉ bằng các lệnh x86 movvà một lệnh jmp toStartở cuối. Ai đó thực sự đã tạo một trình biên dịch thực hiện chính xác điều đó: D
Luaan

5
@Luaan Không nói về rlwimihướng dẫn khét tiếng trên PPC. (Đó là viết tắt của Xoay mặt từ ngay lập tức Chèn mặt nạ.) Lệnh này mất không dưới năm toán hạng (hai thanh ghi và ba giá trị ngay lập tức) và nó đã thực hiện các thao tác sau: Một nội dung thanh ghi được xoay bởi một ca ngay lập tức, mặt nạ là được tạo ra với một lần chạy 1 bit được điều khiển bởi hai toán hạng tức thời khác và các bit tương ứng với 1 bit trong mặt nạ đó trong toán hạng thanh ghi khác được thay thế bằng các bit tương ứng của thanh ghi xoay. Hướng dẫn rất tuyệt :-)
cmaster 16/2/18

7
@ R.Schmitz "Tôi đang lập trình mục đích chung" - thực tế là không, bạn không, bạn đang lập trình cho một mục đích cụ thể (tôi không biết mục đích , nhưng tôi giả sử bạn làm vậy ;-). Có hàng ngàn mục đích để lập trình và các kiểu mã hóa tối ưu cho chúng khác nhau - vì vậy những gì phù hợp với bạn có thể không phù hợp với người khác và ngược lại: Thường thì lời khuyên ở đây là tuyệt đối (" luôn luôn làm X; Y là xấu "Vv) bỏ qua rằng trong một số lĩnh vực, nó hoàn toàn không thực tế để bám vào. Đó là lý do tại sao những lời khuyên trong các cuốn sách như Clean Code phải luôn được thực hiện bằng một nhúm muối (thực tế) :)
psmears 16/2/18

Câu trả lời:


104

Những hướng dẫn này là một la bàn, không phải là bản đồ. Họ chỉ cho bạn một hướng hợp lý . Nhưng họ thực sự không thể nói với bạn một cách tuyệt đối về giải pháp nào là tốt nhất. Tại một số điểm, bạn cần dừng bước vào hướng mà la bàn của bạn đang chỉ, bởi vì bạn đã đến đích.

Clean Code khuyến khích bạn chia mã của mình thành các khối rất nhỏ, rõ ràng. Đó là một hướng đi tốt. Nhưng khi được đưa đến mức cực đoan (như một cách giải thích theo nghĩa đen của lời khuyên được trích dẫn gợi ý), thì bạn sẽ chia mã của bạn thành các phần nhỏ vô dụng. Không có gì thực sự làm bất cứ điều gì, tất cả mọi thứ chỉ là đại biểu. Đây thực chất là một loại mã obfuscation.

Công việc của bạn là cân bằng giữa các nhóm nhỏ hơn là tốt hơn so với việc quá nhỏ là vô dụng. Hãy tự hỏi giải pháp nào đơn giản hơn. Đối với tôi, đó rõ ràng là giải pháp đầu tiên vì nó rõ ràng tập hợp một danh sách. Đây là một thành ngữ được hiểu rõ. Có thể hiểu mã đó mà không cần phải xem chức năng khác.

Nếu có thể làm tốt hơn, thì bằng cách lưu ý rằng, chuyển đổi tất cả các yếu tố từ danh sách này sang danh sách khác, là một mô hình phổ biến thường có thể được trừu tượng hóa bằng cách sử dụng một map()hoạt động chức năng . Trong C #, tôi nghĩ nó được gọi là Select. Một cái gì đó như thế này:

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
    return badaBooms.Select(BadaBoom => CreateFlurp(badaBoom)).ToList();
}

7
Mã vẫn sai, và nó vô hiệu hóa lại bánh xe. Tại sao gọi CreateFlurps(someList)khi BCL đã cung cấp someList.ConvertAll(CreateFlurp)?
Ben Voigt

44
@BenVoigt Đây là một câu hỏi cấp thiết kế. Tôi không quan tâm đến cú pháp chính xác, đặc biệt là vì bảng trắng không có trình biên dịch (và tôi đã viết C # lần cuối trong '09). Quan điểm của tôi không phải là tôi đã hiển thị mã tốt nhất có thể có mà là một bên, đây là một mô hình phổ biến đã được giải quyết. Linq là một cách để làm điều đó, ConvertAll bạn đề cập đến một cách khác . Cảm ơn bạn đã đề nghị thay thế.
amon

1
Câu trả lời của bạn là hợp lý, nhưng thực tế là LINQ trừu tượng hóa logic và giảm câu lệnh xuống một dòng sau khi tất cả dường như mâu thuẫn với lời khuyên của bạn. Như một lưu ý phụ, BadaBoom => CreateFlurp(badaBoom)là dư thừa; bạn có thể truyền CreateFlurpdưới dạng hàm trực tiếp ( Select(CreateFlurp)). (Theo như tôi biết, điều này luôn luôn như vậy.)
jpmc26

2
Lưu ý rằng điều này loại bỏ sự cần thiết cho phương pháp hoàn toàn. Cái tên CreateFlurpsthực sự dễ gây hiểu lầm và khó hiểu hơn là chỉ nhìn thấy badaBooms.Select(CreateFlurp). Cái sau là hoàn toàn khai báo - không có vấn đề gì để phân hủy và do đó không cần một phương pháp nào cả.
Carl Leth

1
@ R.Schmitz Điều đó không khó hiểu, nhưng nó không dễ hiểu hơn badaBooms.Select(CreateFlurp). Bạn tạo một phương thức sao cho tên của nó (mức cao) đại diện cho việc thực hiện (mức thấp). Trong trường hợp này, chúng ở cùng cấp độ, vì vậy để tìm hiểu chính xác những gì đang xảy ra, tôi chỉ cần nhìn vào phương pháp (thay vì nhìn thấy nội tuyến). CreateFlurps(badaBooms)có thể giữ những bất ngờ, nhưng badaBooms.Select(CreateFlurp)không thể. Nó cũng gây hiểu lầm bởi vì nó nhầm yêu cầu Listthay vì một IEnumerable.
Carl Leth

61

Số lượng đối số lý tưởng cho một hàm là 0 (niladic)

Không! Số lượng đối số lý tưởng cho một hàm là một. Nếu nó bằng 0, thì bạn đang đảm bảo rằng hàm phải truy cập thông tin bên ngoài để có thể thực hiện một hành động. "Chú" Bob hiểu điều này rất sai.

Về mã của bạn, ví dụ đầu tiên của bạn chỉ có hai dòng trong khối vì bạn đang tạo một biến cục bộ trên dòng đầu tiên. Xóa nhiệm vụ đó và bạn đang tuân thủ các nguyên tắc mã sạch này:

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
    List<Flurp> flurps = new List<Flurp>();
    foreach (BadaBoom badaBoom in badaBooms)
    {
        flurps.Add(CreateFlurp(badaBoom));
    }
    return flurps;
}

Nhưng đó là mã dài (C #). Chỉ cần làm như:

IEnumerable<Flurp> CreateFlurps(IEnumerable<BadaBoom> badaBooms) =>
    from badaBoom in babaBooms select CreateFlurp(badaBoom);

14
Hàm có đối số bằng 0 có nghĩa là ngụ ý đối tượng gói gọn dữ liệu cần thiết chứ không phải mọi thứ đang tồn tại ở trạng thái toàn cầu bên ngoài đối tượng.
Ryathal

19
@Ryathal, hai điểm: (1) nếu bạn đang nói các phương thức, thì đối với hầu hết (tất cả?) Ngôn ngữ OO, đối tượng đó được suy ra (hoặc được nêu rõ ràng, trong trường hợp của Python) làm tham số đầu tiên. Trong Java, C # vv, tất cả các phương thức là các hàm có ít nhất một tham số. Trình biên dịch chỉ ẩn chi tiết đó từ bạn. (2) Tôi chưa bao giờ đề cập đến "toàn cầu". Trạng thái đối tượng là bên ngoài một phương thức chẳng hạn.
David Arno

17
Tôi khá chắc chắn, khi chú Bob viết "không", ông có nghĩa là "không (không tính điều này)".
Doc Brown

26
@DocBrown, có lẽ vì anh ấy là một fan hâm mộ lớn của việc trộn trạng thái và chức năng trong các đối tượng, do đó, bằng "chức năng", anh ấy có thể đề cập cụ thể đến các phương thức. Và tôi vẫn không đồng ý với anh. Tốt hơn hết là chỉ đưa ra một phương thức những gì nó cần, thay vì để nó lục lọi trong đối tượng để đạt được những gì nó muốn (nghĩa là "nói, đừng hỏi" cổ điển trong hành động).
David Arno

8
@AlessandroTeruzzi, Lý tưởng là một tham số. Zero là quá ít. Đây là lý do tại sao, ví dụ, các ngôn ngữ chức năng chấp nhận một như là số lượng tham số cho mục đích curry (thực tế trong một số ngôn ngữ chức năng, tất cả các chức năng có chính xác một tham số: không hơn; không ít hơn). Curry với các thông số bằng không sẽ là vô nghĩa. Nói rằng "lý tưởng là càng ít càng tốt, ergo zero là tốt nhất" là một ví dụ về reductio ad absurdum .
David Arno

19

Lời khuyên 'Mã sạch' là hoàn toàn sai.

Sử dụng hai hoặc nhiều dòng trong vòng lặp của bạn. Ẩn hai dòng giống nhau trong một hàm có ý nghĩa khi chúng là một số toán học ngẫu nhiên cần một mô tả nhưng nó không làm gì khi các dòng đã được mô tả. 'Tạo' và 'Thêm'

Phương pháp thứ hai mà bạn đề cập trong thực sự không có ý nghĩa gì, vì bạn không bị buộc phải thêm đối số thứ hai để tránh hai dòng.

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
    {
        List<Flurp> flurps = new List<Flurp>();
        foreach (BadaBoom badaBoom in badaBooms)
        {
            flurps.Add(badaBoom .CreateFlurp());
            //or
            badaBoom.AddToListAsFlurp(flurps);
            //or
            flurps.Add(new Flurp(badaBoom));
            //or
            //make flurps a member of the class
            //use linq.Select()
            //etc
        }
        return flurps;
    }

hoặc là

foreach(var flurp in ConvertToFlurps(badaBooms))...

Như đã lưu ý bởi những người khác, lời khuyên rằng chức năng tốt nhất là một chức năng không có đối số bị sai lệch với OOP ở mức tốt nhất và lời khuyên tồi tệ nhất là tồi tệ nhất


Có lẽ bạn muốn chỉnh sửa câu trả lời này để làm cho nó rõ ràng hơn? Câu hỏi của tôi là nếu một thứ vượt trội hơn một thứ khác theo Clean Code. Bạn nói rằng toàn bộ điều đó là sai và sau đó tiếp tục mô tả một trong những lựa chọn tôi đã đưa ra. Tại thời điểm này, câu trả lời này có vẻ như bạn đang theo chương trình nghị sự về Bộ luật chống sạch thay vì thực sự cố gắng trả lời câu hỏi.
R. Schmitz

xin lỗi tôi đã giải thích câu hỏi của bạn vì cho rằng cách thứ nhất là cách 'bình thường', nhưng bạn đã bị đẩy vào câu thứ hai. Nói chung tôi không chống mã sạch, nhưng trích dẫn này rõ ràng là sai
Ewan

19
@ R.Schmitz Tôi đã đọc "Clean Code" và tôi làm theo hầu hết những gì cuốn sách nói. Tuy nhiên, liên quan đến kích thước chức năng hoàn hảo gần như là một tuyên bố, điều đó hoàn toàn sai. Hiệu quả duy nhất là, nó biến mã spaghetti thành mã gạo. Người đọc bị mất trong vô số các chức năng tầm thường chỉ tạo ra ý nghĩa hợp lý khi nhìn thấy cùng nhau. Con người có dung lượng bộ nhớ làm việc hạn chế và bạn có thể quá tải điều đó bằng các câu lệnh hoặc với các hàm. Bạn phải đạt được sự cân bằng giữa hai nếu bạn muốn có thể đọc được. Tránh cực đoan!
cmaster

@cmaster Câu trả lời chỉ có 2 đoạn đầu tiên khi tôi viết bình luận đó. Đó là một cách trả lời tốt hơn bây giờ.
R. Schmitz

7
thẳng thắn tôi thích câu trả lời ngắn hơn của tôi. Có quá nhiều cuộc nói chuyện ngoại giao trong hầu hết các câu trả lời này. Lời khuyên được trích dẫn là hoàn toàn sai, không cần suy đoán về 'ý nghĩa thực sự của nó' hoặc xoay quanh để thử và tìm một cách giải thích tốt.
Ewan

15

Thứ hai chắc chắn là tồi tệ hơn, vì CreateFlurpInListchấp nhận danh sách và sửa đổi danh sách đó, làm cho chức năng không thuần túy và khó lý luận hơn. Không có gì trong tên phương thức cho thấy phương thức chỉ thêm vào danh sách.

Và tôi cung cấp tùy chọn thứ ba, tốt nhất,:

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
    return badaBooms.Select(CreateFlurp).ToList();
}

Và chết tiệt, bạn có thể nội tuyến phương thức đó ngay lập tức nếu chỉ có một nơi được sử dụng, vì bản thân một lớp rõ ràng, vì vậy nó không cần phải được gói gọn bằng phương thức để cho nó có ý nghĩa.


Tôi sẽ không phàn nàn quá nhiều về phương pháp đó "không thuần túy và khó lý luận hơn" (mặc dù đúng), nhưng về nó là một phương pháp hoàn toàn không cần thiết để xử lý một trường hợp đặc biệt. Điều gì sẽ xảy ra nếu tôi muốn tạo một Flurp độc lập, một Flurp được thêm vào một mảng, vào một từ điển, một Flurp sau đó được tra cứu trong một từ điển và loại bỏ Flurp phù hợp, v.v.? Với cùng một đối số, mã Flurp cũng sẽ cần tất cả các phương thức này.
gnasher729

10

Phiên bản một đối số là tốt hơn, nhưng không phải chủ yếu vì số lượng đối số.

Lý do quan trọng nhất là tốt hơn là nó có khớp nối thấp hơn , điều này làm cho nó hữu ích hơn, dễ lý luận hơn, dễ kiểm tra hơn và ít có khả năng biến thành bản sao + dán nhái.

Nếu bạn cung cấp cho tôi với một CreateFlurp(BadaBoom), tôi có thể sử dụng với bất kỳ loại của container sưu tập: Đơn giản Flurp[], List<Flurp>, LinkedList<Flurp>, Dictionary<Key, Flurp>, và vân vân. Nhưng với a CreateFlurpInList(BadaBoom, List<Flurp>), tôi sẽ quay lại với bạn vào ngày mai để yêu cầu chế độ CreateFlurpInBindingList(BadaBoom, BindingList<Flurp>)xem của tôi có thể nhận được thông báo rằng danh sách đã thay đổi. Kinh quá!

Là một lợi ích bổ sung, chữ ký đơn giản hơn có nhiều khả năng phù hợp với các API hiện có. Bạn nói rằng bạn có một vấn đề định kỳ

thay vào đó, tôi thường thấy mình tạo một danh sách từ một danh sách khác

Đó chỉ là vấn đề sử dụng các công cụ có sẵn. Phiên bản ngắn nhất, hiệu quả nhất và tốt nhất là:

var Flurps = badaBooms.ConvertAll(CreateFlurp);

Đây không chỉ là mã ít hơn để bạn viết và kiểm tra, nó còn nhanh hơn, bởi vì List<T>.ConvertAll()đủ thông minh để biết rằng kết quả sẽ có cùng số lượng mục với đầu vào và sắp xếp danh sách kết quả theo đúng kích thước. Trong khi mã của bạn (cả hai phiên bản) yêu cầu phát triển danh sách.


Đừng sử dụng List.ConvertAll. Cách thành ngữ để ánh xạ vô số các đối tượng đến các đối tượng khác nhau trong C # được gọi Select. Lý do duy nhất ConvertAllthậm chí có sẵn ở đây là vì OP đang nhầm lẫn yêu cầu một Listphương thức - nó phải là một IEnumerable.
Carl Leth

6

Giữ mục tiêu tổng thể trong tâm trí: làm cho mã dễ đọc và duy trì.

Thông thường, sẽ có thể nhóm nhiều dòng thành một hàm duy nhất, có ý nghĩa. Làm như vậy trong những trường hợp này. Đôi khi, bạn sẽ cần xem xét lại cách tiếp cận chung của bạn.

Ví dụ, trong trường hợp của bạn, thay thế toàn bộ triển khai bằng var

flups = badaBooms.Select(bb => new Flurp(bb));

có thể là một khả năng Hoặc bạn có thể làm một cái gì đó như

flups.Add(new Flurp(badaBoom))

Đôi khi, giải pháp sạch nhất và dễ đọc nhất sẽ không phù hợp với một dòng. Vì vậy, bạn sẽ có hai dòng. Đừng làm cho mã khó hiểu hơn, chỉ để thực hiện một số quy tắc tùy ý.

Ví dụ thứ hai của bạn là (theo tôi) khó hiểu hơn đáng kể so với lần đầu tiên. Không chỉ là bạn có tham số thứ hai, mà tham số đó được sửa đổi bởi hàm. Tra cứu những gì Clean Code nói về điều đó. (Không có cuốn sách trong tay ngay bây giờ, nhưng tôi khá chắc chắn về cơ bản là "đừng làm điều đó nếu bạn có thể tránh nó").

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.