背景
私は進行中のC#プロジェクトに取り組んでいます。私はC#プログラマーではなく、主にC++プログラマーです。だから私は基本的に簡単でリファクタリングのタスクを割り当てられました。
コードはめちゃくちゃです。それは巨大なプロジェクトです。お客様が新機能とバグ修正を伴う頻繁なリリースを要求したため、他のすべての開発者はコーディング中にブルートフォースアプローチを取ることを余儀なくされました。コードは非常にメンテナンスが難しく、他のすべての開発者もそれに同意しています。
彼らが正しく行ったかどうかを議論するためにここにいるわけではありません。私がリファクタリングしているとき、私のリファクタリングされたコードは複雑に見えるので、私はそれを正しい方法で行っているのだろうかと思っています!ここに簡単な例としての私の仕事があります。
問題
クラスは6つあります:A
、B
、C
、D
、E
およびF
。すべてのクラスには関数ExecJob()
があります。 6つの実装はすべて非常によく似ています。基本的に、最初はA::ExecJob()
が作成されました。次に、B::ExecJob()
のコピーと貼り付けによる変更によってA::ExecJob()
に実装された、わずかに異なるバージョンが必要でした。別のわずかに異なるバージョンが必要な場合、C::ExecJob()
が作成されました。 6つすべての実装には、いくつかの共通コードがあり、次にいくつかの異なるコード行があり、次にいくつかの共通コードなどがあります。次に、実装の簡単な例を示します。
_A::ExecJob()
{
S1;
S2;
S3;
S4;
S5;
}
B::ExecJob()
{
S1;
S3;
S4;
S5;
}
C::ExecJob()
{
S1;
S3;
S4;
}
_
ここで、SN
はまったく同じステートメントのグループです。
それらを共通にするために、別のクラスを作成し、関数内の共通コードを移動しました。パラメータを使用して、実行するステートメントのグループを制御します。
_Base::CommonTask(param)
{
S1;
if (param.s2) S2;
S3;
S4;
if (param.s5) S5;
}
A::ExecJob() // A inherits Base
{
param.s2 = true;
param.s5 = true;
CommonTask(param);
}
B::ExecJob() // B inherits Base
{
param.s2 = false;
param.s5 = true;
CommonTask(param);
}
C::ExecJob() // C inherits Base
{
param.s2 = false;
param.s5 = false;
CommonTask(param);
}
_
この例では、3つのクラスと単純化されたステートメントのみを使用していることに注意してください。実際には、CommonTask()
関数はすべてのパラメーターチェックで非常に複雑に見え、さらに多くのステートメントがあります。また、実際のコードでは、いくつかのCommonTask()
- looking関数があります。
すべての実装は共通のコードを共有しており、ExecJob()
関数はより可愛く見えますが、私を悩ませている2つの問題があります。
CommonTask()
の変更については、6つ(将来的にはさらに増える可能性があります)のすべての機能をテストする必要があります。CommonTask()
はすでに複雑です。時間が経つにつれて複雑になります。私はそれを正しい方法で行っていますか?
はい、あなたは完全に正しい道にいます!
私の経験では、物事が複雑な場合、変更は小さなステップで発生することに気づきました。あなたがやったことは、進化プロセス(またはリファクタリングプロセス)のステップ1です。手順2と手順3は次のとおりです。
ステップ2
class Base {
method ExecJob() {
S1();
S2();
S3();
S4();
S5();
}
method S1() { //concrete implementation }
method S3() { //concrete implementation }
method S4() { //concrete implementation}
abstract method S2();
abstract method S5();
}
class A::Base {
method S2() {//concrete implementation}
method S5() {//concrete implementation}
}
class B::Base {
method S2() { // empty implementation}
method S5() {//concrete implementation}
}
class C::Base {
method S2() { // empty implementation}
method S5() { // empty implementation}
}
これは「テンプレートデザインパターン」であり、リファクタリングプロセスの一歩先を行っています。基本クラスが変更された場合、サブクラス(A、B、C)は影響を受ける必要はありません。新しいサブクラスは比較的簡単に追加できます。ただし、上の図を見ると、抽象化が壊れていることがわかります。 「空の実装」の必要性は良い指標です。抽象化に問題があることを示しています。短期的には許容できる解決策だったかもしれませんが、もっと良い解決策があるようです。
ステップ
interface JobExecuter {
void executeJob();
}
class A::JobExecuter {
void executeJob(){
helper = new Helper();
helper->S1();
helper->S2();
helper->S3();
helper->S4();
helper->S5();
}
}
class B::JobExecuter {
void executeJob(){
helper = new Helper();
helper->S1();
helper->S3();
helper->S4();
helper->S5();
}
}
class C::JobExecuter {
void executeJob(){
helper = new Helper();
helper->S1();
helper->S3();
helper->S4();
}
}
class Base{
void ExecJob(JobExecuter executer){
executer->executeJob();
}
}
class Helper{
void S1(){//Implementation}
void S2(){//Implementation}
void S3(){//Implementation}
void S4(){//Implementation}
void S5(){//Implementation}
}
これは「戦略設計パターン」であり、あなたのケースに適しているようです。ジョブを実行するための異なる戦略があり、各クラス(A、B、C)はそれを異なる方法で実装します。
このプロセスには、ステップ4またはステップ5か、はるかに優れたリファクタリングアプローチがあると思います。ただし、これにより、コードの重複を排除し、変更がローカライズされていることを確認できます。
あなたは実際に正しいことをしています。私がこれを言うのは:
この種のコードは、イベント駆動型設計(特に.NET)と多くのことを共有しています。最も維持しやすい方法は、共有動作をできるだけ小さなチャンクに保つことです。
高レベルのコードで小さなメソッドの束を再利用し、高レベルのコードを共有ベースから除外します。
リーフ/コンクリートの実装には多くのボイラープレートがあります。パニックにならないで、大丈夫です。そのコードはすべて直接的で、理解しやすいものです。物事が壊れたときに時々それを再配置する必要がありますが、それは簡単に変更できます。
高レベルのコードには多くのパターンが表示されます。時々彼らは本物です、ほとんどの場合彼らはそうではありません。そこまでの5つのパラメーターの「構成」はlookと似ていますが、そうではありません。これらは3つのまったく異なる戦略です。
また、これをすべて合成で行うことができ、継承について心配する必要がないことにも注意してください。カップリングが少なくなります。
もし私があなただったら、おそらく最初にもう1つのステップ、UMLベースの研究を追加するでしょう。
すべての共通部分をマージするコードをリファクタリングすることは、常に最善の方法であるとは限らず、優れたアプローチというよりは一時的なソリューションのように聞こえます。
UMLスキームを描画し、シンプルで効果的なものを維持します。「このソフトウェアで何が行われるのか」など、プロジェクトに関するいくつかの基本的な概念を覚えておいてください。 「このソフトウェアを抽象化し、モジュール化し、拡張性を維持するための最良の方法は何ですか...など」 「カプセル化を最善の方法で実装するにはどうすればよいですか?」
私はただこれを言っているだけです。今はコードを気にしないでください。ロジックを気にすればいいだけです。明確なロジックを念頭に置いていると、残りのすべてが本当に簡単なタスクになる可能性があります。あなたが直面している問題の原因は、単に悪い論理によって引き起こされているだけです。
これがどこに向かっているかに関係なく、最初のステップは、明らかに大きなメソッドA::ExecJob
を小さな部分に分割することです。
したがって、代わりに
A::ExecJob()
{
S1; // many lines of code
S2; // many lines of code
S3; // many lines of code
S4; // many lines of code
S5; // many lines of code
}
あなたは得る
A::ExecJob()
{
S1();
S2();
S3();
S4();
S5();
}
A:S1()
{
// many lines of code
}
A:S2()
{
// many lines of code
}
A:S3()
{
// many lines of code
}
A:S4()
{
// many lines of code
}
A:S5()
{
// many lines of code
}
これからは、多くの可能な方法があります。私の考え:Aをクラス階層とExecJobの仮想クラスにして、B、Cを簡単に作成できるようにします。コピーと貼り付けを行わなくても、ExecJob(現在は5行)を変更したものに置き換えるだけです。バージョン。
B::ExecJob()
{
S1();
S3();
S4();
S5();
}
しかし、なぜそれほど多くのクラスがあるのでしょうか? ExecJob
で必要なアクションを通知できるコンストラクターを持つ単一のクラスでそれらすべてを置き換えることができます。
継承が一般的なコードを実装するための最良の方法だとは思いませんが、あなたのアプローチは正しい方向に進んでいるという他の回答にも同意します。 C++からFAQこれは私がこれまでに説明したよりもはるかによく説明できます: http://www.parashift.com/c++-faq/priv-inherit-vs-compos。 html
最初に、継承が本当にここでの仕事に適切なツールであることを確認する必要があります-クラスで使用される関数の共通の場所が必要だからですA
からF
は、基本クラスはここで正しいことです-時には別のヘルパークラスがうまく機能します。そうかもしれないし、そうでないかもしれません。これは、AからFと共通の基本クラスとの間に「is-a」関係があるかどうかに依存します。人工的な名前A-Fからは不可能です。 ここ あなたはこのトピックを扱っているブログ投稿を見つけます。
共通基底クラスがあなたの場合には正しいものであると決めたとしましょう。次に、2番目に行うことは、コードフラグメントS1からS5が、基本クラスの個別のメソッドS1()
からS5()
に実装されていることを確認することです。その後、「ExecJob」関数は次のようになります。
A::ExecJob()
{
S1();
S2();
S3();
S4();
S5();
}
B::ExecJob()
{
S1();
S3();
S4();
S5();
}
C::ExecJob()
{
S1();
S3();
S4();
}
ご覧のとおり、S1からS5は単なるメソッド呼び出しであるため、コードはブロックされなくなり、コードの重複がほぼ完全に削除され、パラメーターチェックが不要になり、複雑さが増すという問題を回避できます。さもないと。
最後に、3番目のステップ(!)としてのみ、それらすべてのExecJobメソッドを基本クラスの1つに組み合わせることを考えるかもしれません。これらの部分の実行は、パラメーターによって、提案したとおりに、またはテンプレートメソッドパターン。実際のコードに基づいて、あなたのケースで努力する価値があるかどうかを自分で決める必要があります。
しかし、大きなメソッドを小さなメソッドに分解する基本的な手法であるIMHOは、パターンを適用するよりもコードの重複を避けるためにはるかに重要です。