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


9

私はすっきりしたコードが大好きで、コードを可能な限り最善の方法でコーディングしたいと思っています。しかし、常に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

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



2
余談:文字列または数値を返すつもりですか?line_number = 0数値デフォルトであり、line_number = line(ラインである文字列値を代入する内容ではない、それの位置
Caleth

3
あなたの最後の例では、2つの既存の機能を再実装:openin。既存の関数を再実装しても問題の分離は増えません。問題はすでに既存の関数で処理されています!
MikeFHay 2018

回答:


10

懸念事項を個別の関数に分割するさまざまな例はすべて同じ問題の影響を受けます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つしかない関数があります。あるテキストで最後に出現するキーワードを検索します。そのテキストがファイルからのものである場合、それは処理する呼び出し側の責任になります。テストするときは、テキストのブロックを渡すだけです。ランタイムコードで使用する場合、最初にファイルが読み取られ、次にこの関数が呼び出されます。それが懸念の本当の分離です。


2
大文字と小文字を区別しない検索について考えます。コメント行をスキップすることを考えてください。関心の分離が異なる場合があります。また、line_number = line間違いです。
9000

2
最後の例でもほぼ同じです
Ewan

1

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

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

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

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

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


2
1行の関数に問題はありません。実際、最も便利な機能のいくつかは、1行のコードのみです。
ジョシュアジョーンズ

2
@JoshuaJones 1行の関数には本質的に問題はありませんが、有用なものを抽象化しない場合は邪魔になることがあります。2点間のデカルト距離を返す1行の関数は非常に便利ですが、に1行がある場合return keyword in textは、組み込みの言語構造の上に不要なレイヤーを追加するだけです。
cariehl

@cariehlなぜreturn keyword in text不要なレイヤーになるのですか?高次関数のパラメーターとしてラムダでそのコードを一貫して使用していることに気付いた場合は、それを関数でラップしてみませんか?
ジョシュアジョーンズ

1
@JoshuaJonesそのコンテキストでは、何か有用なものを抽象化しています。元の例のコンテキストでは、そのような関数が存在する正当な理由はありません。in一般的なPythonキーワードであり、目的を達成し、それ自体で表現力があります。単にラッパー関数を持たせるためにラッパー関数をその周りに記述すると、コードがわかりにくくなり、直感的ではなくなります。
cariehl

0

私の見解:状況によります:-)

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

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

私にとって、あなたの元の例はこの目標のすべてを満たしています(コメントline_number = lineすでに言及されいることによる正確さを除く可能性がありますが、それはここでは重要ではありません)。

事は、SRPが従うべき唯一の原則ではないということです。ありますつもりはないです(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

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

警告として、私はいくつかの非常に大規模なコードベース(数百万のLOC、時には数十年にわたるレガシー)を含む領域で作業します。私はまた、コメントやコードの明確さが、実装が何をしているのかを理解できる有能な開発者に必ずしも翻訳できない特別な領域で働いています(適切な開発者を必ずしも連れて、状態の実装を理解させることはできません) 6か月前に発行された論文に基づいた最新の流体力学の実装。彼はこの領域に特化するためにコードから離れてかなりの時間を費やしていません。これは一般的に、コードベースの特定の部分を効果的に理解して維持できるのは、ほんの数人の開発者トップだけであることを意味します。

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

レゴブロック

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

特に、無限の方法で組み合わせることができる、作成した各「レゴブロック」のシンプルさに感心した一方で、これらのブロックすべてをつなぎ合わせて手の込んだ「仕掛け」を形成するために費やしていた時間と頭脳力を無視しました。さらに、精巧な仕掛けで何かがうまくいかなかったまれで苦しい例では、分散されたレゴのピースとサブセットのすべてを分析する一見無限に続く関数呼び出しのトレースを通して何がうまくいかなかったかを理解するために費やしていた時間を意図的に無視しましたこれらの「レゴ」から作成されたのでなければ、全体としてははるかに単純だったかもしれませんが、それらの組み合わせは、もしそうであれば、ほんの一握りの肉の関数または中程度のクラスとして書かれました。

それでも私は一周し、締め切りが時間を意識するようになったので、自分の努力が自分が間違っていことを私が正しいことよりももっと教えていることに気づき始めました。David Arno文字列処理を必ずしも最大限に分解せずにファイル入力を文字列処理から分離することで指摘されるように、合理的な程度のSoCを達成するためのより実用的な方法があることを、あちこちで理解し始めました。想像できる細かいレベル。

Meatier関数

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

コードベースの別の場所にあるいくつかの離れた関数のロジックが重複していても、適用範囲が狭すぎて使いにくく、テストできないほど懸念される特異な関数であっても、十分にテストされ、信頼性があり、テストはそれがそのように残ることを合理的に保証しますが、この品質を欠く最も分解された柔軟な機能の組み合わせよりも依然として好ましいです。ですから、信頼できるものであれば、最近はより肉味のあるものが好きになるようになりました。

また、ほとんどの時間は、それはあなたが実現するために安価だと私には思えるですあなたはとき、物事のすべての種類のコードに比べて、地獄の火をカスケード接続することなく、新たに追加するには、少なくとも受容され、後知恵でつもりニーズ何か、それを追加し、あなたのコードを提供上がりません「Tつもりそれを必要とし、それを維持するために、実際のピタになることを始めているとき、それをすべて除去する誘惑に直面しています。

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


-1

あなたが遭遇している問題は、あなたが最も縮小された形にあなたの関数を因数分解していないことです。次を見てください:(私はpythonプログラマではないので、少し緩めます)

def lines_from_file(file):
    with open(file, 'r') as text:
        line_number = 1
        lines = []
        for line in text:
            lines.append((line_number, line.strip()))
            line_number += 1
    return lines

def filter(l, func):
    new_l = []
    for x in l:
        if func(x):
            new_l.append(x)
    return new_l

def contains(needle):
    return lambda haystack: needle in haystack

def last(l):
    length = len(l)
    if length > 0:
        return l[length - 1]
    else:
        return None

上記の関数はそれぞれまったく異なる機能を実行するため、これらの関数をさらに分解するのは難しいと思います。これらの機能を組み合わせて、目の前のタスクを実行できます。

lines = lines_from_file('./test_file')
filtered = filter(lines, lambda x : contains('some value')(x[1]))
line = last(filtered)
if line is not None:
    print(line[0])

上記のコード行を簡単に1つの関数にまとめて、目的の動作を正確に実行できます。懸念事項を実際に分離する方法は、複雑な操作を最も因数分解された形式に分解することです。適切に因数分解された関数のグループを作成したら、それらをつなぎ合わせて、より複雑な問題を解決することができます。よく因数分解された関数の1つの良い点は、現在の問題のコンテキスト外では再利用できることが多いことです。


-2

確かに、懸念の分離はあまりありません。ただし、1度しか使用せず、個別にテストすることさえできない関数が存在する可能性があります。彼らは安全にできるインライン化、外部の名前空間に浸透しないようにします。

check_if_keyword_in_string文字列クラスがすでに実装を提供しているので、あなたの例は文字通りを必要としません:keyword in line。これで十分です。しかし、実装を交換することを計画することもできます。たとえば、Boyer-Moore検索を使用したり、ジェネレーターで遅延検索を許可したりできます。それは理にかなっています。

あなたは、find_last_appearance_of_keywordより一般的であり、シーケンス内の項目の最後の外観を見つけることができます。そのためには、既存の実装を使用するか、再利用可能な実装を作成します。また、別のフィルターを使用することもできますを使用して、正規表現を検索したり、大文字と小文字を区別しない一致を検索したりできます。

通常、I / Oを扱うものはすべて別の機能に値するので、get_text_from_fileさまざまな特殊なケースを完全に処理したい場合は、良いアイデアかもしれません。そのIOErrorための外部ハンドラーに依存している場合はそうではありません。

将来的に、たとえば継続行をサポートする必要がある場合(たとえば、 \する必要があり、論理行番号が必要になる。または、行番号を壊さずにコメント行を無視する必要があるかもしれません。

考慮してください:

def get_last_appearance_of_keyword(filename, keyword):
    with open(filename) as f:  # File-opening concern.
        numbered_lines = enumerate(f, start=1)  # Line-numbering concern.
        last_line = None  # Also a concern! Some e.g. prefer -1.
        for line_number, line in numbered_lines:  # The searching concern.
            if keyword in line: # The matching concern, applied.
                last_line = line_number
    # Here the file closes; an I/O concern again.
    return last_line

あなたが望む方法を見てください将来変更される可能性のある懸念事項を検討するとき、または同じコードが他の場所でどのように再利用できるかに気付いただけで、コードをどのように分割し確認してください。

これは、元のshortで甘い関数を作成するときに注意する必要があります。懸念事項を機能として分離する必要がまだない場合でも、できるだけ実用的に分離してください。後でコードを進化させるだけでなく、コードをすぐに理解し、間違いを減らすのに役立ちます。


-4

分離が「多すぎる」のはいつですか?決して。分離しすぎてはいけません。

最後の例はかなり良いですが、forループをa text.GetLines(i=>i.containsKeyword)か何かで単純化できます。

*実用バージョン:機能したら停止します。壊れたときはもっと離してください。


5
「あまり離れすぎてはいけない」これは本当だとは思いません。OPの3番目の例は、一般的なpythonコンストラクトを個別の関数に書き直したものです。「if x in y」を実行するためだけに、本当にまったく新しい機能が必要ですか?
cariehl 2018

@cariehlそのケースを議論する答えを追加する必要があります。私はあなたはそれが実際に機能するためにあなたがそれらの機能にもう少しロジックが必要になることを見つけるだろうと思う
ユアン・

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.