Converti da codice procedurale a codice orientato agli oggetti


16

Ho letto Lavorare in modo efficace con Legacy Code e Clean Code con l'obiettivo di apprendere strategie su come iniziare a ripulire la base di codice esistente di una grande applicazione webforms ASP.NET.

Questo sistema esiste dal 2005 e da allora ha subito numerosi miglioramenti. Inizialmente il codice era strutturato come segue (ed è ancora ampiamente strutturato in questo modo):

  • ASP.NET (aspx / ascx)
  • Code-behind (c #)
  • Livello di logica aziendale (c #)
  • Livello di accesso ai dati (c #)
  • Database (Oracle)

Il problema principale è che il codice è procedurale mascherato da orientato agli oggetti. Violi virtualmente tutte le linee guida descritte in entrambi i libri.

Questo è un esempio di una classe tipica nel livello della logica di business:

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

}

Ha molte duplicazioni, la classe ha una serie di responsabilità, ecc. Ecc. - È generalmente un codice "non pulito".

Tutto il codice in tutto il sistema dipende da implementazioni concrete.

Questo è un esempio di una classe tipica nel livello di accesso ai dati:

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

Questo sistema è stato sviluppato da me e da un piccolo team nel 2005 dopo un corso .NET di 1 settimana. Prima della mia esperienza era in applicazioni client-server. Negli ultimi 5 anni sono arrivato a riconoscere i vantaggi del testing di unità automatizzato, test di integrazione automatizzati e test di accettazione automatizzati (usando Selenium o equivalente), ma l'attuale base di codice sembra impossibile introdurre questi concetti.

Ora stiamo iniziando a lavorare su un importante progetto di miglioramento con tempi ristretti. Il team è composto da 5 sviluppatori .NET - 2 sviluppatori con alcuni anni di esperienza .NET e altri 3 con esperienza .NET scarsa o nulla. Nessuno del team (incluso me stesso) ha esperienza nell'uso di .NET unit test o di mocking framework.

Quale strategia useresti per rendere questo codice più pulito, più orientato agli oggetti, testabile e gestibile?


9
A parte questo, può valere la pena verificare che ci sia una giustificazione dei costi per riscrivere il sistema. Il vecchio codice potrebbe essere brutto, ma se funziona abbastanza bene potrebbe essere più economico mettere i bordi grezzi e investire il tuo tempo di sviluppo altrove.
smithco,

Una possibile giustificazione è quella di ridurre lo sforzo e il costo dei test manuali dopo ogni progetto di miglioramento. Alla fine dell'ultimo progetto, i test manuali sono durati circa 2 mesi. Se l'introduzione di test più automatici riduce questo sforzo a 1-2 settimane, potrebbe valerne la pena.
Anthony,

5
PER CODICE DI LEGATURA, QUESTO STUFF È DARO BUONO!
Giobbe

Sono d'accordo che sia ragionevolmente coerente e strutturato. Il mio obiettivo principale è ridurre gli effetti collaterali del cambiamento. Lo sforzo richiesto per testare manualmente l'intera applicazione dopo (e durante) ogni progetto è enorme. Avevo pensato di utilizzare Selenium per testarlo sul lato client - ho una domanda su ServerFault ( serverfault.com/questions/236546/… ) per ottenere suggerimenti su come ripristinare rapidamente il database. Ritengo che i test di accettazione automatizzati otterrebbero la maggior parte dei vantaggi senza dover eseguire una riscrittura di massa.
Anthony,

Risposte:


16

Citi due libri in cui uno dei messaggi principali è "La regola del boy scout", ovvero ripulisci il codice quando lo tocchi. Se hai un sistema funzionante, una riscrittura massiccia è controproducente. Invece, quando aggiungi nuove funzionalità, assicurati di migliorare il codice così com'è.

  • Scrivi unit test per coprire il codice esistente che devi modificare.
  • Rifattorizzare quel codice in modo che sia più flessibile per il cambiamento (assicurandosi che i test continuino a passare).
  • Scrivi test per la funzionalità nuova / rivista
  • Scrivi il codice per far passare i nuovi test
  • Rifrattore se necessario.

Per approfondire, Feathers parla del test dell'applicazione nelle sue giunture: i punti logici in cui le unità si connettono. È possibile sfruttare una cucitura per creare uno stub o un mock per una dipendenza in modo da poter scrivere test attorno all'oggetto dipendente. Prendiamo la tua AddressBO come esempio

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

C'è un'ovvia giuntura tra AddressBO e AddressDAO. Creiamo un'interfaccia per AddressDAO e consentiamo l'iniezione della dipendenza in 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
  }
}

Ora aggiusti il ​​tuo AddressBO per consentire l'iniezione

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

Qui stiamo usando "l'iniezione di dipendenza dei poveri". Il nostro unico obiettivo è rompere la cucitura e permetterci di testare la AddressBO. Ora nei nostri test unitari possiamo fare un finto IAddressDAO e validare le interazioni tra i due oggetti.


1
Sono d'accordo - mi è piaciuta questa strategia quando ne ho letto. Potremmo passare mesi a ripulire il codice senza davvero aggiungere valore. Se ci concentriamo sulla pulizia di ciò che dobbiamo cambiare durante l'aggiunta di una nuova funzionalità, otteniamo il meglio da entrambi i mondi.
Anthony,

L'unica sfida è scrivere i test unitari per il codice esistente. In primo luogo, mi rivolgo di più ai test di livello più alto, in modo da poter refactoring e aggiungere test unitari con maggiore sicurezza.
Anthony

1
Sì, il meglio che puoi fare è scrivere test per verificare che il codice faccia quello che fa. Puoi provare a creare test che verificano il comportamento corretto ... ma corri il rischio di interrompere altre funzionalità non coperte dai test.
Michael Brown,

Questa è la migliore spiegazione che ho visto per mettere in pratica Feathers "trova una cucitura". Come qualcuno più esperto di procedurale rispetto a OO, c'è una ovvia cucitura tra AddressBO e AddressDAO non era ovvio per me, ma questo esempio aiuta davvero.
SeraM,

5

Se ricordo bene Lavorare in modo efficace con il codice legacy dice che una riscrittura completa non garantisce che il nuovo codice sarebbe migliore del vecchio (dal punto di vista della funzionalità / dei difetti). I refactoring di quel libro riguardano la correzione di bug / l'aggiunta di nuove funzionalità.

Un altro libro che consiglierei è Brownfield Application Development in .NET che in sostanza dice di non fare un pieno anche di riscrittura. Parla di apportare modifiche costanti e iterative ogni volta che aggiungi nuove funzionalità o correggi i difetti. Affronta le considerazioni di costi e benefici e mette in guardia dal tentativo di sminuire troppo contemporaneamente. Mentre si lavora in modo efficace con il codice legacy parla principalmente di come eseguire il refactoring a livello di micro / codice, Brownfield Application Development in .NET , copre principalmente le considerazioni di livello superiore durante il re-factoring (insieme ad alcune cose a livello di codice).

Il libro di Brownfield suggerisce anche di capire quale area del codice ti sta causando più problemi e concentrarti lì. Eventuali altre aree che non richiedono molta manutenzione potrebbero non valere la pena modificarle.


+1 per il libro Brownfield Application Development in .Net
Gabriel Mongeon,

Grazie per la raccomandazione sul libro: lo darò un'occhiata. Dalla panoramica sarà più focalizzato su .NET in particolare rispetto ai due libri che ho citato che sembrano concentrarsi su C, C ++ e Java.
Anthony,

4

Per un'app così legacy, è molto più conveniente iniziare coprendola con test di integrazione di livello superiore (automatizzati) piuttosto che unit test. Quindi, con i test di integrazione come rete di sicurezza, è possibile iniziare il refactoring a piccoli passi se è appropriato, ovvero se il costo del refactoring si ripaga a lungo termine. Come altri hanno notato, questo non è evidente.

Vedi anche questa mia precedente risposta a una domanda simile; spero che lo trovi utile.


Per il momento mi sto orientando verso test di livello superiore. Sto lavorando con il DBA per trovare il modo migliore per ripristinare il database dopo ogni esecuzione del test.
Anthony,

1

Fai molta attenzione a buttare via e riscrivere il codice in esecuzione ( cose che non dovresti mai fare ). Certo può essere brutto, ma se funziona lascialo. Vedi il post sul blog di Joel, sicuramente ha più di 10 anni, ma è ancora sul bersaglio.


Non sono d'accordo con Joel lì. Ciò che ha detto potrebbe essere sembrato rilevante al momento, ma non è la riscrittura di quello che ora si chiama Mozilla Firefox?
CashCow,

1
Sì, ma ha messo fuori gioco Netscape nel processo! Non sta dicendo che ricominciare da capo non sia mai la scelta giusta, ma è qualcosa di cui bisogna stare molto attenti. E il codice OO non è sempre migliore del codice procedurale.
Zaccaria K,

1

Come ha affermato Mike, la "regola del boy scout" è probabilmente la migliore qui, se il codice funziona e non è una fonte costante di segnalazioni di bug, preferirei lasciarlo lì e migliorarlo lentamente nel tempo.

Durante il tuo progetto di miglioramento, consenti nuovi modi di fare le cose. Ad esempio, utilizzare un ORM per nuove funzionalità e ignorare il modello di livello dati esistente. Quando ti imbatti in miglioramenti che devono toccare il codice esistente, potresti essere in grado di spostare parte del codice correlato nel nuovo modo di fare le cose. L'uso di una facciata o di alcuni adattatori in alcuni punti può aiutarti a isolare il vecchio codice, forse anche per livello. Questo potrebbe aiutarti a far morire di fame il vecchio codice nel tempo.

Allo stesso modo questo può aiutarti con l'aggiunta di unit test, puoi iniziare con il nuovo codice che crei e aggiungere lentamente alcuni test per il vecchio codice che devi toccare per i nuovi miglioramenti.


1

Sono entrambi buoni libri. Se inizierai a riscrivere il codice in quel modo, penso che sia importante anche iniziare a coprire il codice con test unitari per mantenerlo stabile mentre lo riscrivi.

Deve essere fatto a piccoli passi e la modifica di quel tipo di codice può facilmente destabilizzare l'intero sistema.

Non modificherei alcun codice su cui non stai lavorando attivamente. Fallo solo sul codice che stai migliorando o risolvendo attivamente. Se qualcosa serve allo scopo ma non è stato modificato da anni, allora lascialo. Lo sta facendo anche se conosci un modo migliore.

Alla fine della giornata l'azienda ha bisogno di produttività. Mentre un codice migliore aumenta la produttività riscrivendo il codice solo perché potrebbe essere scritto meglio, probabilmente non è il modo migliore per apportare valore al tuo prodotto.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.