함수가 복잡하고 변수가 많은 경우 클래스를 만들어야합니까?


40

이 질문은 언어에 구애받지 않지만 완전히 다릅니다. 예를 들어, 객체 지향 프로그래밍 (OOP)은 파이썬 과는 달리 일류 함수가없는 Java 와 다릅니다 .

다시 말해서, Java와 같은 언어로 불필요한 클래스를 작성하는 데는 죄책감이 적지 만 Python과 같이 덜 언어적인 언어에는 더 나은 방법이 있다고 생각합니다.

내 프로그램은 여러 번 비교적 복잡한 작업을 수행해야합니다. 이 작업에는 많은 "부기"가 필요하며 일부 임시 파일 등을 작성하고 삭제해야합니다.

그렇기 때문에 많은 다른 "하위 작업"을 호출해야하는 이유도 있습니다. 하나의 거대한 방법으로 모든 것을 넣는 것은 그리 훌륭하지 않고 모듈 식이며 읽기 가능하지 않습니다.

이제 이것은 내 마음에 오는 접근법입니다.

1. 하나의 공용 메소드 만있는 클래스를 작성하고 서브 변수에 필요한 내부 상태를 인스턴스 변수로 유지하십시오.

다음과 같이 보일 것입니다.

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.

이 접근 방식에서 볼 수있는 문제는이 클래스의 상태가 내부적으로 만 의미가 있으며 유일한 공용 메소드 호출을 제외하고 인스턴스에서 아무것도 수행 할 수 없다는 것입니다.

# 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. 클래스없이 여러 함수를 만들고 내부적으로 필요한 다양한 변수를 인수로 전달합니다.

내가 볼 수있는 문제는 함수간에 많은 변수를 전달해야한다는 것입니다. 또한 기능은 서로 밀접하게 관련되어 있지만 그룹화되지는 않습니다.

3. 2.처럼 상태 변수를 전달하는 대신 전역 변수로 만듭니다.

다른 입력으로 작업을 두 번 이상 수행해야하기 때문에 이것은 전혀 좋지 않습니다.

네 번째, 더 나은 접근법이 있습니까? 그렇지 않다면,이 방법들 중 어느 것이 더 좋을까요? 그 이유는 무엇입니까? 내가 놓친 것이 있습니까?


9
"이 접근 방식에서 볼 수있는 문제는이 클래스의 상태가 내부적으로 만 의미가 있으며, 유일한 공용 메소드 호출 이외의 인스턴스로는 아무 것도 할 수 없다는 것입니다." 이것을 문제로 표현했지만 왜 그렇게 생각하지는 않습니까.
Patrick Maupin

@ 패트릭 마우 핀-당신이 맞아요. 그리고 나는 정말로 모른다, 그것이 문제이다. 다른 것을 사용해야하는 것에 클래스를 사용하고있는 것처럼 느껴지고 파이썬에는 아직 탐색하지 않은 많은 것들이 있으므로 누군가가 더 적합한 것을 제안 할 것이라고 생각했습니다.
iCanLearn

어쩌면 내가하려는 일에 대해 분명한 것일 수도 있습니다. 마찬가지로 Java에서 열거 형 대신 일반 클래스를 사용하는 데 특히 잘못된 점은 없지만 여전히 열거 형이 더 자연스러운 것들이 있습니다. 따라서이 질문은 실제로 내가하려는 일에보다 자연스러운 접근 방법이 있는지에 관한 것입니다.
iCanLearn

3
가능한 중복 수업의 실제 책임
gnat

1
기존 코드를 메소드와 인스턴스 변수에 분산시키고 구조에 대해 아무것도 변경하지 않으면 명확성을 잃지 않습니다. (1) 나쁜 생각입니다.
usr

답변:


47
  1. 하나의 공용 메소드 만있는 클래스를 작성하고 서브 변수에 필요한 내부 상태를 인스턴스 변수로 유지하십시오.

이 접근법에서 볼 수있는 문제는이 클래스의 상태가 내부적으로 만 의미가 있으며 유일한 공용 메소드 호출을 제외하고 인스턴스로 아무것도 할 수 없다는 것입니다.

옵션 1은 올바르게 사용 된 캡슐화 의 좋은 예입니다 . 당신이 원하는 내부 상태가 외부 코드에서 숨겨 질 수 있습니다.

그것이 당신의 클래스가 하나의 공개 메소드를 가지고 있다는 것을 의미한다면, 그렇게하십시오. 유지 관리가 훨씬 쉬울 것입니다.

OOP에서 정확히 하나의 일을하고 작은 공개 표면을 가지고 모든 내부 상태를 숨기고있는 클래스가 있다면 (Charlie Sheen이 말한 것처럼) WINNING 입니다.

  1. 클래스없이 함수를 많이 만들고 내부적으로 필요한 다양한 변수를 인수로 전달하십시오.

내가 볼 수있는 문제는 함수간에 많은 변수를 전달해야한다는 것입니다. 또한 기능은 서로 밀접하게 관련되어 있지만 그룹화되지는 않습니다.

옵션 2는 응집력이 낮습니다 . 유지 관리가 더 어려워집니다.

  1. 2와 같지만 상태 변수를 전달하는 대신 전역 변수로 만듭니다.

옵션 2와 같이 옵션 3은 응집력이 낮지 만 훨씬 더 심각합니다!

역사는 글로벌 변수의 편리함이 가져 오는 잔인한 유지 보수 비용보다 중요하다는 것을 보여주었습니다. 그렇기 때문에 캡슐화에 대해 항상 뛰어 다니는 것과 같은 오래된 방귀 소리가 들립니다.


우승 옵션은 # 1 입니다.


6
# 1은 꽤 추악한 API입니다. 이 클래스를 만들기로 결정했다면 클래스에 위임하고 전체 클래스를 비공개로 만드는 단일 공용 함수를 만들 것입니다. ThingDoer(var1, var2).do_it()do_thing(var1, var2).
user2357112

7
옵션 1이 확실한 승자이지만 한 걸음 더 나아갈 것입니다. 내부 객체 상태를 사용하는 대신 로컬 변수와 메서드 매개 변수를 사용하십시오. 이것은 공공 기능을 재진입하게하여 더 안전합니다.

4
옵션 1의 확장으로 할 수있는 또 하나의 일 : 결과 클래스를 보호하고 (이름 앞에 밑줄 추가) 모듈 수준 함수를 정의 def do_thing(var1, var2): return _ThingDoer(var1, var2).run()하여 외부 API를 조금 더 예쁘게 만듭니다.
Sjoerd Job Postmus

4
1에 대한 귀하의 추론을 따르지 않습니다. 내부 상태는 이미 숨겨져 있습니다. 클래스를 추가해도 변경되지 않습니다. 그러므로 나는 왜 당신이 (1)을 추천하는지 모르겠습니다. 실제로 클래스의 존재를 전혀 노출시킬 필요는 없습니다.
usr

3
인스턴스화 한 다음 즉시 단일 메소드를 호출 한 다음 다시 사용하지 않는 클래스는 주요 코드 냄새입니다. 데이터 흐름이 모호한 경우에만 간단한 함수와 동형입니다 (구현 된 부분은 매개 변수를 전달하는 대신 예외 인스턴스에 변수를 할당하여 통신하므로). 내부 함수가 너무 많은 매개 변수를 사용하여 호출이 복잡하고 다루기 어려운 경우 해당 데이터 흐름을 숨길 때 코드가 복잡해 지지 않습니다 !
Ben

23

# 1은 실제로 나쁜 옵션이라고 생각합니다.

당신의 기능을 고려하자 :

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()

하위 작업 1은 어떤 데이터를 사용합니까? 하위 작업 2에서 사용하는 데이터를 수집합니까? 자체 저장하여 데이터를 전달할 때 기능이 어떻게 관련되어 있는지 알 수 없습니다. 자체를 살펴보면 일부 속성은 생성자, 일부는 the_public_method 호출 및 일부는 무작위로 다른 위치에 있습니다. 제 생각에는 엉망입니다.

2 번은 어떻습니까? 먼저 두 번째 문제를 살펴 보겠습니다.

또한 기능은 서로 밀접하게 관련되어 있지만 그룹화되지는 않습니다.

그것들은 하나의 모듈에있을 것이므로 완전히 그룹화 될 것입니다.

내가 볼 수있는 문제는 함수간에 많은 변수를 전달해야한다는 것입니다.

제 생각에는 이것이 좋습니다. 이것은 알고리즘의 데이터 의존성을 명시 적으로 만듭니다. 전역 변수 또는 자체 변수에 저장하면 종속성을 숨기고 덜 나빠 보이게 만들지 만 여전히 있습니다.

일반적으로 이러한 상황이 발생하면 문제를 분해 할 올바른 방법을 찾지 못했음을 의미합니다. 여러 가지 기능으로 잘못 나누는 것이 어색합니다.

물론 실제 기능을 보지 않으면 좋은 제안이 무엇인지 추측하기가 어렵습니다. 그러나 여기서 다루는 내용에 대해 약간의 힌트를 제공합니다.

내 프로그램은 여러 번 비교적 복잡한 작업을 수행해야합니다. 이 작업에는 많은 "부기"가 필요하며 일부 임시 파일 등을 작성하고 삭제해야합니다.

설치 프로그램이라는 설명에 맞는 예제를 선택하겠습니다. 설치 프로그램은 많은 파일을 복사해야하지만 도중에 취소하면 교체 한 파일을 모두 포함하여 전체 프로세스를 되 감아 야합니다. 이를위한 알고리즘은 다음과 같습니다.

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)

이제 레지스트리 설정, 프로그램 아이콘 등을 수행해야한다는 논리를 곱하면 상당히 혼란스러워합니다.

귀하의 # 1 솔루션은 다음과 같습니다.

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

이렇게하면 전체 알고리즘이 더 명확 해지지 만 서로 다른 부분이 통신하는 방식은 숨겨집니다.

접근법 # 2는 다음과 같습니다.

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)

이제는 데이터 조각이 함수간에 어떻게 이동하는지 분명하지만 매우 어색합니다.

이 함수를 어떻게 작성해야합니까?

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

FileTransactionLog 객체는 컨텍스트 관리자입니다. copy_new_files는 파일을 복사 할 때 FileTransactionLog를 통해 파일을 복사하고 임시 사본 작성을 처리하고 복사 된 파일을 추적합니다. 예외 인 경우 원본 파일을 다시 복사하고 성공한 경우 임시 사본을 삭제합니다.

이것은 우리가 작업의 자연스러운 분해를 발견했기 때문에 작동합니다. 이전에는 응용 프로그램 설치 방법에 대한 논리와 취소 된 설치 복구 방법에 대한 논리를 혼합했습니다. 이제 트랜잭션 로그는 임시 파일 및 부기에 대한 모든 세부 정보를 처리하며이 기능은 기본 알고리즘에 중점을 둘 수 있습니다.

사건이 같은 보트에 있다고 생각합니다. 복잡한 작업을보다 간단하고 우아하게 표현할 수 있도록 부기 요소를 일종의 객체로 추출해야합니다.


9

방법 1의 유일한 단점은 차선의 사용 패턴이기 때문에 가장 좋은 해결책은 캡슐화를 한 단계 더 추진하는 것입니다. 클래스를 사용하고 독립형 함수를 제공하십시오. :

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

이를 통해 코드에 대한 가능한 가장 작은 인터페이스가 있으며 내부적으로 사용하는 클래스는 실제로 공용 함수의 구현 세부 사항이됩니다. 호출 코드는 내부 클래스에 대해 알 필요가 없습니다.


4

한편으로, 질문은 어떻게 든 언어에 구애받지 않습니다. 그러나 구현은 언어와 그 패러다임에 달려 있습니다. 이 경우 여러 패러다임을 지원하는 Python입니다.

솔루션 외에도 더 기능적인 방식으로 상태 를 유지 하면서 작업을 완료 할 가능성도 있습니다. 예 :

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

모든 것이

  • 가독성
  • 일관성
  • 유지 보수성

그러나 코드베이스가 OOP 인 경우 갑자기 일부 파트가 (더) 기능적인 스타일로 작성된 경우 일관성을 위반합니다.


4

코딩 할 수있는 이유는 무엇입니까?

나는 내가 읽은 답변에 반대되는 견해를 제시한다. 이러한 관점에서 모든 대답과 솔직한 질문 자체는 코딩 역학에 중점을 둡니다. 그러나 이것은 디자인 문제입니다.

함수가 복잡하고 변수가 많은 경우 클래스를 만들어야합니까?

예, 디자인에 적합합니다. 클래스 자체 또는 다른 클래스의 일부이거나 동작이 클래스간에 분산 될 수 있습니다.


객체 지향 디자인은 복잡성에 관한 것입니다

OO의 핵심은 시스템 자체 측면에서 코드를 캡슐화하여 크고 복잡한 시스템을 성공적으로 구축하고 유지 관리하는 것입니다. "적절한"디자인은 모든 것이 어떤 클래스에 있다고 말합니다.

OO 디자인은 주로 단일 책임 원칙을 준수하는 집중 수업을 통해 복잡성을 관리합니다. 이 클래스는 시스템의 호흡과 깊이에 걸쳐 구조와 기능을 제공하고 이러한 차원을 상호 작용하고 통합합니다.

이를 감안할 때 시스템에 매달려있는 기능-너무 보편적 인 일반 유틸리티 클래스-은 불충분 한 디자인을 암시하는 코드 냄새라고 종종 말합니다. 나는 동의하는 경향이있다.


OO 설계는 자연스럽게 복잡성을 관리합니다 . OOP는 자연스럽게 불필요한 복잡성을 도입합니다.
Miles Rout

"불필요한 복잡성"은 "오, 너무 많은 수업입니다"와 같은 동료의 귀중한 의견을 떠올리게합니다. 경험상 주스가 짜낼 가치가 있다고 말합니다. 가장 좋은 방법은 1-3 줄 길이의 메서드를 사용하는 거의 모든 클래스와 모든 클래스가 수행하는 모든 메서드의 복잡성을 최소화하는 것입니다. 두 컬렉션을 비교하고 복제본을 반환 하는 단일 짧은 LOC-물론 그 뒤에 코드가 있습니다. "많은 코드"를 복잡한 코드와 혼동하지 마십시오. 어쨌든 무료는 없습니다.
radarbob

1
복잡한 방식으로 상호 작용하는 많은 "간단한"코드는 소량의 복잡한 코드보다 훨씬 나쁩니다.
Miles Rout

1
소량의 복잡한 코드에는 복잡성포함되어 있습니다 . 복잡하지만 거기에만 있습니다. 누출되지 않습니다. 복잡하고 복잡한 경계선이나 벽이 없기 때문에 실제로 복잡하고 이해하기 어려운 방식으로 함께 작동하는 개별적으로 간단한 조각이 많이있는 경우.
Miles Rout

3

표준 Python 라이브러리에 존재하는 것과 요구 사항을 비교 한 다음 어떻게 구현되는지 살펴보십시오.

객체가 필요하지 않은 경우에도 함수 내에서 함수를 정의 할 수 있습니다. 함께 파이썬 3 새가 nonlocal당신이 당신의 부모 함수에서 변수를 변경할 수 있도록 선언.

추상화와 구현 정리를 구현하기 위해 함수 내에 간단한 개인 클래스를 두는 것이 여전히 유용 할 수 있습니다.


감사. 그리고 표준 라이브러리에서 내가 질문에있는 것처럼 들리는 것을 알고 있습니까?
iCanLearn

중첩 함수를 사용하여 무언가를 인용하는 것이 어렵다는 것을 알았습니다. 실제로 nonlocal현재 설치된 Python 라이브러리에서 찾을 수 없습니다 . 아마도 클래스 textwrap.py가있는 마음을 가질 TextWrapper수도 있지만 def wrap(text)단순히 TextWrapper 인스턴스 를 만들고 함수를 호출하여 .wrap()반환하는 함수도 있습니다. 클래스를 사용하되 편리한 함수를 추가하십시오.
meuh
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.