web-dev-qa-db-ja.com

懸念の分離:分離が「多すぎる」のはいつですか?

私はすっきりとしたコードが大好きで、常にコードを可能な限り最善の方法でコーディングしたいと思っています。しかし、常に1つありましたが、本当に理解できませんでした。

メソッドに関する「懸念の分離」が多すぎるのはいつですか?

次のメソッドがあるとします。

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

この方法はそのままでも問題ないと思います。それはシンプルで読みやすく、名前からもわかるように、はっきりとわかります。しかし、それは「ただ1つのこと」を実際に行っているのではありません。実際にファイルを開き、それを見つけます。これは、それをさらに分割できることも意味します(「単一の責任の原則」も考慮する)。

バリエーションB(まあ、これはどういうわけか理にかなっています。このようにして、テキスト内のキーワードの最後の出現を見つけるアルゴリズムを簡単に再利用できますが、「多すぎる」ように見えます。理由は説明できませんが、 「そのとおりです):

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

バリエーションC(これは私の考えではとんでもないことです。基本的には、1行を2行だけで別のメソッドにカプセル化しています。ただし、機能のリクエストのため、何かを開く方法が将来変更される可能性があると主張することもできます。 、何度も変更したくないので1回だけ変更するため、カプセル化してメイン関数をさらに分離します):

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

だから私の質問:このコードを書く正しい方法は何ですか?なぜ他のアプローチは正しいですか間違っているのですか?私は常に学んだ:分離、しかしそれが単に多すぎるときは決して。そして、将来的に、それが「適切」であり、再度コーディングするときにそれ以上分離する必要がないことをどのように確認できますか?

9
TheOnionMaster

懸念を別々の関数に分割するさまざまな例はすべて同じ問題の影響を受けます。ファイルの依存関係をget_last_appearance_of_keywordにハードコーディングしています。これにより、テストの実行時にファイルシステムに存在するファイルに応答する必要があるため、テストが困難になります。これは、もろいテストにつながります。

したがって、元の関数を次のように変更するだけです。

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

これで、責任が1つしかない関数があります。あるテキストで最後に出現するキーワードを検索します。そのテキストがファイルからのものである場合、それは呼び出し側の対処する責任になります。テストするときは、テキストのブロックを渡すだけです。ランタイムコードで使用する場合、最初にファイルが読み取られ、次にこの関数が呼び出されます。それが懸念の分離です。

10
David Arno

単一責任の原則では、クラスは単一の機能を処理する必要があり、この機能は内部に適切にカプセル化する必要があります。

あなたの方法は正確に何をしますか?キーワードの最後の出現を取得します。メソッド内の各行はこれに向かって機能し、それは他のものとは関係なく、最終結果は1つだけです。つまり、このメソッドを他のものに分割する必要はありません。

原則の背後にある主な考え方は、最終的には複数のことを行うべきではないということです。ファイルを開いてそのままにしておき、他の方法で使用できるようにするために、2つのことを実行します。または、この方法に関連するデータを永続化する場合も、2つのことを行います。

これで、「ファイルを開く」行を抽出して、処理するファイルオブジェクトをメソッドに受信させることができますが、これはSRPに準拠しようとするよりも技術的なリファクタリングです。

これは、オーバーエンジニアリングの良い例です。考えすぎないでください。そうしないと、1行のメソッドがたくさん集まってしまいます。

私の見解:状況によって異なります:-)

私の意見では、コードは優先順位の高い順に、この目標を達成する必要があります。

  1. すべての要件を満たす(つまり、必要なことを正しく実行する)
  2. 読みやすく、理解しやすい
  3. リファクタリングしやすい
  4. 適切なコーディングプラクティス/原則に従う

私にとって、あなたの元の例はこの目標のすべてを通過します(line_number = line(コメント で既に言及されていることですが、それはここでは重要ではありません)。

事は、SRPが従うべき唯一の原則ではないということです。 You An’n Gonna Need It(YAGNI) (他の多くのものも)もあります。原則が衝突するとき、それらをバランスさせる必要があります。

最初の例は完全に読みやすく、必要に応じて簡単にリファクタリングできますが、SRPにできるだけ従わない場合があります。

3番目の例の各メソッドも完全に読みやすくなっていますが、すべてを理解することは容易ではありません。ただし、SRPに従います。

メソッドの分割から今すぐ何も得ていないので、それを行わないでください。理解しやすい代替案があるためです。

要件の変化に応じて、メソッドを適宜リファクタリングできます。実際、「オールインワン」はリファクタリングするのに簡単かもしれません:任意の基準に一致する最後の行を見つけたいと想像してください。ここで、行が基準に一致するかどうかを評価するために、いくつかの述語ラムダ関数を渡す必要があります。

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

最後の例では、3レベルの深さで述語を渡す必要があります。つまり、最後の1つの動作を変更するためだけに3つのメソッドを変更します。

ファイルの読み取りを分割しても(通常、私を含め、多くの人が便利だと思われるリファクタリング)、予期しない結果が生じる可能性があることに注意してください。ファイル全体をメモリに読み込んで、文字列としてメソッドに渡す必要があります。ファイルが大きい場合は、目的に合わない可能性があります。

結論:原則は、一歩下がって他のすべての要因を考慮に入れずに極端に従うべきではありません。

おそらく「メソッドの早期分割」は 早期最適化 の特別なケースと考えることができますか? ;-)

0
siegi

これは、簡単な正解と不正解がない、私の頭の中のバランスのとれた質問のようなものです。私のキャリア全体を通して、自分の傾向やミスを含め、ここで私の個人的な経験を共有するアプローチを採用します。 YMMVはかなり。

警告として、私はいくつかの非常に大規模なコードベース(数百万のLOC、時には数十年にわたるレガシー)を含む領域で作業します。私はまた、コメントやコードの明快さのどれもが実装が何をしているかを理解できる有能な開発者に必ずしも翻訳できない特別な領域で働いています(私たちは必ずしもまともな開発者を連れて彼に状態の実装を理解させることはできません) 6か月前に発行された論文に基づいた最新の流体力学の実装。彼はこの領域に特化するためにコードから離れてかなりの時間を費やしていません。これは一般的に、コードベースの特定の部分を効果的に理解および維持できるのはごく少数の開発者トップのみであることを意味します。さらに、それは多くのコードがすぐに時代遅れになり、今日の時代遅れの技術が今日時代遅れになっているコンピュータグラフィックス業界がどれほどの速さで動くかを考えると、廃止や場合によっては大規模な置き換えの原因となる可能性があることを意味します。

私の特定の経験と、おそらくこの業界の独特の性質と組み合わせると、SoC、DRYを利用して、機能の実装を可能な限り読みやすくするだけでなく、YAGNI、デカップリング、テスト容易性のために最大限の再利用性を実現することは、もはや生産的ではありませんでした。テストを作成し、インターフェイスのドキュメントを作成します(そのため、実装に必要な専門知識が多すぎる場合でも、少なくともインターフェイスの使用方法を知っています)。最終的にはソフトウェアを出荷します。

レゴブロック

私は実際には、キャリアの初期のある時点で、まったく反対の方向に向かう傾向がありました。私は、Modern C++ Designの関数型プログラミングとポリシークラスデザイン、テンプレートメタプログラミングなどにとても興奮しました。特に、これらの小さな機能(「原子」など)をすべて組み合わせて(「分子」を形成する)無限に見える方法で組み合わせて目的の結果を得ることができる、最もコンパクトで直交する設計に興奮しました。ほとんどすべてを数行のコードで構成される関数として記述したくなりました。このような短い関数には本質的に問題があるわけではありません(適用範囲は非常に広く、コードを明確にすることができます)。関数が数行以上にわたる場合、コードに問題があると考えるという独断的な方向に進みます。そして、私はその種類のコードからいくつかの本当にきちんとしたおもちゃやいくつかの製品コードを手に入れましたが、私は時計を無視していました:時間と日と週が流れていました。

特に、無限の方法で組み合わせることができる、作成した各「レゴブロック」のシンプルさに感心した一方で、これらのすべてのブロックをつなぎ合わせて手の込んだ「仕掛け」を形成するために費やしていた時間と頭脳力を無視しました。さらに、精巧な仕掛けで何かがうまくいかなかったまれで痛みを伴う例では、分散されたレゴのピースとサブセットのすべてを分析する一見無限に続く関数呼び出しのトレースを通して何がうまくいかなかったかを理解するために費やしていた時間を意図的に無視しましたこれらの「レゴ」から作成されたのでなければ、全体としてははるかに単純だったかもしれませんが、それらの組み合わせは、もしそうなら、ほんの一握りのより肉厚な関数または中程度のクラスとして書かれました。これは、必ずしもこのように使用することを意図していないツールや言語の使用方法によって悪化した可能性もあります(実際の関数型言語を使用してこれらのアイデアを実装していなかったなど)。あるいは、開発者が十分ではなかったかもしれません。これをすべて適切な時間でうまく機能させる(その可能性を受け入れることができます)。

それでも一周し、締め切りが時間を意識するようになったので、自分の努力が自分のやっていることをもっと教えていることに気づき始めました間違った私がやっていたことよりright文字列を必ずしも分解せずにファイル入力を文字列処理から分離することによってDavid Arnoが指摘するように、合理的な程度のSoCを達成するためのより実用的な方法があることを、あちこちでより機能的なオブジェクトとコンポーネントに再度感謝し始めました想像できる最も細かいレベルまで処理します。

メジャー関数

さらに、いくつかのコードの重複や論理的な重複さえも許容できるようになりました(私はコピーアンドペーストコーディングとは言っていません。私が話しているすべては「バランス」を見つけることです)提供される関数は繰り返し変更が発生する傾向がなく、その使用法に関して文書化されており、ほとんどの場合、機能が文書化されていることと正しく一致することを確認するために十分にテストされていますそのままです。再利用性は信頼性に大きく関係していることに気づき始めました。

コードベースの他の場所にあるいくつかの離れた関数のロジックを複製し、それを提供したとしても、適用範囲が狭すぎて使用やテストができないほど懸念事項がまだ単数である最も機能の多い関数でさえも気づきました十分にテストされ、信頼性があり、テストはそれがその方法のままであることを合理的に保証しますが、この品質を欠く最も分解された柔軟な機能の組み合わせよりも依然として好ましいです。 reliableであれば、私は最近、より肉厚なものが好きになるようになりました。

また、ほとんどの場合、コードが少なくともあれば、Are何かを後から必要として追加する必要があることに気付く方が安いようです。地獄の火を重ねることなく新しい追加を受け入れ、あなたがそれを必要としないときそれをすべて削除する誘惑に直面したときにあらゆる種類のことをコーディングするよりもそれが維持するための本当のPITAになり始めているとき。

それが私が学んだことです。これらは私がこの文脈で後から見て個人的に学ぶために最も必要であると私が考えた教訓であり、警告としてそれは塩の粒で取られるべきです。 YMMV。しかし、うまくいけば、妥当な時間内にユーザーを満足させ、効果的に維持する製品を出荷するための適切な種類のバランスを見つけるのに役立つことになるでしょう。

0
Dragon Energy