Konversi dari Prosedural ke Kode Berorientasi Objek

16

Saya telah membaca Bekerja Efektif dengan Legacy Code dan Clean Code dengan tujuan mempelajari strategi tentang cara mulai membersihkan basis kode yang ada dari aplikasi webforms ASP.NET yang besar.

Sistem ini sudah ada sejak 2005 dan sejak itu telah mengalami sejumlah penyempurnaan. Awalnya kode ini disusun sebagai berikut (dan sebagian besar masih terstruktur dengan cara ini):

  • ASP.NET (aspx / ascx)
  • Kode di belakang (c #)
  • Lapisan Logika Bisnis (c #)
  • Lapisan Akses Data (c #)
  • Database (Oracle)

Masalah utama adalah bahwa kode tersebut adalah prosedural yang menyamar sebagai berorientasi objek. Ini hampir melanggar semua pedoman yang dijelaskan dalam kedua buku.

Ini adalah contoh kelas tipikal dalam Lapisan Logika Bisnis:

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

}

Ini memiliki banyak duplikasi, kelas memiliki sejumlah tanggung jawab, dll, dll - itu hanya kode 'tidak bersih' yang umumnya.

Semua kode di seluruh sistem tergantung pada implementasi konkret.

Ini adalah contoh kelas khas di Lapisan Akses Data:

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

Sistem ini dikembangkan oleh saya dan tim kecil pada tahun 2005 setelah 1 minggu kursus .NET. Sebelumnya, pengalaman saya ada di aplikasi client-server. Selama 5 tahun terakhir saya telah mengakui manfaat pengujian unit otomatis, pengujian integrasi otomatis, dan pengujian penerimaan otomatis (menggunakan Selenium atau yang setara) tetapi basis kode saat ini tampaknya tidak mungkin untuk memperkenalkan konsep-konsep ini.

Kami sekarang mulai bekerja pada proyek peningkatan besar dengan kerangka waktu yang ketat. Tim ini terdiri dari 5 pengembang .NET - 2 pengembang dengan beberapa tahun pengalaman .NET dan 3 lainnya dengan sedikit atau tanpa pengalaman .NET. Tak satu pun dari tim (termasuk saya) yang memiliki pengalaman dalam menggunakan .NET unit testing atau kerangka kerja mengejek.

Strategi apa yang akan Anda gunakan untuk membuat kode ini lebih bersih, lebih berorientasi objek, dapat diuji dan dipelihara?

Anthony
sumber
9
Sebagai tambahan, mungkin perlu memeriksa dua kali bahwa ada justifikasi biaya untuk menulis ulang sistem. Kode lama mungkin jelek, tetapi jika bekerja dengan cukup baik, mungkin lebih murah jika menggunakan sisi yang kasar dan menginvestasikan waktu pengembangan Anda di tempat lain.
smithco
Salah satu justifikasi yang mungkin adalah mengurangi upaya dan biaya pengujian ulang manual setelah setiap proyek peningkatan. Pada akhir proyek terakhir, pengujian manual berlangsung sekitar 2 bulan. Jika pengenalan pengujian yang lebih otomatis mengurangi upaya ini menjadi 1-2 minggu, mungkin itu layak dilakukan.
Anthony
5
UNTUK KODE LEGASI, STUFF INI TELAH BAIK!
Pekerjaan
Saya setuju itu cukup konsisten dan terstruktur. Tujuan utama saya adalah mengurangi efek samping dari perubahan. Upaya yang diperlukan untuk menguji seluruh aplikasi secara manual setelah (dan selama) setiap proyek sangat besar. Saya telah berpikir tentang menggunakan Selenium untuk mengujinya melalui sisi klien - Saya punya pertanyaan tentang ServerFault ( serverfault.com/questions/236546/… ) untuk mendapatkan saran tentang cara cepat mengembalikan database. Saya merasa pengujian penerimaan otomatis akan mendapatkan sebagian besar manfaat tanpa harus melakukan penulisan ulang besar-besaran.
Anthony

Jawaban:

16

Anda menyebutkan dua buku yang salah satu pesan utamanya adalah "Aturan Pramuka" yaitu membersihkan kode saat Anda menyentuhnya. Jika Anda memiliki sistem kerja, penulisan ulang besar-besaran adalah kontra-produktif. Alih-alih, saat Anda menambahkan fungsionalitas baru, pastikan Anda meningkatkan kodenya.

  • Tulis tes unit untuk mencakup kode yang ada yang perlu Anda ubah.
  • Refactor kode itu sehingga lebih lentur untuk perubahan (pastikan tes Anda masih lulus).
  • Tulis tes untuk fungsionalitas baru / revisi
  • Tulis kode untuk lulus ujian baru
  • Reaktor sesuai kebutuhan.

Untuk menyelam lebih jauh, Feathers berbicara tentang pengujian aplikasi di jahitannya: titik logis di mana unit terhubung. Anda dapat memanfaatkan jahitan untuk membuat rintisan atau tiruan untuk dependensi sehingga Anda dapat menulis tes di sekitar objek dependen. Mari kita ambil AddressBO Anda sebagai contoh

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

Ada perbedaan yang jelas antara AddressBO dan AddressDAO. Mari kita membuat antarmuka untuk AddressDAO dan memungkinkan dependensi untuk disuntikkan ke 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
  }
}

Sekarang Anda menyiapkan AddressBO untuk memungkinkan injeksi

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

Di sini kita menggunakan "injeksi ketergantungan orang miskin." Satu-satunya tujuan kami adalah untuk memecahkan jahitan dan memungkinkan kami untuk menguji AddressBO. Sekarang dalam unit test kami, kami dapat membuat tiruan IAddressDAO dan memvalidasi interaksi antara dua objek.

Michael Brown
sumber
1
Saya setuju - saya suka strategi ini ketika saya membacanya. Kita bisa menghabiskan waktu berbulan-bulan membersihkan kode tanpa benar-benar menambah nilai. Jika kita fokus membersihkan apa yang perlu kita ubah sambil menambahkan fitur baru, kita mendapatkan yang terbaik dari kedua dunia.
Anthony
Satu-satunya tantangan adalah menulis unit test untuk kode yang ada. Saya lebih condong ke arah tes tingkat tinggi terlebih dahulu sehingga kami dapat melakukan refactor dan menambahkan tes unit dengan lebih percaya diri.
Anthony
1
Ya yang terbaik yang dapat Anda lakukan adalah menulis tes yang memverifikasi kode melakukan apa yang dilakukannya. Anda dapat mencoba membuat tes yang memverifikasi perilaku yang benar ... tetapi Anda berisiko melanggar fungsionalitas lain yang tidak dicakup oleh tes.
Michael Brown
Ini adalah penjelasan terbaik yang pernah saya lihat untuk mempraktikkan Feathers "temukan lapisannya". Sebagai seseorang yang lebih berpengalaman dalam prosedural daripada OO, Ada jahitan yang jelas antara AddressBO dan AddressDAO tidak jelas bagi saya, tetapi contoh ini sangat membantu.
SeraM
5

Jika saya ingat benar Bekerja Efektif dengan Legacy Code mengatakan penulisan ulang penuh tidak menjamin bahwa kode baru akan lebih baik daripada yang lama (dari sudut pandang fungsionalitas / cacat). Perbaikan dalam buku itu adalah untuk saat memperbaiki bug / menambahkan fitur baru.

Buku lain yang akan saya rekomendasikan adalah Pengembangan Aplikasi Brownfield di .NET yang pada dasarnya mengatakan untuk tidak menulis ulang secara penuh juga. Ini berbicara tentang membuat perubahan yang stabil dan berulang setiap kali Anda menambahkan fitur baru atau memperbaiki cacat. Ini membahas pertimbangan biaya vs manfaat dan memperingatkan tentang mencoba menggigit terlalu banyak pada satu waktu. Sementara Bekerja Efektif dengan Legacy Code sebagian besar berbicara tentang cara refactor pada tingkat mikro / kode, Pengembangan Aplikasi Brownfield di .NET , sebagian besar mencakup pertimbangan tingkat yang lebih tinggi ketika re-factoring (bersama dengan beberapa hal tingkat kode juga).

Buku Brownfield juga menyarankan mencari tahu area mana dari kode yang menyebabkan Anda paling banyak kesulitan dan fokus di sana. Area lain mana pun yang tidak membutuhkan banyak perawatan mungkin tidak layak diubah.

Mat
sumber
+1 untuk buku Pengembangan Aplikasi Brownfield di .Net
Gabriel Mongeon
Terima kasih atas rekomendasi buku - saya akan memeriksanya. Dari ikhtisar itu akan lebih difokuskan pada. NET khusus daripada dua buku yang saya sebutkan yang tampaknya berfokus pada C, C ++ dan Java.
Anthony
4

Untuk aplikasi lawas seperti itu, jauh lebih hemat biaya untuk memulai dengan menutupinya dengan tes integrasi tingkat tinggi (otomatis) daripada tes unit. Kemudian dengan tes integrasi sebagai jaring pengaman Anda, Anda dapat memulai refactoring dalam langkah-langkah kecil jika sesuai, yaitu jika biaya refactoring membayar sendiri kembali dalam jangka panjang. Seperti yang telah dicatat oleh orang lain, ini tidak terbukti dengan sendirinya.

Lihat juga jawaban saya sebelumnya untuk pertanyaan serupa; semoga bermanfaat.

Péter Török
sumber
Saya condong ke arah tes tingkat yang lebih tinggi untuk memulai. Saya bekerja dengan DBA untuk mencari cara terbaik untuk mengembalikan database setelah setiap pengujian.
Anthony
1

Berhati-hatilah dengan membuang dan menulis ulang kode yang sedang berjalan ( Hal-hal yang tidak boleh Anda lakukan ). Tentu itu mungkin jelek, tetapi jika berhasil biarkan saja. Lihat posting blog Joel, tentu berumur 10+ tahun, tetapi masih tepat sasaran.

Zachary K
sumber
Saya tidak setuju dengan Joel di sana. Apa yang dia katakan mungkin terasa relevan pada saat itu tetapi bukankah penulisan ulang itu yang sekarang disebut Mozilla Firefox?
CashCow
1
Ya, tetapi proses ini membuat netscape keluar dari bisnis! Itu tidak mengatakan bahwa memulai dari awal bukanlah pilihan yang tepat tetapi sesuatu yang harus sangat berhati-hati. Dan kode OO tidak selalu lebih baik dari pada kode prosedural.
Zachary K
1

Seperti yang dinyatakan Mike 'aturan pramuka' mungkin yang terbaik di sini, jika kodenya berfungsi dan bukan sumber laporan bug yang konstan, saya lebih suka membiarkannya duduk di sana dan memperbaikinya perlahan seiring waktu.

Selama proyek peningkatan Anda memungkinkan cara-cara baru dalam melakukan sesuatu. Misalnya menggunakan ORM untuk fitur baru dan memotong pola lapisan data yang ada. Saat Anda mengalami peningkatan yang perlu menyentuh kode yang ada, Anda mungkin dapat memindahkan beberapa kode terkait ke cara baru dalam melakukan sesuatu. Menggunakan fasad atau beberapa adaptor di tempat dapat membantu Anda mengisolasi kode lama, bahkan mungkin per lapisan. Ini bisa membantu Anda membuat kode lama kelaparan.

Demikian pula ini dapat membantu Anda dengan menambahkan tes unit, Anda dapat mulai dengan kode baru yang Anda buat dan perlahan-lahan menambahkan beberapa tes untuk kode lama Anda harus menyentuh untuk perangkat tambahan baru.

Joppe
sumber
1

Keduanya buku yang bagus. Jika Anda akan mulai menulis ulang kode dengan cara itu, saya pikir penting juga untuk mulai menutup kode dengan tes unit untuk membantu menjaga kestabilannya saat Anda menulis ulang.

Itu harus dilakukan dalam langkah-langkah kecil dan memodifikasi kode semacam itu dapat dengan mudah mengacaukan seluruh sistem.

Saya tidak akan mengubah kode apa pun yang sedang tidak Anda kerjakan. Hanya lakukan ini pada kode yang Anda secara aktif tingkatkan atau perbaiki. Jika sesuatu melayani tujuan itu tetapi belum dimodifikasi dalam beberapa tahun kemudian tinggalkan saja. Melakukan hal itu bahkan jika Anda tahu cara yang lebih baik.

Pada akhirnya perusahaan membutuhkan produktivitas. Sementara kode yang lebih baik meningkatkan produktivitas, menulis ulang kode hanya karena itu bisa ditulis lebih baik mungkin bukan cara terbaik untuk membawa nilai pada produk Anda.

Yang terhormat Chow
sumber