Khi nào mô hình của nhóm Do Do Thing trở nên có hại?


21

Vì mục đích tranh luận ở đây, một hàm mẫu in nội dung của từng dòng tệp đã cho.

Phiên bản 1:

void printFile(const string & filePath) {
  fstream file(filePath, ios::in);
  string line;
  while (std::getline(file, line)) {
    cout << line << endl;
  }
}

Tôi biết rằng các hàm nên làm một việc ở một mức độ trừu tượng. Đối với tôi, mặc dù mã ở trên có khá nhiều thứ và khá nguyên tử.

Một số cuốn sách (như Clean Code của Robert C. Martin) dường như đề nghị chia mã trên thành các chức năng riêng biệt.

Phiên bản 2:

void printFile(const string & filePath) {
  fstream file(filePath, ios::in);
  printLines(file);
}

void printLines(fstream & file) {
  string line;
  while (std::getline(file, line)) {
    printLine(line);
  }
}

void printLine(const string & line) {
  cout << line << endl;
}

Tôi hiểu những gì họ muốn đạt được (mở tệp / đọc dòng / dòng in), nhưng đó không phải là một chút quá mức?

Phiên bản gốc rất đơn giản và theo một nghĩa nào đó đã thực hiện một điều - in một tệp.

Phiên bản thứ hai sẽ dẫn đến một số lượng lớn các chức năng thực sự nhỏ có thể dễ đọc hơn nhiều so với phiên bản đầu tiên.

Trong trường hợp này, không nên để mã ở một nơi chứ?

Tại thời điểm nào mô hình "Do One Thing" trở nên có hại?


13
Loại thực hành mã hóa này luôn dựa trên từng trường hợp. Không bao giờ có một cách tiếp cận duy nhất.
iammilind

1
@Alex - Câu trả lời được chấp nhận theo nghĩa đen không liên quan gì đến câu hỏi. Tôi thấy điều đó thực sự kỳ quặc.
ChaosPandion

2
Tôi lưu ý rằng phiên bản tái cấu trúc của bạn bị lộn ngược, điều này góp phần vào việc thiếu khả năng đọc. Đọc sách xuống các tập tin, bạn sẽ mong đợi để xem printFile, printLinesvà cuối cùng printLine.
Anthony Pegram

1
@Kev, tôi một lần nữa chỉ có thể không đồng ý, đặc biệt với phân loại đó. Đó không phải là nghề giáo, đó là vấn đề! Đó là OP nói cụ thể phiên bản thứ hai có thể không đọc được. Đó là OP đặc biệt trích dẫn Clean Code là nguồn cảm hứng cho phiên bản thứ hai. Nhận xét của tôi về cơ bản là Clean Code sẽ không bắt anh ta viết mã theo cách đó. Thứ tự thực sự quan trọng đối với khả năng đọc, bạn đọc tập tin giống như bạn đọc một bài báo, ngày càng chi tiết hơn cho đến khi bạn cơ bản trở nên không quan tâm.
Anthony Pegram

1
Giống như bạn sẽ không mong đợi đọc một bài thơ ngược, bạn cũng sẽ không thấy mức độ chi tiết thấp nhất là điều đầu tiên trong một lớp cụ thể. Để quan điểm của bạn, này đang mất ít thời gian để nhanh chóng sắp xếp thông qua, nhưng tôi sẽ chỉ giả mã này không phải là mã duy nhất anh sẽ ghi. Theo quan điểm của tôi, nếu anh ấy sẽ trích dẫn Clean Code, thì điều tối thiểu anh ấy có thể làm là tuân theo nó. Nếu mã không đúng thứ tự, chắc chắn nó sẽ khó đọc hơn so với cách khác.
Anthony Pegram

Câu trả lời:


15

Tất nhiên, điều này chỉ đặt ra câu hỏi " một thứ là gì?" Là đọc một dòng một điều và viết một dòng khác? Hoặc là sao chép một dòng từ luồng này sang luồng khác để được coi là một điều? Hoặc sao chép một tập tin?

Không có câu trả lời khó, khách quan cho điều đó. Tuỳ bạn. Bạn có thể quyết định. Bạn phải quyết định. Mục tiêu chính của mô hình "làm một việc" có lẽ là tạo ra mã dễ hiểu nhất có thể, vì vậy bạn có thể sử dụng nó làm hướng dẫn. Thật không may, điều này cũng không thể đo lường một cách khách quan, vì vậy phải dựa vào cảm giác ruột của bạn và "WTF?" đếm trong mã xem xét .

IMO một hàm chỉ bao gồm một dòng mã hiếm khi gây rắc rối. Bạn printLine()không có lợi thế hơn khi sử dụng std::cout << line << '\n'1 trực tiếp. Nếu tôi thấy printLine(), tôi phải giả sử nó làm những gì tên của nó nói, hoặc tìm kiếm và kiểm tra. Nếu tôi thấy std::cout << line << '\n', tôi biết ngay những gì nó làm, bởi vì đây là cách chính tắc để xuất nội dung của một chuỗi thành một dòng std::cout.

Tuy nhiên, một mục tiêu quan trọng khác của mô hình là cho phép tái sử dụng mã và đó là một biện pháp khách quan hơn nhiều. Ví dụ, trong phiên bản thứ 2 của bạn printLines() có thể dễ dàng được viết để nó là một thuật toán hữu ích phổ biến, sao chép các dòng từ luồng này sang luồng khác:

void copyLines(std::istream& is, std::ostream& os)
{
  std::string line;
  while( std::getline(is, line) );
    os << line << '\n';
  }
}

Một thuật toán như vậy cũng có thể được sử dụng lại trong các bối cảnh khác.

Sau đó, bạn có thể đặt mọi thứ cụ thể cho trường hợp sử dụng này vào một hàm gọi thuật toán chung này:

void printFile(const std::string& filePath) {
  std::ifstream file(filePath.c_str());
  printLines(file, std::cout);
}

1 Lưu ý rằng tôi đã sử dụng '\n'chứ không phải std::endl. '\n'nên là lựa chọn mặc định để xuất một dòng mới , std::endllà trường hợp kỳ lạ .


2
+1 - Tôi hầu như đồng ý, nhưng tôi nghĩ có nhiều thứ hơn là "cảm giác ruột". Vấn đề là khi mọi người đánh giá "một điều" bằng cách đếm chi tiết thực hiện. Đối với tôi, hàm nên thực hiện (và tên của nó mô tả) một sự trừu tượng rõ ràng duy nhất. Bạn không bao giờ nên đặt tên hàm là "do_x_and_y". Việc thực hiện có thể và nên thực hiện một số điều (đơn giản hơn) - và mỗi điều đơn giản hơn có thể được phân tách thành một số điều thậm chí đơn giản hơn, v.v. Đó chỉ là phân rã chức năng với một quy tắc bổ sung - rằng các hàm (và tên của chúng) nên mỗi mô tả một khái niệm / nhiệm vụ / bất cứ điều gì rõ ràng.
Steve314

@ Steve314: Tôi chưa liệt kê chi tiết triển khai như các khả năng. Sao chép các dòng từ luồng này sang luồng khác rõ ràng là một điều trừu tượng. Hoặc là nó? Và thật dễ dàng để tránh do_x_and_y()bằng cách đặt tên hàm do_everything()thay thế. Vâng, đó là một ví dụ ngớ ngẩn, nhưng nó cho thấy quy tắc này thậm chí không ngăn được các ví dụ cực đoan nhất về thiết kế xấu. IMO đây một quyết định cảm giác ruột nhiều như một quyết định của các công ước. Mặt khác, nếu nó là khách quan, bạn có thể đưa ra một số liệu cho nó - mà bạn không thể.
sbi

1
Tôi không có ý định mâu thuẫn - chỉ đề nghị bổ sung. Tôi đoán điều tôi quên nói là, từ câu hỏi, việc phân tách thành printLinevv là hợp lệ - mỗi câu hỏi là một sự trừu tượng duy nhất - nhưng điều đó không có nghĩa là cần thiết. printFileđã là "một điều". Mặc dù bạn có thể phân tách nó thành ba mức trừu tượng riêng biệt thấp hơn, bạn không phải phân tách ở mọi mức độ trừu tượng có thể . Mỗi chức năng phải thực hiện "một việc", nhưng không phải mọi "khả năng" có thể cần phải là một chức năng. Di chuyển quá nhiều phức tạp vào biểu đồ cuộc gọi có thể là một vấn đề.
Steve314

7

Có một chức năng chỉ làm "một điều" là một phương tiện cho hai kết thúc mong muốn, không phải là một điều răn từ Thiên Chúa:

  1. Nếu chức năng của bạn chỉ thực hiện "một điều", nó sẽ giúp bạn tránh trùng lặp mã và phình to API vì bạn sẽ có thể soạn các hàm để thực hiện những việc phức tạp hơn thay vì có sự bùng nổ kết hợp của các hàm cấp cao hơn, ít có khả năng tổng hợp .

  2. Có các chức năng chỉ làm "một điều" có thể làm cho mã dễ đọc hơn. Điều này phụ thuộc vào việc bạn có đạt được sự rõ ràng và dễ dàng hơn trong việc suy luận bằng cách tách rời mọi thứ so với việc bạn mất đi tính dài dòng, không xác định và chi phí khái niệm của các cấu trúc cho phép bạn tách rời mọi thứ.

Do đó, "một điều" là không thể tránh khỏi sự chủ quan và phụ thuộc vào mức độ trừu tượng có liên quan đến chương trình của bạn. Nếu printLinesđược coi là một hoạt động cơ bản, duy nhất và là cách duy nhất để in các dòng mà bạn quan tâm hoặc thấy trước sự quan tâm, thì đối với mục đích của bạn printLineschỉ làm một việc. Trừ khi bạn tìm thấy phiên bản thứ hai dễ đọc hơn (tôi không), phiên bản đầu tiên vẫn ổn.

Nếu bạn bắt đầu cần kiểm soát nhiều hơn đối với mức độ trừu tượng thấp hơn và kết thúc bằng sự trùng lặp tinh vi và vụ nổ kết hợp (nghĩa là printLinestên tệp và tách biệt hoàn toàn printLinescho fstreamcác đối tượng, thì printLinesbàn điều khiển và printLinestệp cho tệp) printLinessẽ thực hiện nhiều hơn một thứ ở cấp độ sự trừu tượng mà bạn quan tâm.


Tôi sẽ thêm một phần ba và đó là các chức năng nhỏ hơn được kiểm tra dễ dàng hơn. Vì có lẽ có ít đầu vào cần thiết hơn nếu chức năng chỉ làm một việc, điều đó giúp kiểm tra độc lập dễ dàng hơn.
PersonalNexus

@PersonalNexus: Tôi phần nào đồng ý về vấn đề thử nghiệm, nhưng IMHO thật ngớ ngẩn khi kiểm tra chi tiết triển khai. Đối với tôi, một bài kiểm tra đơn vị nên kiểm tra "một điều" như được định nghĩa trong câu trả lời của tôi. Bất cứ điều gì tốt hơn đều làm cho các bài kiểm tra của bạn trở nên dễ vỡ hơn (vì việc thay đổi chi tiết triển khai sẽ yêu cầu các bài kiểm tra của bạn thay đổi) và mã của bạn gây khó chịu, gián tiếp, v.v. (vì bạn sẽ thêm chỉ định để hỗ trợ kiểm tra).
dsimcha

6

Ở quy mô này, nó không thành vấn đề. Việc thực hiện chức năng đơn là hoàn toàn rõ ràng và dễ hiểu. Tuy nhiên, việc thêm một chút phức tạp sẽ khiến cho việc phân chia vòng lặp khỏi hành động trở nên rất hấp dẫn. Ví dụ: giả sử bạn cần in các dòng từ một tập hợp các tệp được chỉ định bởi một mẫu như "* .txt". Sau đó, tôi sẽ tách lặp đi lặp lại từ hành động:

printLines(FileSet files) {
   files.each({ 
       file -> file.eachLine({ 
           line -> printLine(line); 
       })
   })
}

Bây giờ việc lặp lại tập tin có thể được kiểm tra riêng.

Tôi phân chia các chức năng để đơn giản hóa việc kiểm tra hoặc để cải thiện khả năng đọc. Nếu hành động được thực hiện trên mỗi dòng dữ liệu đủ phức tạp để đảm bảo nhận xét, thì tôi chắc chắn sẽ chia nó thành một chức năng riêng biệt.


4
Tôi nghĩ bạn đóng đinh nó. Nếu chúng ta cần một nhận xét để giải thích một dòng, thì luôn luôn là lúc trích xuất một phương thức.
Roger CS Wernersson

5

Trích xuất các phương pháp khi bạn cảm thấy cần một bình luận để giải thích mọi thứ.

Viết các phương thức hoặc chỉ làm những gì tên của chúng nói một cách rõ ràng hoặc kể một câu chuyện bằng cách gọi các phương thức được đặt tên khéo léo.


3

Ngay cả trong trường hợp đơn giản của bạn, bạn vẫn thiếu chi tiết rằng Nguyên tắc Trách nhiệm duy nhất sẽ giúp bạn quản lý tốt hơn. Ví dụ, những gì xảy ra khi có vấn đề với việc mở tệp. Thêm xử lý ngoại lệ để tăng cường chống lại các trường hợp cạnh truy cập tệp sẽ thêm 7-10 dòng mã vào chức năng của bạn.

Sau khi bạn mở tệp, bạn vẫn không an toàn. Nó có thể được lấy từ bạn (đặc biệt nếu đó là một tệp trên mạng), bạn có thể hết bộ nhớ, một số trường hợp cạnh có thể xảy ra mà bạn muốn làm cứng và sẽ làm hỏng chức năng nguyên khối của bạn.

Một đường kẻ, đường in có vẻ vô hại. Nhưng khi chức năng mới được thêm vào máy in tệp (phân tích và định dạng văn bản, hiển thị cho các loại màn hình khác nhau, v.v.), nó sẽ phát triển và bạn sẽ cảm ơn chính mình sau này.

Mục tiêu của SRP là cho phép bạn suy nghĩ về một nhiệm vụ duy nhất tại một thời điểm. Nó giống như phá vỡ một khối lớn văn bản thành nhiều đoạn để người đọc có thể hiểu được điểm bạn đang cố gắng vượt qua. Phải mất thêm một ít thời gian để viết mã tuân thủ các nguyên tắc này. Nhưng khi làm như vậy, chúng tôi làm cho việc đọc mã đó dễ dàng hơn. Hãy nghĩ về bản thân tương lai của bạn sẽ hạnh phúc như thế nào khi anh ấy phải theo dõi một lỗi trong mã và tìm thấy nó được phân chia gọn gàng.


2
Tôi ủng hộ câu trả lời này vì tôi thích logic mặc dù tôi không đồng ý với nó! Cung cấp cấu trúc dựa trên một số suy nghĩ phức tạp về những gì có thể xảy ra trong tương lai là phản tác dụng. Mã Factorise khi bạn cần. Đừng trừu tượng cho đến khi bạn cần. Mã hiện đại bị làm phiền bởi những người cố gắng tuân theo các quy tắc thay vì chỉ viết mã hoạt động và điều chỉnh nó một cách miễn cưỡng . Lập trình viên giỏi thì lười biếng .
Yttrill

Cảm ơn đã bình luận. Lưu ý Tôi không ủng hộ việc trừu tượng hóa sớm, chỉ phân chia các thao tác logic để sau này dễ thực hiện hơn.
Michael Brown

2

Cá nhân tôi thích cách tiếp cận thứ hai hơn, bởi vì nó giúp bạn tiết kiệm công việc trong tương lai và buộc tư duy "làm thế nào để làm theo cách chung". Mặc dù trong trường hợp của bạn, Phiên bản 1 tốt hơn Phiên bản 2 - chỉ vì các vấn đề được giải quyết bởi Phiên bản 2 quá tầm thường và cụ thể. Tôi nghĩ rằng nó nên được thực hiện theo cách sau (bao gồm sửa lỗi được đề xuất bởi Nawaz):

Các chức năng tiện ích chung:

void printLine(ostream& output, const string & line) { 
    output << line << endl; 
} 

void printLines(istream& input, ostream& output) { 
    string line; 
    while (getline(input, line)) {
        printLine(output, line); 
    } 
} 

Chức năng dành riêng cho tên miền:

void printFile(const string & filePath, ostream& output = std::cout) { 
    fstream file(filePath, ios::in); 
    printLines(file, output); 
} 

Bây giờ printLinesprintLinecó thể làm việc không chỉ với fstream, mà với bất kỳ luồng nào.


2
Tôi không đồng ý. printLine()Hàm đó không có giá trị. Xem câu trả lời của tôi .
sbi

1
Chà, nếu chúng ta giữ printLine () thì chúng ta có thể thêm một trình trang trí có thêm số dòng hoặc tô màu cú pháp. Có nói rằng, tôi sẽ không trích xuất các phương pháp đó cho đến khi tôi tìm thấy một lý do.
Roger CS Wernersson

2

Mọi mô hình , (không nhất thiết là điều bạn đã trích dẫn) phải tuân theo đòi hỏi một số kỷ luật, và do đó - tạo ra "tự do ngôn luận" - dẫn đến một chi phí ban đầu (ít nhất là vì bạn phải học nó!). Theo nghĩa này, mọi mô hình đều có thể trở nên có hại khi chi phí cho chi phí đó không được bù đắp bằng lợi thế mà mô hình được thiết kế để tự giữ.

Câu trả lời thực sự cho câu hỏi, do đó, đòi hỏi một khả năng tốt để "thấy trước" tương lai, như:

  • Tôi ngay bây giờ bắt buộc phải làm AB
  • Xác suất là gì, trong tương lai gần tôi sẽ bắt buộc phải làm A-B+(tức là một cái gì đó trông giống A và B, nhưng chỉ khác một chút)?
  • Xác suất trong một tương lai xa hơn, rằng A + sẽ trở thành A*hay là A*-gì?

Nếu xác suất đó tương đối cao, sẽ là một cơ hội tốt nếu trong khi nghĩ về A và B- Tôi cũng nghĩ về các biến thể có thể có của chúng, do đó cô lập các phần chung để tôi có thể sử dụng lại chúng.

Nếu xác suất đó rất thấp (bất kỳ biến thể nào xung quanh Avề cơ bản không có gì nhiều hơn Achính nó), nghiên cứu cách phân hủy A hơn nữa rất có thể sẽ dẫn đến lãng phí thời gian.

Chỉ là một ví dụ, để tôi kể cho bạn câu chuyện có thật này:

Trong cuộc sống quá khứ của tôi như một giáo viên, tôi phát hiện ra rằng -on dụng tối đa projects- của học sinh hầu như tất cả trong số họ cung cấp chức năng riêng của họ để tính toán chiều dài của một chuỗi C .

Sau một số điều tra tôi phát hiện ra rằng, là một vấn đề thường xuyên, tất cả các sinh viên nảy ra ý tưởng sử dụng một chức năng cho điều đó. Sau khi nói với họ rằng có một hàm thư viện cho ( strlen), nhiều người trong số họ trả lời rằng vì vấn đề rất đơn giản và tầm thường, nên họ viết hàm riêng của họ (2 dòng mã) hiệu quả hơn là tìm kiếm hướng dẫn sử dụng thư viện C (đó là năm 1984, đã quên WEB và google!) theo thứ tự chữ cái nghiêm ngặt để xem có chức năng sẵn sàng cho việc đó không.

Đây là một ví dụ trong đó mô hình "không phát minh lại bánh xe" có thể trở nên có hại, nếu không có một danh mục bánh xe hiệu quả!


2

Ví dụ của bạn là tốt để được sử dụng trong một công cụ vứt đi cần thiết ngày hôm qua để thực hiện một số nhiệm vụ cụ thể. Hoặc như một công cụ quản trị được quản trị viên trực tiếp kiểm soát. Bây giờ làm cho nó mạnh mẽ để phù hợp với khách hàng của bạn.

Thêm xử lý lỗi / ngoại lệ thích hợp với các thông điệp có ý nghĩa. Có thể bạn cần xác minh tham số, bao gồm các quyết định phải đưa ra, ví dụ cách xử lý các tệp không tồn tại. Thêm chức năng ghi nhật ký, có thể với các cấp độ khác nhau như thông tin và gỡ lỗi. Thêm ý kiến ​​để đồng nghiệp nhóm của bạn biết những gì đang xảy ra ở đó. Thêm tất cả các phần thường được bỏ qua cho ngắn gọn và để lại như một bài tập cho người đọc khi đưa ra các ví dụ mã. Đừng quên bài kiểm tra đơn vị của bạn.

Hàm nhỏ đẹp và khá tuyến tính của bạn đột nhiên kết thúc trong một mớ hỗn độn phức tạp, xin được chia thành các hàm riêng biệt.


2

IMO nó trở nên có hại khi đi xa đến mức một chức năng hầu như không làm gì cả ngoài việc ủy ​​thác công việc cho chức năng khác, bởi vì đó là dấu hiệu cho thấy nó không còn là một sự trừu tượng của bất cứ điều gì và suy nghĩ dẫn đến các chức năng đó luôn có nguy cơ làm những điều tồi tệ hơn ...

Từ bài viết gốc

void printLine(const string & line) {
  cout << line << endl;
}

Nếu bạn đủ tầm cỡ, bạn có thể nhận thấy printLine vẫn thực hiện hai việc: viết dòng vào cout và thêm ký tự "dòng cuối". Một số người có thể muốn xử lý điều đó bằng cách tạo các chức năng mới:

void printLine(const string & line) {
  reallyPrintLine(line);
  addEndLine();
}

void reallyPrintLine(const string & line) {
  cout << line;
}

void addEndLine() {
  cout << endl;
}

Ồ không, bây giờ chúng tôi đã làm cho vấn đề thậm chí còn tồi tệ hơn! Bây giờ thậm chí OBVIOUS rằng printLine thực hiện HAI điều !!! 1! Nó không tạo ra nhiều sự ngu ngốc để tạo ra những "công việc" ngớ ngẩn nhất mà người ta có thể tưởng tượng chỉ để thoát khỏi vấn đề không thể tránh khỏi đó là in một dòng bao gồm in chính dòng đó và thêm một ký tự cuối dòng.

void printLine(const string & line) {
  for (int i=0; i<2; i++)
    reallyPrintLine(line, i);
}

void reallyPrintLine(const string & line, int action) {
  cout << (action==0?line:endl);
}

1

Câu trả lời ngắn ... nó phụ thuộc.

Hãy nghĩ về điều này: điều gì sẽ xảy ra nếu trong tương lai, bạn sẽ không chỉ muốn in ra đầu ra tiêu chuẩn, mà là một tệp.

Tôi biết YAGNI là gì, nhưng tôi chỉ nói có thể có trường hợp cần thực hiện một số việc triển khai, nhưng hoãn lại. Vì vậy, có thể kiến ​​trúc sư hoặc bất cứ điều gì biết rằng chức năng đó cũng cần có thể in ra một tệp, nhưng không muốn thực hiện ngay bây giờ. Vì vậy, anh ta tạo ra chức năng bổ sung này vì vậy, trong tương lai, bạn chỉ cần thay đổi đầu ra ở một nơi. Có ý nghĩa?

Tuy nhiên, nếu bạn chắc chắn rằng bạn chỉ cần đầu ra trong giao diện điều khiển, nó không thực sự có ý nghĩa nhiều. Viết một "bao bọc" trên cout <<dường như vô dụng.


1
Nhưng nói đúng ra, chức năng printLine có phải là một mức độ trừu tượng khác với việc lặp qua các dòng không?

@Petr Tôi đoán vậy, đó là lý do tại sao họ đề nghị bạn tách chức năng. Tôi nghĩ rằng khái niệm này là chính xác, nhưng bạn cần áp dụng nó trên cơ sở từng trường hợp.

1

Toàn bộ lý do có những cuốn sách dành các chương cho những ưu điểm của "làm một việc" là vẫn có những nhà phát triển ngoài đó viết các hàm dài 4 trang và có điều kiện 6 cấp. Nếu mã của bạn đơn giản và rõ ràng, bạn đã làm đúng.


0

Như các áp phích khác đã bình luận, làm một điều là một vấn đề quy mô.

Tôi cũng sẽ đề xuất rằng ý tưởng One Thing là ngăn chặn mọi người mã hóa bằng tác dụng phụ. Điều này được minh họa bằng cách ghép nối tiếp trong đó các phương thức phải được gọi theo một thứ tự cụ thể để có kết quả 'đúng'.

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.