現実の世界での経験がまだ少ない人にとっては、保守可能なコードの概念は、典型的な優良慣行のルールに従っているとしても、少しあいまいです。直感的には、コードは適切に記述できるが、たとえば、絶え間なく変化する可能性のある情報を配線している場合は保守できないことがわかりますが、コードを調べて、保守可能かどうかを判断するのは依然として困難です。
だから問題は、保守性を損なうものは何ですか?何を探すべきですか?
保守性には多くの側面がありますが、IMOで最も重要なのは 疎結合 と 高凝集性 です。基本的に、優れたコードを使用している場合、コードベース全体を頭に置いておかなくても、あちこちで小さな変更を加えることができます。悪いコードでは、もっと多くのことを考慮に入れなければなりません:ここで修正すれば、他の場所で壊れます。
100以上のkLOCのコードがある場合、これは非常に重要です。コードのフォーマット、コメント、変数の名前付けなど、よく言及される「適切な慣行」のルールは、結合/凝集の問題に比べると表面的なものです。
結合/凝集の問題は、測定や迅速な確認が容易でないことです。それを行うための学術的な試みがいくつかあり、静的コード分析ツールもいくつかあるかもしれませんが、私が知る限り、すぐにコードを使用するのにどれほどの困難が生じるかについて信頼できる見積もりは得られません。
重複したコード:
プログラムのメンテナンスコストに関しては、コピーと貼り付けはおそらく最も高価な操作の1つです。
わざわざこのコードを共通のコンポーネントに移動するのはなぜか、コピーしてコピーするだけですはい...プログラマはおそらく1時間節約しました仕事のように物事をこのようにしています。しかし、後でバグが発生します...もちろん、実行中のコードの半分だけで修正されます。つまり、後でまだ回帰がログに記録され、さらに別の修正同じものになります。
これは明白に聞こえますが、物事の壮大な計画では、それは狡猾です。プログラマーはより多くのことを成し遂げるために良いように見えます。開発チームのマネージャーは、納期どおりに納品したことを認めています。そして、問題が発見されるのはずっと後のことなので、本当の犯人に責任はありません。
「リグレッション」のためにリリースを中止しなければならなかった回数を数えませんでした。完了したら、もう一度修正して、新しいビルドをフープにプッシュします。つまり、6か月前の1人の開発者の1時間の時間(約CAD 40 $)は、1人の上級開発者の1日+ 1人のジュニア開発者の半日+ 1日の遅延プロジェクトはすでに遅れています...
あなたは数学をします...
これを引っ張る次のものは、彼はすぐに走るほうがいいと誓います。
他の回答のルールに違反するコードは維持するのが確かに悪いですが、 すべてのコードを維持するのは難しい 、したがって コードが少ないほど良い です。欠陥はコードの量と強く相関します したがって、コードが多いほど、バグが多くなります 。コードが特定のサイズを超えると、維持することはできません 頭の中にあるデザイン全体 。最後に、コードが増えると、最終的にはエンジニアが増えることになります。つまり、コスト、通信の問題、管理の問題が増えることになります。
ただし、より複雑ではなく、より短いコードを記述する必要があることに注意してください。最も保守しやすいコードは単純で、冗長性を含まず、コードをできるだけ短くします。 API呼び出しが十分に単純である限り、タスクを他のプログラムまたはAPIに委任することも、実行可能なソリューションになります。
一言で言えば、余分なコードは保守性の問題です
皮肉なことに、システムを「将来にわたって保証」しようとする明示的な試みは、多くの場合、維持を困難にします。
マジックナンバーや文字列をハードコーディングしないことは1つのことですが、それをさらに進めて、ロジックを構成ファイルに入れ始めます。これは、メンテナンスの負担に、不明瞭な新しい(おそらく設計が不適切な)プログラミング言語のパーサーとインタープリターを追加することを意味します。
他の理由で追加された抽象化と間接化の層mightが将来変更され、直接それに依存したくない場合は、純粋なメンテナンスポイズンと [〜#〜] yagni [〜#〜] 。 SLF4J FAQ は、このあまりにも一般的なアイデアの問題を説明しています。
ロギングラッパーを書くことはそれほど難しくないと思われるので、一部の開発者はSLF4Jをラップして、それがクラスパスにすでに存在する場合にのみリンクするように誘惑され、SLF4JをWombatのオプションの依存関係にします。依存関係の問題を解決することに加えて、ラッパーはWombatをSLF4JのAPIから分離し、Wombatでのログ記録が将来にわたって確実であることを保証します。
一方、SLF4Jラッパーは定義によりSLF4Jに依存します。同じ一般的なAPIを使用する必要があります。将来、大幅に異なる新しいロギングAPIが登場した場合、ラッパーを使用するコードを、SLF4Jを直接使用するコードと同じように新しいAPIに移行することは困難です。したがって、ラッパーがコードの将来性を保証する可能性は低いですが、SLF4Jの上に追加の間接参照を追加することで、コード自体をより複雑にします。
最後の半文は、SLF4J自体がこのラッパーも同様に適用するロギングラッパーであることをほのめかしています(ただし、必要な唯一のラッパーになるための確実な試みをしている場合を除きますand下に他のすべての共通JavaロギングAPI)。
私の経験では、保守性の主な要因はconsistencyです。プログラムが奇妙で不条理な方法で動作する場合でも、どこでも同じように実行すると、メンテナンスが容易になります。誰がいつ作成したかに応じて、同様のタスクに異なる命名体系、設計パターン、アルゴリズムなどを使用するコードを扱うことは、主要なPITAです。
単体テストされていないコードは保守性を損なうと思います。コードが単体テストでカバーされている場合、コードを変更すると、関連する単体テストが失敗するため、何が壊れているかを確信できます。
ここで他の提案と一緒にユニットテストを行うと、コードが堅牢になり、変更したときに、どのような影響があるかを正確に把握できます。
保守可能なコードを知る唯一の真の方法は、大規模なコードベース(バグ修正と機能要求の両方)の保守に参加することです。
以下について学習します。
私はいくつかの「悪夢」を持っています。私はそれらをこのリストのトップに載せたことはありませんが、間違いなくいくつかの苦労を引き起こしました。
メガオブジェクト/ページ/機能:(いわゆる「 神オブジェクト " アンチパターン =):すべてを1つのオブジェクトのメソッド(主にJava world)、ページ(主にphpおよびasp))、メソッド(主にVB6)に配置します。
私は1つのphpスクリプトに含まれているプログラマーからのコードを維持する必要がありました:htmlコード、ビジネスロジック、mysqlへの呼び出しはすべて混乱しています。何かのようなもの:
foeach( item in sql_query("Select * from Items")) {
<a href="/go/to/item['id']">item['name']</a>
}
よく知られていないか、廃止される可能性がある言語の不明瞭な機能に依存しています。
これは、phpとプリセット変数で再び発生しました。ショートストーリー:php v4の一部の設定では、要求パラメーターが変数に自動的に割り当てられました。そのため http://mysite.com?id=44 は、「魔法のように」値 '42'の変数$ idを生成します。
これはメンテナンスの悪夢でした。データがどこから来たのかわからないだけでなく、php v5が完全に削除(+1)したため、そのバージョンの後にトレーニングされた人々は、何が起こっているのか、そしてその理由さえ理解できませんでした。
メンテナンスが必要なのは、要件が動くターゲットであることです。私は、将来起こり得る変更が予想されるコードを確認し、それらが発生した場合にそれらを処理する方法を検討しました。
保守性の測定値があります。新しい要件が発生したか、既存の要件が変更されたとします。次に、ソースコードへの変更が実装され、実装が完了して修正されるまで、関連するバグが修正されます。次に、diffを実行して、後のコードベースと前のコードベースの間で実行します。ドキュメントの変更が含まれている場合は、それらをコードベースに含めます。 diffから、変更を実行するために必要なコードの挿入、削除、および置換の数Nを取得できます。
Nが小さいほど、過去および将来の要件の変更の大まかな平均として、コードの保守性が向上します。その理由は、プログラマーは騒々しいチャンネルだからです。彼らは間違いを犯します。彼らがしなければならない変更が多ければ多いほど、彼らが犯す間違いも多くなり、それらはすべてバグになり、すべてのバグはそもそも作るよりも見つけて修正することが難しくなります。
だから私は、Do n't Repeat Yourself(DRY)またはcookie-cutter-codeと呼ばれるものに従うという答えに同意します。
私はまた、ドメイン固有言語(DSL)が提供されているへの動きにも同意しています。 「高度な抽象化」を扱うことによって。これは必ずしもNを減らすわけではありません。Nを減らす方法は、問題のドメインの概念により密接にマップする言語(既存の言語の上に定義されたものにすぎない場合があります)に入る方法です。
保守性は、必ずしもプログラマーがすぐに始められることを意味するわけではありません。私が自分の経験から使用する例は Differential Execution です。価格は重要な学習曲線です。報酬は、ユーザーインターフェイスダイアログ、特に動的に変化する要素を持つダイアログのソースコードが大幅に削減されることです。単純な変更のNは2または3程度です。より複雑な変更のNは5または6程度です。Nがそのように小さい場合、バグが発生する可能性が大幅に減少し、「うまくいく」ように見えます。
反対に、Nが通常20〜30の範囲にあるコードを見ました。
これは実際には長いコメントほどの回答ではありません(これらの回答はすでに提示されているためです)。
2つの要因、つまりクリーンで使いやすいインターフェースと繰り返しのないことを真摯に取り組むことで、コードが大幅に改善され、時間の経過とともにプログラマーがはるかに改善されたことがわかりました。
冗長なコードを排除することは難しい場合があり、トリッキーなパターンを思い付く必要があります。
私は通常、何を変更しなければならないかを分析し、それを私の目標にします。たとえば、データベースを更新するためにクライアントでGUIを実行している場合、「別の」もの(DBにリンクされている別のコントロール)を追加するために何が必要ですか?DBに行を追加する必要があります。コンポーネントの位置、それだけです。
したがって、それが最低限の場合、その中にコードはありません。コードベースに触れずに実行できるはずです。これを達成するのは驚くほど難しく、いくつかのツールキットはそれを実行します(通常は不十分です)が、それは目標です。近づくのはどれくらい難しいですか?ひどくありません。私はそれを行うことができ、いくつかの方法でゼロコードでそれを行いました.1つはDB内のテーブルの名前で新しいGUIコンポーネントにタグを付けることによるもので、もう1つはXMLファイルを作成することによるものです-XMLファイル(または、嫌いならYAML) XML)は、バリデーターや特別なアクションをフィールドにリンクして、パターンを非常に柔軟にすることができるため、非常に便利です。
また、ソリューションを正しく実装するのにそれほど時間はかかりません。ほとんどのプロジェクトを出荷するまでには、実際には安価です。
セッターとゲッター(「Beanパターン」)、ジェネリックスと匿名の内部クラスに大きく依存している場合、おそらくこのようにコーディングすることは一般的ではありません。上記の例では、これらのいずれかを強制しようとすると、本当にあなたを台無しにします。セッターとゲッターは新しい属性にコードを使用することを強制し、ジェネリックスはクラス(コードが必要)をインスタンス化することを強制します&匿名の内部クラスは他の場所で再利用するのが簡単ではない傾向があります。実際に一般的にコーディングしている場合、これらの言語構造は悪くありませんが、パターンの視覚化が難しくなる可能性があります。まったく無意味な例の場合:
user.setFirstName(screen.getFirstName());
user.setLastName(screen.getLastName());
見た目はよく、実際にはまったく冗長ではありませんが、少なくとも修正できる方法ではありませんよね?ただし、「ミドルネーム」を追加するときに行を追加するため、「冗長コード」になります。
user.getNameAttributesFrom(screen);
このタスクに新しいコードは必要ありません。「画面」の一部の属性に「名前」属性のタグが付けられている必要があるだけですが、住所をコピーできません。
user.getAttributesFrom(screen, NAME_FIELDS, ADDRESS_FIELDS);
Var-argsを使用すると、画面から収集する(enumからの)属性のグループを含めることができます。ただし、コードを編集して、必要な属性のタイプを変更する必要があります。 「NAME_FIELDS」は単純な列挙型インスタンスであることに注意してください。「画面」のフィールドは、正しい列挙型に配置するように設計されている場合、この列挙型でタグ付けされています。変換テーブルはありません。
Attribute[] attrs=new Attributes[]{NAME_FIELDS, ADDRESS_FIELDS, FRIENDS_FIELDS};
user.getAttributesFrom(screen, attrs);
これで、「データ」を変更しているところに到達しました。これは通常、コードにデータを入れたままにしておきます。なぜなら、「十分に良い」からです。次のステップは、必要に応じてデータを外部化することです。これにより、テキストファイルからデータを読み取ることができますが、ほとんどありません。習慣に入ると、このようにリファクタリングが「ロールアップ」されることがよくあります。そこで行ったことだけで、新しい列挙型とパターンが作成され、他の多くのリファクタリングでロールバックされます。
最後に、このような問題に対する優れた一般的な解決策は言語に依存しないことに注意してください。ルーターのコマンドラインからテキストGUIを解析し、画面上でテキストを更新してデバイスに書き戻すために、コードごとに配置されないソリューションを実装しました-VB = 3.冗長なコードを記述しないという原則に専念する必要があります。たとえそれを行うために2倍のコードを記述する必要がある場合でもです。
クリーンなインターフェース(完全に因数分解され、違法なものが通過することを許可しない)も重要です。 2つのコードユニット間のインターフェースを正しく因数分解すると、インターフェースのどちらかの側のコードを無傷で操作できるようになり、新しいユーザーがインターフェースにクリーンに実装できるようになります。
上記のパターン以外に、最も重要なことの1つは
読み取り可能なコード、コードベースに配置したコードのすべての1行が100回以上読み取られ、人々がバグを追跡しているため、30分を費やしてそれぞれが何をしているかを理解したくない時間。
したがって、実際にボトルネックでない限り、「最適化された」コードを巧みに使わないでください。コメントを付け、元の読み取り可能な(そして機能する)コードをコメント(またはテスト用の代替コンパイルパス)に入れて、必要なもののリファレンスとしてください。行う。
これには、説明的な関数、パラメーター、変数名、および特定のアルゴリズムが別のアルゴリズムよりも選ばれる理由を説明するコメントが含まれます
保守性は問題の複雑さと密接に結びついていると私は思います。これは主観的な問題になる場合があります。単一の開発者が大規模なコードベースを正常に維持し、一貫して成長させることができる状況を見てきましたが、他の人が彼の場所に足を踏み入れたとき、それは維持不可能な混乱のように見えます-彼らはまったく異なるメンタルモデルを持っているからです。
パターンと実践は本当に役立ちます(すばらしいアドバイスについては他の回答を参照してください)。ただし、元のソリューションがファサード、アダプター、および不要な抽象化の背後で失われると、それらの悪用によりさらに多くの問題が発生する可能性があります。
一般に、理解には経験が伴います。学習する優れた方法は、他の人が同様の問題をどのように解決したかを分析し、その実装の強みと弱みを見つけようとすることです。
いくつかの優れたソフトウェア開発原則に違反するほとんどすべてのものが保守性を損ないます。コードを見て、それがどれほど保守可能であるかを評価したい場合は、いくつかの特定のものを選択できます原則を確認し、違反の程度を確認してください。
[〜#〜] dry [〜#〜] の原則は、出発点として適しています。コードで繰り返される情報の量はどれくらいですか? [〜#〜] yagni [〜#〜] も非常に役立ちます。必要のない機能はどれくらいありますか?
[〜#〜] dry [〜#〜] および [〜#〜] yagni [〜#〜] について調査すると、他の原則が見つかり、一般的なソフトウェア開発の原則として適用されます。オブジェクト指向プログラミング言語を使用していて、より具体的にしたい場合、 [〜#〜] solid [〜#〜] 原則は、優れたオブジェクト指向設計のためのいくつかの優れたガイドラインを提供します。
これらの原則に違反するコード傾向は、保守しにくくなります。ここでtendを強調します。これは、一般的に言えば、特に目的のために作成された場合、あらゆる種類の原則に(穏やかに)違反することが合法である場合があるためです。保守性の。
コードのにおい を探すことも役立ちます。 Martin Fowlerによる Refactoring-Improving the Design of Existing Code をご覧ください。そこでは、コードのにおいの例を見つけることができます。これは、保守性の低いコードに対する感覚を鋭くします。
多すぎる機能
機能自体は、実装するために追加のコードを記述する必要があるため、保守性を損ないますが、機能のないアプリケーションは無意味なので、機能を追加します。ただし、不要な機能を除外することで、コードをより保守しやすくすることができます。