懸念の分離:それが「多すぎる」分離であるのはいつですか。

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

方法に関して「懸念の分離」が多すぎるのはいつですか。

次のような方法があるとしましょう。

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行を1行で2つのメソッドにカプセル化しています。そして何度も変更したくないのですが、一度だけそれをカプセル化してメイン関数をさらに分離するだけです)。

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

だから私の質問は今:このコードを書くための正しい方法は何ですか、そしてなぜ他のアプローチは正しいか間違っている?私はいつも学びました:分離、しかしそれが単純に多すぎる時は決してありませんでした。そして、それが「ちょうどいい」ということ、そしてもう一度コーディングするときにそれ以上分離する必要がないということを、どうすれば将来、確実に確認できますか。

6
追加された 著者 gnat,
最後の例では、2つの既存の関数 openin を再実装します。既存の機能を再実装しても懸念事項の分離は増えません。懸念事項はすでに既存の機能で処理されています。
追加された 著者 user35925,
余談:あなたは文字列や数字を返すつもりですか? line_number = 0 は数値のデフォルト値で、 line_number = line は文字列値(の位置ではなく contents )を割り当てます/ i>)
追加された 著者 Caleth,

6 答え

懸念を別々の関数に分割するさまざまな例はすべて同じ問題を抱えています。それでも、ファイルの依存関係を 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つの責任を持つ機能を持っています:あるテキストの中でキーワードの最後の出現を見つける。そのテキストがファイルからのものである場合は、それが呼び出し側の責任となります。テストするときは、テキストブロックを渡すだけで済みます。ランタイムコードでそれを使うとき、最初にファイルが読まれて、それからこの関数が呼ばれます。それが本当の関心事の分離です。

6
追加された
大文字と小文字を区別しない検索について考えてください。コメント行をスキップすることを考えてください。関心事の分離が異なる可能性があります。また、 line_number = line は明らかに間違いです。
追加された 著者 Rorick,
また、最後の例でもこれを行っています。
追加された 著者 Ewan,

それはいつ「分離し過ぎ」ですか。しないでください。あなたはあまり多くの分離を持つことはできません。

Your last example is pretty good, but you could maybe simplify the for loop with a text.GetLines(i=>i.containsKeyword) or something.

*実用的なバージョン:それが動作するときに停止します。それが壊れたらもっと分けなさい。

4
追加された
@cariehlあなたはそのケースを主張する答えを追加する必要があります。実際にうまくいくためには、それらの関数にもう少しロジックが必要だと思うでしょう。
追加された 著者 Ewan,
追加された 著者 Ewan,
「あなたはそれほど離れてはいけません」これは本当だとは思わない。 OPの3番目の例は、一般的なpython構成を別々の関数に書き換えることです。 'if x in y'を実行するためだけに、まったく新しい関数が本当に必要ですか?
追加された 著者 Chthonic One,

私は、懸念がそれほど分離されることは決してないと言っています。しかし、あなたが一度だけ使用する機能があるかもしれません、そして別々にテストさえしません。それらは安全にインライン化することができます

あなたの例は文字通り check_if_keyword_in_string を必要としません。なぜなら文字列クラスはすでに実装を提供しているからです:しかし、実装を入れ替えることを計画するかもしれません。 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

将来変更される可能性がある懸念を考慮するとき、または同じコードが他の場所でどのように再利用できるかに気付いたという理由で、コードをどのように 分割するのかをご覧ください。

これは、あなたがオリジナルの短くて甘い関数を書くときに気をつけるべきことです。まだ懸念を機能として分離する必要がない場合でも、できるだけ実用的に分離してください。後でコードを進化させるのに役立つだけでなく、すぐにコードをよりよく理解し、ミスを少なくするのに役立ちます。

2
追加された

あなたが遭遇している問題は、あなたがあなたの関数をそれらの最も縮小された形に因数分解していないということです。以下を見てください。(私は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つの素晴らしいことは、それらが手元の現在の問題の文脈の外でしばしば再利用可能であるということです。

1
追加された

単一の責任の原則では、クラスは単一の機能を処理し、この機能は適切に内部にカプセル化されるべきであると述べています。

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

原則の背後にある主なアイデアは、あなたが最後に複数のことをしてはいけないということです。たぶんあなたはそのファイルを開いてそのままにして他の方法がそれを使用できるようにするでしょう、あなたは2つのことをするでしょう。あるいは、このメソッドに関連したデータを保持するのであれば、やはり2つのことがあります。

さて、あなたは "open file"の行を抽出してそのメソッドに動作するファイルオブジェクトを受け取らせることができますが、それはSRPに従うことを試みるよりも技術的なリファクタリングです。

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

1
追加された
@JoshuaJones単一行関数には本質的に 何も悪いことはありませんが、有用なものが抽象化されていなければ、それらは邪魔になるかもしれません。 2点間のデカルト距離を返す1行関数は非常に便利ですが、テキスト内のreturnキーワードにワンライナーがある場合、それは単に不要なレイヤーを追加するだけです。言語構成要素です。
追加された 著者 Chthonic One,
@JoshuaJonesその文脈では、あなたは有用な何かを抽象化しています。元の例の文脈では、そのような関数が存在するのには正当な理由はありません。 in はPythonの一般的なキーワードであり、目的を達成し、それ自体が表現力豊かです。ラッパー関数を持つためだけにその周りにラッパー関数を書くと、コードがわかりにくくなり、直感的にわかりにくくなります。
追加された 著者 Chthonic One,
1行関数には全く問題がありません。実際、最も有用な機能のいくつかは単一行のコードです。
追加された 著者 dfdffewfw,
@cariehl キーワードをテキストで返すが不要なレイヤーになるのはなぜですか。高階関数のパラメータとしてラムダ内のそのコードを一貫して使用していると感じたら、それを関数にラップしないでください。
追加された 著者 dfdffewfw,

私の考えでは、それは:-)によって異なります

私の意見では、コードは優先順位の高い順に、この目標を満たすべきです。

  1. すべての要件を満たす(つまり、正しく機能するようになる)
  2. 読みやすく、読みやすく理解しやすい
  3. リファクタリングしやすい
  4. 優れたコーディング慣行/原則に従う

私の場合、あなたの最初の例はこの目標をすべてパスしています(おそらくコメントが、それはここでのポイントではありません)。

問題は、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レベルの述語を渡す必要があります。すなわち、最後のものの振る舞いを修正するために3つのメソッドを修正します。

ファイルの読み取りを分割しても(通常は私を含めて多くの場合有用であると思われるリファクタリングでさえ)、予期しない結果が生じる可能性があります。ファイルが大きい場合、それはあなたが望むものではないかもしれません。

結論:一歩後退せずに他のすべての要因を考慮に入れずに原則を極限まで追いかけるべきではありません。

「メソッドの時期尚早な分割」は、時期尚早な最適化の特殊なケースと考えられますか;-)

0
追加された