Đây có phải là một mô hình tốt: thay thế một chức năng dài bằng một loạt lambdas?


14

Gần đây tôi gặp phải tình huống sau.

class A{
public:
    void calculate(T inputs);
}

Thứ nhất, Ađại diện cho một đối tượng trong thế giới vật lý, đó là một lập luận mạnh mẽ cho việc không tách lớp lên. Bây giờ, calculate()hóa ra là một chức năng khá dài và phức tạp. Tôi nhận thấy ba cấu trúc có thể cho nó:

  • viết nó như một bức tường của văn bản - lợi thế - tất cả thông tin đều ở một nơi
  • viết privatecác hàm tiện ích trong lớp và sử dụng chúng trong calculatecác nhược điểm của cơ thể - phần còn lại của lớp không biết / quan tâm / hiểu về các phương thức đó
  • viết calculatetheo cách sau:

    void A::calculate(T inputs){    
        auto lambda1 = () [] {};    
        auto lambda2 = () [] {};    
        auto lambda3 = () [] {};
    
        lambda1(inputs.first_logical_chunk);
        lambda2(inputs.second_logical_chunk);
        lambda3(inputs.third_logical_chunk);
    }

Điều này có thể được coi là một thực hành tốt hay xấu? Liệu phương pháp này tiết lộ bất kỳ vấn đề? Nói chung, tôi có nên coi đây là một cách tiếp cận tốt khi tôi lại phải đối mặt với tình huống tương tự không?


BIÊN TẬP:

class A{
    ...
public:
    // Reconfiguration of the algorithm.
    void set_colour(double colour);
    void set_density(double density);
    void set_predelay(unsigned long microseconds);
    void set_reverb_time(double reverb_time, double room_size);
    void set_drywet(double left, double right);
    void set_room_size(double value);;

private:
    // Sub-model objects.
    ...
}

Tất cả những phương pháp đó:

  • nhận được một giá trị
  • tính toán một số giá trị khác mà không sử dụng trạng thái
  • gọi một số "đối tượng mô hình phụ" để thay đổi trạng thái của chúng.

Nó chỉ ra rằng, ngoại trừ set_room_size(), các phương thức đó chỉ đơn giản chuyển giá trị được yêu cầu cho các đối tượng phụ. set_room_size()mặt khác, một vài màn hình của các công thức tối nghĩa và sau đó (2) thực hiện một nửa màn hình gọi setters đối tượng phụ để áp dụng các kết quả thu được khác nhau. Do đó, tôi đã tách chức năng thành hai lambdas và gọi chúng ở cuối chức năng. Nếu tôi có thể chia nó thành nhiều phần hợp lý hơn, tôi sẽ tách được nhiều lambdas hơn.

Bất kể, mục tiêu của câu hỏi hiện tại là xác định xem cách suy nghĩ đó có nên tồn tại hay không, tốt nhất là không thêm giá trị (khả năng đọc, khả năng duy trì, khả năng gỡ lỗi, v.v.).


2
Bạn nghĩ gì khi sử dụng lambdas sẽ mang lại cho bạn chức năng gọi đó sẽ không?
Blrfl

1
Firstly, A represents an object in the physical world, which is a strong argument for not splitting the class up.Chắc chắn Ađại diện cho dữ liệu về một đối tượng có thể tồn tại trong thế giới vật lý. Bạn có thể có một trường hợp Akhông có đối tượng thực và đối tượng thực mà không có đối tượng A, vì vậy đối xử với họ như họ là một và giống nhau là vô nghĩa.
Doval

@Blrfl, đóng gói - không ai calculate()biết nhưng sẽ biết về các chức năng phụ đó.
Vorac

Nếu tất cả những tính toán đó có liên quan đến chỉ A, điều đó sẽ làm cho nó trở nên cực kỳ.
Blrfl

1
"Thứ nhất, Ađại diện cho một đối tượng trong thế giới vật lý, đó là một lập luận mạnh mẽ cho việc không tách lớp lên." Thật không may, tôi đã nói điều này khi tôi bắt đầu lập trình. Phải mất nhiều năm tôi mới nhận ra rằng đó là một khúc côn cầu ngựa. Đó là một lý do khủng khiếp để nhóm các thứ. Tôi không thể nói rõ những gì lý do tốt để điều nhóm (ít nhất là sự hài lòng của tôi), nhưng điều đó một là một, bạn nên loại bỏ ngay bây giờ. Cuối cùng, tất cả là "mã tốt" là nó hoạt động đúng, tương đối dễ hiểu và tương đối dễ thay đổi (nghĩa là những thay đổi không có tác dụng phụ kỳ lạ).
jpmc26

Câu trả lời:


13

Không, đây không phải là một mô hình tốt

Những gì bạn đang làm là chia một hàm thành các hàm nhỏ hơn bằng lambda. Tuy nhiên, có một công cụ tốt hơn nhiều để phá vỡ các chức năng: chức năng.

Lambdas hoạt động, như bạn đã thấy, nhưng chúng có nghĩa là nhiều hơn rất nhiều so với việc đơn giản chia một hàm thành các bit cục bộ. Lambdas làm:

  • Đóng cửa. Bạn có thể sử dụng các biến trong phạm vi bên ngoài bên trong lambda. Điều này rất mạnh mẽ và rất phức tạp.
  • Tái chỉ định. Trong khi ví dụ của bạn làm cho việc thực hiện bài tập trở nên khó khăn, người ta luôn phải chú ý đến ý tưởng rằng mã có thể hoán đổi các chức năng bất cứ lúc nào.
  • Chức năng hạng nhất. Bạn có thể chuyển một hàm lambda sang một hàm khác, thực hiện một thứ gọi là "lập trình hàm".

Ngay khi bạn đưa lambdas vào hỗn hợp, nhà phát triển tiếp theo để xem mã phải tải ngay lập tức tất cả các quy tắc đó để chuẩn bị xem cách mã của bạn hoạt động. Họ không biết rằng bạn sẽ không sử dụng tất cả chức năng đó. Điều này là rất tốn kém, so với các lựa chọn thay thế.

Nó giống như sử dụng một cái cuốc để làm vườn của bạn. Bạn biết rằng bạn chỉ sử dụng nó để đào những cái lỗ nhỏ cho những bông hoa năm nay, nhưng những người hàng xóm sẽ cảm thấy lo lắng.

Hãy xem xét rằng tất cả những gì bạn đang làm là nhóm mã nguồn của bạn một cách trực quan. Trình biên dịch không thực sự quan tâm rằng bạn đã chế biến mọi thứ với lambdas. Trên thực tế, tôi hy vọng trình tối ưu hóa sẽ hoàn tác ngay lập tức mọi thứ bạn vừa làm khi biên dịch. Bạn hoàn toàn phục vụ cho người đọc tiếp theo (Cảm ơn vì đã làm điều đó, ngay cả khi chúng tôi không đồng ý về phương pháp luận! Mã được đọc thường xuyên hơn nhiều so với nó được viết!). Tất cả bạn đang làm là nhóm chức năng.

  • Các hàm được đặt cục bộ trong luồng mã nguồn cũng sẽ hoạt động tốt mà không cần gọi lambda. Một lần nữa, tất cả những gì quan trọng là người đọc có thể đọc nó.
  • Nhận xét ở đầu hàm nói rằng "chúng tôi chia chức năng này thành ba phần", theo sau là các hàng dài lớn // ------------------giữa mỗi phần.
  • Bạn cũng có thể đặt từng phần của phép tính vào phạm vi riêng của nó. Điều này có tiền thưởng ngay lập tức chứng minh vượt ra ngoài mọi nghi ngờ rằng không có chia sẻ biến giữa các phần.

EDIT: Từ việc xem chỉnh sửa của bạn bằng mã ví dụ, tôi nghiêng về phía ký hiệu nhận xét là sạch nhất, với dấu ngoặc để thực thi các ranh giới mà các bình luận đề xuất. Tuy nhiên, nếu bất kỳ chức năng nào có thể tái sử dụng trong các chức năng khác, tôi khuyên bạn nên sử dụng các chức năng thay thế

void A::set_room_size(double value)
{
    {
        // Part 1: {description of part 1}
        ...
    }
    // ------------------------
    {
        // Part 2: {description of part 2}
        ...
    }
    // ------------------------
    {
        // Part 3: {description of part 3}
        ...
    }
}

Vì vậy, nó không phải là một con số khách quan, nhưng tôi sẽ chủ quan cho rằng sự hiện diện đơn thuần của các hàm lambda khiến tôi chú ý hơn gấp 10 lần cho mỗi dòng mã, bởi vì chúng có rất nhiều tiềm năng để trở nên phức tạp nguy hiểm, nhanh chóng và làm như vậy một dòng mã trông ngây thơ (như count++)
Cort Ammon - Tái lập Monica

Điểm tuyệt vời. Tôi thấy đó là một vài lợi thế của cách tiếp cận với lambdas - (1) mã liền kề cục bộ và với phạm vi cục bộ (điều này sẽ bị mất bởi các hàm cấp tệp) (2) đảm bảo rằng không có biến cục bộ nào được chia sẻ giữa các đoạn mã. Vì vậy, những lợi thế đó có thể được bảo tồn bằng cách tách calculate()thành {}các khối và khai báo dữ liệu được chia sẻ ở calculate()phạm vi. Tôi nghĩ rằng bằng cách thấy rằng lambdas không bắt được, một người đọc sẽ không bị vướng bận bởi sức mạnh của lambdas.
Vorac

"Tôi mặc dù bằng cách nhìn thấy lambdas không bắt được, một người đọc sẽ không bị vướng bận bởi sức mạnh của lambdas." Đó thực sự là một tuyên bố công bằng, nhưng gây tranh cãi, mà đánh vào trung tâm của ngôn ngữ học. Các từ thường có ý nghĩa vượt ra ngoài các ký hiệu của chúng. Cho dù ý nghĩa của tôi lambdalà không công bằng, hoặc nếu bạn thô lỗ buộc mọi người tuân theo các biểu thị nghiêm ngặt không phải là một câu hỏi dễ trả lời. Trên thực tế, bạn có thể hoàn toàn chấp nhận được việc sử dụng lambdatheo cách đó tại công ty của bạn và hoàn toàn không thể chấp nhận được ở công ty của tôi và không ai thực sự phải sai!
Cort Ammon - Phục hồi Monica

Ý kiến ​​của tôi về ý nghĩa của lambda bắt nguồn từ việc tôi lớn lên trên C ++ 03 chứ không phải C + 11. Tôi đã dành nhiều năm để phát triển sự đánh giá cao đối với những nơi cụ thể mà C ++ đã bị tổn thương do thiếu lambda, chẳng hạn như for_eachchức năng. Theo đó, khi tôi thấy một trường lambdahợp không phù hợp với một trong những trường hợp rắc rối dễ phát hiện đó, giả định đầu tiên tôi đặt ra là nó có thể sẽ được sử dụng cho lập trình chức năng, vì nó không cần thiết. Đối với nhiều nhà phát triển, lập trình chức năng là một tư duy hoàn toàn khác với lập trình OO hoặc thủ tục.
Cort Ammon - Phục hồi Monica

Cảm ơn vì đã giải thích. Tôi là một lập trình viên mới làm quen và bây giờ rất vui vì tôi đã hỏi - trước khi thói quen được hình thành.
Vorac

20

Tôi nghĩ rằng bạn đã đưa ra một giả định xấu:

Thứ nhất, A đại diện cho một đối tượng trong thế giới vật lý, đó là một lập luận mạnh mẽ cho việc không tách lớp lên.

Tôi không đồng ý với điều này. Ví dụ, nếu tôi có một lớp đại diện cho một chiếc xe hơi, tôi chắc chắn sẽ muốn tách nó ra, bởi vì tôi chắc chắn muốn một lớp nhỏ hơn để đại diện cho lốp xe.

Bạn nên chia chức năng này thành các chức năng riêng tư nhỏ hơn. Nếu nó thực sự có vẻ tách biệt với phần khác của lớp, thì đó có thể là một dấu hiệu cho thấy lớp nên được tách ra. Tất nhiên là rất khó để nói mà không có một ví dụ rõ ràng.

Tôi thực sự không thấy lợi thế của việc sử dụng các hàm lambda trong trường hợp này, vì nó không thực sự làm cho mã sạch hơn. Chúng được tạo ra để hỗ trợ lập trình kiểu chức năng, nhưng đây không phải là điều đó.

Những gì bạn đã viết giống một chút với các đối tượng hàm lồng nhau kiểu Javascript. Đó lại là một dấu hiệu cho thấy họ thuộc về nhau. Bạn có chắc chắn, rằng bạn không nên tạo một lớp riêng cho họ?

Tóm lại, tôi không nghĩ rằng đây là một mô hình tốt.

CẬP NHẬT

Nếu bạn không thấy bất kỳ cách nào để đóng gói chức năng này trong một lớp có ý nghĩa, thì bạn có thể tạo các hàm trợ giúp trong phạm vi tệp, không phải là thành viên của lớp. Đây là C ++ sau tất cả, thiết kế OO không phải là một điều bắt buộc.


Nghe có vẻ hợp lý, nhưng thật khó để tôi có thể tưởng tượng việc thực hiện. Lớp phạm vi tệp với các phương thức tĩnh? Lớp, được định nghĩa bên trong A(thậm chí có thể là functor với tất cả các phương thức khác)? Lớp được khai báo và định nghĩa bên trong calculate()(cái này trông rất giống ví dụ lambda của tôi). Như một sự giải thích rõ ràng, calculate()là một trong những họ phương thức ( calculate_1(), calculate_2()v.v.) trong đó tất cả đều đơn giản, chỉ có điều này là 2 màn hình công thức.
Vorac

@Vorac: Tại sao calculate()lâu hơn tất cả các phương pháp khác?
Kevin

@Vorac Thật sự rất khó để giúp đỡ mà không thấy mã của bạn. Bạn cũng có thể gửi nó?
Gábor Angyal

@Kevin, các công thức cho tất cả mọi thứ được đưa ra trong các yêu cầu. Mã được đăng.
Vorac

4
Tôi sẽ nâng cấp điều này 100 lần nếu tôi có thể. "Mô hình hóa các đối tượng trong thế giới thực" là khởi đầu của vòng xoáy tử thần Thiết kế đối tượng. Đó là một lá cờ đỏ khổng lồ trong bất kỳ mã nào.
Fred the Magic Wonder Dog
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.