แปลงจากกระบวนงานเป็นรหัสที่เน้นวัตถุ


16

ฉันอ่านการทำงานอย่างมีประสิทธิภาพด้วย Legacy CodeและClean Codeโดยมีเป้าหมายของกลยุทธ์การเรียนรู้เกี่ยวกับวิธีเริ่มทำความสะอาดโค้ด - ฐานที่มีอยู่ของแอปพลิเคชัน ASP.NET webforms ขนาดใหญ่

ระบบนี้มีมาตั้งแต่ปีพ. ศ. 2548 และตั้งแต่นั้นมาก็มีการปรับปรุงหลายอย่าง แต่เดิมรหัสมีโครงสร้างดังนี้ (และยังคงเป็นโครงสร้างส่วนใหญ่ด้วยวิธีนี้):

  • ASP.NET (aspx / ascx)
  • รหัส - หลัง (c #)
  • ชั้นตรรกะทางธุรกิจ (c #)
  • ชั้นการเข้าถึงข้อมูล (c #)
  • ฐานข้อมูล (Oracle)

ปัญหาหลักคือรหัสคือการปลอมแปลงตามขั้นตอนเป็นเชิงวัตถุ มันเป็นการละเมิดแนวทางทั้งหมดที่อธิบายไว้ในหนังสือทั้งสองเล่ม

นี่คือตัวอย่างของคลาสทั่วไปใน Business Logic Layer:

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

}

มันมีจำนวนมากซ้ำชั้นมีความรับผิดชอบจำนวนมาก ฯลฯ - มันเป็นเพียงแค่รหัส 'ไม่สะอาด'

รหัสทั้งหมดในระบบขึ้นอยู่กับการใช้งานที่เป็นรูปธรรม

นี่คือตัวอย่างของคลาสทั่วไปใน Data Access Layer:

    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 หลังจากหลักสูตร NET 1 สัปดาห์ ก่อนหน้านี้ประสบการณ์ของฉันเคยอยู่ในแอปพลิเคชันไคลเอนต์เซิร์ฟเวอร์ ในช่วง 5 ปีที่ผ่านมาฉันได้ตระหนักถึงประโยชน์ของการทดสอบหน่วยอัตโนมัติการทดสอบการรวมอัตโนมัติและการทดสอบการยอมรับอัตโนมัติ (โดยใช้ซีลีเนียมหรือเทียบเท่า) แต่ฐานรหัสปัจจุบันดูเหมือนว่าเป็นไปไม่ได้ที่จะแนะนำแนวคิดเหล่านี้

ตอนนี้เราเริ่มที่จะทำงานในโครงการเพิ่มประสิทธิภาพที่สำคัญด้วยกรอบเวลาที่ จำกัด ทีมประกอบด้วยนักพัฒนา. NET 5 คน - นักพัฒนา 2 คนที่มีประสบการณ์ NET เพียงไม่กี่ปีและอีก 3 คนที่มีประสบการณ์ NET น้อยหรือไม่มีเลย ไม่มีทีม (รวมถึงตัวฉันเอง) ที่มีประสบการณ์ในการใช้การทดสอบหน่วย. NET หรือการเยาะเย้ยกรอบงาน

คุณใช้กลยุทธ์ใดในการทำให้โค้ดนี้สะอาดขึ้นตรวจสอบและบำรุงรักษาได้ดีขึ้น


9
นอกจากนี้อาจเป็นการตรวจสอบสองครั้งที่คุ้มค่ากับการเขียนระบบใหม่ รหัสเก่าอาจน่าเกลียด แต่ถ้ามันใช้งานได้ดีมันอาจจะถูกกว่าหากใช้ขอบที่หยาบและใช้เวลาในการพัฒนาที่อื่น
smithco

เหตุผลหนึ่งที่เป็นไปได้คือลดความพยายามและค่าใช้จ่ายในการทดสอบซ้ำด้วยตนเองหลังจากโครงการปรับปรุงทุกครั้ง ในตอนท้ายของโครงการที่ผ่านมาการทดสอบด้วยตนเองดำเนินไปประมาณ 2 เดือน หากการแนะนำการทดสอบอัตโนมัติมากขึ้นช่วยลดความพยายามนี้ลงเหลือ 1-2 สัปดาห์อาจคุ้มค่า
แอนโทนี่

5
สำหรับรหัสทางกฎหมายสิ่งนี้เป็นสิ่งที่ดี!
งาน

ฉันเห็นด้วยว่ามีความสอดคล้องและมีโครงสร้าง เป้าหมายหลักของฉันคือการลดผลข้างเคียงของการเปลี่ยนแปลง ความพยายามในการทดสอบแอปพลิเคชันทั้งหมดด้วยตนเองหลังจาก (และระหว่าง) แต่ละโครงการมีขนาดใหญ่มาก ฉันคิดเกี่ยวกับการใช้ซีลีเนียมเพื่อทดสอบผ่านฝั่งไคลเอ็นต์ - ฉันมีคำถามเกี่ยวกับ ServerFault ( serverfault.com/questions/236546/ ...... ) เพื่อรับคำแนะนำเกี่ยวกับการคืนค่าฐานข้อมูลอย่างรวดเร็ว ฉันรู้สึกว่าการทดสอบการยอมรับอัตโนมัติจะได้รับประโยชน์มากที่สุดโดยไม่ต้องเขียนซ้ำขนาดใหญ่
แอนโทนี่

คำตอบ:


16

คุณพูดถึงหนังสือสองเล่มที่หนึ่งในข้อความหลักคือ "กฎลูกเสือ" คือล้างรหัสเมื่อคุณสัมผัส หากคุณมีระบบที่ใช้งานได้การเขียนซ้ำครั้งใหญ่จะช่วยแก้ปัญหาได้ ในขณะที่คุณเพิ่มฟังก์ชันการทำงานใหม่ให้ตรวจสอบให้แน่ใจว่าคุณปรับปรุงโค้ดตามที่ควรจะเป็น

  • เขียนการทดสอบหน่วยเพื่อให้ครอบคลุมรหัสที่มีอยู่ที่คุณต้องการเปลี่ยน
  • สร้าง Refactor code ขึ้นใหม่เพื่อให้มีความยืดหยุ่นในการเปลี่ยนแปลงมากขึ้น
  • เขียนการทดสอบสำหรับฟังก์ชันใหม่ / แก้ไข
  • เขียนรหัสเพื่อให้การทดสอบใหม่ผ่าน
  • Refactor ตามความจำเป็น

ในการดำน้ำเพิ่มเติมใน Feathers พูดคุยเกี่ยวกับการทดสอบการใช้งานที่ตะเข็บของมัน: จุดตรรกะที่หน่วยเชื่อมต่อ คุณสามารถใช้ประโยชน์จากรอยต่อในการสร้างต้นขั้วหรือจำลองสำหรับการพึ่งพาเพื่อให้คุณสามารถเขียนการทดสอบรอบวัตถุที่พึ่งพา ลองนำ AddressBO ของคุณมาเป็นตัวอย่าง

public class AddressBO
{
    public TransferObject GetAddress(string addressID)
    {
        if (StringUtils.IsNull(addressID))
        {
            throw new ValidationException("Address ID must be entered");
        }

        AddressDAO addressDAO = new AddressDAO();
        return addressDAO.GetAddress(addressID);
    }
}

มีตะเข็บที่ชัดเจนระหว่าง AddressBO และ AddressDAO มาสร้างส่วนต่อประสานสำหรับ AddressDAO แล้วปล่อยให้การพึ่งพาเข้าไปใน AddressBO

public interface IAddressDAO
{
  TransferObject GetAddress(addressID);
  //add other interface methods here.
}

public class AddressDAO:GenericDAO, IAddressDAO
{
  public TransferObject GetAddress(string addressID)
  {
    ///implementation goes here
  }
}

ตอนนี้คุณหมอ AddressBO ของคุณเพื่อให้การฉีด

public class AddressBO
{
    private IAddressDAO _addressDAO;
    public AddressBO()
    {
      _addressDAO = new AddressDAO();
    }

    public AddressBO(IAddressDAO addressDAO)
    {
      _addressDAO = addressDAO;
    }

    public TransferObject GetAddress(string addressID)
    {
        if (StringUtils.IsNull(addressID))
        {
            throw new ValidationException("Address ID must be entered");
        }
        //call the injected AddressDAO
        return _addressDAO.GetAddress(addressID);
    }
}

ที่นี่เรากำลังใช้ "การฉีดพึ่งพาคนจน" เป้าหมายเดียวของเราคือทำลายรอยต่อและอนุญาตให้เราทดสอบ AddressBO ตอนนี้ในการทดสอบหน่วยของเราเราสามารถจำลอง IAddressDAO และตรวจสอบการมีปฏิสัมพันธ์ระหว่างวัตถุทั้งสอง


1
ฉันเห็นด้วย - ฉันชอบกลยุทธ์นี้เมื่อฉันอ่านเกี่ยวกับเรื่องนี้ เราสามารถใช้เวลาหลายเดือนในการทำความสะอาดโค้ดโดยไม่เพิ่มมูลค่าจริงๆ หากเรามุ่งเน้นการทำความสะอาดสิ่งที่เราจำเป็นต้องเปลี่ยนในขณะที่เพิ่มคุณสมบัติใหม่เราจะได้สิ่งที่ดีที่สุดทั้งสองโลก
Anthony

ความท้าทายเพียงอย่างเดียวคือการเขียนการทดสอบหน่วยสำหรับรหัสที่มีอยู่ ฉันเอนตัวไปสู่การทดสอบระดับสูงขึ้นก่อนเพื่อให้เราสามารถปรับโครงสร้างและเพิ่มการทดสอบหน่วยด้วยความมั่นใจมากขึ้น
แอนโทนี่

1
ใช่ดีที่สุดที่คุณสามารถทำได้คือการทดสอบการเขียนที่ตรวจสอบรหัสทำในสิ่งที่มันทำ คุณสามารถลองสร้างการทดสอบที่ตรวจสอบพฤติกรรมที่ถูกต้อง ... แต่คุณเสี่ยงต่อการทำลายฟังก์ชันการทำงานอื่น ๆ ที่ไม่ครอบคลุมโดยการทดสอบ
Michael Brown

นี่คือคำอธิบายที่ดีที่สุดที่ฉันเคยเห็นสำหรับการวาง feathers "หาตะเข็บ" สู่การปฏิบัติ ในฐานะที่เป็นคนรอบรู้ในขั้นตอนมากกว่า OO มีตะเข็บที่เห็นได้ชัดระหว่าง AddressBO และ AddressDAOไม่ชัดเจนสำหรับฉัน แต่ตัวอย่างนี้ช่วยได้จริงๆ
SeraM

5

ถ้าฉันจำได้ถูกต้อง ใช้งานได้อย่างมีประสิทธิภาพด้วยรหัสมรดกบอกว่าการเขียนใหม่ทั้งหมดไม่รับประกันว่ารหัสใหม่จะดีกว่ารุ่นเก่า (จากมุมมองการทำงาน / ข้อบกพร่อง) refactorings ในหนังสือเล่มนั้นมีไว้สำหรับเมื่อแก้ไขข้อบกพร่อง / เพิ่มคุณสมบัติใหม่

หนังสืออีกเล่มที่ฉันอยากจะแนะนำก็คือBrownfield Application Development ใน. NETซึ่งโดยทั่วไปบอกว่าจะไม่เขียนแบบเต็มเช่นกัน มันพูดถึงการเปลี่ยนแปลงที่มั่นคงซ้ำ ๆ ทุกครั้งที่คุณเพิ่มคุณสมบัติใหม่หรือแก้ไขข้อบกพร่อง มันระบุถึงการพิจารณาด้านต้นทุนเทียบกับผลประโยชน์และเตือนเกี่ยวกับการพยายามปิดบิตมากเกินไปในคราวเดียว ในขณะที่ทำงานอย่างมีประสิทธิภาพด้วย Legacy Codeส่วนใหญ่พูดถึงวิธีการ refactor ในระดับไมโคร / รหัส Brownfield Application Development ใน. NET , ส่วนใหญ่ครอบคลุมการพิจารณาในระดับที่สูงขึ้นเมื่อทำการแฟคตอริ่งอีกครั้ง

หนังสือของ Brownfield แนะนำให้คุณค้นหาว่าส่วนใดของรหัสที่ทำให้คุณเดือดร้อนที่สุดและมุ่งไปที่นั่น พื้นที่อื่น ๆ ที่ไม่ต้องการการบำรุงรักษาจำนวนมากอาจไม่คุ้มค่าที่จะเปลี่ยนแปลง


+1 สำหรับหนังสือ Brownfield Application Development ใน. Net
Gabriel Mongeon

ขอบคุณสำหรับคำแนะนำหนังสือ - ฉันจะดูมัน จากภาพรวมมันจะเพ่งความสนใจไปที่. NET มากกว่าหนังสือสองเล่มที่ฉันพูดถึงซึ่งดูเหมือนว่าจะเน้นไปที่ C, C ++ และ Java
แอนโทนี่

4

สำหรับแอปรุ่นเก่ามันคุ้มค่ากว่าที่จะเริ่มต้นโดยการทดสอบแบบบูรณาการในระดับสูง (อัตโนมัติ) มากกว่าการทดสอบหน่วย จากนั้นด้วยการทดสอบการรวมเป็นเครือข่ายความปลอดภัยของคุณคุณสามารถเริ่มต้นการเปลี่ยนโครงสร้างใหม่ในขั้นตอนเล็ก ๆหากมันเหมาะสมเช่นถ้าค่าใช้จ่ายในการ refactoring จ่ายเองในระยะยาว ดังที่คนอื่น ๆ ได้สังเกตเห็นสิ่งนี้ไม่ใช่ตัวตนที่เห็นได้ชัด

ดูคำตอบก่อนหน้านี้ของฉันสำหรับคำถามที่คล้ายกัน หวังว่าคุณจะพบว่ามีประโยชน์


ฉันพิงการทดสอบระดับสูงเพื่อเริ่มต้น ฉันทำงานกับ DBA เพื่อหาวิธีที่ดีที่สุดในการคืนค่าฐานข้อมูลหลังจากการทดสอบแต่ละครั้ง
Anthony

1

ระมัดระวังเป็นอย่างมากกับการทิ้งและเขียนโค้ดที่กำลังรันอยู่ ( สิ่งที่คุณไม่ควรทำ ) แน่นอนว่ามันอาจจะน่าเกลียด แต่ถ้ามันใช้ได้ก็เป็นไปได้ ดูโพสต์บล็อกของโจเอลตรวจสอบว่ามีอายุตั้งแต่ 10 ปีขึ้นไป แต่ยังคงเป้าหมายอยู่


ฉันไม่เห็นด้วยกับโจเอลที่นั่น สิ่งที่เขาพูดอาจรู้สึกว่าเกี่ยวข้องกันในเวลานั้น แต่ไม่ใช่สิ่งที่เขียนใหม่ตอนนี้เรียกว่า Mozilla Firefox?
CashCow

1
ใช่ แต่มันทำให้เน็ตสเคปเลิกกิจการ! การไม่พูดว่าการเริ่มต้นใหม่ไม่ใช่ทางเลือกที่ถูกต้อง แต่เป็นสิ่งที่ต้องระวังเป็นอย่างยิ่ง และรหัส OO ก็ไม่ได้ดีกว่ารหัสขั้นตอนแล้วเสมอไป
Zachary K

1

ดังที่ไมค์กล่าวว่า 'กฎลูกเสือ' น่าจะดีที่สุดที่นี่ถ้ารหัสใช้งานได้และไม่ใช่แหล่งรายงานข้อผิดพลาดคงที่ฉันต้องการให้มันนั่งที่นั่นและปรับปรุงอย่างช้าๆเมื่อเวลาผ่านไป

ในระหว่างการปรับปรุงโครงการของคุณอนุญาตให้วิธีการใหม่ในการทำสิ่งต่าง ๆ ตัวอย่างเช่นใช้ ORM สำหรับคุณสมบัติใหม่และข้ามรูปแบบชั้นข้อมูลที่มีอยู่ เมื่อคุณพบกับการปรับปรุงที่ต้องสัมผัสรหัสที่มีอยู่คุณอาจจะสามารถย้ายรหัสที่เกี่ยวข้องไปยังวิธีการทำสิ่งใหม่ ๆ ได้ การใช้ส่วนหน้าหรืออะแดปเตอร์บางตัวในสถานที่สามารถช่วยคุณแยกรหัสเก่าหรือแม้แต่ต่อเลเยอร์ สิ่งนี้จะช่วยให้คุณอดรหัสเก่าเมื่อเวลาผ่านไป

ในทำนองเดียวกันสิ่งนี้สามารถช่วยคุณในการเพิ่มการทดสอบหน่วยคุณสามารถเริ่มต้นด้วยรหัสใหม่ที่คุณทำและเพิ่มการทดสอบบางอย่างสำหรับรหัสเก่าที่คุณต้องสัมผัสเพื่อรับการปรับปรุงใหม่


1

นี่เป็นหนังสือที่ดีทั้งคู่ หากคุณจะเริ่มเขียนรหัสใหม่ในลักษณะเช่นนั้นฉันคิดว่ามันเป็นสิ่งสำคัญเช่นกันที่จะต้องเริ่มครอบคลุมรหัสด้วยการทดสอบหน่วยเพื่อช่วยให้มีเสถียรภาพในขณะที่คุณเขียนใหม่

จะต้องทำในขั้นตอนเล็ก ๆ และปรับเปลี่ยนรหัสชนิดที่สามารถทำให้ระบบทั้งหมดไม่เสถียร

ฉันจะไม่แก้ไขโค้ดใด ๆ ที่คุณไม่ได้ใช้งาน ทำสิ่งนี้กับรหัสที่คุณกำลังปรับปรุงหรือแก้ไขอยู่ ถ้ามีอะไรให้บริการตามวัตถุประสงค์ แต่ยังไม่ได้รับการแก้ไขในอีกหลายปีให้ทิ้งไป มันกำลังทำอยู่แม้ว่าคุณจะรู้วิธีที่ดีกว่าก็ตาม

ในตอนท้ายของวันที่ บริษัท ต้องการผลผลิต ในขณะที่โค้ดที่ดีขึ้นจะเพิ่มโค้ดการเขียนเพิ่มผลผลิตเพียงเพราะมันสามารถเขียนได้ดีกว่าอาจไม่ใช่วิธีที่ดีที่สุดในการนำมูลค่ามาสู่ผลิตภัณฑ์ของคุณ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.