Tách biệt các mối quan tâm: Khi nào thì quá nhiều cách biệt với nhau?


9

Tôi thực sự yêu thích mã sạch và tôi luôn muốn mã hóa mã của mình theo cách tốt nhất có thể. Nhưng luôn có một điều, tôi không thực sự hiểu:

Khi nào có quá nhiều "sự phân tách mối quan tâm" liên quan đến các phương pháp?

Giả sử chúng ta có phương pháp sau:

def get_last_appearance_of_keyword(file, keyword):
    with open(file, 'r') as file:
        line_number = 0
        for line in file:
            if keyword in line:
                line_number = line
        return line_number

Tôi nghĩ rằng phương pháp này là tốt như nó là. Nó đơn giản, dễ đọc và rõ ràng, tên của nó nói gì. Nhưng: Nó không thực sự làm "chỉ một điều". Nó thực sự mở tập tin, và sau đó tìm thấy nó. Điều đó có nghĩa là tôi có thể chia nó hơn nữa (Cũng xem xét "Nguyên tắc trách nhiệm duy nhất"):

Biến thể B (Chà, điều này có ý nghĩa nào đó. Bằng cách này, chúng ta có thể dễ dàng sử dụng lại thuật toán tìm sự xuất hiện cuối cùng của từ khóa trong một văn bản, nhưng nó có vẻ như "quá nhiều". Tôi không thể giải thích tại sao, nhưng tôi chỉ "cảm thấy "theo cách đó):

def get_last_appearance_of_keyword(file, keyword):
    with open(file, 'r') as text_from_file:
        line_number = find_last_appearance_of_keyword(text_from_file, keyword) 
    return line_number

def find_last_appearance_of_keyword(text, keyword):
    line_number = 0
    for line in text:
        if keyword in line:
            line_number = line
    return line_number

Biến thể C (Điều này theo tôi là vô lý. Về cơ bản, chúng tôi đang gói gọn một lớp lót vào một phương thức khác chỉ với một dòng hai lần. Nhưng người ta có thể lập luận rằng cách mở một cái gì đó có thể thay đổi trong tương lai, vì một số yêu cầu tính năng và vì chúng tôi không muốn thay đổi nó nhiều lần, nhưng chỉ một lần, chúng tôi chỉ gói gọn nó và tách chức năng chính của chúng tôi hơn nữa):

def get_last_appearance_of_keyword(file, keyword):
    text_from_file = get_text_from_file(file)
    line_number = find_keyword_in_text(text_from_file, keyword)
    return line_number 

def get_text_from_file(file):
    with open(file, 'r') as text:
        return text

def find_last_appearance_of_keyword(text, keyword):
    line_number = 0
    for line in text:
        if check_if_keyword_in_string(line, keyword):
            line_number = line         
    return line_number

def check_if_keyword_in_string(text, keyword):
    if keyword in string:
        return true
    return false

Vì vậy, câu hỏi của tôi bây giờ: cách viết mã này là đúng và tại sao các cách tiếp cận khác đúng hay sai? Tôi luôn học được: Ly thân, nhưng không bao giờ khi nó đơn giản là quá nhiều. Và làm thế nào tôi có thể chắc chắn trong tương lai, rằng nó "vừa phải" và nó không cần phải phân tách nhiều hơn khi tôi mã hóa lại?



2
Ngoài ra: bạn có ý định trả về một chuỗi hoặc một số? line_number = 0là một mặc định số và line_number = linegán giá trị chuỗi (là nội dung của dòng không phải là vị trí của nó )
Caleth

3
Trong ví dụ cuối cùng của bạn, bạn thực hiện lại hai chức năng hiện có: openin. Thực hiện lại các chức năng hiện có không làm tăng sự tách biệt các mối quan tâm, mối quan tâm đã được xử lý trong chức năng hiện có!
MikeFHay

Câu trả lời:


10

Các ví dụ khác nhau của bạn về việc phân tách mối quan tâm thành các chức năng riêng biệt đều gặp phải cùng một vấn đề: bạn vẫn khó mã hóa phụ thuộc tệp vào get_last_appearance_of_keyword. Điều này làm cho chức năng đó khó kiểm tra vì giờ đây nó phải trả lời trên một tệp hiện có trong hệ thống tệp khi chạy thử. Điều này dẫn đến các bài kiểm tra giòn.

Vì vậy, tôi chỉ cần thay đổi chức năng ban đầu của bạn thành:

def get_last_appearance_of_keyword(text, keyword):
    line_number = 0
    for line in text:
        if keyword in line:
            line_number = line
    return line_number

Bây giờ bạn có một chức năng chỉ có một trách nhiệm: tìm sự xuất hiện cuối cùng của từ khóa trong một số văn bản. Nếu văn bản đó đến từ một tệp, đó sẽ trở thành trách nhiệm của người gọi. Khi kiểm tra, bạn có thể chuyển qua một khối văn bản. Khi sử dụng nó với mã thời gian chạy, đầu tiên tệp được đọc, sau đó hàm này được gọi. Đó là sự tách biệt thực sự của mối quan tâm.


2
Hãy suy nghĩ về tìm kiếm không phân biệt chữ hoa chữ thường. Hãy suy nghĩ về việc bỏ qua các dòng bình luận. Sự tách biệt các mối quan tâm có thể trở nên khác nhau. Ngoài ra, line_number = linerõ ràng là một sai lầm.
9000

2
cũng là ví dụ cuối cùng thực hiện điều này
Ewan

1

Nguyên tắc trách nhiệm duy nhất nói rằng một lớp nên chăm sóc một phần chức năng duy nhất và chức năng này cần được gói gọn bên trong.

Chính xác thì phương pháp của bạn làm gì? Nó có sự xuất hiện cuối cùng của một từ khóa. Mỗi dòng bên trong phương thức hoạt động theo hướng này và nó không liên quan đến bất kỳ thứ gì khác, và kết quả cuối cùng chỉ là một và một. Nói cách khác: bạn không phải chia phương pháp này thành bất kỳ điều gì khác.

Ý tưởng chính đằng sau nguyên tắc là bạn không nên làm nhiều hơn một việc cuối cùng. Có thể bạn mở tệp và cũng để nó như vậy để các phương pháp khác có thể sử dụng nó, bạn sẽ làm hai việc. Hoặc nếu bạn muốn duy trì dữ liệu liên quan đến phương pháp này, một lần nữa, hai điều.

Bây giờ, bạn có thể trích xuất dòng "tệp mở" và làm cho phương thức nhận đối tượng tệp để làm việc, nhưng đó là một cấu trúc lại kỹ thuật hơn là cố gắng tuân thủ SRP.

Đây là một ví dụ tốt về kỹ thuật hơn. Đừng suy nghĩ quá nhiều hoặc bạn sẽ kết thúc với một loạt các phương pháp một dòng.


2
Hoàn toàn không có gì sai với các hàm một dòng. Trong thực tế, một số hàm hữu ích nhất chỉ là một dòng mã.
Joshua Jones

2
@JoshuaJones Không có gì vốn đã sai với chức năng một dòng, nhưng họ có thể là một trở ngại nếu họ không bất cứ điều gì trừu tượng hữu ích. Hàm một dòng để trả về khoảng cách cartesian giữa hai điểm là rất hữu ích, nhưng nếu bạn có một lớp lót cho return keyword in text, đó chỉ là thêm một lớp không cần thiết lên trên cấu trúc ngôn ngữ tích hợp.
cariehl

@cariehl Tại sao sẽ return keyword in textlà một lớp không cần thiết? Nếu bạn thấy mình luôn sử dụng mã đó trong lambda như một tham số trong các hàm bậc cao hơn, tại sao không bọc nó trong một hàm?
Joshua Jones

1
@JoshuaJones Trong bối cảnh đó, bạn đang trừu tượng hóa một cái gì đó hữu ích. Trong bối cảnh của ví dụ ban đầu, không có lý do chính đáng cho chức năng như vậy tồn tại. inlà một từ khóa Python phổ biến, nó hoàn thành mục tiêu và tự nó biểu cảm. Viết một hàm bao bọc xung quanh nó chỉ vì mục đích có một hàm bao bọc che khuất mã, làm cho nó ít trực quan hơn ngay lập tức.
cariehl

0

Tôi chịu trách nhiệm về nó: Nó phụ thuộc :-)

Theo tôi, mã phải đáp ứng mục tiêu này, được ưu tiên theo thứ tự:

  1. Hoàn thành tất cả các yêu cầu (nghĩa là nó thực hiện đúng những gì nó cần)
  2. Dễ đọc và dễ theo dõi / hiểu
  3. Dễ dàng tái cấu trúc
  4. Thực hiện theo các nguyên tắc / nguyên tắc mã hóa tốt

Đối với tôi, ví dụ ban đầu của bạn vượt qua tất cả các mục tiêu này (ngoại trừ có thể là tính chính xác vì line_number = lineđiều đã được đề cập trong các nhận xét , nhưng đó không phải là vấn đề ở đây).

Vấn đề là, SRP không phải là nguyên tắc duy nhất để tuân theo. Ngoài ra còn có You Ar't Gonna Need It (YAGNI) (trong số nhiều người khác). Khi các nguyên tắc va chạm, bạn cần phải cân bằng chúng.

Ví dụ đầu tiên của bạn là hoàn toàn dễ đọc, dễ dàng cấu trúc lại khi bạn cần, nhưng có thể không theo dõi SRP nhiều như nó có thể.

Mỗi phương pháp trong ví dụ thứ ba của bạn cũng hoàn toàn dễ đọc nhưng toàn bộ điều này không còn dễ hiểu nữa vì bạn phải cắm tất cả các phần lại với nhau trong tâm trí. Nó không theo SRP mặc dù.

Vì bạn không nhận được bất cứ điều gì ngay bây giờ từ việc chia phương pháp của mình, đừng làm điều đó, bởi vì bạn có một cách thay thế dễ hiểu hơn.

Khi yêu cầu của bạn thay đổi, bạn có thể cấu trúc lại phương thức cho phù hợp. Trong thực tế, "tất cả trong một thứ" có thể dễ dàng hơn để cấu trúc lại: Hãy tưởng tượng bạn muốn tìm dòng cuối cùng phù hợp với một số tiêu chí tùy ý. Bây giờ bạn chỉ cần chuyển một số hàm lambda vị ngữ để đánh giá xem một dòng có khớp với tiêu chí này hay không.

def get_last_match(file, predicate):
    with open(file, 'r') as file:
        line_number = 0
        for line in file:
            if predicate matches line:
                line_number = line
        return line_number

Trong ví dụ cuối cùng của bạn, bạn cần vượt qua 3 cấp độ vị ngữ sâu, tức là sửa đổi 3 phương thức chỉ để sửa đổi hành vi của cấp độ cuối cùng.

Lưu ý rằng ngay cả việc chia nhỏ việc đọc tệp ra (một phép tái cấu trúc thường có vẻ như hữu ích, bao gồm cả tôi) có thể có những hậu quả không mong muốn: Bạn cần đọc toàn bộ tệp vào bộ nhớ để truyền nó dưới dạng chuỗi cho phương thức của bạn. Nếu các tệp lớn có thể không phải là những gì bạn muốn.

Điểm mấu chốt: Nguyên tắc không bao giờ nên theo sau đến cùng cực mà không lùi một bước và tính tất cả các yếu tố khác.

Có lẽ "chia tách sớm các phương pháp" có thể được coi là một trường hợp đặc biệt của tối ưu hóa sớm ? ;-)


0

Đây giống như một câu hỏi cân bằng trong tâm trí tôi không có câu trả lời đúng sai dễ dàng. Tôi sẽ chỉ đi theo một cách tiếp cận chia sẻ kinh nghiệm cá nhân của tôi ở đây bao gồm cả xu hướng và sai lầm của riêng tôi trong suốt sự nghiệp của tôi. YMMV đáng kể.

Là một người báo trước, tôi làm việc trong các lĩnh vực liên quan đến một số cơ sở mã hóa quy mô rất lớn (hàng triệu LỘC, đôi khi là di sản kéo dài hàng thập kỷ). Tôi cũng làm việc trong một khu vực đặc biệt, nơi không có lượng bình luận hay mã rõ ràng nào có thể dịch cho bất kỳ nhà phát triển có thẩm quyền nào có thể hiểu được việc triển khai đang làm gì -Có triển khai động lực học chất lỏng dựa trên một bài báo được xuất bản 6 tháng trước mà anh ta không dành nhiều thời gian cho bộ luật chuyên về lĩnh vực này). Điều này thường có nghĩa là chỉ một số nhà phát triển đứng đầu có thể hiểu và duy trì hiệu quả bất kỳ phần cụ thể nào của cơ sở mã.

Với kinh nghiệm đặc biệt của tôi và có lẽ kết hợp với tính chất đặc biệt của ngành công nghiệp này, tôi không còn thấy hiệu quả khi sử dụng SoC, DRY, thực hiện chức năng càng dễ đọc càng tốt, thậm chí có thể sử dụng lại các giới hạn tối đa của nó để ủng hộ YAGNI, tách rời, kiểm tra, kiểm tra viết, tài liệu giao diện (vì vậy ít nhất chúng tôi biết cách sử dụng giao diện ngay cả khi việc triển khai đòi hỏi quá nhiều kiến ​​thức chuyên môn) và cuối cùng là vận chuyển phần mềm.

Khối Lego

Tôi thực sự có xu hướng đi ngược lại hoàn toàn ban đầu tại một số điểm trước đó trong sự nghiệp của tôi. Tôi đã rất phấn khích khi lập trình chức năng và thiết kế lớp chính sách trong Thiết kế hiện đại C ++ và siêu lập trình mẫu và vv. Đặc biệt, tôi rất hào hứng với các thiết kế nhỏ gọn và trực giao nhất, nơi bạn có tất cả các chức năng nhỏ này (như "nguyên tử") mà bạn có thể kết hợp với nhau (để tạo thành "phân tử") theo những cách dường như vô tận để có được kết quả mong muốn. Nó khiến tôi muốn viết hầu hết mọi thứ như các hàm bao gồm một vài dòng mã và không nhất thiết phải có bất cứ điều gì sai với hàm ngắn như vậy (nó vẫn có thể rất rộng về khả năng áp dụng và làm rõ mã), ngoại trừ tôi đã bắt đầu đi theo hướng giáo điều khi nghĩ rằng mã của tôi có gì đó sai nếu bất kỳ chức năng nào kéo dài hơn một vài dòng. Và tôi đã nhận được một số đồ chơi thực sự gọn gàng và thậm chí một số mã sản xuất từ ​​loại mã đó nhưng tôi đã bỏ qua đồng hồ: giờ và ngày và tuần trôi qua.

Đặc biệt, trong khi tôi ngưỡng mộ sự đơn giản của từng "khối lego" nhỏ mà tôi đã tạo ra mà tôi có thể kết hợp theo những cách vô hạn, tôi đã bỏ qua lượng thời gian và trí tuệ mà tôi đã đưa vào để ghép tất cả các khối này lại với nhau để tạo thành một "khối" phức tạp. Ngoài ra, trong những trường hợp hiếm hoi nhưng đau đớn khi xảy ra sự cố với công cụ phức tạp này, tôi cố tình bỏ qua thời gian tôi đang cố gắng tìm ra điều gì đã xảy ra khi truy tìm một chuỗi chức năng dường như vô tận để phân tích từng mảnh và các tập con lego phi tập trung của sự kết hợp của chúng khi toàn bộ mọi thứ có thể đơn giản hơn rất nhiều nếu nó không được tạo ra từ các "legos" này, nếu bạn muốn, và chỉ được viết dưới dạng một số ít các chức năng thịt hơn hoặc một lớp trọng lượng trung bình.

Tuy nhiên, tôi vẫn đi vòng tròn đầy đủ và khi thời hạn buộc tôi phải ý thức hơn về thời gian, tôi bắt đầu nhận ra rằng những nỗ lực của tôi đang dạy tôi nhiều hơn về những gì tôi đã làm sai hơn những gì tôi đang làm đúng . Tôi bắt đầu đánh giá cao chức năng và đối tượng / thành phần thịt hơn ở đây và đó, rằng có nhiều cách thực tế hơn để đạt được mức độ SoC hợp lý như David Arnochỉ ra bằng cách tách đầu vào tệp khỏi xử lý chuỗi mà không nhất thiết phải phân tách xử lý chuỗi xuống hầu hết mức độ hạt có thể tưởng tượng.

Chức năng thịt

Và thậm chí nhiều hơn vì vậy tôi bắt đầu ổn với ngay cả một số sao chép mã, thậm chí một số sao chép logic (tôi không nói sao chép và dán mã hóa, tất cả mọi thứ tôi đang nói là tìm "cân bằng"), với điều kiện là chức năng không dễ bị để phát sinh các thay đổi lặp đi lặp lại và được ghi lại theo cách sử dụng và hầu hết được kiểm tra kỹ lưỡng để đảm bảo chức năng của nó phù hợp chính xác với những gì nó được ghi nhận và thực hiện theo cách đó. Tôi bắt đầu nhận ra rằng khả năng sử dụng lại phần lớn gắn liền với độ tin cậy .

Tôi đã nhận ra rằng ngay cả chức năng cơ bản nhất vẫn còn đủ đơn giản để không bị áp dụng quá hẹp và quá khó sử dụng và kiểm tra, ngay cả khi nó sao chép một số logic trong một số chức năng xa ở nơi khác trong codebase, và cung cấp cho nó được kiểm tra tốt và đáng tin cậy và các thử nghiệm hợp lý đảm bảo nó vẫn như vậy, vẫn thích hợp hơn với các chức năng kết hợp linh hoạt và phân rã nhất mà thiếu chất lượng này. Vì vậy, tôi đã đến để thích một số thứ thịt hơn những ngày này đủ tốt nếu nó đáng tin cậy .

Nó cũng có vẻ với tôi rằng hầu hết thời gian, nó rẻ hơn để nhận ra bạn sẽ cần một cái gì đó trong nhận thức muộn màng và thêm nó, cung cấp mã của bạn ít nhất là tiếp thu để bổ sung mới mà không cần tầng Hellfire, hơn để mã tất cả các loại vật khi bạn aren 't sẽ cần nó và sau đó phải đối mặt với sự cám dỗ của việc loại bỏ tất cả khi nó bắt đầu trở thành một Pita thực để duy trì.

Vì vậy, đó là những gì tôi đã học được, đó là những bài học mà tôi cho là cần thiết nhất để cá nhân tôi học được trong nhận thức muộn màng trong bối cảnh này, và như một lời cảnh báo nên được thực hiện bằng một hạt muối. YMMV. Nhưng hy vọng rằng điều đó có thể có giá trị với bạn trong việc giúp bạn tìm ra loại cân bằng phù hợp để vận chuyển các sản phẩm khiến người dùng của bạn hài lòng trong một khoảng thời gian hợp lý và duy trì chúng một cách hiệu quả.


-1

Vấn đề mà bạn đang gặp phải, đó là bạn không bao hàm các chức năng của mình ở dạng giảm nhất. Hãy xem những điều sau: (Tôi không phải là một lập trình viên trăn, vì vậy hãy cắt cho tôi một chút chùng)

def lines_from_file(file):
    with open(file, 'r') as text:
        line_number = 1
        lines = []
        for line in text:
            lines.append((line_number, line.strip()))
            line_number += 1
    return lines

def filter(l, func):
    new_l = []
    for x in l:
        if func(x):
            new_l.append(x)
    return new_l

def contains(needle):
    return lambda haystack: needle in haystack

def last(l):
    length = len(l)
    if length > 0:
        return l[length - 1]
    else:
        return None

Mỗi chức năng trên làm một cái gì đó hoàn toàn khác nhau, và tôi tin rằng bạn sẽ gặp khó khăn trong việc kiểm tra các chức năng đó hơn nữa. Chúng ta có thể kết hợp các chức năng đó để hoàn thành nhiệm vụ trong tầm tay.

lines = lines_from_file('./test_file')
filtered = filter(lines, lambda x : contains('some value')(x[1]))
line = last(filtered)
if line is not None:
    print(line[0])

Các dòng mã trên có thể dễ dàng được kết hợp thành một hàm duy nhất để thực hiện chính xác những gì bạn đang muốn làm. Cách để thực sự tách biệt mối quan tâm là chia các hoạt động phức tạp thành hình thức bao thanh toán nhất của chúng. Khi bạn có một nhóm các chức năng được bao gồm tốt, bạn có thể bắt đầu ghép chúng lại với nhau để giải quyết vấn đề phức tạp hơn. Một điều tốt đẹp về các chức năng bao gồm tốt, là chúng thường có thể tái sử dụng bên ngoài bối cảnh của vấn đề hiện tại.


-2

Tôi muốn nói rằng thực sự không bao giờ có quá nhiều mối quan tâm. Nhưng có thể có các chức năng mà bạn chỉ sử dụng một lần và thậm chí không kiểm tra riêng. Chúng có thể được nội tuyến một cách an toàn , giữ cho sự tách biệt không bị thấm vào không gian tên bên ngoài.

Ví dụ của bạn theo nghĩa đen là không cần check_if_keyword_in_string, bởi vì lớp chuỗi đã cung cấp một triển khai: keyword in linelà đủ. Nhưng bạn có thể lên kế hoạch hoán đổi các triển khai, ví dụ như sử dụng tìm kiếm Boyer-Moore hoặc cho phép tìm kiếm lười biếng trong trình tạo; sau đó nó sẽ có ý nghĩa.

Bạn find_last_appearance_of_keywordcó thể nói chung hơn và tìm sự xuất hiện cuối cùng của một mục trong chuỗi. Vì vậy, bạn có thể sử dụng một triển khai hiện có hoặc thực hiện tái sử dụng. Ngoài ra, nó có thể có một bộ lọc khác , để bạn có thể tìm kiếm một biểu thức chính quy hoặc cho các kết quả không phân biệt chữ hoa chữ thường, v.v.

Thông thường bất cứ điều gì liên quan đến I / O đều xứng đáng có chức năng riêng biệt, vì vậy get_text_from_filecó thể là ý tưởng tốt nếu bạn muốn xử lý hoàn toàn các trường hợp đặc biệt khác nhau. Nó có thể không nếu bạn dựa vào một IOErrorxử lý bên ngoài cho điều đó.

Ngay cả việc đếm dòng có thể là một mối quan tâm riêng nếu trong tương lai bạn có thể cần hỗ trợ, ví dụ như các dòng tiếp tục (ví dụ với \) và sẽ cần số dòng logic. Hoặc bạn có thể cần bỏ qua các dòng bình luận, mà không ngắt đánh số dòng.

Xem xét:

def get_last_appearance_of_keyword(filename, keyword):
    with open(filename) as f:  # File-opening concern.
        numbered_lines = enumerate(f, start=1)  # Line-numbering concern.
        last_line = None  # Also a concern! Some e.g. prefer -1.
        for line_number, line in numbered_lines:  # The searching concern.
            if keyword in line: # The matching concern, applied.
                last_line = line_number
    # Here the file closes; an I/O concern again.
    return last_line

Xem cách bạn có thể muốn phân tách mã của mình khi bạn xem xét một số mối lo ngại có thể thay đổi trong tương lai hoặc chỉ vì bạn nhận thấy cách mã tương tự có thể được sử dụng lại ở nơi khác.

Đây là điều cần lưu ý khi bạn viết hàm ngắn và ngọt ban đầu. Ngay cả khi bạn chưa cần các mối quan tâm tách biệt như các chức năng, hãy tách chúng ra càng nhiều càng thiết thực. Nó không chỉ giúp phát triển mã sau này, mà còn giúp hiểu rõ hơn về mã ngay lập tức và ít mắc lỗi hơn.


-4

Khi nào thì nó tách ra quá nhiều Không bao giờ. Bạn không thể có quá nhiều sự tách biệt.

Ví dụ cuối cùng của bạn là khá tốt, nhưng bạn có thể đơn giản hóa vòng lặp for bằng một text.GetLines(i=>i.containsKeyword)hoặc một cái gì đó.

* Phiên bản thực tế: Dừng khi nó hoạt động. Riêng hơn khi nó vỡ.


5
"Bạn không thể có quá nhiều sự tách biệt." Tôi không nghĩ điều này là đúng. Ví dụ thứ ba của OP chỉ là việc viết lại các cấu trúc python thông thường thành các chức năng riêng biệt. Tôi có thực sự cần một chức năng hoàn toàn mới chỉ để thực hiện 'if x in y' không?
cariehl

@cariehl bạn nên thêm một câu trả lời cho trường hợp đó. Tôi nghĩ rằng bạn sẽ thấy rằng để nó thực sự hoạt động, bạn sẽ cần logic hơn một chút trong các chức năng đó
Ewan

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.