Перетворити з процедурного в об'єктно-орієнтований код


16

Я читав ефективні роботи з Legacy Code та Clean Code з метою вивчення стратегій щодо того, як почати очищення існуючої бази коду великого додатку для веб-форм ASP.NET.

Ця система існує з 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 >>
}

Ця система була розроблена мною і невеликою командою ще в 2005 році після 1 тижневого курсу .NET. До цього мій досвід був у програмах клієнт-сервер. Протягом останніх 5 років я визнав переваги автоматизованого тестування одиниць, автоматизованого тестування інтеграції та автоматизованого тестування приймання (з використанням селену чи еквівалента), але нинішня база коду видається неможливою для впровадження цих понять.

Зараз ми починаємо працювати над великим проектом вдосконалення з обмеженими часовими рамками. Команда складається з 5 розробників .NET - 2 розробника з декількома роками досвіду .NET і 3 інших, які мають мало. Ніхто з команди (включаючи мене) не має досвіду використання тестування .NET-блоку або знущання з фреймворків.

Яку стратегію ви б використали, щоб зробити цей код більш чистим, більш об'єктно-орієнтованим, перевіреним та ремонтом?


9
В сторону, можливо, варто подвоїти перевірку наявності виправдання витрат для переписування системи. Старий код може бути некрасивим, але якщо він працює досить добре, можливо, дешевше поставити його з грубими краями та інвестувати час на розробку в інше місце.
smithco

Одне можливе обгрунтування - зменшити зусилля та витрати на ручне повторне тестування після кожного проекту з удосконалення. Наприкінці останнього проекту ручне тестування тривало близько 2 місяців. Якщо впровадження більш автоматизованого тестування скоротить ці зусилля до 1-2 тижнів, це може бути варто.
Ентоні

5
ДЛЯ ЛЕГАЦІЙНОГО КОДУ ЦЕЙ СТЕЙФ ЗАЙМАЄ ДОБРО!
Робота

Я погоджуюся, що це досить послідовно та структуровано. Моя головна мета - зменшити побічні ефекти змін. Зусилля, необхідні для тестування всієї програми після (і під час) кожного проекту, є величезними. Я думав про те, щоб використати Selenium для тестування його на стороні клієнта - у мене є питання щодо ServerFault ( serverfault.com/questions/236546/… ), щоб отримати пропозиції щодо швидкого відновлення бази даних. Я відчуваю, що автоматичне тестування прийняття отримало б більшість переваг, не потребуючи масового переписування.
Антоній

Відповіді:


16

Ви згадуєте дві книги, в яких одне з основних повідомлень - "Правило скаутського хлопця", тобто очищайте код, коли ви торкаєтесь його. Якщо у вас працює робоча система, масове перезапис є контрпродуктивним. Натомість, додаючи нову функціональність, переконайтеся, що ви покращуєте код у такому стані.

  • Напишіть одиничні тести, щоб покрити існуючий код, який потрібно змінити.
  • Refactor цей код, щоб він був більш надійним для змін (переконайтеся, що ваші тести все-таки пройдуть).
  • Напишіть тести на нову / переглянутий функціонал
  • Напишіть код, щоб пройти нові тести
  • Refactor за необхідності.

Щоб пірнути далі, Пірс розповідає про тестування програми у своїх швах: логічні точки, в яких з'єднуються одиниці. Ви можете скористатися швом, щоб створити заглушку або макет для залежності, щоб ви могли писати тести навколо залежного об'єкта. Візьмемо ваш приклад 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
Я згоден - ця стратегія мені сподобалась, коли я читав про неї. Ми могли витратити місяці на очищення коду, не дійсно додаючи значення. Якщо ми зосередимось на очищенні того, що нам потрібно змінити, додаючи нову функцію, ми отримаємо найкраще з обох світів.
Антоній

Єдиним завданням є написання одиничних тестів для існуючого коду. Я спершу більше схиляюся до випробувань вищого рівня, щоб ми могли рефакторировать і додавати одиничні тести з більшою впевненістю.
Антоній

1
Так, найкраще, що ви можете зробити, це написати тести, які підтверджують, що код робить те, що він робить. Ви можете спробувати створити тести, які підтверджують правильність поведінки ..., але ви ризикуєте порушити іншу функціональність, яка не покрита тестами.
Майкл Браун

Це найкраще пояснення, яке я бачив для того, щоб пір'я "знайти шов" на практиці. Оскільки хтось більше розбирається в процедурному відношенні, ніж OO, існує очевидний шов між AddressBO та AddressDAO не був очевидним для мене, але цей приклад справді допомагає.
SeraM

5

Якщо я пам’ятаю правильну роботу «Ефективно з Legacy Code», говорить про те, що перезапис у повному обсязі не гарантує, що новий код буде кращим за старий (з точки зору функціональності / дефектів). Повторні роботи в цій книзі призначені для виправлення помилок / додавання нових функцій.

Ще одна книга, яку я рекомендував би - це Brownfield Application Development в .NET, яка в основному говорить, що також не робити повну перезапис. Він говорить про внесення постійних, ітеративних змін кожного разу, коли ви додаєте нові функції або виправляєте дефекти. Він вирішує питання, пов'язані з витратами та вигодами, і попереджає про спробу викусити занадто багато за один раз. У той час як ефективно працює з Legacy Code, в основному йдеться про те, як змінити рефакторинг на рівні мікро / коду, Brownfield Application Development в .NET , в основному, охоплює міркування вищого рівня при повторному факторингу (разом з деякими матеріалами на рівні коду).

Книга Браунфілда також пропонує з’ясувати, яка область коду доставляє вам найбільше проблем і зосередитись на ньому. Будь-які інші сфери, які не потребують великого обслуговування, можуть не варто змінювати.


+1 для книги "Розробка додатків Браунфілд" у .Net
Габріель Монджен

Дякую за рекомендацію книги - я погляну на це. З огляду він буде більш зосереджений на .NET конкретно, ніж на двох згаданих мною книгах, які, здається, зосереджені на C, C ++ та Java.
Антоній

4

Для такої застарілої програми набагато вигідніше почати, покриваючи її (автоматизованими) тестами інтеграції вищого рівня, а не одиничними тестами. Тоді, використовуючи тести інтеграції як вашу мережу безпеки, ви можете почати рефакторинг невеликими кроками, якщо це доречно, тобто якщо витрати на рефакторинг окупляться протягом тривалого періоду. Як зазначали інші, це не є очевидним.

Дивіться також цю попередню мою відповідь на подібне питання; сподіваємось, ви вважаєте це корисним.


Я схиляюся до випробувань вищого рівня для початку. Я працюю з DBA, щоб розробити найкращий спосіб відновити базу даних після кожного тестового виконання.
Ентоні

1

Будьте дуже обережні з викиданням та перезаписом бігового коду ( речі, які ви ніколи не повинні робити ). Звичайно, це може бути некрасиво, але якщо він працює, залиште його. Дивіться допис у блозі Джоела, впевнений, що йому 10 років, але це все ще правильно на цілі.


Я не погоджуюся з Джоелем там. Те, що він сказав, могло відчути себе актуальним у той час, але чи не переписати те, що зараз називається Mozilla Firefox?
CashCow

1
Так, але це ставить мережевий пейзаж поза бізнесом! Це не говорить про те, що починати спочатку ніколи не є правильним вибором, але це щось дуже обережне. І код ОО не завжди краще, ніж процедурний код.
Захарій К

1

Як заявив Майк, "правило хлопчика-скаута", мабуть, найкраще тут, якщо код працює і не є постійним джерелом звітів про помилки, я вважаю за краще дозволити йому сидіти там і вдосконалювати його повільно з часом.

Під час проекту з вдосконалення дозвольте створити нові способи здійснення дій. Наприклад, використовувати ORM для нових функцій та обійти існуючий шаблон рівня даних. Коли ви стикаєтеся з вдосконаленнями, які потребують дотику до існуючого коду, можливо, ви зможете перенести частину відповідного коду на новий спосіб виконання дій. Використання місцями фасаду чи деяких адаптерів може допомогти вам виділити старий код, можливо, навіть на шар. Це може допомогти вам голодувати за старий код через час.

Так само це може допомогти вам з додаванням одиничних тестів, ви можете почати з нового коду, який ви створюєте, і повільно додавати кілька тестів до старого коду, який потрібно торкнутися для нових вдосконалень.


1

Це обидві хороші книги. Якщо ви збираєтеся розпочати перезапис коду таким чином, я думаю, що важливо також почати покривати код одиничними тестами, щоб зберегти його стабільним під час переписування.

Це потрібно робити невеликими кроками, і зміна такого коду може легко дестабілізувати всю систему.

Я б не змінював жоден код, над яким ви активно не працюєте. Робіть це лише за кодом, який ви активно вдосконалюєте або фіксуєте. Якщо щось служить його цілі, але не змінювалося роками, просто залиште це. Це робиться справа, навіть якщо ти знаєш кращий спосіб.

Наприкінці дня компанія потребує продуктивності. Хоча кращий код збільшує продуктивність коду для перезапису лише тому, що він міг бути написаний краще, мабуть, не найкращий спосіб принести значення вашому продукту.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.