Tôi có nên tạo một lớp nếu hàm của tôi phức tạp và có nhiều biến không?


40

Câu hỏi này hơi khó hiểu về ngôn ngữ, nhưng không hoàn toàn, vì Lập trình hướng đối tượng (OOP) khác nhau, ví dụ, Java , không có các hàm hạng nhất, so với trong Python .

Nói cách khác, tôi cảm thấy ít tội lỗi hơn khi tạo các lớp không cần thiết trong một ngôn ngữ như Java, nhưng tôi cảm thấy có thể có một cách tốt hơn trong các ngôn ngữ ít nồi hơi như Python.

Chương trình của tôi cần thực hiện một thao tác tương đối phức tạp nhiều lần. Hoạt động đó đòi hỏi rất nhiều "sổ sách kế toán", phải tạo và xóa một số tệp tạm thời, v.v.

Đó là lý do tại sao nó cũng cần phải gọi rất nhiều "hoạt động phụ" khác - đưa mọi thứ vào một phương thức lớn không phải là rất hay, mô-đun, có thể đọc được, v.v.

Bây giờ đây là những cách tiếp cận đến với tâm trí của tôi:

1. Tạo một lớp chỉ có một phương thức công khai và giữ trạng thái bên trong cần thiết cho các hoạt động con trong các biến thể hiện của nó.

Nó sẽ trông giống như thế này:

class Thing:

    def __init__(self, var1, var2):
        self.var1 = var1
        self.var2 = var2
        self.var3 = []

    def the_public_method(self, param1, param2):
        self.var4 = param1
        self.var5 = param2
        self.var6 = param1 + param2 * self.var1
        self.__suboperation1()
        self.__suboperation2()
        self.__suboperation3()


    def __suboperation1(self):
        # Do something with self.var1, self.var2, self.var6
        # Do something with the result and self.var3
        # self.var7 = something
        # ...
        self.__suboperation4()
        self.__suboperation5()
        # ...

    def suboperation2(self):
        # Uses self.var1 and self.var3

#    ...
#    etc.

Vấn đề tôi thấy với cách tiếp cận này là trạng thái của lớp này chỉ có ý nghĩa trong nội bộ và nó không thể làm gì với các thể hiện của nó ngoại trừ gọi phương thức công khai duy nhất của chúng.

# Make a thing object
thing = Thing(1,2)

# Call the only method you can call
thing.the_public_method(3,4)

# You don't need thing anymore

2. Tạo một loạt các hàm không có lớp và chuyển các biến cần thiết bên trong khác nhau giữa chúng (dưới dạng đối số).

Vấn đề tôi thấy với điều này là tôi phải vượt qua rất nhiều biến giữa các hàm. Ngoài ra, các chức năng sẽ liên quan chặt chẽ với nhau, nhưng chúng sẽ không được nhóm lại với nhau.

3. Thích 2. nhưng làm cho các biến trạng thái toàn cầu thay vì chuyển chúng.

Điều này sẽ không tốt chút nào, vì tôi phải thực hiện thao tác nhiều lần, với các đầu vào khác nhau.

Có một cách tiếp cận thứ tư, tốt hơn? Nếu không, một trong những cách tiếp cận này sẽ tốt hơn, và tại sao? Có thiếu thứ gì không?


9
"Vấn đề tôi thấy với cách tiếp cận này là trạng thái của lớp này chỉ có ý nghĩa trong nội bộ và không thể làm bất cứ điều gì với các thể hiện của nó ngoại trừ gọi phương thức công khai duy nhất của chúng." Bạn đã nói rõ đây là một vấn đề, nhưng không phải tại sao bạn nghĩ nó như vậy.
Patrick Maupin

@Patrick Maupin - Bạn nói đúng. Và tôi không thực sự biết, đó là vấn đề. Tôi cảm thấy như mình đang sử dụng các lớp học cho một thứ mà nên sử dụng thứ khác, và có rất nhiều thứ trong Python tôi chưa khám phá nên tôi nghĩ có lẽ ai đó sẽ gợi ý thứ gì đó phù hợp hơn.
iCanLearn

Có lẽ đó là về việc rõ ràng về những gì tôi đang cố gắng làm. Giống như, tôi không thấy bất cứ điều gì sai trái khi sử dụng các lớp thông thường thay cho enum trong Java, nhưng vẫn có những điều mà enum tự nhiên hơn. Vì vậy, câu hỏi này thực sự là về cách tiếp cận tự nhiên hơn với những gì tôi đang cố gắng thực hiện.
iCanLearn


1
Nếu bạn chỉ phân tán mã hiện tại của mình thành các phương thức và biến thể hiện và không thay đổi gì về cấu trúc của nó thì bạn sẽ không giành được gì ngoài việc mất đi sự rõ ràng. (1) là một ý tưởng tồi.
usr

Câu trả lời:


47
  1. Tạo một lớp chỉ có một phương thức công khai và giữ trạng thái bên trong cần thiết cho các phép toán con trong các biến thể hiện của nó.

Vấn đề tôi thấy với cách tiếp cận này là trạng thái của lớp này chỉ có ý nghĩa trong nội bộ và không thể làm bất cứ điều gì với các thể hiện của nó ngoại trừ gọi phương thức công khai duy nhất của chúng.

Tùy chọn 1 là một ví dụ tốt về đóng gói được sử dụng đúng. Bạn muốn trạng thái nội bộ được ẩn khỏi mã bên ngoài.

Nếu điều đó có nghĩa là lớp của bạn chỉ có một phương thức công khai, thì nó cũng vậy. Nó sẽ dễ dàng hơn nhiều để duy trì.

Trong OOP nếu bạn có một lớp thực hiện chính xác 1 điều, có bề mặt công khai nhỏ và giữ tất cả trạng thái bên trong của nó, thì bạn (như Charlie Sheen sẽ nói) CHIẾN THẮNG .

  1. Tạo một loạt các hàm không có lớp và chuyển các biến cần thiết bên trong khác nhau giữa chúng (dưới dạng đối số).

Vấn đề tôi thấy với điều này là tôi phải vượt qua rất nhiều biến giữa các hàm. Ngoài ra, các chức năng sẽ liên quan chặt chẽ với nhau, nhưng sẽ không được nhóm lại với nhau.

Lựa chọn 2 chịu sự gắn kết thấp . Điều này sẽ làm cho việc bảo trì khó khăn hơn.

  1. Giống như 2. nhưng làm cho các biến trạng thái toàn cầu thay vì chuyển chúng.

Tùy chọn 3, giống như tùy chọn 2, chịu sự gắn kết thấp, nhưng nghiêm trọng hơn nhiều!

Lịch sử đã chỉ ra rằng sự tiện lợi của các biến toàn cầu lớn hơn nhiều so với chi phí bảo trì tàn bạo mà nó mang lại. Đó là lý do tại sao bạn nghe thấy những cái rắm cũ như tôi nói về việc đóng gói mọi lúc.


Tùy chọn chiến thắng là # 1 .


6
# 1 là một API khá xấu, mặc dù. Nếu tôi quyết định tạo một lớp cho việc này, có lẽ tôi sẽ tạo một chức năng công khai duy nhất ủy thác cho lớp và làm cho cả lớp riêng tư. ThingDoer(var1, var2).do_it()vs do_thing(var1, var2).
user2357112 hỗ trợ Monica

7
Trong khi tùy chọn 1 là một người chiến thắng rõ ràng, tôi sẽ tiến thêm một bước. Thay vì sử dụng trạng thái đối tượng nội bộ, hãy sử dụng các biến cục bộ và tham số phương thức. Điều này làm cho chức năng công cộng trở lại, an toàn hơn.

4
Một điều nữa bạn có thể làm trong phần mở rộng của tùy chọn 1: làm cho lớp kết quả được bảo vệ (thêm dấu gạch dưới phía trước tên), sau đó xác định hàm cấp mô-đun def do_thing(var1, var2): return _ThingDoer(var1, var2).run(), để làm cho API bên ngoài đẹp hơn một chút.
Sjoerd Công việc Postmus

4
Tôi không làm theo lý luận của bạn cho 1. Trạng thái nội bộ đã bị ẩn. Thêm một lớp học không thay đổi điều đó. Do đó tôi không thấy lý do tại sao bạn đề xuất (1). Trong thực tế không cần thiết phải phơi bày sự tồn tại của lớp.
usr

3
Bất kỳ lớp nào bạn khởi tạo và sau đó gọi ngay một phương thức duy nhất và sau đó không bao giờ sử dụng lại là một mùi mã lớn, với tôi. Nó đồng hình với một hàm đơn giản, chỉ với luồng dữ liệu bị che khuất (vì đã phá vỡ giao tiếp thực hiện bằng cách gán các biến trên cá thể vứt bỏ thay vì truyền tham số). Nếu các hàm bên trong của bạn có quá nhiều tham số thì các cuộc gọi rất phức tạp và khó sử dụng, mã sẽ không trở nên ít phức tạp hơn khi bạn ẩn dataflow đó!
Ben

23

Tôi nghĩ rằng # 1 thực sự là một lựa chọn tồi.

Hãy xem xét chức năng của bạn:

def the_public_method(self, param1, param2):
    self.var4 = param1
    self.var5 = param2 
    self.var6 = param1 + param2 * self.var1
    self.__suboperation1()
    self.__suboperation2()
    self.__suboperation3()

Những phần dữ liệu nào mà suboperation1 sử dụng? Nó có thu thập dữ liệu được sử dụng bởi suboperation2 không? Khi bạn truyền dữ liệu xung quanh bằng cách tự lưu trữ dữ liệu, tôi không thể biết các phần chức năng có liên quan như thế nào. Khi tôi nhìn vào bản thân, một số thuộc tính là từ hàm tạo, một số từ lệnh gọi đến the_public_method và một số thuộc tính được thêm ngẫu nhiên ở những nơi khác. Theo tôi, đó là một mớ hỗn độn.

Còn số 2 thì sao? Chà, trước tiên, hãy xem xét vấn đề thứ hai với nó:

Ngoài ra, các chức năng sẽ liên quan chặt chẽ với nhau, nhưng sẽ không được nhóm lại với nhau.

Chúng sẽ nằm trong một mô-đun cùng nhau, vì vậy chúng hoàn toàn sẽ được nhóm lại với nhau.

Vấn đề tôi thấy với điều này là tôi phải vượt qua rất nhiều biến giữa các hàm.

Theo tôi, điều này là tốt. Điều này làm rõ ràng các phụ thuộc dữ liệu trong thuật toán của bạn. Bằng cách lưu trữ chúng cho dù trong các biến toàn cục hoặc trên bản thân, bạn che giấu các phụ thuộc và làm cho chúng có vẻ ít tệ hơn, nhưng chúng vẫn còn đó.

Thông thường, khi tình huống này phát sinh, điều đó có nghĩa là bạn chưa tìm đúng cách để phân tách vấn đề của mình. Bạn đang cảm thấy khó xử khi chia thành nhiều chức năng vì bạn đang cố gắng phân chia sai cách.

Tất nhiên, không nhìn thấy chức năng thực tế của bạn, thật khó để đoán xem đâu sẽ là một gợi ý hay. Nhưng bạn đưa ra một chút gợi ý về những gì bạn đang giải quyết ở đây:

Chương trình của tôi cần thực hiện một thao tác tương đối phức tạp nhiều lần. Hoạt động đó đòi hỏi rất nhiều "sổ sách kế toán", phải tạo và xóa một số tệp tạm thời, v.v.

Hãy để tôi chọn một ví dụ về một cái gì đó phù hợp với mô tả của bạn, một trình cài đặt. Trình cài đặt phải sao chép một loạt các tệp, nhưng nếu bạn hủy bỏ nó một phần, bạn cần phải tua lại toàn bộ quá trình bao gồm cả việc đặt lại bất kỳ tệp nào bạn đã thay thế. Thuật toán này trông giống như:

def install_program():
    copied_files = []
    try:
        for filename in FILES_TO_COPY:
           temporary_file = create_temporary_file()
           copy(target_filename(filename), temporary_file)
           copied_files = [target_filename(filename), temporary_file)
           copy(source_filename(filename), target_filename(filename))
     except CancelledException:
        for source_file, temp_file in copied_files:
            copy(temp_file, source_file)
     else:
        for source_file, temp_file in copied_files:
            delete(temp_file)

Bây giờ nhân logic đó ra để phải thực hiện cài đặt đăng ký, biểu tượng chương trình, v.v. và bạn đã gặp khá nhiều rắc rối.

Tôi nghĩ rằng giải pháp số 1 của bạn trông giống như:

class Installer:
    def install(self):
        try:
            self.copy_new_files()
        except CancellationError:
            self.restore_original_files()
        else:
            self.remove_temp_files()

Điều này không làm cho thuật toán tổng thể rõ ràng hơn, nhưng nó che giấu cách các phần khác nhau giao tiếp.

Cách tiếp cận số 2 trông giống như:

def install_program():
    try:
       temp_files = copy_new_files()
    except CancellationError as error:
       restore_old_files(error.files_that_were_copied)
    else:
       remove_temp_files(temp_files)

Bây giờ nó rõ ràng làm thế nào các phần của dữ liệu di chuyển giữa các chức năng, nhưng nó rất khó xử.

Vậy chức năng này nên được viết như thế nào?

def install_program():
    with FileTransactionLog() as file_transaction_log:
         copy_new_files(file_transaction_log)

Đối tượng FileTransactionLog là một trình quản lý bối cảnh. Khi copy_new_files sao chép một tệp, nó sẽ thực hiện thông qua FileTransactionLog xử lý việc sao chép tạm thời và theo dõi những tệp nào đã được sao chép. Trong trường hợp ngoại lệ, nó sao chép các tệp gốc trở lại và trong trường hợp thành công, nó sẽ xóa các bản sao tạm thời.

Điều này hoạt động bởi vì chúng tôi đã tìm thấy một sự phân rã tự nhiên hơn của nhiệm vụ. Trước đây, chúng tôi đã xen kẽ logic về cách cài đặt ứng dụng với logic về cách khôi phục cài đặt bị hủy. Bây giờ nhật ký giao dịch xử lý tất cả các chi tiết về các tệp tạm thời và sổ sách kế toán, và chức năng có thể tập trung vào thuật toán cơ bản.

Tôi nghi ngờ rằng trường hợp của bạn là trong cùng một chiếc thuyền. Bạn cần trích xuất các yếu tố kế toán vào một số loại đối tượng để nhiệm vụ phức tạp của bạn có thể được thể hiện đơn giản và thanh lịch hơn.


9

Vì nhược điểm rõ ràng duy nhất của phương thức 1 là mô hình sử dụng dưới mức tối ưu của nó, tôi nghĩ giải pháp tốt nhất là điều khiển đóng gói thêm một bước nữa: Sử dụng một lớp, nhưng cũng cung cấp một hàm đứng miễn phí, chỉ xây dựng đối tượng, gọi phương thức và trả về :

def publicFunction(var1, var2, param1, param2)
    thing = Thing(var1, var2)
    thing.theMethod(param1, param2)

Cùng với đó, bạn có giao diện nhỏ nhất có thể với mã của mình và lớp mà bạn sử dụng nội bộ thực sự trở thành một chi tiết triển khai của chức năng công khai của bạn. Gọi mã không bao giờ cần biết về lớp nội bộ của bạn.


4

Một mặt, câu hỏi bằng cách nào đó là bất khả tri ngôn ngữ; nhưng mặt khác, việc thực hiện phụ thuộc vào ngôn ngữ và mô thức của nó. Trong trường hợp này, nó là Python, hỗ trợ nhiều mô hình.

Bên cạnh các giải pháp của bạn, cũng có khả năng, để thực hiện các thao tác hoàn thành không trạng thái theo cách có chức năng hơn, ví dụ:

def outer(param1, param2):
    def inner1(param1, param2, param3):
        pass
    def inner2(param1, param2):
        pass
    return inner2(inner1(param1),param2,param3)

Tất cả là do

  • khả năng đọc
  • Tính nhất quán
  • khả năng bảo trì

Nhưng -Nếu cơ sở mã của bạn là OOP, nó vi phạm tính nhất quán nếu đột nhiên một số phần được viết theo kiểu (nhiều hơn).


4

Tại sao thiết kế khi tôi có thể được mã hóa?

Tôi đưa ra một cái nhìn trái ngược với các câu trả lời tôi đã đọc. Từ quan điểm đó, tất cả các câu trả lời và thẳng thắn ngay cả câu hỏi tập trung vào cơ học mã hóa. Nhưng đây là một vấn đề thiết kế.

Tôi có nên tạo một lớp nếu hàm của tôi phức tạp và có nhiều biến không?

Vâng, vì nó có ý nghĩa cho thiết kế của bạn. Nó có thể là một lớp cho chính nó hoặc một phần của một số lớp khác hoặc hành vi của nó có thể được phân phối giữa các lớp.


Thiết kế hướng đối tượng là về sự phức tạp

Quan điểm của OO là xây dựng và duy trì thành công các hệ thống lớn, phức tạp bằng cách đóng gói mã theo chính hệ thống. Thiết kế "đúng" cho biết mọi thứ đều thuộc một số lớp.

Thiết kế OO tự nhiên quản lý sự phức tạp chủ yếu thông qua các lớp tập trung tuân thủ nguyên tắc trách nhiệm duy nhất. Các lớp này cung cấp cấu trúc và chức năng trong suốt hơi thở và độ sâu của hệ thống, tương tác và tích hợp các kích thước này.

Do đó, người ta thường nói rằng các chức năng lơ lửng trên hệ thống - lớp tiện ích chung quá phổ biến - là một mùi mã cho thấy một thiết kế không đủ. Tôi có xu hướng đồng ý.


Thiết kế OO tự nhiên quản lý sự phức tạp OOP tự nhiên giới thiệu sự phức tạp không cần thiết.
Miles Rout

"Sự phức tạp không cần thiết" khiến tôi nhớ đến những bình luận vô giá từ đồng nghiệp như "ồ, đó là quá nhiều lớp học." Kinh nghiệm cho tôi biết rằng nước ép là giá trị ép. Tốt nhất tôi thấy hầu như toàn bộ các lớp có các phương thức dài 1-3 dòng và với mỗi lớp thực hiện thì độ phức tạp một phần của bất kỳ phương thức đã cho nào được giảm thiểu: Một LỘC ngắn duy nhất so sánh hai bộ sưu tập và trả về các bản sao - tất nhiên có mã đằng sau đó. Đừng nhầm lẫn "nhiều mã" với mã phức tạp. Trong mọi trường hợp, không có gì là miễn phí.
radarbob

1
Rất nhiều mã "đơn giản" tương tác theo cách phức tạp là CÁCH tồi tệ hơn một lượng nhỏ mã phức tạp.
Miles Rout

1
Một lượng nhỏ mã phức tạp đã chứa độ phức tạp . Có sự phức tạp, nhưng chỉ có ở đó. Nó không bị rò rỉ ra ngoài. Khi bạn có rất nhiều mảnh đơn giản riêng lẻ hoạt động cùng nhau theo một cách thực sự phức tạp và khó hiểu, điều đó khó hiểu hơn nhiều vì không có biên giới hoặc bức tường cho sự phức tạp.
Miles Rout

3

Tại sao không so sánh nhu cầu của bạn với một cái gì đó tồn tại trong thư viện Python tiêu chuẩn, và sau đó xem cách nó được thực hiện?

Lưu ý, nếu bạn không cần một đối tượng, bạn vẫn có thể xác định các hàm trong các hàm. Với Python 3nonlocalkhai báo mới để cho phép bạn thay đổi các biến trong hàm cha.

Bạn vẫn có thể thấy hữu ích khi có một số lớp riêng đơn giản bên trong hàm của mình để triển khai trừu tượng hóa và thu dọn các hoạt động.


Cảm ơn. Và bạn có biết bất cứ điều gì trong thư viện tiêu chuẩn nghe giống như những gì tôi có trong câu hỏi không?
iCanLearn

Tôi cảm thấy khó khăn khi trích dẫn một cái gì đó bằng cách sử dụng các hàm lồng nhau, thực sự tôi không tìm thấy nonlocalbất cứ nơi nào trong libs python hiện đang cài đặt của tôi. Có lẽ bạn có thể lấy trái tim từ textwrap.pyđó có một TextWrapperlớp, nhưng cũng có một def wrap(text)hàm chỉ đơn giản là tạo một TextWrapper thể hiện, gọi .wrap()phương thức trên nó và trả về. Vì vậy, sử dụng một lớp, nhưng thêm một số chức năng tiện lợi.
meuh
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.