Konwertuj kod proceduralny na obiektowy

16

Czytałem Efektywna praca ze starszymi kodami i czystym kodem w celu opracowania strategii uczenia się, w jaki sposób rozpocząć czyszczenie istniejącej bazy kodu dużej aplikacji WWW ASP.NET.

System ten istnieje od 2005 r. I od tego czasu został poddany wielu udoskonaleniom. Pierwotnie kod miał następującą strukturę (i nadal jest w dużej mierze zbudowany w ten sposób):

  • ASP.NET (aspx / ascx)
  • Kod z tyłu (c #)
  • Warstwa logiki biznesowej (c #)
  • Warstwa dostępu do danych (c #)
  • Baza danych (Oracle)

Głównym problemem jest to, że kod podszywa się pod proceduralnie jako obiektowy. To praktycznie narusza wszystkie wytyczne opisane w obu książkach.

To jest przykład typowej klasy w warstwie logiki biznesowej:

    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 >>

}

Ma wiele powielania, klasa ma szereg obowiązków itp. Itd. - jest to po prostu kod „nieczyszczony”.

Cały kod w całym systemie zależy od konkretnych implementacji.

To jest przykład typowej klasy w warstwie dostępu do danych:

    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 >>
}

System ten został opracowany przeze mnie i mały zespół w 2005 roku po tygodniowym kursie .NET. Wcześniej moje doświadczenie dotyczyło aplikacji klient-serwer. W ciągu ostatnich 5 lat zauważyłem zalety automatycznych testów jednostkowych, automatycznych testów integracji i automatycznych testów akceptacyjnych (przy użyciu Selenium lub równoważnego), ale obecna baza kodu wydaje się niemożliwa do wprowadzenia tych koncepcji.

Zaczynamy teraz pracować nad dużym projektem udoskonalenia z napiętymi ramami czasowymi. Zespół składa się z 5 programistów .NET - 2 programistów z kilkuletnim doświadczeniem .NET i 3 innych z niewielkim lub zerowym doświadczeniem .NET. Żaden z członków zespołu (w tym ja) nie ma doświadczenia w używaniu testowania jednostkowego .NET lub kpowania z frameworków.

Jakiej strategii użyłbyś, aby uczynić ten kod czystszym, bardziej obiektowym, testowalnym i łatwym w utrzymaniu?

Anthony
źródło
9
Nawiasem mówiąc, warto dwukrotnie sprawdzić, czy przepisanie systemu jest uzasadnione kosztami. Stary kod może być brzydki, ale jeśli działa wystarczająco dobrze, taniej jest umieścić surowe krawędzie i zainwestować swój czas w rozwój gdzie indziej.
smithco,
Jednym z możliwych uzasadnień jest zmniejszenie wysiłku i kosztów ręcznego ponownego testowania po każdym projekcie ulepszeń. Pod koniec ostatniego projektu testy ręczne trwały około 2 miesięcy. Jeśli wprowadzenie bardziej zautomatyzowanych testów zmniejszy ten wysiłek do 1-2 tygodni, być może warto.
Anthony
5
W ZAKRESIE KODEKSU LEGACYJNEGO, NINIEJSZYM POTRZEBUJE DOBRZE!
Job
Zgadzam się, że jest dość spójny i zorganizowany. Moim głównym celem jest ograniczenie skutków ubocznych zmian. Wysiłek wymagany do ręcznego przetestowania całej aplikacji po (i podczas) każdego projektu jest ogromny. Myślałem o użyciu Selenium do przetestowania go po stronie klienta - mam pytanie dotyczące ServerFault ( serverfault.com/questions/236546 / ... ), aby uzyskać sugestie dotyczące szybkiego przywrócenia bazy danych. Uważam, że zautomatyzowane testy akceptacyjne przyniosłyby większość korzyści bez konieczności masowego przepisywania.
Anthony

Odpowiedzi:

16

Wspominasz o dwóch książkach, w których jedną z głównych wiadomości jest „Reguła skautowa”, tj. Wyczyść kod po jego dotknięciu. Jeśli masz działający system, masowe przepisywanie przyniesie efekt przeciwny do zamierzonego. Zamiast tego, dodając nową funkcjonalność, popraw kod w jego obecnej postaci.

  • Napisz testy jednostkowe obejmujące istniejący kod, który musisz zmienić.
  • Zmodyfikuj ten kod, aby był bardziej podatny na zmiany (upewniając się, że testy nadal przebiegają).
  • Napisz testy nowej / poprawionej funkcjonalności
  • Napisz kod, aby nowe testy przeszły pomyślnie
  • Refaktoryzuj w razie potrzeby.

Aby zagłębić się dalej, Feathers mówi o testowaniu aplikacji w jej szwach: logiczne punkty, w których łączą się jednostki. Możesz skorzystać ze szwu, aby utworzyć kod pośredniczący lub próbny dla zależności, aby móc pisać testy wokół obiektu zależnego. Weźmy twój AddressBO jako przykład

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);
    }
}

Istnieje oczywisty szew między AddressBO a AddressDAO. Utwórzmy interfejs dla AddressDAO i pozwólmy na wstrzyknięcie zależności do 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
  }
}

Teraz dokonasz dokowania adresu AddressBO, aby umożliwić wstrzyknięcie

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);
    }
}

Tutaj używamy „zastrzyku zależności biednego człowieka”. Naszym jedynym celem jest przełamanie szwu i umożliwienie nam przetestowania AddressBO. Teraz w naszych testach jednostkowych możemy wykonać próbny IAddressDAO i zweryfikować interakcje między dwoma obiektami.

Michael Brown
źródło
1
Zgadzam się - podobała mi się ta strategia, kiedy o niej czytałem. Możemy spędzić miesiące na usuwaniu kodu bez dodawania wartości. Jeśli skupimy się na oczyszczeniu tego, co musimy zmienić, dodając nową funkcję, otrzymamy to, co najlepsze z obu światów.
Anthony
Jedynym wyzwaniem jest napisanie testów jednostkowych dla istniejącego kodu. Najpierw skłaniam się ku testom wyższego poziomu, abyśmy mogli przefakturować i dodać testy jednostkowe z większą pewnością.
Anthony
1
Tak, najlepsze, co możesz zrobić, to napisać testy weryfikujące, czy kod robi to, co robi. Możesz spróbować utworzyć testy, które weryfikują prawidłowe zachowanie ... ale ryzykujesz uszkodzenie innych funkcji, które nie są objęte testami.
Michael Brown
To najlepsze wytłumaczenie, jakie widziałem w praktyce, aby Pióra „znalazły szew”. Jako ktoś bardziej zaznajomiony z procedurami niż OO, istnieje oczywisty szew między AddressBO a AddressDAO, który nie był dla mnie oczywisty, ale ten przykład naprawdę pomaga.
SeraM
5

Jeśli dobrze pamiętam, Efektywna praca ze starszym kodem mówi, że pełne przepisanie nie gwarantuje, że nowy kod będzie lepszy od starego (z punktu widzenia funkcjonalności / wad). Refaktoryzacje w tej książce służą do naprawiania błędów / dodawania nowych funkcji.

Kolejną książką, którą poleciłbym, jest Brownfield Application Development w .NET, która w zasadzie mówi, aby nie robić także pełnego przepisywania. Mówi o wprowadzaniu stałych, iteracyjnych zmian za każdym razem, gdy dodajesz nowe funkcje lub naprawiasz wady. Odnosi się do rozważań dotyczących kosztów i korzyści i ostrzega przed próbą zbytniego odgryzienia za jednym razem. Podczas gdy Efektywna praca ze starszym kodem mówi głównie o tym, jak refaktoryzować na poziomie mikro / kodu, Brownfield Application Development w .NET , obejmuje głównie kwestie wyższego poziomu przy ponownym faktorowaniu (wraz z niektórymi rzeczami na poziomie kodu).

Książka Brownfielda sugeruje także, aby dowiedzieć się, który obszar kodu powoduje najwięcej problemów i skupić się tam. Wszelkie inne obszary, które nie wymagają dużej konserwacji, mogą nie być warte zmiany.

Matt
źródło
+1 za książkę Brownfield Application Development w .Net
Gabriel Mongeon
Dzięki za rekomendację książki - przyjrzę się jej. Z przeglądu będzie bardziej skoncentrowany na .NET niż na dwóch wspomnianych przeze mnie książkach, które wydają się skupiać na C, C ++ i Javie.
Anthony
4

W przypadku takiej starszej aplikacji znacznie bardziej opłacalne jest rozpoczęcie od objęcia jej (automatycznymi) testami integracji wyższego poziomu, a nie testami jednostkowymi. Następnie z testami integracyjnymi jako siatką bezpieczeństwa, możesz rozpocząć refaktoryzację małymi krokami, jeśli jest to właściwe, tj. Jeśli koszt refaktoryzacji zwróci się w dłuższej perspektywie. Jak zauważyli inni, nie jest to oczywiste.

Zobacz także moją wcześniejszą odpowiedź na podobne pytanie; mam nadzieję, że okażą się przydatne.

Péter Török
źródło
Zaczynam od testów na wyższym poziomie. Współpracuję z DBA, aby znaleźć najlepszy sposób na przywrócenie bazy danych po każdym wykonaniu testu.
Anthony
1

Bądź bardzo ostrożny przy wyrzucaniu i przepisywaniu działającego kodu ( rzeczy, których nigdy nie powinieneś robić ). Jasne, może to być brzydkie, ale jeśli działa, zostaw to. Zobacz post na blogu Joela, z pewnością ma ponad 10 lat, ale nadal jest trafiony w cel.

Zachary K.
źródło
Nie zgadzam się z Joelem. To, co powiedział, mogło w tym czasie wydawać się istotne, ale czy nie przepisuje tego, co nazywa się teraz Mozilla Firefox?
CashCow
1
Tak, ale w tym procesie netscape przestał działać! Nie oznacza to, że rozpoczynanie od nowa nigdy nie jest właściwym wyborem, ale należy zachować ostrożność. A kod OO nie zawsze jest lepszy niż kod proceduralny.
Zachary K
1

Jak stwierdził Mike, „zasada zwiadowcy” jest prawdopodobnie najlepsza tutaj, jeśli kod działa i nie jest stałym źródłem raportów o błędach, wolałbym pozwolić mu tam siedzieć i powoli go ulepszać.

Podczas projektu ulepszeń uwzględnij nowe sposoby robienia rzeczy. Na przykład użyj ORM dla nowych funkcji i pomiń istniejący wzorzec warstwy danych. Po napotkaniu ulepszeń, które muszą dotykać istniejącego kodu, możesz być w stanie przenieść część pokrewnego kodu na nowy sposób robienia rzeczy. Użycie fasady lub niektórych adapterów w niektórych miejscach może pomóc w wyodrębnieniu starego kodu, być może nawet na warstwę. To może pomóc ci zagłodzić stary kod z czasem.

Podobnie może to pomóc w dodawaniu testów jednostkowych, możesz zacząć od nowego kodu, który tworzysz i powoli dodawać testy dla starego kodu, którego musisz dotknąć, aby uzyskać nowe ulepszenia.

Joppe
źródło
1

To są dobre książki. Jeśli zamierzasz zacząć przepisywać kod w ten sposób, myślę, że ważne jest, aby zacząć również obejmować kod testami jednostkowymi, aby pomóc zachować stabilność podczas przepisywania.

Należy to zrobić małymi krokami, a modyfikacja tego rodzaju kodu może łatwo zdestabilizować cały system.

Nie zmodyfikowałbym żadnego kodu, nad którym nie aktywnie pracujesz. Rób to tylko na kodzie, który aktywnie poprawiasz lub naprawiasz. Jeśli coś służy swojemu celowi, ale nie było modyfikowane od lat, po prostu to zostaw. Robi to, nawet jeśli znasz lepszy sposób.

Na koniec firma potrzebuje wydajności. Chociaż lepszy kod zwiększa wydajność przepisywania kodu tylko dlatego, że można go lepiej napisać, prawdopodobnie nie jest to najlepszy sposób na zwiększenie wartości produktu.

Honorowy Chow
źródło