手続き型からオブジェクト指向コードへの変換


16

大規模なASP.NET Webフォームアプリケーションの既存のコードベースのクリーンアップを開始する方法に関する戦略を学習することを目的として、「レガシーコードクリーンコードの効果的な使用」を読んでいます。

このシステムは2005年から使用されており、それ以来多くの機能強化が行われています。もともと、コードは次のように構成されていました(そして、まだ大部分はこのように構成されています):

  • ASP.NET(aspx / ascx)
  • コードビハインド(c#)
  • ビジネスロジックレイヤー(c#)
  • データアクセス層(c#)
  • データベース(Oracle)

主な問題は、コードがオブジェクト指向のように手続き的に偽装されていることです。これは、両方の本で説明されているすべてのガイドラインに事実上違反しています。

これは、ビジネスロジックレイヤーの典型的なクラスの例です。

    public class AddressBO
{
    public TransferObject GetAddress(string addressID)
    {
        if (StringUtils.IsNull(addressID))
        {
            throw new ValidationException("Address ID must be entered");
        }

        AddressDAO addressDAO = new AddressDAO();
        return addressDAO.GetAddress(addressID);
    }

    public TransferObject Insert(TransferObject addressDetails)
    {
        if (StringUtils.IsNull(addressDetails.GetString("EVENT_ID")) ||
            StringUtils.IsNull(addressDetails.GetString("LOCALITY")) ||
            StringUtils.IsNull(addressDetails.GetString("ADDRESS_TARGET")) ||
            StringUtils.IsNull(addressDetails.GetString("ADDRESS_TYPE_CODE")) ||
            StringUtils.IsNull(addressDetails.GetString("CREATED_BY")))
        {
            throw new ValidationException(
                "You must enter an Event ID, Locality, Address Target, Address Type Code and Created By.");
        }

        string addressID = Sequence.GetNextValue("ADDRESS_ID_SEQ");
        addressDetails.SetValue("ADDRESS_ID", addressID);

        string syncID = Sequence.GetNextValue("SYNC_ID_SEQ");
        addressDetails.SetValue("SYNC_ADDRESS_ID", syncID);

        TransferObject syncDetails = new TransferObject();

        Transaction transaction = new Transaction();

        try
        {
            AddressDAO addressDAO = new AddressDAO();
            addressDAO.Insert(addressDetails, transaction);

            // insert the record for the target
            TransferObject addressTargetDetails = new TransferObject();
            switch (addressDetails.GetString("ADDRESS_TARGET"))
            {
                case "PARTY_ADDRESSES":
                    {
                        addressTargetDetails.SetValue("ADDRESS_ID", addressID);
                        addressTargetDetails.SetValue("ADDRESS_TYPE_CODE",
                                                      addressDetails.GetString("ADDRESS_TYPE_CODE"));
                        addressTargetDetails.SetValue("PARTY_ID", addressDetails.GetString("PARTY_ID"));
                        addressTargetDetails.SetValue("EVENT_ID", addressDetails.GetString("EVENT_ID"));
                        addressTargetDetails.SetValue("CREATED_BY", addressDetails.GetString("CREATED_BY"));

                        addressDAO.InsertPartyAddress(addressTargetDetails, transaction);

                        break;
                    }
                case "PARTY_CONTACT_ADDRESSES":
                    {
                        addressTargetDetails.SetValue("ADDRESS_ID", addressID);
                        addressTargetDetails.SetValue("ADDRESS_TYPE_CODE",
                                                      addressDetails.GetString("ADDRESS_TYPE_CODE"));
                        addressTargetDetails.SetValue("PUBLIC_RELEASE_FLAG",
                                                      addressDetails.GetString("PUBLIC_RELEASE_FLAG"));
                        addressTargetDetails.SetValue("CONTACT_ID", addressDetails.GetString("CONTACT_ID"));
                        addressTargetDetails.SetValue("EVENT_ID", addressDetails.GetString("EVENT_ID"));
                        addressTargetDetails.SetValue("CREATED_BY", addressDetails.GetString("CREATED_BY"));

                        addressDAO.InsertContactAddress(addressTargetDetails, transaction);

                        break;
                    }

                << many more cases here >>
                default:
                    {
                        break;
                    }
            }

            // synchronise
            SynchronisationBO synchronisationBO = new SynchronisationBO();
            syncDetails = synchronisationBO.Synchronise("I", transaction,
                                                        "ADDRESSES", addressDetails.GetString("ADDRESS_TARGET"),
                                                        addressDetails, addressTargetDetails);


            // commit
            transaction.Commit();
        }
        catch (Exception)
        {
            transaction.Rollback();
            throw;
        }

        return new TransferObject("ADDRESS_ID", addressID, "SYNC_DETAILS", syncDetails);
    }


    << many more methods are here >>

}

それには多くの重複があり、クラスには多くの責任などがあります-それは一般的に「クリーンでない」コードです。

システム全体のコードはすべて、具体的な実装に依存しています。

これは、データアクセスレイヤーの典型的なクラスの例です。

    public class AddressDAO : GenericDAO
{
    public static readonly string BASE_SQL_ADDRESSES =
        "SELECT " +
        "  a.address_id, " +
        "  a.event_id, " +
        "  a.flat_unit_type_code, " +
        "  fut.description as flat_unit_description, " +
        "  a.flat_unit_num, " +
        "  a.floor_level_code, " +
        "  fl.description as floor_level_description, " +
        "  a.floor_level_num, " +
        "  a.building_name, " +
        "  a.lot_number, " +
        "  a.street_number, " +
        "  a.street_name, " +
        "  a.street_type_code, " +
        "  st.description as street_type_description, " +
        "  a.street_suffix_code, " +
        "  ss.description as street_suffix_description, " +
        "  a.postal_delivery_type_code, " +
        "  pdt.description as postal_delivery_description, " +
        "  a.postal_delivery_num, " +
        "  a.locality, " +
        "  a.state_code, " +
        "  s.description as state_description, " +
        "  a.postcode, " +
        "  a.country, " +
        "  a.lock_num, " +
        "  a.created_by, " +
        "  TO_CHAR(a.created_datetime, '" + SQL_DATETIME_FORMAT + "') as created_datetime, " +
        "  a.last_updated_by, " +
        "  TO_CHAR(a.last_updated_datetime, '" + SQL_DATETIME_FORMAT + "') as last_updated_datetime, " +
        "  a.sync_address_id, " +
        "  a.lat," +
        "  a.lon, " +
        "  a.validation_confidence, " +
        "  a.validation_quality, " +
        "  a.validation_status " +
        "FROM ADDRESSES a, FLAT_UNIT_TYPES fut, FLOOR_LEVELS fl, STREET_TYPES st, " +
        "     STREET_SUFFIXES ss, POSTAL_DELIVERY_TYPES pdt, STATES s " +
        "WHERE a.flat_unit_type_code = fut.flat_unit_type_code(+) " +
        "AND   a.floor_level_code = fl.floor_level_code(+) " +
        "AND   a.street_type_code = st.street_type_code(+) " +
        "AND   a.street_suffix_code = ss.street_suffix_code(+) " +
        "AND   a.postal_delivery_type_code = pdt.postal_delivery_type_code(+) " +
        "AND   a.state_code = s.state_code(+) ";


    public TransferObject GetAddress(string addressID)
    {
        //Build the SELECT Statement
        StringBuilder selectStatement = new StringBuilder(BASE_SQL_ADDRESSES);

        //Add WHERE condition
        selectStatement.Append(" AND a.address_id = :addressID");

        ArrayList parameters = new ArrayList{DBUtils.CreateOracleParameter("addressID", OracleDbType.Decimal, addressID)};

        // Execute the SELECT statement
        Query query = new Query();
        DataSet results = query.Execute(selectStatement.ToString(), parameters);

        // Check if 0 or more than one rows returned
        if (results.Tables[0].Rows.Count == 0)
        {
            throw new NoDataFoundException();
        }
        if (results.Tables[0].Rows.Count > 1)
        {
            throw new TooManyRowsException();
        }

        // Return a TransferObject containing the values
        return new TransferObject(results);
    }


    public void Insert(TransferObject insertValues, Transaction transaction)
    {
        // Store Values
        string addressID = insertValues.GetString("ADDRESS_ID");
        string syncAddressID = insertValues.GetString("SYNC_ADDRESS_ID");
        string eventID = insertValues.GetString("EVENT_ID");
        string createdBy = insertValues.GetString("CREATED_BY");

        // postal delivery
        string postalDeliveryTypeCode = insertValues.GetString("POSTAL_DELIVERY_TYPE_CODE");
        string postalDeliveryNum = insertValues.GetString("POSTAL_DELIVERY_NUM");

        // unit/building
        string flatUnitTypeCode = insertValues.GetString("FLAT_UNIT_TYPE_CODE");
        string flatUnitNum = insertValues.GetString("FLAT_UNIT_NUM");
        string floorLevelCode = insertValues.GetString("FLOOR_LEVEL_CODE");
        string floorLevelNum = insertValues.GetString("FLOOR_LEVEL_NUM");
        string buildingName = insertValues.GetString("BUILDING_NAME");

        // street
        string lotNumber = insertValues.GetString("LOT_NUMBER");
        string streetNumber = insertValues.GetString("STREET_NUMBER");
        string streetName = insertValues.GetString("STREET_NAME");
        string streetTypeCode = insertValues.GetString("STREET_TYPE_CODE");
        string streetSuffixCode = insertValues.GetString("STREET_SUFFIX_CODE");

        // locality/state/postcode/country
        string locality = insertValues.GetString("LOCALITY");
        string stateCode = insertValues.GetString("STATE_CODE");
        string postcode = insertValues.GetString("POSTCODE");
        string country = insertValues.GetString("COUNTRY");

        // esms address
        string esmsAddress = insertValues.GetString("ESMS_ADDRESS");

        //address/GPS
        string lat = insertValues.GetString("LAT");
        string lon = insertValues.GetString("LON");
        string zoom = insertValues.GetString("ZOOM");

        //string validateDate = insertValues.GetString("VALIDATED_DATE");
        string validatedBy = insertValues.GetString("VALIDATED_BY");
        string confidence = insertValues.GetString("VALIDATION_CONFIDENCE");
        string status = insertValues.GetString("VALIDATION_STATUS");
        string quality = insertValues.GetString("VALIDATION_QUALITY");


        // the insert statement
        StringBuilder insertStatement = new StringBuilder("INSERT INTO ADDRESSES (");
        StringBuilder valuesStatement = new StringBuilder("VALUES (");

        ArrayList parameters = new ArrayList();

        // build the insert statement
        insertStatement.Append("ADDRESS_ID, EVENT_ID, CREATED_BY, CREATED_DATETIME, LOCK_NUM ");
        valuesStatement.Append(":addressID, :eventID, :createdBy, SYSDATE, 1 ");
        parameters.Add(DBUtils.CreateOracleParameter("addressID", OracleDbType.Decimal, addressID));
        parameters.Add(DBUtils.CreateOracleParameter("eventID", OracleDbType.Decimal, eventID));
        parameters.Add(DBUtils.CreateOracleParameter("createdBy", OracleDbType.Varchar2, createdBy));

        // build the insert statement
        if (!StringUtils.IsNull(syncAddressID))
        {
            insertStatement.Append(", SYNC_ADDRESS_ID");
            valuesStatement.Append(", :syncAddressID");
            parameters.Add(DBUtils.CreateOracleParameter("syncAddressID", OracleDbType.Decimal, syncAddressID));
        }

        if (!StringUtils.IsNull(postalDeliveryTypeCode))
        {
            insertStatement.Append(", POSTAL_DELIVERY_TYPE_CODE");
            valuesStatement.Append(", :postalDeliveryTypeCode ");
            parameters.Add(DBUtils.CreateOracleParameter("postalDeliveryTypeCode", OracleDbType.Varchar2, postalDeliveryTypeCode));
        }

        if (!StringUtils.IsNull(postalDeliveryNum))
        {
            insertStatement.Append(", POSTAL_DELIVERY_NUM");
            valuesStatement.Append(", :postalDeliveryNum ");
            parameters.Add(DBUtils.CreateOracleParameter("postalDeliveryNum", OracleDbType.Varchar2, postalDeliveryNum));
        }

        if (!StringUtils.IsNull(flatUnitTypeCode))
        {
            insertStatement.Append(", FLAT_UNIT_TYPE_CODE");
            valuesStatement.Append(", :flatUnitTypeCode ");
            parameters.Add(DBUtils.CreateOracleParameter("flatUnitTypeCode", OracleDbType.Varchar2, flatUnitTypeCode));
        }

        if (!StringUtils.IsNull(lat))
        {
            insertStatement.Append(", LAT");
            valuesStatement.Append(", :lat ");
            parameters.Add(DBUtils.CreateOracleParameter("lat", OracleDbType.Decimal, lat));
        }

        if (!StringUtils.IsNull(lon))
        {
            insertStatement.Append(", LON");
            valuesStatement.Append(", :lon ");
            parameters.Add(DBUtils.CreateOracleParameter("lon", OracleDbType.Decimal, lon));
        }

        if (!StringUtils.IsNull(zoom))
        {
            insertStatement.Append(", ZOOM");
            valuesStatement.Append(", :zoom ");
            parameters.Add(DBUtils.CreateOracleParameter("zoom", OracleDbType.Decimal, zoom));
        }

        if (!StringUtils.IsNull(flatUnitNum))
        {
            insertStatement.Append(", FLAT_UNIT_NUM");
            valuesStatement.Append(", :flatUnitNum ");
            parameters.Add(DBUtils.CreateOracleParameter("flatUnitNum", OracleDbType.Varchar2, flatUnitNum));
        }

        if (!StringUtils.IsNull(floorLevelCode))
        {
            insertStatement.Append(", FLOOR_LEVEL_CODE");
            valuesStatement.Append(", :floorLevelCode ");
            parameters.Add(DBUtils.CreateOracleParameter("floorLevelCode", OracleDbType.Varchar2, floorLevelCode));
        }

        if (!StringUtils.IsNull(floorLevelNum))
        {
            insertStatement.Append(", FLOOR_LEVEL_NUM");
            valuesStatement.Append(", :floorLevelNum ");
            parameters.Add(DBUtils.CreateOracleParameter("floorLevelNum", OracleDbType.Varchar2, floorLevelNum));
        }

        if (!StringUtils.IsNull(buildingName))
        {
            insertStatement.Append(", BUILDING_NAME");
            valuesStatement.Append(", :buildingName ");
            parameters.Add(DBUtils.CreateOracleParameter("buildingName", OracleDbType.Varchar2, buildingName));
        }

        if (!StringUtils.IsNull(lotNumber))
        {
            insertStatement.Append(", LOT_NUMBER");
            valuesStatement.Append(", :lotNumber ");
            parameters.Add(DBUtils.CreateOracleParameter("lotNumber", OracleDbType.Varchar2, lotNumber));
        }

        if (!StringUtils.IsNull(streetNumber))
        {
            insertStatement.Append(", STREET_NUMBER");
            valuesStatement.Append(", :streetNumber ");
            parameters.Add(DBUtils.CreateOracleParameter("streetNumber", OracleDbType.Varchar2, streetNumber));
        }

        if (!StringUtils.IsNull(streetName))
        {
            insertStatement.Append(", STREET_NAME");
            valuesStatement.Append(", :streetName ");
            parameters.Add(DBUtils.CreateOracleParameter("streetName", OracleDbType.Varchar2, streetName));
        }

        if (!StringUtils.IsNull(streetTypeCode))
        {
            insertStatement.Append(", STREET_TYPE_CODE");
            valuesStatement.Append(", :streetTypeCode ");
            parameters.Add(DBUtils.CreateOracleParameter("streetTypeCode", OracleDbType.Varchar2, streetTypeCode));
        }

        if (!StringUtils.IsNull(streetSuffixCode))
        {
            insertStatement.Append(", STREET_SUFFIX_CODE");
            valuesStatement.Append(", :streetSuffixCode ");
            parameters.Add(DBUtils.CreateOracleParameter("streetSuffixCode", OracleDbType.Varchar2, streetSuffixCode));
        }

        if (!StringUtils.IsNull(locality))
        {
            insertStatement.Append(", LOCALITY");
            valuesStatement.Append(", :locality");
            parameters.Add(DBUtils.CreateOracleParameter("locality", OracleDbType.Varchar2, locality));
        }

        if (!StringUtils.IsNull(stateCode))
        {
            insertStatement.Append(", STATE_CODE");
            valuesStatement.Append(", :stateCode");
            parameters.Add(DBUtils.CreateOracleParameter("stateCode", OracleDbType.Varchar2, stateCode));
        }

        if (!StringUtils.IsNull(postcode))
        {
            insertStatement.Append(", POSTCODE");
            valuesStatement.Append(", :postcode ");
            parameters.Add(DBUtils.CreateOracleParameter("postcode", OracleDbType.Varchar2, postcode));
        }

        if (!StringUtils.IsNull(country))
        {
            insertStatement.Append(", COUNTRY");
            valuesStatement.Append(", :country ");
            parameters.Add(DBUtils.CreateOracleParameter("country", OracleDbType.Varchar2, country));
        }

        if (!StringUtils.IsNull(esmsAddress))
        {
            insertStatement.Append(", ESMS_ADDRESS");
            valuesStatement.Append(", :esmsAddress ");
            parameters.Add(DBUtils.CreateOracleParameter("esmsAddress", OracleDbType.Varchar2, esmsAddress));
        }

        if (!StringUtils.IsNull(validatedBy))
        {
            insertStatement.Append(", VALIDATED_DATE");
            valuesStatement.Append(", SYSDATE ");
            insertStatement.Append(", VALIDATED_BY");
            valuesStatement.Append(", :validatedBy ");
            parameters.Add(DBUtils.CreateOracleParameter("validatedBy", OracleDbType.Varchar2, validatedBy));
        }


        if (!StringUtils.IsNull(confidence))
        {
            insertStatement.Append(", VALIDATION_CONFIDENCE");
            valuesStatement.Append(", :confidence ");
            parameters.Add(DBUtils.CreateOracleParameter("confidence", OracleDbType.Decimal, confidence));
        }

        if (!StringUtils.IsNull(status))
        {
            insertStatement.Append(", VALIDATION_STATUS");
            valuesStatement.Append(", :status ");
            parameters.Add(DBUtils.CreateOracleParameter("status", OracleDbType.Varchar2, status));
        }

        if (!StringUtils.IsNull(quality))
        {
            insertStatement.Append(", VALIDATION_QUALITY");
            valuesStatement.Append(", :quality ");
            parameters.Add(DBUtils.CreateOracleParameter("quality", OracleDbType.Decimal, quality));
        }

        // finish off the statement
        insertStatement.Append(") ");
        valuesStatement.Append(")");

        // build the insert statement
        string sql = insertStatement + valuesStatement.ToString();

        // Execute the INSERT Statement
        Dml dmlDAO = new Dml();
        int rowsAffected = dmlDAO.Execute(sql, transaction, parameters);

        if (rowsAffected == 0)
        {
            throw new NoRowsAffectedException();
        }
    }

    << many more methods go here >>
}

このシステムは、1週間の.NETコースの後、2005年に私と小さなチームによって開発されました。以前は、クライアントサーバーアプリケーションでの経験がありました。過去5年間、自動化された単体テスト、自動化された統合テスト、および自動化された受け入れテスト(Seleniumまたは同等のものを使用)の利点を認識するようになりましたが、現在のコードベースではこれらの概念を導入することは不可能なようです。

私たちは現在、厳しい時間枠で大規模な拡張プロジェクトに取り組み始めています。チームは、5人の.NET開発者で構成されています。2人の開発者は数年の.NETの経験があり、3人は.NETの経験がほとんどまたはまったくありません。チーム(私を含む)は、.NET単体テストまたはモックフレームワークの使用経験がありません。

このコードをよりクリーンで、オブジェクト指向で、テスト可能で保守しやすいものにするために、どの戦略を使用しますか


9
余談ですが、システムの書き換えにはコストの正当性があることを再確認する価値があります。古いコードは見苦しいかもしれませんが、十分に機能する場合は、粗いエッジを付けて開発時間を他の場所に投資する方が安くなる可能性があります。
スミスコ

考えられる理由の1つは、すべての拡張プロジェクトの後に手動で再テストする労力とコストを削減することです。最後のプロジェクトの終わりに、手動テストは約2か月にわたって行われました。より自動化されたテストの導入により、この作業が1〜2週間に短縮される場合、それだけの価値があるかもしれません。
アンソニー

5
レガシーコードについては、このスタッフは非常に良いです!
仕事

合理的に一貫性があり、構造化されていることに同意します。私の主な目的は、変化の副作用を減らすことです。各プロジェクトの後(およびプロジェクト中)にアプリケーション全体を手動でテストするのに必要な労力は膨大です。Seleniumを使用してクライアント側でテストすることを考えていました-ServerFault(serverfault.com/questions/236546/…)に質問があり、データベースをすばやく元に戻すための提案を得ることができます。自動化された受け入れテストは、大規模な書き直しをすることなく、ほとんどの利点を得ると思います。
アンソニー

回答:


16

主なメッセージの1つが「ボーイスカウトルール」である2冊の本に言及しています。つまり、触れたときにコードをクリーンアップします。稼働中のシステムがある場合、大規模な書き直しは逆効果です。代わりに、新しい機能を追加するときに、コードをそのまま改善してください。

  • 変更する必要がある既存のコードをカバーする単体テストを作成します。
  • そのコードをリファクタリングして、変更に対してより柔軟になるようにします(テストに合格することを確認します)。
  • 新しい/改訂された機能のテストを書く
  • 新しいテストに合格するためのコードを書く
  • 必要に応じてリファクタリングします。

さらに詳しく説明するために、Feathersはアプリケーションをその継ぎ目でテストすること、つまりユニットが接続する論理ポイントについて話しています。シームを利用して、依存関係のスタブまたはモックを作成し、依存オブジェクトに関するテストを作成できます。AddressBOを例に取りましょう

public class AddressBO
{
    public TransferObject GetAddress(string addressID)
    {
        if (StringUtils.IsNull(addressID))
        {
            throw new ValidationException("Address ID must be entered");
        }

        AddressDAO addressDAO = new AddressDAO();
        return addressDAO.GetAddress(addressID);
    }
}

AddressBOとAddressDAOの間には明らかな継ぎ目があります。AddressDAOのインターフェイスを作成し、依存関係をAddressBOに注入できるようにします。

public interface IAddressDAO
{
  TransferObject GetAddress(addressID);
  //add other interface methods here.
}

public class AddressDAO:GenericDAO, IAddressDAO
{
  public TransferObject GetAddress(string addressID)
  {
    ///implementation goes here
  }
}

今、あなたは注射を許可するためにあなたのAddressBOをドクター

public class AddressBO
{
    private IAddressDAO _addressDAO;
    public AddressBO()
    {
      _addressDAO = new AddressDAO();
    }

    public AddressBO(IAddressDAO addressDAO)
    {
      _addressDAO = addressDAO;
    }

    public TransferObject GetAddress(string addressID)
    {
        if (StringUtils.IsNull(addressID))
        {
            throw new ValidationException("Address ID must be entered");
        }
        //call the injected AddressDAO
        return _addressDAO.GetAddress(addressID);
    }
}

ここでは、「貧しい人の依存性注入」を使用しています。私たちの唯一の目標は、継ぎ目を破り、AddressBOをテストできるようにすることです。ユニットテストでは、モックIAddressDAOを作成し、2つのオブジェクト間の相互作用を検証できます。


1
私は同意します-私はそれについて読んだときにこの戦略が好きでした。本当に価値を追加することなく、コードをクリーンアップするのに何ヶ月も費やすことができました。新しい機能を追加しながら、変更する必要があるものをクリーンアップすることに集中すれば、両方の長所を活用できます。
アンソニー

唯一の課題は、既存のコードの単体テストを作成することです。より信頼性の高い単体テストのリファクタリングと追加ができるように、まずはより高いレベルのテストに重点を置いています。
アンソニー

1
はい、できる最善のことは、コードが何をするかを検証するテストを書くことです。正しい動作を検証するテストを作成することもできますが、テストでカバーされていない他の機能を破壊するリスクがあります。
マイケルブラウン

これは、羽を「継ぎ目を見つける」ことを実践するために見た最も良い説明です。OOよりも手続きに詳しい人として、AddressBOとAddressDAOの間には明らかな継ぎ目がありますが、私には明らかではありませんでしたが、この例は本当に役立ちます。
SeraM

5

私が正しいことを覚えていれば、レガシーコードで効果的に働くことは、完全な書き換えは新しいコードが古いものよりも優れていることを保証しません(機能/欠陥の観点から)と言います。その本のリファクタリングは、バグの修正/新機能の追加を目的としています。

私がお勧めするもう1つの本は、.NETのBrownfield Application Developmentです。新しい機能を追加したり、欠陥を修正したりするたびに、安定した反復的な変更を行うことについて説明しています。費用対利益の考慮事項に対処し、一度に多すぎることをやめようとすることについて警告します。ながら作業を効率的にレガシーコードで、主にどのようにマイクロ/コードレベルでのリファクタリングに語る.NETでブラウンフィールドアプリケーション開発の際に再ファクタリング(あまりにもいくつかのコード・レベルのものと一緒に)、主に高レベルの考慮事項をカバーしています。

また、Brownfieldの本は、コードのどの領域が最も問題を引き起こしてそこに集中しているのかを把握することを提案しています。多くのメンテナンスを必要としない他の領域は、変更する価値がない場合があります。


.Netのブラウンフィールドアプリケーション開発の書籍の+1
Gabriel Mongeon

本の推薦をありがとう-それを見てみましょう。概要から、C、C ++、およびJavaに焦点を当てていると思われる2冊の本よりも、特に.NETに焦点を当てます。
アンソニー

4

このようなレガシアプリの場合、単体テストではなく、(自動化された)より高いレベルの統合テストでカバーすることから始める方がはるかに費用効率が高くなります。統合テストをセーフティネットとして使用することで、適切な場合、つまりリファクタリングのコストが長期にわたって利益をもたらす場合など、小さなステップでリファクタリングを開始できます。他の人が指摘したように、これは自明ではありません。

同様の質問に対する私の以前のこの回答も参照してください。それがあなたの役に立つことを願っています。


私は最初に、より高いレベルのテストに傾倒しています。私はDBAと協力して、テストを実行するたびにデータベースを元に戻す最適な方法を探しています。
アンソニー

1

実行中のコードの破棄と書き直しには注意してください(絶対にすべきではないこと)。確かにitいかもしれませんが、機能する場合はそのままにします。Joelのブログ投稿を参照してください。10歳以上であることは確かですが、まだ目標を達成しています。


私はそこでジョエルに反対します。彼が言ったことは、当時は関連性があると感じていたかもしれませんが、現在のMozilla Firefoxと呼ばれるものの書き換えではありませんか?
CashCow

1
はい。ただし、Netscapeを廃業にしています。最初からやり直すことは決して正しい選択ではないということではなく、非常に注意すべきことです。また、オブジェクト指向コードは、手続き型コードよりも常に優れているとは限りません。
ザカリーK

1

マイクが「ボーイスカウトルール」がここでおそらく最良であると述べたように、コードが機能し、バグレポートの一定のソースでない場合は、時間をかけてゆっくりと改善することをお勧めします。

拡張プロジェクト中に、物事の新しい方法を可能にします。たとえば、新しい機能にはORMを使用し、既存のデータレイヤーパターンをバイパスします。既存のコードを変更する必要がある機能強化を実行すると、関連するコードの一部を新しい方法に移行できる場合があります。場所でファサードまたはいくつかのアダプターを使用すると、おそらくレイヤーごとに古いコードを分離するのに役立ちます。これは、時間の経過とともに古いコードを枯渇させるのに役立ちます。

同様に、これは単体テストの追加に役立ちます。作成する新しいコードから開始し、新しい機能強化のために触れる必要がある古いコードのテストを徐々に追加できます。


1

どちらも良い本です。その方法でコードの書き換えを開始する場合は、コードを書き換える際に安定性を保つために、コードを単体テストでカバーし始めることも重要だと思います。

小さなステップで行う必要があり、そのようなコードを変更すると、システム全体が簡単に不安定になる可能性があります。

あなたが積極的に取り組んでいないコードは変更しません。これは、積極的に強化または修正しているコードでのみ行ってください。何かが目的を果たしているが、何年も変更されていない場合は、そのままにしておきます。より良い方法を知っていても、それはそれです。

結局のところ、会社には生産性が必要です。より良いコードは生産性を向上させますが、コードを書き直すだけでコードを書き直すことは、おそらく製品に価値をもたらす最良の方法ではありません。

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