Có phải luôn luôn là một cách tốt nhất để viết một hàm cho bất cứ điều gì cần lặp lại hai lần?


131

Chính tôi, tôi không thể chờ đợi để viết một chức năng khi tôi cần phải làm một cái gì đó nhiều hơn hai lần. Nhưng khi nói đến những thứ chỉ xuất hiện hai lần, thì khó khăn hơn một chút.

Đối với mã cần nhiều hơn hai dòng, tôi sẽ viết một hàm. Nhưng khi đối mặt với những thứ như:

print "Hi, Tom"
print "Hi, Mary"

Tôi ngần ngại viết:

def greeting(name):
    print "Hi, " + name

greeting('Tom')
greeting('Mary')

Cái thứ hai có vẻ quá nhiều phải không?


Nhưng nếu chúng ta có:

for name in vip_list:
    print name
for name in guest_list:
    print name

Và đây là cách thay thế:

def print_name(name_list):
    for name in name_list:
        print name

print_name(vip_list)
print_name(guest_list)

Mọi thứ trở nên khó khăn, không? Bây giờ thật khó để quyết định.

Ý kiến ​​của bạn về điều này là gì?


145
Tôi coi alwaysnevernhư những lá cờ đỏ. Có một thứ gọi là "bối cảnh", trong đó các quy tắc alwaysnever, ngay cả khi tốt nói chung, có thể không phù hợp. Coi chừng các nhà phát triển phần mềm giao dịch tuyệt đối. ;)
async

7
Trong ví dụ cuối cùng của bạn, bạn có thể làm : from itertools import chain; for name in chain(vip_list, guest_list): print(name).
Bakuriu

14
@ user16547 Chúng tôi gọi đó là những 'nhà phát triển sithware'!
Brian

6
Từ Zen of Python của Tim Peters:Special cases aren't special enough to break the rules. Although practicality beats purity.
Zenon

3
Ngoài ra, hãy xem xét nếu bạn muốn "mã ẩm" hoặc "mã khô" xem stackoverflow.com/questions/6453235/ mẹo
Ian

Câu trả lời:


201

Mặc dù đó là một yếu tố quyết định tách ra một chức năng, số lần lặp lại một cái gì đó không phải là yếu tố duy nhất. Nó thường có ý nghĩa để tạo ra một chức năng cho một cái gì đó chỉ được thực hiện một lần . Nói chung, bạn muốn tách một hàm khi:

  • Nó đơn giản hóa từng lớp trừu tượng riêng lẻ.
  • Bạn có tên hay, có ý nghĩa cho các chức năng tách, vì vậy bạn thường không cần phải nhảy qua lại giữa các lớp trừu tượng để hiểu điều gì đang diễn ra.

Ví dụ của bạn không đáp ứng tiêu chí đó. Bạn đang đi từ một lớp lót đến một lớp lót, và những cái tên không thực sự mua cho bạn bất cứ điều gì về sự rõ ràng. Điều đó đang được nói, các chức năng đơn giản là rất hiếm ngoài các bài hướng dẫn và bài tập ở trường. Hầu hết các lập trình viên có xu hướng sai lầm quá xa theo cách khác.


Đây là điểm tôi bỏ lỡ trong câu trả lời của RobertHarvey.
Doc Brown

1
Thật. Tôi thường chia các hàm phức tạp thành các chương trình con nội tuyến - không có hit hoàn hảo, nhưng sạch sẽ. Phạm vi ẩn danh là tốt quá.
imallett

3
Ngoài ra còn có một vấn đề về phạm vi. Có thể không có ý nghĩa khi viết hàm print_name (name_list) mà cả lớp hoặc toàn bộ mô-đun có thể nhìn thấy, nhưng nó có thể (vẫn là một sự kéo dài trong trường hợp này) có nghĩa là tạo một hàm cục bộ trong một hàm để dọn sạch một hàm vài dòng mã lặp đi lặp lại.
Joshua Taylor

6
Một điểm khác tôi có thể thêm là tiêu chuẩn hóa. Trong ví dụ đầu tiên, một lời chào đã được thực hiện và có thể có ý nghĩa khi gắn cái này vào một chức năng để bạn chắc chắn rằng bạn đang chào hỏi cả hai người như nhau. Đặc biệt là xem xét lời chào có thể thay đổi trong tương lai :)
Svish

1
+1 chỉ dành cho Nó thường có ý nghĩa để tạo một chức năng cho một cái gì đó chỉ được thực hiện một lần. Tôi đã từng được hỏi làm thế nào để thiết kế các bài kiểm tra cho một chức năng mô hình hóa hành vi của động cơ tên lửa. Độ phức tạp chu kỳ của chức năng đó là vào những năm 90 và đó không phải là một câu lệnh chuyển đổi với 90 trường hợp (xấu, nhưng có thể kiểm tra được). Nó thay vào đó là một mớ hỗn độn được viết bởi các kỹ sư và hoàn toàn không thể kiểm chứng được. Phản ứng của tôi chỉ là, rằng nó là không thể kiểm chứng và cần phải viết lại. Họ làm theo lời khuyên của tôi!
David Hammen

104

Chỉ khi sự trùng lặp là cố ý chứ không phải vô tình .

Hoặc, đặt một cách khác:

Chỉ khi bạn mong đợi chúng sẽ cùng phát triển trong tương lai.


Đây là lý do tại sao:

Đôi khi, hai đoạn mã chỉ xảy ra để trở thành giống nhau mặc dù họ không có gì để làm với nhau. Trong trường hợp đó, bạn phải chống lại sự thôi thúc kết hợp chúng, bởi vì lần sau khi ai đó thực hiện bảo trì cho một trong số họ, người đó sẽ không mong đợi các thay đổi sẽ lan truyền đến một người gọi không tồn tại trước đó và kết quả là chức năng đó có thể bị hỏng. Do đó, bạn phải chỉ tính ra mã khi nó có ý nghĩa, không phải bất cứ khi nào nó có vẻ làm giảm kích thước mã.

Quy tắc của ngón tay cái:

Nếu mã chỉ trả về dữ liệu mới và không sửa đổi dữ liệu hiện có hoặc có các tác dụng phụ khác, thì rất có thể an toàn để xác định là một chức năng riêng biệt. (Tôi không thể tưởng tượng bất kỳ kịch bản nào sẽ gây ra sự cố mà không thay đổi hoàn toàn ngữ nghĩa dự định của hàm, tại thời điểm đó, tên hàm hoặc chữ ký cũng sẽ thay đổi và dù sao bạn cũng cần cẩn thận. )


12
+1 Tôi nghĩ rằng đây là sự cân nhắc quan trọng nhất. Không một nhà tái cấu trúc nào mong đợi sự thay đổi của họ là lần cuối cùng, vì vậy hãy xem xét những thay đổi của bạn sẽ ảnh hưởng đến những thay đổi trong tương lai của mã.
yoniLavi

2
Đó là ruby, nhưng bản screencast này về Bản sao trùng hợp từ Avdi Grimm vẫn liên quan đến câu hỏi: youtube.com/watch?v=E4FluFOC1zM
DGM

60

Không, nó không phải luôn luôn là một thực hành tốt nhất.

Tất cả những thứ khác bằng nhau, mã tuyến tính, từng dòng dễ đọc hơn là nhảy xung quanh các lệnh gọi hàm. Một cuộc gọi chức năng không tầm thường luôn lấy các tham số, vì vậy bạn phải sắp xếp tất cả những điều đó và thực hiện các bước nhảy theo ngữ cảnh tinh thần từ cuộc gọi chức năng đến cuộc gọi chức năng. Luôn ưu tiên sự rõ ràng của mã tốt hơn, trừ khi bạn có lý do chính đáng cho việc tối nghĩa (chẳng hạn như có được các cải tiến hiệu suất cần thiết).

Vậy tại sao mã được tái cấu trúc thành các phương thức riêng biệt sau đó? Để cải thiện tính mô-đun. Để thu thập chức năng quan trọng đằng sau một phương thức riêng lẻ và đặt cho nó một tên có ý nghĩa. Nếu bạn không hoàn thành điều đó, thì bạn không cần những phương pháp riêng biệt đó.


58
Mã dễ đọc là quan trọng nhất.
Tom Robinson

27
Tôi đã nêu lên câu trả lời này nếu bạn không quên một trong những lý do hàng đầu để tạo chức năng: tạo ra một sự trừu tượng bằng cách đặt một phần chức năng là một cái tên hay. Nếu bạn đã thêm điểm đó, thì rõ ràng mã tuyến tính, từng dòng không phải lúc nào cũng dễ đọc hơn mã sử dụng các tóm tắt được định dạng tốt.
Doc Brown

17
Tôi đồng ý với Doc Brown, mã không nhất thiết phải dễ đọc hơn từng dòng so với khi được trừu tượng hóa đúng. Tên hàm tốt làm cho các hàm cấp cao RẤT dễ đọc (vì bạn đang đọc có ý định, không thực hiện) và các hàm cấp thấp dễ đọc hơn vì bạn đang thực hiện một nhiệm vụ, chính xác là một nhiệm vụ và không có gì ngoài nhiệm vụ đó. Điều này làm cho chức năng ngắn gọn và chính xác.
Câu chuyện Jon

19
Why take the performance hit of a function call when you don't get any significant benefit by doing so?Hiệu suất gì đạt? Trong trường hợp chung, các hàm nhỏ được nội tuyến và chi phí không đáng kể cho các hàm lớn. CPython có thể không nội tuyến, nhưng không ai sử dụng nó cho hiệu suất. Tôi cũng đặt câu hỏi một line by line code...chút. Sẽ dễ dàng hơn nếu bạn đang cố gắng mô phỏng sự thực thi trong não của bạn. Nhưng một trình sửa lỗi sẽ thực hiện công việc tốt hơn và cố gắng chứng minh điều gì đó về vòng lặp bằng cách thực hiện theo cách thực hiện của nó giống như cố gắng chứng minh một tuyên bố về số nguyên bằng cách lặp qua tất cả chúng.
Doval

6
@Doval tăng điểm hợp lệ RẤT - khi mã được biên dịch, hàm được đặt nội tuyến (và trùng lặp) và do đó KHÔNG có hiệu năng thời gian chạy, mặc dù có một biên dịch nhỏ. Điều đó không đúng với các ngôn ngữ được giải thích, nhưng ngay cả như vậy, chức năng gọi là một tỷ lệ nhỏ của thời gian thực hiện.
Câu chuyện Jon

25

Trong ví dụ cụ thể của bạn, việc tạo một hàm có vẻ quá mức cần thiết; thay vào đó tôi sẽ đặt câu hỏi: có thể là lời chào đặc biệt này sẽ thay đổi trong tương lai? Làm sao vậy

Các chức năng không chỉ đơn giản được sử dụng để bọc chức năng và dễ dàng sử dụng lại, mà còn để dễ dàng sửa đổi. Nếu các yêu cầu thay đổi, mã dán sao chép sẽ cần được tìm kiếm và thay đổi bằng tay, trong khi với một chức năng, mã chỉ cần được sửa đổi một lần.

Ví dụ của bạn sẽ được hưởng lợi từ điều này không thông qua một hàm - vì logic gần như không tồn tại - nhưng có thể là một GREET_OPENINGhằng số. Hằng số này sau đó có thể được tải từ tệp để chương trình của bạn có thể dễ dàng thích ứng với các ngôn ngữ khác nhau, ví dụ. Lưu ý rằng đây là một giải pháp thô sơ, một chương trình phức tạp hơn có thể sẽ cần một cách tinh tế hơn để theo dõi i18n, nhưng một lần nữa, nó phụ thuộc vào yêu cầu so với công việc phải làm.

Cuối cùng, đó là tất cả các yêu cầu có thể có, và lên kế hoạch trước để giảm bớt công việc cho bản thân trong tương lai.


Sử dụng hằng số GREET_OPENING giả định tất cả các lời chào có thể theo cùng một thứ tự. Kết hôn với một người có tiếng mẹ đẻ làm hầu hết mọi thứ trái ngược với tiếng Anh khiến tôi không bằng lòng với giả định này.
Loren Pechtel

22

Cá nhân tôi đã áp dụng quy tắc ba - điều mà tôi sẽ biện minh bằng cách gọi YAGNI . Nếu tôi cần làm gì đó một lần tôi sẽ viết mã đó, hai lần tôi có thể chỉ sao chép / dán (vâng, tôi chỉ thừa nhận sao chép / dán!) Vì tôi sẽ không cần nó nữa, nhưng nếu tôi cần làm như vậy điều đó một lần nữa sau đó tôi sẽ tái cấu trúc và trích xuất đoạn đó thành phương pháp riêng của nó, tôi đã chứng minh, với chính tôi, rằng tôi sẽ cần nó một lần nữa.

Tôi đồng ý với những gì Karl Bielefeldt và Robert Harvey đã nói và cách giải thích của tôi về những gì họ đang nói là quy tắc ghi đè là dễ đọc. Nếu nó làm cho mã của tôi dễ đọc hơn thì hãy xem xét việc tạo một hàm, hãy ghi nhớ những thứ như DRYSLAP . Nếu tôi có thể tránh việc thay đổi mức độ trừu tượng trong chức năng của mình thì tôi thấy việc quản lý trong đầu dễ dàng hơn, do đó, không nhảy giữa các chức năng (nếu tôi không thể hiểu chức năng này chỉ đơn giản bằng cách đọc tên của nó) có nghĩa là ít công tắc hơn trong tôi quá trình tinh thần.
Tương tự như vậy, không phải chuyển ngữ cảnh giữa các hàm và mã nội tuyến, chẳng hạn như print "Hi, Tom"hoạt động với tôi, trong trường hợp này tôi có thể trích xuất một hàm PrintNames() iff phần còn lại của chức năng tổng thể của tôi chủ yếu là các cuộc gọi chức năng.


3
Wiki c2 có một trang tốt về Quy tắc ba: c2.com/cgi/wiki?RuleOfThree
pimlottc

13

Nó hiếm khi rõ ràng, vì vậy bạn cần cân nhắc các lựa chọn:

  • Hạn chót (sửa chữa phòng máy chủ càng sớm càng tốt)
  • Khả năng đọc mã (có thể ảnh hưởng đến sự lựa chọn một trong hai cách)
  • Mức độ trừu tượng của logic được chia sẻ (liên quan đến ở trên)
  • Yêu cầu sử dụng lại (nghĩa là có cùng một logic quan trọng, hoặc chỉ thuận tiện ngay bây giờ)
  • Khó chia sẻ (đây là nơi trăn tỏa sáng, xem bên dưới)

Trong C ++, tôi thường tuân theo quy tắc ba (tức là lần thứ ba tôi cần điều tương tự, tôi tái cấu trúc nó thành một phần có thể chia sẻ chính xác), nhưng với kinh nghiệm ban đầu, việc lựa chọn đó sẽ dễ dàng hơn khi bạn biết nhiều hơn về phạm vi, phần mềm & tên miền bạn đang làm việc.

Tuy nhiên, trong Python, nó khá nhẹ để sử dụng lại, thay vì lặp lại, logic. Nhiều hơn so với nhiều ngôn ngữ khác (ít nhất là trong lịch sử), IMO.

Vì vậy, hãy xem xét việc chỉ sử dụng lại logic cục bộ, ví dụ bằng cách tạo danh sách từ các đối số cục bộ:

def foo():
    for name_list in (vip_list, guest_list): # can be list of tuples, for many args
        for name in name_list:
            print name

Bạn có thể sử dụng một bộ dữ liệu và chia trực tiếp vào vòng lặp for, nếu bạn cần một vài đối số:

def foo2():
    for header, name_list in (('vips': vip_list), ('people': guest_list)): 
        print header + ": "
        for name in name_list:
            print name

hoặc tạo một hàm cục bộ (có thể là ví dụ thứ hai của bạn là vậy), điều này làm cho logic sử dụng lại rõ ràng nhưng cũng cho thấy rõ rằng print_name () không được sử dụng bên ngoài hàm:

def foo():
    def print_name(name_list):
        for name in name_list:
            print name

    print_name(vip_list)
    print_name(guest_list)

Các chức năng được ưu tiên đặc biệt là khi bạn cần hủy bỏ logic ở giữa (nghĩa là sử dụng trả về), vì phá vỡ hoặc ngoại lệ có thể làm lộn xộn mọi thứ một cách không cần thiết.

Hoặc là tốt hơn so với việc lặp lại cùng một logic, IMO và cũng ít lộn xộn hơn so với việc khai báo một hàm toàn cầu / lớp chỉ được sử dụng bởi một người gọi (mặc dù hai lần).


Tôi đã đăng một bình luận tương tự về phạm vi của chức năng mới trước khi tôi thấy câu trả lời này. Tôi rất vui vì có một câu trả lời giải quyết khía cạnh đó của câu hỏi.
Joshua Taylor

1
+1 - đây có thể không phải là loại câu trả lời mà OP đang tìm kiếm, nhưng, ít nhất là về loại câu hỏi mà anh ấy hỏi, tôi nghĩ đây là câu trả lời hợp lý nhất. Đây là loại điều có thể sẽ được đưa vào ngôn ngữ cho bạn, bằng cách nào đó.
Patrick Collins

@PatrickCollins: Cảm ơn bạn đã bình chọn! :) Tôi đã thêm một số cân nhắc để làm cho câu trả lời đầy đủ hơn.
Macke

9

Tất cả các thực tiễn tốt nhất có một lý do cụ thể, mà bạn có thể tham khảo để trả lời một câu hỏi như vậy. Bạn nên luôn luôn tránh các gợn sóng, khi một thay đổi tiềm năng trong tương lai cũng có nghĩa là thay đổi các thành phần khác.

Vì vậy, trong ví dụ của bạn: Nếu bạn cho rằng bạn có một lời chào tiêu chuẩn và bạn muốn đảm bảo nó giống nhau cho tất cả mọi người, thì hãy viết

def std_greeting(name):
    print "Hi, " + name

for name in ["Tom", "Mary"]:
    std_greeting(name)   # even the function call should be written only once

Nếu không, bạn phải chú ý và thay đổi lời chào tiêu chuẩn ở hai nơi, nếu điều đó xảy ra để thay đổi.

Tuy nhiên, nếu "Hi" giống nhau chỉ là tình cờ và việc thay đổi một trong những lời chào không nhất thiết dẫn đến thay đổi cho người kia, thì hãy tách chúng ra. Do đó, hãy tách chúng ra nếu có lý do hợp lý để tin rằng thay đổi sau có nhiều khả năng:

print "Hi, Tom"
print "Hello, Mary"

Đối với phần thứ hai của bạn, để quyết định mức độ "gói" vào các chức năng có thể phụ thuộc vào hai yếu tố:

  • giữ các khối nhỏ để dễ đọc
  • giữ các khối đủ lớn để những thay đổi xảy ra chỉ trong một vài khối. Không cần phải phân cụm quá nhiều vì khó theo dõi kiểu gọi.

Lý tưởng nhất là khi thay đổi xảy ra, bạn sẽ nghĩ "Tôi phải thay đổi hầu hết các khối sau" chứ không phải "Tôi phải thay đổi mã ở đâu đó trong một khối lớn".


Chỉ là một trò lừa bịp ngu ngốc khi xem xét vòng lặp của bạn - nếu thực sự chỉ cần một số ít tên được mã hóa cứng để lặp lại, và chúng tôi không hy vọng chúng thay đổi, tôi cho rằng sẽ không dễ sử dụng hơn một chút danh sách. for name in "Tom", "Mary": std_greeting(name)
yoniLavi

Chà, để tiếp tục người ta có thể nói rằng một danh sách mã hóa cứng sẽ không xuất hiện ở giữa mã và dù sao cũng sẽ là một biến :) Nhưng đúng vậy, tôi thực sự đã quên rằng bạn có thể bỏ qua các dấu ngoặc ở đó.
Gerenuk

5

Khía cạnh quan trọng nhất của các hằng số và hàm được đặt tên không nhiều đến mức chúng làm giảm số lượng gõ, mà là chúng "đính kèm" các vị trí khác nhau nơi chúng được sử dụng. Đối với ví dụ của bạn, hãy xem xét bốn kịch bản:

  1. Cần phải thay đổi cả hai lời chào theo cùng một cách, [ví dụ như Bonjour, TomBonjour, Mary].

  2. Cần phải thay đổi một lời chào nhưng để lại lời chào khác vì đó là [ví dụ Hi, TomGuten Tag, Mary].

  3. Cần phải thay đổi cả hai lời chào khác nhau [ví dụ Hello, TomHowdy, Mary].

  4. Không phải lời chào bao giờ cần phải được thay đổi.

Nếu không bao giờ cần phải thay đổi lời chào, thì thực sự không có vấn đề gì. Sử dụng một chức năng được chia sẻ sẽ có tác dụng tự nhiên rằng việc thay đổi bất kỳ lời chào nào cũng sẽ thay đổi chúng theo cùng một cách. Nếu tất cả các lời chào nên thay đổi theo cùng một cách, đó sẽ là một điều tốt. Tuy nhiên, nếu họ không nên, thì lời chào của mỗi người sẽ phải được mã hóa hoặc chỉ định riêng; bất kỳ công việc nào được thực hiện khiến chúng sử dụng một chức năng hoặc thông số kỹ thuật chung sẽ phải được hoàn tác (và tốt nhất là không nên thực hiện ngay từ đầu).

Để chắc chắn, không phải lúc nào cũng có thể dự đoán được tương lai, nhưng nếu người ta có lý do để tin rằng nhiều khả năng cả hai lời chào sẽ cần phải thay đổi cùng nhau, đó sẽ là một yếu tố có lợi cho việc sử dụng mã chung (hoặc hằng số có tên) ; nếu một người có lý do để tin rằng nhiều khả năng một hoặc cả hai có thể cần phải thay đổi để chúng khác nhau, thì đó sẽ là một yếu tố chống lại việc cố gắng sử dụng mã chung.


Tôi thích câu trả lời này bởi vì (đối với ví dụ đơn giản này ít nhất), bạn gần như có thể tính toán sự lựa chọn thông qua một số lý thuyết trò chơi.
RubberDuck

@RubberDuck: Tôi nghĩ rằng quá ít sự chú ý thường được đặt ra cho câu hỏi về những điều nên hay không nên "bí danh" - trong nhiều khía cạnh, và sẽ hy vọng rằng hai nguyên nhân phổ biến nhất của lỗi là (1) tin rằng bí danh / đính kèm khi họ không, hoặc (2) thay đổi một cái gì đó mà không nhận ra rằng những thứ khác được gắn vào nó. Những vấn đề như vậy phát sinh trong việc thiết kế cả cấu trúc mã và dữ liệu, nhưng tôi không nhớ là đã từng thấy nhiều sự chú ý dành cho khái niệm này. Bí danh nói chung không quan trọng khi chỉ có một thứ gì đó, hoặc nếu nó sẽ không bao giờ thay đổi, nhưng ...
supercat

@RubberDuck: ... nếu có hai thứ gì đó và chúng khớp nhau, điều quan trọng là phải biết liệu thay đổi cái này có nên giữ giá trị của cái kia không đổi, hay nên giữ mối quan hệ không đổi (nghĩa là giá trị của cái kia sẽ thay đổi). Tôi thường thấy trọng lượng lớn được trao cho những lợi thế của việc giữ các giá trị không đổi, nhưng trọng lượng ít hơn nhiều so với tầm quan trọng của các mối quan hệ .
supercat

Tôi hoàn toàn đồng ý. Tôi chỉ tập trung vào cách mà trí tuệ thông thường nói với DRY nó, nhưng về mặt thống kê , nhiều khả năng mã lặp lại sau đó sẽ không được lặp lại mã. Về cơ bản, bạn có tỷ lệ cược 2-1.
RubberDuck

@RubberDuck: Tôi đưa ra hai kịch bản trong đó việc tách rời là quan trọng và một tình huống tốt hơn là không đính kèm điều gì về khả năng tương đối của các tình huống khác nhau xảy ra. Điều quan trọng là nhận ra khi hai đoạn mã khớp với nhau "bởi sự trùng hợp" hoặc bởi vì chúng có cùng ý nghĩa cơ bản. Hơn nữa, khi có nhiều hơn hai mục, các kịch bản bổ sung phát sinh trong đó một người cuối cùng muốn làm cho một hoặc nhiều khác biệt với các mục khác, nhưng vẫn thay đổi hai hoặc nhiều hơn theo cách giống hệt nhau; đồng thời, ngay cả khi một hành động chỉ được sử dụng ở một nơi ...
supercat

5

Tôi nghĩ bạn nên có một lý do để tạo ra một thủ tục. Có một số lý do để tạo ra một thủ tục. Những cái mà tôi nghĩ là quan trọng nhất là:

  1. trừu tượng
  2. mô-đun
  3. điều hướng mã
  4. cập nhật tính nhất quán
  5. đệ quy
  6. thử nghiệm

Trừu tượng

Một thủ tục có thể được sử dụng để trừu tượng một thuật toán chung từ các chi tiết của các bước cụ thể trong thuật toán. Nếu tôi có thể tìm thấy một sự trừu tượng mạch lạc phù hợp, điều này giúp tôi cấu trúc cách tôi nghĩ về những gì thuật toán làm. Ví dụ, tôi có thể thiết kế một thuật toán hoạt động trên các danh sách mà không nhất thiết phải xem xét biểu diễn danh sách.

Tính mô đun

Các thủ tục có thể được sử dụng để tổ chức mã thành các mô-đun. Các mô-đun thường có thể được điều trị riêng. Ví dụ, được xây dựng và thử nghiệm riêng biệt.

Mô-đun được thiết kế tốt thường nắm bắt một số đơn vị chức năng có ý nghĩa nhất quán. Do đó, chúng thường có thể được sở hữu bởi các đội khác nhau, được thay thế hoàn toàn bằng một giải pháp thay thế hoặc tái sử dụng trong một bối cảnh khác.

Điều hướng mã

Trong các hệ thống lớn, việc tìm mã liên quan đến một hành vi hệ thống cụ thể có thể khó khăn. Tổ chức phân cấp của mã trong các thư viện, mô-đun và thủ tục có thể giúp với thách thức này. Đặc biệt nếu bạn cố gắng a) tổ chức chức năng dưới các tên có ý nghĩa và có thể dự đoán được b) đặt các thủ tục liên quan đến chức năng tương tự gần nhau.

Cập nhật tính nhất quán

Trong các hệ thống lớn, có thể khó tìm thấy tất cả các mã cần được thay đổi để đạt được một thay đổi cụ thể trong hành vi. Sử dụng các thủ tục để tổ chức các chức năng của chương trình có thể làm cho điều này dễ dàng hơn. Đặc biệt, nếu mỗi bit chức năng của chương trình của bạn chỉ xuất hiện một lần và trong một nhóm các thủ tục gắn kết, thì ít có khả năng (theo kinh nghiệm của tôi) rằng bạn sẽ bỏ lỡ một nơi nào đó mà bạn nên cập nhật hoặc cập nhật không nhất quán.

Lưu ý rằng bạn nên tổ chức các thủ tục dựa trên chức năng và sự trừu tượng trong chương trình. Không dựa trên việc hai bit mã có xảy ra giống nhau tại thời điểm này hay không.

Đệ quy

Sử dụng đệ quy yêu cầu bạn tạo các thủ tục

Kiểm tra

Bạn thường có thể kiểm tra các thủ tục độc lập với nhau. Việc kiểm tra các phần khác nhau của cơ thể của một thủ tục một cách độc lập sẽ khó khăn hơn, bởi vì bạn thường phải thực hiện phần đầu tiên của quy trình trước phần thứ hai. Quan sát này cũng thường áp dụng cho các phương pháp khác để chỉ định / xác minh hành vi chương trình.

Phần kết luận

Nhiều điểm trong số này liên quan đến tính dễ hiểu của chương trình. Bạn có thể nói rằng các quy trình là một cách để tạo một ngôn ngữ mới, cụ thể cho miền của bạn, để tổ chức, viết và đọc, về các vấn đề và quy trình trong miền của bạn.


4

Không phải trường hợp nào bạn đưa ra có vẻ đáng để tái cấu trúc.

Trong cả hai trường hợp, bạn đang thực hiện một thao tác được thể hiện rõ ràng và cô đọng, và không có nguy cơ thay đổi không nhất quán được thực hiện.

Tôi khuyên bạn luôn tìm cách cô lập mã trong đó:

  1. Có một nhiệm vụ có thể xác định, có ý nghĩa mà bạn có thể tách ra

  2. Hoặc

    a. nhiệm vụ rất phức tạp để thể hiện (có tính đến các chức năng có sẵn cho bạn) hoặc

    b. nhiệm vụ được thực hiện nhiều lần và bạn cần một cách để đảm bảo rằng nó được thay đổi theo cùng một cách ở cả hai nơi,

Nếu điều kiện 1 không được thỏa mãn thì bạn sẽ không tìm thấy giao diện phù hợp cho mã: nếu bạn cố gắng ép buộc, bạn sẽ kết thúc với vô số tham số, nhiều thứ bạn muốn trả về, rất nhiều logic theo ngữ cảnh và hoàn toàn có thể là không thể tìm thấy một cái tên hay Hãy thử ghi lại giao diện mà bạn nghĩ đến đầu tiên, đó là một cách tốt để kiểm tra giả thuyết rằng đó là gói chức năng phù hợp và đi kèm với phần thưởng bổ sung mà khi bạn viết mã, nó đã được suy đoán và ghi lại!

2a là có thể mà không cần 2b: đôi khi tôi đã lấy mã ra khỏi luồng ngay cả khi tôi biết nó chỉ được sử dụng một lần, đơn giản vì di chuyển nó đi nơi khác và thay thế nó bằng một dòng duy nhất có nghĩa là bối cảnh mà nó được gọi đột nhiên dễ đọc hơn nhiều (đặc biệt nếu đó là một khái niệm dễ dàng mà ngôn ngữ xảy ra gây khó khăn cho việc thực hiện). Nó cũng rõ ràng khi đọc hàm trích xuất trong đó logic bắt đầu và kết thúc và nó làm gì.

2b là có thể mà không cần 2a: Bí quyết là có cảm giác về loại thay đổi nào có nhiều khả năng. Có nhiều khả năng chính sách công ty của bạn sẽ thay đổi từ nói "Xin chào" ở mọi nơi thành "Xin chào" mọi lúc hay lời chào của bạn thay đổi thành giọng điệu trang trọng hơn nếu bạn gửi yêu cầu thanh toán hoặc xin lỗi vì ngừng dịch vụ? Đôi khi có thể là do bạn đang sử dụng một thư viện bên ngoài mà bạn không chắc chắn và muốn có thể trao đổi nhanh chóng cho một thư viện khác: việc triển khai có thể thay đổi ngay cả khi chức năng không hoạt động.

Tuy nhiên, thông thường, bạn sẽ có một số hỗn hợp 2a và 2b. Và bạn sẽ phải sử dụng phán đoán của mình, có tính đến giá trị kinh doanh của mã, tần suất được sử dụng, cho dù đó là tài liệu tốt và được hiểu, trạng thái của bộ thử nghiệm của bạn, v.v.

Nếu bạn nhận thấy cùng một logic logic được sử dụng nhiều lần, bằng mọi cách, bạn nên tận dụng cơ hội để xem xét tái cấu trúc. Nếu bạn bắt đầu các câu lệnh mới nhiều hơn nmức độ thụt lề trong hầu hết các ngôn ngữ, thì đó là một trình kích hoạt khác, ít quan trọng hơn một chút (chọn một giá trị làn mà bạn thấy thoải mái với ngôn ngữ đã cho của mình: ví dụ: Python có thể là 6).

Chừng nào bạn còn suy nghĩ về những điều này và hạ gục những con quái vật mã spaghetti lớn, bạn sẽ ổn thôi - đừng mất quá nhiều thời gian để lo lắng về việc tái cấu trúc của bạn cần phải tinh tế đến mức nào mà bạn không có thời gian cho những thứ như bài kiểm tra hoặc tài liệu làm việc về. Nếu nó cần làm, thời gian sẽ trả lời.


3

Nói chung, tôi đồng ý với Robert Harvey, nhưng muốn thêm một trường hợp để phân chia chức năng thành các chức năng. Để cải thiện khả năng đọc. Hãy xem xét một trường hợp:

def doIt(smth,smthElse)
    for x in getDataFromSomething(smth,smthElse):
        if not check(x,smth):
            continue
        process(x,smthElse)
        store(x) 

Mặc dù các lệnh gọi hàm đó không được sử dụng ở bất kỳ nơi nào khác, nhưng có thể tăng đáng kể khả năng đọc nếu cả 3 hàm này dài đáng kể và có các vòng lặp lồng nhau, v.v.


2

Không có quy tắc cứng và nhanh nào phải được áp dụng, nó sẽ phụ thuộc vào chức năng thực tế sẽ được sử dụng. Đối với in tên đơn giản, tôi sẽ không sử dụng một hàm nhưng nếu đó là một phép toán tổng hợp thì đó sẽ là một câu chuyện khác. Sau đó, bạn sẽ tạo một hàm ngay cả khi nó chỉ được gọi hai lần, điều này là để đảm bảo rằng tổng toán luôn luôn giống nhau nếu được thay đổi. Trong một ví dụ khác, nếu bạn đang thực hiện bất kỳ hình thức xác nhận nào, bạn sẽ sử dụng một hàm, vì vậy trong ví dụ của bạn nếu bạn cần kiểm tra tên dài hơn 5 ký tự, bạn sẽ sử dụng một hàm để đảm bảo các xác nhận tương tự luôn được thực hiện.

Vì vậy, tôi nghĩ rằng bạn đã trả lời câu hỏi của riêng bạn khi bạn nói "Đối với các mã cần nhiều hơn hai dòng, tôi sẽ phải viết một func". Nói chung, bạn sẽ sử dụng một hàm nhưng bạn cũng phải sử dụng logic của riêng mình để xác định xem có bất kỳ loại giá trị gia tăng nào khi sử dụng hàm không.


2

Tôi đã tin vào điều gì đó về tái cấu trúc mà tôi chưa thấy đề cập ở đây, tôi biết đã có rất nhiều câu trả lời ở đây, nhưng tôi nghĩ rằng điều này là mới.

Tôi đã là một nhà tái cấu trúc tàn nhẫn và là một người tin tưởng mạnh mẽ vào DRY kể từ trước khi các điều khoản phát sinh. Chủ yếu là vì tôi gặp khó khăn trong việc giữ một codebase lớn trong đầu và một phần vì tôi thích mã hóa DRY và tôi không thích bất cứ điều gì về mã hóa C & P, thực tế nó rất đau và chậm kinh khủng đối với tôi.

Vấn đề là, nhấn mạnh vào DRY đã cho tôi rất nhiều thực hành trong một số kỹ thuật mà tôi hiếm khi thấy người khác sử dụng. Rất nhiều người khăng khăng rằng Java rất khó hoặc không thể tạo ra DRY, nhưng thực sự họ không thử.

Một ví dụ từ rất lâu trước đây có phần giống với ví dụ của bạn. Mọi người có xu hướng nghĩ rằng tạo java GUI là khó. Tất nhiên đó là nếu bạn mã như thế này:

Menu m=new Menu("File");
MenuItem save=new MenuItem("Save")
save.addAction(saveAction); // I forget the syntax, but you get the idea
m.add(save);
MenuItem load=new MenuItem("Load")
load.addAction(loadAction)

Bất cứ ai nghĩ rằng điều này thật điên rồ là hoàn toàn đúng, nhưng đó không phải là lỗi của Java - không bao giờ nên viết mã theo cách này. Các cuộc gọi phương thức này là các hàm dự định được bọc trong các hệ thống khác. Nếu bạn không thể tìm thấy một hệ thống như vậy, hãy xây dựng nó!

Rõ ràng là bạn không thể viết mã như vậy nên bạn cần lùi lại và xem xét vấn đề, mã lặp lại đó thực sự đang làm gì? Đó là chỉ định một số chuỗi và mối quan hệ của chúng (một cây) và nối các lá của cây đó với các hành động. Vì vậy, những gì bạn thực sự muốn là nói:

class Menu {
    @MenuItem("File|Load")
    public void fileLoad(){...}
    @MenuItem("File|Save")
    public void fileSave(){...}
    @MenuItem("Edit|Copy")
    public void editCopy(){...}...

Khi bạn đã xác định mối quan hệ của mình theo một cách ngắn gọn và mô tả thì bạn viết một phương thức để giải quyết nó - trong trường hợp này bạn lặp lại các phương thức của lớp được truyền vào và xây dựng một cây sau đó sử dụng phương thức đó để xây dựng Menu của bạn và Hành động cũng như (rõ ràng) hiển thị một menu. Bạn sẽ không bị trùng lặp và phương pháp của bạn có thể tái sử dụng ... và có thể dễ viết hơn so với số lượng lớn các menu đã có, và nếu bạn thực sự thích lập trình, bạn sẽ có nhiều niềm vui hơn. Điều này không khó - phương pháp bạn cần viết có lẽ ít dòng hơn so với việc tạo menu bằng tay!

Điều đó là, để làm tốt điều này, bạn cần phải thực hành, rất nhiều. Bạn cần giỏi phân tích chính xác thông tin duy nhất trong các phần lặp đi lặp lại, trích xuất thông tin đó và tìm ra cách thể hiện nó tốt. Học cách sử dụng các công cụ như phân tích chuỗi và chú thích giúp ích rất nhiều. Học để thực sự rõ ràng về báo cáo lỗi và tài liệu là rất quan trọng là tốt.

Bạn có được thực hành "Miễn phí" chỉ bằng cách mã hóa tốt - rất có thể là một khi bạn đã thành thạo, bạn sẽ thấy rằng mã hóa một cái gì đó DRY (bao gồm viết một công cụ có thể sử dụng lại) nhanh hơn so với sao chép và dán và tất cả các lỗi, lỗi trùng lặp và những thay đổi khó khăn mà loại mã hóa gây ra.

Tôi không nghĩ rằng tôi có thể tận hưởng công việc của mình nếu tôi không thực hành các kỹ thuật DRY và các công cụ xây dựng nhiều nhất có thể. Nếu tôi phải giảm chi phí để không phải sao chép và dán chương trình, tôi sẽ lấy nó.

Vì vậy, quan điểm của tôi là:

  • Sao chép & Dán tốn nhiều thời gian hơn trừ khi bạn không biết cách tái cấu trúc tốt.
  • Bạn học cách tái cấu trúc tốt bằng cách thực hiện nó - bằng cách nhấn mạnh vào DRY ngay cả trong những trường hợp khó khăn và tầm thường nhất.
  • Bạn là một lập trình viên, nếu bạn cần một công cụ nhỏ để tạo mã DRY, hãy xây dựng nó.

1

Nói chung, nên chia một cái gì đó thành một chức năng của nó để cải thiện khả năng đọc mã của bạn, hoặc, nếu phần được lặp đi lặp lại là một phần cốt lõi của hệ thống. Trong ví dụ trên, nếu bạn cần chào người dùng tùy thuộc vào miền địa phương, thì sẽ có ý nghĩa khi có chức năng chào hỏi riêng biệt.


1

Như một hệ quả của nhận xét của @ DocBrown đối với @RobertHarvey về việc sử dụng các hàm để tạo ra sự trừu tượng: nếu bạn không thể đưa ra một tên hàm thông tin đầy đủ mà không ngắn gọn hoặc rõ ràng hơn mã đằng sau nó, bạn sẽ không trừu tượng hóa nhiều . Không có bất kỳ lý do tốt khác để làm cho nó một chức năng, không cần phải làm như vậy.

Ngoài ra, một tên hàm hiếm khi nắm bắt được ngữ nghĩa đầy đủ của nó, vì vậy bạn có thể phải kiểm tra định nghĩa của nó, nếu nó không đủ phổ biến để làm quen. Đặc biệt là ngoài ngôn ngữ chức năng, bạn có thể cần biết liệu nó có tác dụng phụ hay không, lỗi nào có thể xảy ra và cách chúng được phản hồi, độ phức tạp thời gian của nó, liệu nó có phân bổ và / hoặc giải phóng tài nguyên hay không và liệu đó có phải là luồng không an toàn

Tất nhiên, theo định nghĩa, chúng tôi chỉ xem xét các hàm đơn giản ở đây, nhưng điều đó đi cả hai chiều - trong những trường hợp như vậy, nội tuyến không có khả năng thêm độ phức tạp. Hơn nữa, người đọc có thể sẽ không nhận ra rằng đó là một chức năng đơn giản cho đến khi anh ta nhìn. Ngay cả với một IDE liên kết bạn với định nghĩa, nhảy trực quan xung quanh là một trở ngại cho sự hiểu biết.

Nói chung, mã bao thanh toán đệ quy thành nhiều hơn, các hàm nhỏ hơn cuối cùng sẽ đưa bạn đến điểm mà các hàm không còn có ý nghĩa độc lập, bởi vì các đoạn mã chúng được tạo ra từ nhỏ hơn, các đoạn đó có ý nghĩa nhiều hơn từ ngữ cảnh được tạo ra bởi các mã xung quanh. Tương tự như vậy, hãy nghĩ về nó giống như một bức tranh: nếu bạn phóng to quá gần, bạn không thể nhận ra những gì bạn đang nhìn.


1
Ngay cả khi các câu lệnh đơn giản nhất gói nó trong một hàm sẽ làm cho nó dễ đọc hơn. Đôi khi việc bọc nó trong một hàm là quá mức nhưng nếu bạn không thể đưa ra một tên hàm ngắn gọn và rõ ràng hơn những gì các câu lệnh đang làm thì đó có thể là một dấu hiệu xấu. Tiêu cực của bạn cho việc sử dụng các chức năng không thực sự là một vấn đề lớn. Các tiêu cực cho nội tuyến là một thỏa thuận lớn hơn nhiều. Nội tuyến làm cho nó khó đọc hơn, khó bảo trì hơn và khó sử dụng hơn.
vui mừng

@pllee: Tôi đã trình bày lý do tại sao bao thanh toán vào các chức năng trở nên phản tác dụng khi bị cực đoan (lưu ý rằng câu hỏi cụ thể là đưa nó đến cực đoan, không phải là trường hợp chung.) Bạn đã không đưa ra lập luận chống lại những điểm này, nhưng chỉ đơn thuần tuyên bố nó 'nên' khác biệt. Viết rằng vấn đề đặt tên là 'lẽ' một dấu hiệu xấu không phải là một lập luận cho rằng nó có thể tránh được trong các trường hợp xem xét ở đây (lập luận của tôi, tất nhiên, là nó ít nhất một cảnh báo - mà bạn có thể đã đạt đến điểm chỉ trừu tượng hóa vì lợi ích riêng của nó.)
sdenham 15/1/2015

Tôi đã đề cập đến 3 tiêu cực của nội tuyến không chắc chắn nơi tôi chỉ tuyên bố nó "nên khác biệt". Tất cả những gì tôi đang nói là nếu bạn không thể đưa ra một cái tên thì nội tuyến của bạn có thể đang làm quá nhiều. Ngoài ra tôi nghĩ rằng bạn đang thiếu một điểm lớn ở đây về các phương pháp thậm chí nhỏ bé từng được sử dụng. Các phương thức được đặt tên rất nhỏ tồn tại để bạn không cần phải biết mã đang cố gắng làm gì (không cần phải nhảy IDE). Ví dụ, ngay cả câu lệnh đơn giản của `if (day == 0 || day == 6)` vs `if (isWeekend (day))` trở nên dễ đọc hơn và ánh xạ về mặt tinh thần. Bây giờ nếu bạn cần lặp lại tuyên bố đó isWeekendtrở thành không có trí tuệ.
vui mừng

@pllee Bạn cho rằng đó là "có thể là một dấu hiệu xấu" nếu bạn không thể tìm thấy một tên ngắn hơn, rõ ràng hơn cho hầu hết các đoạn mã lặp đi lặp lại, nhưng đó dường như là một vấn đề của đức tin - bạn không đưa ra lập luận hỗ trợ (để nói chung trường hợp, bạn cần nhiều hơn các ví dụ.) isWeekend có một tên có ý nghĩa, vì vậy cách duy nhất nó có thể thất bại trong tiêu chí của tôi là nếu nó không ngắn hơn đáng kể cũng không rõ ràng hơn so với việc thực hiện. Bằng cách lập luận rằng bạn nghĩ rằng đó là cái sau, bạn đang khẳng định rằng nó không phải là một ví dụ đối lập với vị trí của tôi (FWIW, tôi đã sử dụng isWeekend mình.)
sdenham

Tôi đã đưa ra một ví dụ trong đó ngay cả một câu lệnh đơn có thể dễ đọc hơn và tôi đã đưa ra các phủ định của mã nội tuyến. Đó là ý kiến ​​khiêm tốn của tôi và tất cả những gì tôi đang nói và không, tôi không tranh cãi hay nói về "phương pháp không thể đặt tên". Tôi không thực sự chắc chắn những gì khó hiểu về điều đó hoặc tại sao bạn nghĩ rằng tôi cần một "bằng chứng lý thuyết" cho ý kiến ​​của tôi. Tôi đã kiểm tra khả năng đọc được cải thiện. Bảo trì khó hơn vì nếu hunk mã thay đổi, nó cần đến N số điểm (đọc về DRY). Rõ ràng là khó sử dụng hơn trừ khi bạn nghĩ sao chép dán nếu cách sử dụng lại mã khả thi.
vui mừ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.