web-dev-qa-db-ja.com

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

オブジェクト指向プログラミング(OOP)は、たとえば Java が異なるため、言語に依存しませんが完全ではありません。 Python

言い換えれば、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番目のより良いアプローチはありますか?そうでない場合、これらのアプローチのどちらがより良いでしょうか、そしてなぜですか?行方不明のものはありますか?

40
iCanLearn
  1. Publicメソッドを1つだけ持ち、サブオペレーションに必要な内部状態をインスタンス変数に保持するクラスを作成します。

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

オプション1は、正しく使用されたencapsulationの良い例です。あなたはwant内部状態を外部コードから隠したいです。

それがあなたのクラスが1つのパブリックメソッドしか持っていないことを意味するならば、そうです。メンテナンスがはるかに簡単になります。

OOPで、ちょうど1つのことを行うクラスがあり、小さなパブリックサーフェスを持ち、その内部状態をすべて隠している場合、あなたは(チャーリーシーンが言うように)[〜#〜]勝利[〜#〜]

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

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

オプション2は低い凝集力の影響を受けます。これにより、メンテナンスが困難になります。

  1. 2.に似ていますが、状態変数を渡すのではなく、グローバルにします。

オプション3は、オプション2と同様に、凝集度が低いという問題がありますが、さらに深刻です。

歴史は、グローバル変数の利便性が、それがもたらす残忍な保守コストよりも重要であることを示しています。だからこそ、私のような古いおならがいつもカプセル化について怒鳴っているのを聞くのです。


勝者は#1です。

46
MetaFight

私は#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を介してコピーします。例外の場合は元のファイルをコピーし、成功した場合は一時的なコピーを削除します。

これは、タスクのより自然な分解を見つけたため機能します。以前は、アプリケーションをインストールする方法に関するロジックと、キャンセルされたインストールを回復する方法に関するロジックを混在させていました。これで、トランザクションログは一時ファイルと簿記に関するすべての詳細を処理し、関数は基本的なアルゴリズムに集中できます。

あなたの事件は同じボートにあると思います。複雑なタスクをより簡単かつエレガントに表現できるように、簿記要素を何らかのオブジェクトに抽出する必要があります。

24
Winston Ewert

メソッド1の明らかな唯一の欠点はその最適でない使用パターンであるため、カプセル化をさらに一歩進めることが最善の解決策であると思います。 :

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

これにより、コードへのインターフェイスが最小になり、内部で使用するクラスは実際にはパブリック関数の実装の詳細になります。呼び出しコードは、内部クラスを知る必要はありません。

一方で、問題はどういうわけか言語にとらわれないです。しかし一方で、実装は言語とそのパラダイムに依存します。この場合は、複数のパラダイムをサポートするPythonです。

あなたの解決策に加えて、操作をより機能的な方法で完了するstatelessを実行する可能性もあります。

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

それはすべてに降りてくる

  • 読みやすさ
  • 一貫性
  • 保守性

ただし、コードベースがOOPの場合、突然一部の部分が(より)関数的なスタイルで記述されると、整合性に違反します。

4
Thomas Junk

コーディングできるのになぜデザインするのか?

私は読んだ答えに逆説を提示します。その観点から見ると、すべての回答、そして率直に言って質問自体もコーディングの仕組みに焦点を当てています。しかし、これは設計上の問題です。

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

はい、あなたのデザインにとって理にかなっています。それ自体がクラスであるか、他のクラスの一部であるか、その動作がクラス間で分散されている可能性があります。


オブジェクト指向設計は複雑さについてです

OOのポイントは、システム自体の観点からコードをカプセル化することにより、大規模で複雑なシステムを正常に構築および維持することです。「適切な」設計は、すべてがいくつかのクラスにあると言います。

オブジェクト指向の設計では、主に、単一の責任の原則に準拠した重点クラスを介して複雑さを自然に管理します。これらのクラスは、システムの呼吸と深さ全体にわたって構造と機能を提供し、これらの次元を相互に作用させて統合します。

それを考えると、システムについてぶら下がっている関数-あまりにもユビキタスな一般的なユーティリティクラス-は、設計が不十分であることを示唆するコードのにおいであるとよく言われます。私は同意する傾向があります。

4
radarbob

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

オブジェクトが必要ない場合でも、関数内で関数を定義できます。 Python を使用すると、親関数の変数を変更できる新しいnonlocal宣言があります。

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

3
meuh