절차 적 코드에서 객체 지향 코드로 변환


16

대규모 ASP.NET 웹 양식 응용 프로그램의 기존 코드베이스를 정리하는 방법에 대한 학습 전략을 목표 로 레거시 코드클린 코드를 사용한 효과적인 작업을 읽었 습니다.

이 시스템은 2005 년부터 사용되어 왔으며 그 이후로 여러 가지 기능이 향상되었습니다. 원래 코드는 다음과 같이 구성되었습니다 (그리고 여전히 대부분 이런 식으로 구성됩니다).

  • ASP.NET (aspx / ascx)
  • 코드 숨김 (C #)
  • 비즈니스 로직 계층 (C #)
  • 데이터 액세스 계층 (C #)
  • 데이터베이스 (오라클)

주요 문제는 코드가 객체 지향적 절차 적 가면극이라는 점입니다. 두 책 모두에 설명 된 모든 지침을 사실상 위반합니다.

다음은 비즈니스 로직 계층의 일반적인 클래스 예입니다.

    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 년 동안 저는 자동화 된 단위 테스트, 자동화 된 통합 테스트 및 자동화 된 수락 테스트 (셀레늄 또는 이와 동등한 기능 사용)의 이점을 인식하게되었지만 현재 코드 기반에서는 이러한 개념을 도입하는 것이 불가능 해 보입니다.

우리는 이제 엄격한 시간 프레임으로 주요 개선 프로젝트를 시작하고 있습니다. 이 팀은 5 명의 .NET 개발자로 구성되어 있습니다. 2 년 동안 .NET 경험을 가진 개발자 2 명과 .NET 경험이 거의 없거나 전혀없는 3 명의 개발자로 구성되어 있습니다. 자신을 포함한 팀은 .NET 단위 테스트 또는 모의 프레임 워크 사용 경험이 없습니다.

이 코드를보다 깨끗하고 객체 지향적이며 테스트 및 유지 관리하기 위해 어떤 전략을 사용 하시겠습니까?


9
제쳐두고, 시스템을 다시 작성하는 데 비용이 정당하다는 것을 다시 확인하는 것이 좋습니다. 이전 코드는 추악한 경우도 있지만 제대로 작동하면 거친 부분을 넣고 개발 시간을 다른 곳에 투자하는 것이 더 저렴할 수 있습니다.
smithco

하나의 가능한 정당화는 모든 개선 프로젝트 후에 수동 재시험의 노력과 비용을 줄이는 것입니다. 마지막 프로젝트가 끝날 무렵, 수동 테스트는 약 2 개월 동안 진행되었습니다. 보다 자동화 된 테스트를 도입하여이 노력을 1-2 주로 줄이면 그만한 가치가 있습니다.
Anthony

5
레거시 코드의 경우,이 학생은 기분이 좋습니다!
직업

나는 그것이 합리적으로 일관성 있고 구조적이라는 데 동의합니다. 저의 주요 목표는 변화의 부작용을 줄이는 것입니다. 각 프로젝트 후 (및 동안) 전체 응용 프로그램을 수동으로 테스트하는 데 많은 노력이 필요합니다. Selenium을 사용하여 클라이언트 측을 통해 테스트하는 것에 대해 생각했습니다 .ServerFault ( serverfault.com/questions/236546/… ) 에 대한 질문이 있어 데이터베이스를 빠르게 되돌릴 수있는 방법에 대한 제안을받습니다. 자동화 된 승인 테스트는 대규모 재 작성 없이도 대부분의 이점을 얻을 수 있다고 생각합니다.
Anthony

답변:


16

주요 메시지 중 하나가 "보이 스카우트 규칙"인 두 권의 책에 대해 언급했습니다. 즉, 코드를 만질 때 코드를 정리하십시오. 작동중인 시스템이있는 경우 대량 재 작성은 비생산적입니다. 대신, 새로운 기능을 추가 할 때 코드를 그대로 개선하십시오.

  • 변경해야하는 기존 코드를 포괄하는 단위 테스트를 작성하십시오.
  • 변경에보다 유연하도록 코드를 리팩터링하십시오 (테스트가 여전히 통과하는지 확인).
  • 새로운 기능 / 수정 된 기능에 대한 테스트 작성
  • 새로운 테스트를 통과시키기위한 코드 작성
  • 필요에 따라 리 팩터하십시오.

더 자세히 알아보기 위해 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를 만들고 두 개체 간의 상호 작용을 확인할 수 있습니다.


1
동의합니다-이 전략을 읽을 때이 전략이 마음에 들었습니다. 실제로 가치를 추가하지 않고도 코드를 정리하는 데 몇 개월을 소비 할 수 있습니다. 새로운 기능을 추가하면서 변경해야 할 사항을 정리하는 데 집중하면 두 가지 이점을 모두 누릴 수 있습니다.
Anthony

유일한 과제는 기존 코드에 대한 단위 테스트를 작성하는 것입니다. 더 높은 수준의 테스트에 더 집중하고 있으므로 단위 테스트를보다 확실하게 리팩토링하고 추가 할 수 있습니다.
Anthony

1
예, 코드가 수행하는 작업을 확인하는 테스트를 작성하는 것이 최선입니다. 올바른 동작 을 확인하는 테스트를 만들 수는 있지만 테스트에 포함되지 않은 다른 기능을 중단 할 위험이 있습니다.
마이클 브라운

이것은 깃털을 "이음새 찾기"에 적용 할 때 제가 본 최고의 설명입니다. 누군가가 OO보다 절차에 정통한 것처럼 AddressBO와 AddressDAO 사이에는 분명한 이음새가 없었지만이 예제는 실제로 도움이됩니다.
SeraM

5

내가 레거시 코드를 효과적으로 사용 한다고 생각한다면, 다시 작성할 때 전체 코드 를 작성한다고해서 새로운 코드가 이전 코드보다 더 나을 것이라고 보장 할 수는 없습니다 (기능 / 결함의 관점에서). 이 책의 리팩토링은 버그를 수정하거나 새로운 기능을 추가 할 때 사용됩니다.

내가 추천 할 또 다른 책 은 .NET의 Brownfield Application Development입니다 . 새로운 기능을 추가하거나 결함을 수정할 때마다 꾸준하고 반복적 인 변경을하는 것에 대해 설명합니다. 비용 대 이익 고려 사항을 해결하고 한 번에 너무 많이 물지 않도록 경고합니다. 레거시 코드를 효과적으로 사용 하면서 마이크로 / 코드 레벨에서 리팩토링하는 방법에 대해 주로 이야기 하지만 .NET의 Brownfield Application Development는 리팩토링시 일부 코드 레벨과 함께 고려해야 할 사항을 대부분 다루고 있습니다.

Brownfield 책은 또한 코드의 어느 부분이 가장 문제가되고 초점을 맞추고 있는지 알아내는 것을 제안합니다. 유지 보수가 많이 필요하지 않은 다른 영역은 변경할 가치가 없습니다.


.Net의 Brownfield Application Development 책에 +1
Gabriel Mongeon

책 추천에 감사드립니다-한번 살펴 보겠습니다. 개요에서 C, C ++ 및 Java에 중점을 둔 필자가 언급 한 두 권의 책보다 .NET에 중점을 둡니다.
Anthony

4

이러한 레거시 앱의 경우 단위 테스트가 아닌 (자동화 된) 더 높은 수준의 통합 테스트로 시작하는 것이 훨씬 비용 효율적입니다. 그런 다음 안전망으로 통합 테스트를 수행 하는 것이 적절한 경우, 즉 리팩토링 비용이 장기적으로 자신을 상환하는 경우 작은 단계로 리팩토링을 시작할 수 있습니다 . 다른 사람들이 지적했듯이, 이것은 자명하지 않습니다.

비슷한 질문에 대한 나의 이전 답변 도 참조하십시오 . 도움이 되길 바랍니다.


나는 더 높은 수준의 테스트로 시작하고 있습니다. 각 테스트 실행 후 데이터베이스를 되 돌리는 가장 좋은 방법을 찾기 위해 DBA와 협력하고 있습니다.
Anthony

1

실행 코드를 버리고 다시 작성하는 데 매우주의하십시오 ( 하지 말아야 할 것 ). 물론 못 생겼을 수도 있지만 작동하면 그대로 두십시오. Joel의 블로그 게시물을 참조하십시오. 10 세 이상은되었지만 여전히 목표에 있습니다.


나는 거기서 Joel과 동의하지 않습니다. 그가 말한 것은 당시에는 적절하다고 생각했지만 현재 Mozilla Firefox를 다시 쓰지는 않습니까?
CashCow

1
네,하지만 그것은 프로세스에서 넷스케이프를 중단 시켰습니다! 다시 시작하는 것이 올바른 선택이 아니라 매우 조심해야 할 것이라 말하는 것입니다. 그리고 OO 코드가 절차 코드보다 항상 좋은 것은 아닙니다.
Zachary K

1

Mike가 말한 것처럼 '보이 스카우트 규칙'이 여기에 가장 좋을 것입니다. 코드가 작동하고 버그 보고서의 지속적인 소스가 아닌 경우 시간이 지남에 따라 천천히 배치하고 개선하는 것이 좋습니다.

향상 프로젝트 중에는 새로운 방식으로 작업을 수행 할 수 있습니다. 예를 들어 새 기능에 ORM을 사용하고 기존 데이터 계층 패턴을 무시하십시오. 기존 코드를 터치해야하는 향상된 기능을 사용할 때 관련 코드 중 일부를 새로운 방식으로 옮길 수 있습니다. 장소에서 파사드 나 일부 어댑터를 사용하면 레이어 당 심지어 이전 코드를 격리하는 데 도움이 될 수 있습니다. 이렇게하면 시간이 지남에 따라 이전 코드가 고갈되는 데 도움이 될 수 있습니다.

마찬가지로 단위 테스트를 추가하는 데 도움이 될 수 있습니다. 새 코드로 시작하여 새로운 개선 사항을 위해 터치해야하는 이전 코드에 대한 테스트를 천천히 추가 할 수 있습니다.


1

그것들은 모두 좋은 책입니다. 그런 식으로 코드를 다시 작성하려면 코드를 다시 작성할 때 코드를 안정적으로 유지하기 위해 단위 테스트로 코드를 다루는 것이 중요하다고 생각합니다.

작은 단계로 수행해야하며 이러한 종류의 코드를 수정하면 전체 시스템을 쉽게 불안정하게 만들 수 있습니다.

적극적으로 작업하지 않는 코드는 수정하지 않습니다. 적극적으로 개선하거나 수정하는 코드에서만이 작업을 수행하십시오. 무언가를 제공하는 것이 목적이지만 수년 동안 수정되지 않은 경우 그대로 두십시오. 더 나은 방법을 알고 있더라도 그렇게하고 있습니다.

하루가 끝나면 회사는 생산성이 필요합니다. 더 나은 코드는 생산성을 높이는 반면 코드를 다시 작성하면 더 잘 작성 될 수 있기 때문에 제품에 가치를 부여하는 가장 좋은 방법은 아닐 것입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.