関数が複雑で変数が多い場合、クラスを作成する必要がありますか?


40

オブジェクト指向プログラミング(OOP)は、たとえばPythonとは異なり、ファーストクラスの機能を持たないJavaとは異なるため、この質問は多少言語に依存しませんが、完全ではありません。

言い換えれば、Javaのような言語で不必要なクラスを作成することに対する罪悪感は減りますが、Pythonのような定型的でない言語にはもっと良い方法があるかもしれないと感じています。

私のプログラムは、比較的複雑な操作を何度も行う必要があります。その操作には多くの「ブックキーピング」が必要であり、いくつかの一時ファイルを作成および削除する必要があります。

そのため、他の多くの「サブオペレーション」も呼び出す必要があります。すべてを1つの巨大なメソッドに入れるのは、あまり良くなく、モジュール式で、読みやすいなどではありません。

今、これらは私の頭に浮かぶアプローチです:

1.パブリックメソッドを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.と似ていますが、状態変数を渡す代わりにグローバルにします。

異なる入力で操作を複数回行う必要があるため、これはまったく役に立ちません。

4番目の、より良いアプローチがありますか?そうでない場合、これらのアプローチのどれがより良いでしょう、そしてなぜですか?私が見逃しているものはありますか?


9
「このアプローチで見られる問題は、このクラスの状態が内部でのみ意味を持ち、唯一のパブリックメソッドを呼び出すことを除いてそのインスタンスで何もできないことです。」あなたはこれを問題として明確にしたが、それがなぜだと思うかではない。
パトリックモーピン

@パトリック・モーピン-そのとおりです。そして、私は本当に知りません、それが問題です。何か他のものを使用するためにクラスを使用しているように感じます。Pythonにはまだ検討していないことがたくさんあるので、誰かがもっと適切なものを提案するかもしれないと思いました。
iCanLearn

多分それは私がやろうとしていることについて明確にすることです。同様に、Javaの列挙型の代わりに通常のクラスを使用することで特に悪いことはありませんが、それでも列挙型がより自然なものがあります。したがって、この質問は、私がやろうとしていることに対して、より自然なアプローチがあるかどうかに関するものです。
iCanLearn


1
既存のコードをメソッドとインスタンス変数に分散し、その構造について何も変更しない場合、何も勝ちませんが、明確さは失われます。(1)は悪い考えです。
usr

回答:


47
  1. パブリックメソッドを1つだけ持つクラスを作成し、そのインスタンス変数にサブ操作に必要な内部状態を保持します。

このアプローチで見られる問題は、このクラスの状態が内部でのみ意味を持ち、唯一のパブリックメソッドを呼び出すことを除いてそのインスタンスで何もできないことです。

オプション1は、正しく使用されるカプセル化の良い例です。あなたはしたい内部状態を外部のコードから非表示にします。

クラスにパブリックメソッドが1つしかないことを意味する場合は、そうする必要があります。メンテナンスがずっと簡単になります。

OOPで、クラスを1つだけ実行し、小さなパブリックサーフェスを持ち、その内部状態をすべて非表示にすると、(Charlie Sheenが言うように)WINNINGになります。

  1. クラスなしで一連の関数を作成し、それらの間で内部的に必要なさまざまな変数を(引数として)渡します。

これに関して私が見る問題は、関数間で多くの変数を渡さなければならないことです。また、機能は互いに密接に関連していますが、グループ化されません。

オプション2の凝集度低い。これにより、メンテナンスがより困難になります。

  1. 2.と似ていますが、状態変数を渡す代わりにグローバルにします。

オプション3は、オプション2と同様に、凝集力が低くなりますが、より深刻です。

歴史から、グローバル変数の利便性は、それがもたらす残忍な保守コストを上回ることが示されています。だからこそ、私のような古いオナラは、カプセル化についていつも不平を言うのを聞くのです。


勝利のオプションは#1です。


6
ただし、#1はかなりいAPIです。このためにクラスを作成することにした場合、おそらく、クラスに委任する単一のパブリック関数を作成し、クラス全体をプライベートにします。ThingDoer(var1, var2).do_it()do_thing(var1, var2)
user2357112は、19:06にMonica

7
オプション1が明確な勝者ですが、さらに一歩進めます。内部オブジェクト状態を使用する代わりに、ローカル変数とメソッドパラメーターを使用します。これにより、パブリック関数が再入可能になり、より安全になります。

4
オプション1の拡張でできることの1つは、結果のクラスを保護し(名前の前にアンダースコアを追加する)、モジュールレベルの関数を定義def do_thing(var1, var2): return _ThingDoer(var1, var2).run()して、外部APIを少しきれいにすることです。
シェードジョブポストマス

4
1の理由には従いません。内部状態はすでに隠されています。クラスを追加してもそれは変わりません。したがって、あなたが(1)を推薦する理由はわかりません。実際、クラスの存在を公開する必要はまったくありません。
usr

3
あなたがインスタンス化し、すぐに単一のメソッドを呼び出し、その後二度と使用しないクラスは、私にとって大きなコードの匂いです。単純な関数と同型であり、データフローが不明瞭です(実装の破片はパラメーターを渡す代わりにスローアウェイインスタンスに変数を割り当てることで通信します)。内部関数が非常に多くのパラメーターを取り、呼び出しが複雑で扱いにくい場合、そのデータフローを非表示にしてもコードはそれほど複雑になりません!
ベン

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

suboperation1が使用するデータはどれですか?サブオペレーション2で使用されるデータを収集しますか?自分でデータを保存してデータをやり取りする場合、機能の各部分がどのように関連しているかはわかりません。自分自身を見たとき、属性の一部はコンストラクターからのものであり、一部はthe_public_methodの呼び出しからのものであり、一部は他の場所にランダムに追加されたものです。私の意見では、それは混乱です。

ナンバー2はどうですか?それでは、まず、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を介してファイルをコピーします。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デザインは自然に複雑さを管理する OOPは自然に不必要な複雑さをもたらします。
マイルルーティング

「不必要な複雑さ」は、「ああ、クラスが多すぎる」などの同僚からの貴重なコメントを思い起こさせます。経験から、ジュースは絞る価値があることがわかります。それ私は方法の長い1-3線で、すべてのクラスは、任意の方法のそれの一部の複雑さをしているとの事実上全体のクラスを参照してください最高の状態で最小化された単一の短い LOCは、2つのコレクションを比較して重複を返す-もちろんその背後にあるコードがあります。「大量のコード」と複雑なコードを混同しないでください。いずれにしても、無料のものはありません。
レーダーボブ

1
複雑な方法で対話する多くの「単純な」コードは、少量の複雑なコードよりもはるかに悪いです。
マイルルーティング

1
少量の複雑なコードには複雑さ含まれいます。複雑さはありますが、そこだけです。漏れません。本当に複雑で理解しにくい方法で一緒に機能する個別の単純な作品がたくさんある場合、複雑さの境界線や壁がないため、はるかに混乱します。
マイルルーティング

3

ニーズを標準Pythonライブラリに存在するものと比較して、それがどのように実装されているかを見てみませんか?

オブジェクトが必要ない場合でも、関数内で関数を定義できることに注意してください。Pythonの3の新しいありnonlocal、あなたの親関数内の変数を変更することができるようにする宣言が。

抽象化を実装し、操作を整理するために、関数内にいくつかの単純なプライベートクラスを用意すると便利です。


ありがとう。そして、私が質問で持っているもののように聞こえる標準ライブラリの何かを知っていますか?
iCanLearn

ネストされた関数を使用して何かを引用するのは難しいと思いますが、実際にnonlocal現在インストールされているPythonライブラリにはどこも見つかりません。おそらくtextwrap.pyTextWrapperクラスを持っているdef wrap(text)だけでなく、単にTextWrapper インスタンスを作成し、その.wrap()メソッドを呼び出してリターンする関数も持っていることに気を配ることができます。クラスを使用しますが、いくつかの便利な機能を追加します。
-meuh
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.