ในบทนี้ ผมได้รวบรวมเทคนิคการทำลาย dependency ไว้หลายเทคนิค รายการนี้ไม่ได้ครอบคลุมทั้งหมด; นี่เป็นเพียง เทคนิคบางส่วนที่ผมใช้ร่วมกับทีมเพื่อแยกคลาสออกจากกันให้พอที่จะนำไปทดสอบได้ ในทางเทคนิคแล้ว เทคนิคเหล่านี้คือการ refactor—แต่ละเทคนิคล้วนรักษาพฤติกรรมเดิมไว้ แต่ต่างจากการ refactor ส่วนใหญ่ที่เคยเขียนถึงในวงการ ซอฟต์แวร์จนถึงตอนนี้ การ refactor เหล่านี้มีจุดประสงค์เพื่อทำโดยไม่มี tests เพื่อให้มี tests เข้ามาใช้งาน ในกรณีส่วนใหญ่ ถ้าคุณทำตาม ขั้นตอนอย่างระมัดระวัง โอกาสที่จะผิดพลาดมีน้อย ซึ่งไม่ได้หมายความว่ามันปลอดภัยสมบูรณ์แบบ การทำผิดพลาดยังเกิดขึ้นได้เมื่อคุณนำไปใช้ ดังนั้นคุณควรใช้ความระมัดระวังเมื่อใช้งาน ก่อนที่คุณจะใช้ การ refactor เหล่านี้ ให้ดู Chapter 23 , How Do I Know That I'm Not Breaking Anything? เคล็ดลับในบทนั้นจะช่วยให้คุณใช้เทคนิคเหล่านี้อย่างปลอดภัย เพื่อให้คุณสามารถมี tests เข้ามาใช้งาน เมื่อคุณทำได้ คุณจะสามารถเปลี่ยนแปลงที่ลุกล้ำกว่าเดิมได้ด้วยความมั่นใจมากขึ้นว่าคุณไม่ได้ทำให้อะไรพัง

เทคนิคเหล่านี้ไม่ได้ทำให้ดีไซน์ของคุณดีขึ้นในทันที ในความเป็นจริง ถ้าคุณมีเซนส์ด้านดีไซน์ที่ดี เทคนิคบางอย่าง เหล่านี้จะทำให้คุณรู้สึกสะดุ้ง เทคนิคเหล่านี้สามารถช่วยให้คุณนำ methods, classes, และกลุ่มของ classes มาอยู่ภายใต้การทดสอบ และระบบของคุณจะบำรุงรักษาได้ง่ายขึ้นเพราะเหตุนั้น เมื่อถึงจุดนั้น คุณสามารถใช้ test-supported refactorings เพื่อทำให้ดีไซน์สะอาดขึ้น


มีการ refactor บางส่วนในบทนี้ที่ Martin Fowler ได้อธิบายไว้ในหนังสือของเขา Refactoring: Improving the Design of Existing Code (Addison-Wesley, 1999) ผมได้รวมมันไว้ที่นี่พร้อมขั้นตอนที่แตกต่างออกไป โดยปรับแต่งให้สามารถใช้งาน ได้อย่างปลอดภัยโดยไม่ต้องมี tests


Adapt Parameter (ปรับ Parameter)

เมื่อผมทำการเปลี่ยนแปลง methods ผมมักจะเจอปัญหา dependency ที่เกิดจาก parameters ของ method บางครั้งผมพบว่า มันยากที่จะสร้าง parameter ที่ต้องการ; ในบางครั้ง ผมจำเป็นต้องทดสอบผลกระทบของ method ที่มีต่อ parameter ในหลายๆ กรณี คลาสของ parameter ไม่ได้ทำให้เรื่องนี้ง่าย ถ้าคลาสนั้นเป็นคลาสที่ผมสามารถแก้ไขได้ ผมสามารถใช้ Extract Interface ( 362 ) เพื่อทำลาย dependency Extract Interface มักจะเป็นตัวเลือกที่ดีที่สุดเมื่อต้องจัดการกับ parameter dependencies

โดยทั่วไป เราต้องการทำอะไรง่ายๆ เพื่อทำลาย dependencies ที่ขัดขวางการทดสอบ อะไรที่ไม่มีความเสี่ยง ที่จะเกิดข้อผิดพลาด อย่างไรก็ตาม ในบางกรณี Extract Interface ( 362 ) ใช้ไม่ได้ผลดีนัก ถ้าชนิดของ parameter เป็นระดับต่ำหรือเฉพาะเจาะจงกับเทคโนโลยี implementation บางอย่าง การสกัด interface อาจจะไม่เกิดประโยชน์หรือเป็นไปไม่ได้


ใช้ Adapt Parameter เมื่อคุณไม่สามารถใช้ Extract Interface ( 362 ) บนคลาสของ parameter ได้ หรือเมื่อ parameter นั้นยากต่อการสร้างของปลอม


นี่คือตัวอย่าง:

public class ARMDispatcher
{
    public void populate(HttpServletRequest request) {
        String [] values
            = request.getParameterValues(pageStateName);
        if (values != null && values.length  > 0)
        {
            marketBindings.put(pageStateName + getDateStamp(),
                               values[0]);
        }
        ...
    }
    ...
}

ในคลาสนี้ method populate รับค่า HttpServletRequest เป็น parameter HttpServletRequest เป็น interface ที่เป็นส่วนหนึ่งของมาตรฐาน J2EE ของ Sun สำหรับ Java ถ้าเราจะทดสอบ populate อย่างที่เป็นตอนนี้ เราจะต้องสร้างคลาสที่ implement HttpServletRequest และเตรียมทางให้เติมค่าของ parameter ที่จำเป็นต้องคืนค่าภายใต้การทดสอบ เอกสาร Java SDK ปัจจุบันแสดงว่ามีประมาณ 23 method declarations บน HttpServletRequest และนั่นยังไม่นับ declarations จาก superinterface ที่เราต้อง implement ด้วย มันจะดีมากถ้าใช้ Extract Interface ( 362 ) เพื่อสร้าง interface ที่แคบกว่าซึ่งให้เฉพาะ methods ที่เราต้องการ แต่เราไม่สามารถ extract interface จาก interface อื่นได้ ใน Java เราจะต้องให้ HttpServletRequest extend ตัวที่เรากำลังสกัดออกมา และเราไม่สามารถแก้ไข standard interface แบบนั้นได้ โชคดีที่เรายังมี ทางเลือกอื่น

มีไลบรารี mock object หลายตัวสำหรับ J2EE ถ้าเราโหลดมาสักตัว เราสามารถใช้ mock สำหรับ HttpServletRequest และทำการทดสอบที่เราต้องการได้ ซึ่งช่วยประหยัดเวลาได้มาก ถ้าเราไปทางนี้ เราจะไม่ต้องเสียเวลา สร้าง fake servlet request ด้วยมือ ดังนั้นดูเหมือนว่าเราจะมีทางออก—หรือว่าเรามี?

เมื่อผมทำลาย dependencies ผมมักจะมองไปข้างหน้าและดูว่าผลลัพธ์จะมีหน้าตาอย่างไร แล้วผมถึงตัดสินใจ ว่าผมจะอยู่กับผลที่ตามมาได้หรือไม่ ในกรณีนี้ โค้ด production ของเราจะดูใกล้เคียงเดิม และเรา จะทำงานหนักมากเพื่อคง HttpServletRequest ซึ่งเป็น API interface ไว้ในที่เดิม มีวิธีทำให้โค้ดดูดีขึ้นและทำให้การทำลาย dependency ง่ายขึ้นมั้ย? จริงๆ แล้วมี เราสามารถ wrap parameter ที่ส่งเข้ามาและทำลาย dependency ของเรากับ API interface ทั้งหมดได้ เมื่อเราทำอย่างนั้นแล้ว โค้ดจะมีลักษณะดังนี้:

public class ARMDispatcher
    public void populate( ParameterSource source ) {
        String values = source.getParameterForName(pageStateName);
        if (value != null) {
            marketBindings.put(pageStateName + getDateStamp(),
                               value);
        }
        ...
    }
}

เราทำอะไรไปบ้าง? เราได้ introduce interface ใหม่ชื่อ ParameterSource ณ จุดนี้ method เดียวที่มีคือ getParameterForName ซึ่งต่างจาก method HttpServletRequest getParmeterValue ที่ getParameterForName คืนค่าแค่ String เดียว เราเขียน method แบบนั้นเพราะเราสนใจเฉพาะ parameter แรกในบริบทนี้เท่านั้น


จงพัฒนาไปสู่ interfaces ที่สื่อถึงความรับผิดชอบมากกว่ารายละเอียด implementation ซึ่งจะทำให้โค้ด อ่านง่ายและบำรุงรักษาได้ง่ายขึ้น


นี่คือคลาสปลอมที่ implement ParameterSource เราสามารถใช้มันในการทดสอบของเรา:

class FakeParameterSource implements ParameterSource
{
    public String value;

    public String getParameterForName(String name) {
        return value;
    }
}

และ production parameter source มีลักษณะดังนี้:

class ServletParameterSource implements ParameterSource
{
    private HttpServletRequest request;

    public ServletParameterSource(HttpServletRequest request) {
        this.request = request;
    }

    String getParameterValue(String name) {
        String [] values = request.getParameterValues(name);
        if (values == null || values.length < 1)
            return null;
        return values[0];
    }
}

เมื่อมองดูเผินๆ อาจดูเหมือนเรากำลังทำให้โค้ดสวยขึ้นโดยไม่จำเป็น แต่ปัญหาที่แพร่หลายใน legacy code bases ก็คือมักจะไม่มีเลเยอร์ของ abstraction; โค้ดที่สำคัญที่สุดในระบบมักจะนั่งปนเปอยู่กับการเรียกใช้ API ระดับต่ำ เราได้เห็นแล้วว่าสิ่งนี้ทำให้การทดสอบยากขึ้น แต่ปัญหาก็ไม่ได้จำกัดอยู่แค่การทดสอบเท่านั้น โค้ดจะเข้าใจยากขึ้น เมื่อมันรกไปด้วย interfaces กว้างๆ ที่มี methods มากมายที่ไม่เคยถูกใช้ เมื่อคุณสร้าง abstractions ที่แคบและตรงกับสิ่งที่คุณต้องการ โค้ดของคุณจะสื่อสารได้ดีขึ้นและคุณจะได้ seam ที่ดีกว่า

ถ้าเรามุ่งไปที่การใช้ ParameterSource ในตัวอย่าง เราจะแยกตรรกะการ populate ออกจาก source ที่เฉพาะเจาะจง เราจะไม่ถูกผูกติดกับ interfaces เฉพาะของ J2EE อีกต่อไป


Adapt Parameter เป็นกรณีหนึ่งที่เราไม่ Preserve Signatures ( 312 ) ใช้ความระมัดระวังเป็นพิเศษ


Adapt Parameter อาจมีความเสี่ยงถ้า interface ที่คุณสร้างให้กับคลาส parameter แตกต่างจาก interface ปัจจุบันของ parameter มากเกินไป ถ้าเราไม่ระมัดระวังเมื่อทำการเปลี่ยนแปลงเหล่านั้น เราอาจจะ สร้าง bugs ที่ละเอียดอ่อนได้ เช่นเคย จำไว้ว่าเป้าหมายคือการทำลาย dependencies ให้พอที่จะมี tests เข้ามาใช้งาน ความโน้มเอียงของคุณควรเป็นไปในทางที่ทำให้คุณรู้สึกมั่นใจมากกว่าน่าจะเป็นการเปลี่ยนแปลงที่ให้โครงสร้างที่ดีที่สุด ส่วนนั้นสามารถมาทีหลังเมื่อคุณมี tests แล้ว ตัวอย่างเช่น ในกรณีนี้เราอาจต้องการปรับ ParameterSource เพื่อให้ clients ไม่ต้องตรวจสอบ null เมื่อเรียกใช้ methods ของมัน (ดู Null Object Pattern ( 112 ) สำหรับรายละเอียด)


ความปลอดภัยมาก่อน เมื่อคุณมี tests เข้ามาใช้งานแล้ว คุณสามารถเปลี่ยนแปลงที่ลุกล้ำได้ด้วยความมั่นใจมากขึ้น


Steps (ขั้นตอน)

ในการใช้ Adapt Parameter ให้ทำตามขั้นตอนต่อไปนี้:

1. สร้าง interface ใหม่ที่คุณจะใช้ใน method ทำให้มันง่ายและสื่อสารได้ดีที่สุด แต่พยายามอย่าสร้าง interface ที่จะต้องมีการเปลี่ยนแปลงที่ไม่สำคัญใน method

2. สร้าง production implementer สำหรับ interface ใหม่

3. สร้าง fake implementer สำหรับ interface

4. เขียน test case ง่ายๆ โดยส่ง fake ให้กับ method

5. ทำการเปลี่ยนแปลงที่จำเป็นใน method เพื่อใช้ parameter ใหม่

6. รัน test ของคุณเพื่อยืนยันว่าคุณสามารถทดสอบ method โดยใช้ fake ได้

Break Out Method Object (แยก Method Object)

Methods ที่ยาวมากเป็นเรื่องที่จัดการยากในหลายๆ แอปพลิเคชัน บ่อยครั้งถ้าคุณสามารถสร้าง instance ของคลาสที่มี methods เหล่านั้นและนำมันเข้า test harness คุณก็เริ่มเขียน tests ได้ ในบางกรณี งานที่ต้องใช้เพื่อทำให้คลาสสามารถสร้าง instance แยกต่างหากได้นั้นมีมาก มันอาจจะมากเกินไปสำหรับการเปลี่ยนแปลงที่คุณต้องการทำ ถ้า method ที่คุณต้องการจัดการมีขนาดเล็กและไม่ใช้ instance data ให้ใช้ Expose Static Method ( 345 ) เพื่อนำการเปลี่ยนแปลงของคุณมาอยู่ภายใต้การทดสอบ ในทางกลับกัน ถ้า method ของคุณใหญ่หรือใช้ instance data และ methods ให้พิจารณาใช้ Break Out Method Object โดยสรุปแล้ว แนวคิดเบื้องหลังการ refactor นี้คือการย้าย method ที่ยาวไปยังคลาสใหม่ อ็อบเจกต์ที่คุณสร้างโดยใช้คลาสใหม่นั้นเรียกว่า method objects เพราะมันรวบรวมโค้ดของ method เดียว หลังจากที่คุณใช้ Break Out Method Object แล้ว คุณมักจะสามารถเขียน tests สำหรับคลาสใหม่ได้ง่ายกว่าสำหรับ method เก่า Local variables ใน method เก่า สามารถกลายเป็น instance variables ในคลาสใหม่ ซึ่งมักจะทำให้การทำลาย dependencies และการย้ายโค้ด ไปยังสถานะที่ดีกว่าทำได้ง่ายขึ้น

นี่คือตัวอย่างใน C++ (ส่วนใหญ่ของคลาสและ method ถูกลบออกไปเพื่อรักษาความเป็นระเบียบ):

class GDIBrush
{
public:
    void draw(vector<point>& renderingRoots,
              ColorMatrix& colors,
              vector<point>& selection);
    ...

private:
    void drawPoint(int x, int y, COLOR color);
    ...

};

void GDIBrush::draw(vector<point>& renderingRoots,
                    ColorMatrix& colors,
                    vector<point>& selection)
{
    for(vector<points>::iterator it = renderingRoots.begin();
            it != renderingRoots.end();
            ++it) {
        point p = *it;
        ...

        drawPoint(p.x, p.y, colors[n]);
    }
    ...

}

คลาส GDIBrush มี method ที่ยาวชื่อ draw. เราไม่สามารถเขียน tests ให้มันได้ง่ายๆ และมันจะยากมากที่จะสร้าง instance ของ GDIBrush ใน test harness เรามาใช้ Break Out Method Object เพื่อย้าย draw ไปยังคลาสใหม่

ขั้นแรกคือการสร้างคลาสใหม่ที่จะทำงาน drawing เราสามารถเรียกมันว่า Renderer หลังจากที่สร้างมันแล้ว เราให้มันมี public constructor อาร์กิวเมนต์ของ constructor ควรเป็น reference ไปยังคลาสเดิม และอาร์กิวเมนต์ของ method เดิม เราต้อง Preserve Signatures ( 312 ) ในส่วนหลัง

class Renderer
{
public:
    Renderer( GBIBrush *brush,
              vector<point>& renderingRoots,
              ColorMatrix &colors,
              vector<point>& selection);
    ...

};

หลังจากที่เราสร้าง constructor แล้ว เราเพิ่ม instance variables สำหรับอาร์กิวเมนต์ของ constructor แต่ละตัวและ initialize พวกมัน เราทำสิ่งนี้เป็นชุดของการ cut/copy/paste เช่นกัน เพื่อ Preserve Signatures ( 312 ) .

class Renderer
{
private:
     GDIBrush *brush;
     vector<point>& renderingRoots;
     ColorMatrix& colors;
     vector<point>& selection;

public:
    Renderer(GDIBrush *brush,
             vector<point>& renderingRoots,
             ColorMatrix& colors,
             vector<point>& selection)
        : brush(brush), renderingRoots(renderingRoots),
          colors(colors), selection(selection)
        {}

};

คุณอาจกำลังมองสิ่งนี้แล้วพูดว่า "หืม ดูเหมือนว่าเราจะอยู่ในตำแหน่งเดิมนะ เรากำลังรับ reference ไปยัง GDIBrush, และเราไม่สามารถสร้าง instance ของมันใน test harness ได้ แล้วมันจะมีประโยชน์อะไร?" เดี๋ยวก่อน เราจะลงเอยที่ จุดที่ต่างออกไป

หลังจากที่เราสร้าง constructor แล้ว เราสามารถเพิ่ม method อีกตัวในคลาส ซึ่งเป็น method ที่จะทำงานที่เคยทำใน draw() method เราสามารถเรียกมันว่า draw() เช่นกัน

class Renderer
{
private:
    GDIBrush *brush;
    vector<point>& renderingRoots;
    ColorMatrix& colors;
    vector<point>& selection;

public:
    Renderer(GDIBrush *brush,
             vector<point>& renderingRoots,
             ColorMatrix& colors,
             vector<point>& selection)
        : brush(brush), renderingRoots(renderingRoots),
          colors(colors), selection(selection)
        {}

     void draw();
};

ตอนนี้เราเพิ่ม body ของ method draw() ไปที่ Renderer เราคัดลอก body ของ draw() เดิมไปยังอันใหม่แล้ว Lean on the Compiler ( 315 ) .

void Renderer::draw()
{
    for(vector<points>::iterator it = renderingRoots.begin();
            it != renderingRoots.end();
            ++it) {
        point p = *it;
        ...
        drawPoint(p.x, p.y, colors[n]);
    }
    ...

}

ถ้า draw() บน Renderer มี reference ใดๆ ไปยัง instance variables หรือ methods จาก GDIBrush การ compile จะล้มเหลว เพื่อให้มันสำเร็จ เราสามารถสร้าง getters สำหรับตัวแปรและทำให้ methods ที่มันขึ้นอยู่กับเป็น public ในกรณีนี้ มี dependency อยู่ที่เดียว คือ private method ที่ชื่อ drawPoint หลังจากที่เราให้มันเป็น public บน GDIBrush แล้ว เราสามารถเข้าถึงมันจาก reference ของ Renderer ได้และโค้ดก็ compile ได้

ตอนนี้เราสามารถให้ draw ของ GDIBrush delegate ไปยัง Renderer ใหม่

void GDIBrush::draw(vector<point>& renderingRoots,
                    ColorMatrix &colors,
                    vector<point>& selection)
{
     Renderer renderer(this, renderingRoots,
                       colors, selection);
     renderer.draw();
}

กลับมาที่ dependency ของ GDIBrush ถ้าเราไม่สามารถสร้าง instance GDIBrush ใน test harness ได้ เราสามารถใช้ Extract Interface เพื่อทำลาย dependency กับ GDIBrush โดยสิ้นเชิง ส่วนในหัวข้อ Extract Interface ( 362 ) มีรายละเอียด แต่โดยย่อคือ เราสร้าง interface class ที่ว่างเปล่าและให้ GDIBrush implement มัน ในกรณีนี้ เราสามารถเรียกมันว่า PointRenderer เพราะ drawPoint คือ method บน GDIBrush ที่เราต้องเข้าถึงจริงๆ ใน Renderer จากนั้นเราเปลี่ยน reference ที่ Renderer ถืออยู่จาก GDIBrush เป็น PointRenderer compile และให้ compiler บอกเราว่า methods ใดบ้างที่ต้องอยู่บน interface นี่คือลักษณะของโค้ดเมื่อเสร็จสิ้น:

class PointRenderer
{
    public:
        virtual void drawPoint(int x, int y, COLOR color) = 0;
};

class GDIBrush : public PointRenderer
{
public:
    void drawPoint(int x, int y, COLOR color);
    ...
};

class Renderer
{
private:
     PointRender *pointRenderer;
    vector<point>& renderingRoots;
    ColorMatrix& colors;
    vector<point>& selection;

public:
    Renderer( PointRenderer *renderer ,
             vector<point>& renderingRoots,
             ColorMatrix& colors,
             vector<point>& selection)
        : pointRenderer(pointRenderer),
          renderingRoots(renderingRoots),
          colors(colors), selection(selection)
        {}

    void draw();
};

void Renderer::draw()
{
    for(vector<points>::iterator it = renderingRoots.begin();
            it != renderingRoots.end();
            ++it) {
        point p = *it;
        ...
        pointRenderer->drawPoint(p.x,p.y,colors[n]);
    }
    ...

}

Figure 25.1 แสดงให้เห็นในรูปแบบ UML

Figure 25.1 GDIBrush after Break Out Method Object (หลังจาก Break Out Method Object)

image

จุดสิ้นสุดของเราแปลกนิดหน่อย เรามีคลาส ( GDIBrush ) ที่ implement interface ใหม่ ( PointRenderer ) และการใช้งานเดียวของ interface นั้นคือโดยอ็อบเจกต์ (a Renderer ) ที่ถูกสร้างโดยคลาสนั้น คุณอาจรู้สึกไม่สบายใจเพราะเราได้ทำให้รายละเอียดที่เป็น private ในคลาสเดิมกลายเป็น public เพื่อที่จะใช้เทคนิคนี้ ตอนนี้ method drawPoint ที่เคยเป็น private ใน GDIBrush ก็ถูกเปิดเผยสู่สาธารณะ สิ่งสำคัญที่ต้องสังเกตคือ นี่ไม่ใช่จุดจบจริงๆ

เมื่อเวลาผ่านไป คุณจะเริ่มรู้สึกเบื่อหน่ายกับความจริงที่ว่าคุณไม่สามารถสร้าง instance ของคลาสเดิมใน test harness ได้ และคุณจะทำลาย dependencies เพื่อให้ทำได้ จากนั้นคุณจะมองหาทางเลือกอื่น เช่น PointRenderer จำเป็นต้องเป็น interface หรือไม่? มันสามารถเป็นคลาสที่ถือ GDIBrush ได้หรือไม่? ถ้าได้ บางทีคุณอาจเริ่มย้ายไปยังดีไซน์ที่อยู่บนพื้นฐานแนวคิดใหม่ของ Renderers

นั่นเป็นเพียงหนึ่งในการ refactor ง่ายๆ ที่เราอาจทำได้เมื่อเรานำคลาสมาอยู่ภายใต้การทดสอบ โครงสร้างที่ได้อาจนำไปสู่การปรับปรุงอีกมากมาย


Break Out Method Object มีหลายรูปแบบ ในกรณีที่ง่ายที่สุด method เดิมไม่ได้ใช้ instance variables หรือ methods ใดๆ จากคลาสเดิม เลยไม่จำเป็นต้องส่ง reference ไปยังคลาสเดิม

ในกรณีอื่นๆ method ใช้เฉพาะข้อมูลจากคลาสเดิม ในบางครั้ง การนำข้อมูลนี้ใส่ในคลาสใหม่ที่เก็บข้อมูลและส่งมันเป็นอาร์กิวเมนต์ให้กับ method object ก็สมเหตุสมผล

กรณีที่ผมแสดงในส่วนนี้คือกรณีที่แย่ที่สุด เราต้องใช้ methods บนคลาสเดิม ดังนั้นเราจึงใช้ Extract Interface ( 362 ) และเริ่มสร้าง abstraction ระหว่าง method object และคลาสเดิม


Steps (ขั้นตอน)

คุณสามารถใช้ขั้นตอนเหล่านี้เพื่อทำ Break out Method Object อย่างปลอดภัยโดยไม่มี tests:

1. สร้างคลาสที่จะเก็บโค้ดของ method

2. สร้าง constructor สำหรับคลาสและ Preserve Signatures ( 312 ) เพื่อให้มีสำเนาที่เหมือนกันของอาร์กิวเมนต์ที่ method ใช้ ถ้า method ใช้ instance data หรือ methods จากคลาสเดิม ให้เพิ่ม reference ไปยังคลาสเดิมเป็นอาร์กิวเมนต์แรกของ constructor

3. สำหรับแต่ละอาร์กิวเมนต์ใน constructor ให้ประกาศ instance variable และกำหนดชนิดเดียวกันกับตัวแปรนั้น Preserve Signatures ( 312 ) โดยการคัดลอกอาร์กิวเมนต์ทั้งหมดไปยังคลาสโดยตรงและจัดรูปแบบเป็น instance variable declarations กำหนดอาร์กิวเมนต์ทั้งหมดให้กับ instance variables ใน constructor

4. สร้าง execution method ที่ว่างเปล่าบนคลาสใหม่ โดยปกติ method นี้มักถูกเรียกว่า run(). เราใช้ชื่อ draw ในตัวอย่าง

5. คัดลอก body ของ method เก่าไปยัง execution method และ compile เพื่อ Lean on the Compiler ( 315 )

6. ข้อความผิดพลาดจาก compiler จะบ่งชี้ว่า method ยังคงใช้ methods หรือ variables จากคลาสเก่าอยู่ที่ไหน ในแต่ละกรณี ให้ทำสิ่งที่จำเป็นเพื่อให้ method compile ได้ ในบางกรณี ก็แค่เปลี่ยนการเรียกใช้ให้ใช้ reference ไปยังคลาสเดิม ในกรณีอื่น คุณอาจต้องทำให้ methods เป็น public บนคลาสเดิมหรือแนะนำ getters เพื่อไม่ให้ต้องทำให้ instance variables เป็น public

7. หลังจากคลาสใหม่ compile ได้แล้ว กลับไปที่ method เดิมและเปลี่ยนให้มันสร้าง instance ของคลาสใหม่และ delegate งานไปให้มัน

8. ถ้าจำเป็น ให้ใช้ Extract Interface ( 362 ) เพื่อทำลาย dependency กับคลาสเดิม

Definition Completion (เติม Definition)

ในบางภาษา เราสามารถประกาศ type ในที่หนึ่งและกำหนดมันในอีกที่หนึ่ง ภาษาที่ความสามารถนี้เด่นชัดที่สุดคือ C และ C++ ในทั้งสองภาษา เราสามารถประกาศ function หรือ method ในที่หนึ่งและกำหนดมันที่อื่น โดยปกติใน implementation file เมื่อเรามีความสามารถนี้ เราสามารถใช้มันเพื่อทำลาย dependencies

นี่คือตัวอย่าง:

class CLateBindingDispatchDriver : public CDispatchDriver
{
public:
                CLateBindingDispatchDriver ();
    virtual    ~CLateBindingDispatchDriver ();

    ROOTID      GetROOTID (int id) const;

    void        BindName (int id,
                          OLECHAR FAR *name);
    ...

private:
    CArray<ROOTID, ROOTID& > rootids;

};

นี่คือการประกาศคลาสเล็กๆ ในแอปพลิเคชัน C++ ผู้ใช้สร้าง CLateBindingDispatchDrivers และใช้ method BindName เพื่อเชื่อมโยงชื่อกับ IDs เราต้องการให้มีวิธีที่แตกต่างในการผูกชื่อเมื่อเราใช้คลาสนี้ใน test ใน C++ เราสามารถทำได้โดยใช้ Definition Completion method BindName ถูกประกาศในไฟล์ header ของคลาส เราจะให้ definition ที่แตกต่างภายใต้ test ได้อย่างไร? เรารวม header ที่มี class declaration นี้ในไฟล์ test และให้ alternate definitions สำหรับ methods ก่อน tests ของเรา

#include "LateBindingDispatchDriver.h"

CLateBindingDispatchDriver::CLateBindingDispatchDriver() {}

CLateBindingDispatchDriver::~CLateBindingDispatchDriver() {}

ROOTID GetROOTID (int id) const { return ROOTID(-1); }

void BindName(int id, OLECHAR FAR *name) {}

TEST(AddOrder,BOMTreeCtrl)
{
     CLateBindingDispatchDriver driver;
     CBOMTreeCtrl  ctrl(&driver);

     ctrl.AddOrder(COrderFactory::makeDefault());
     LONGS_EQUAL(1, ctrl.OrderCount());
}

เมื่อเรากำหนด methods เหล่านี้โดยตรงในไฟล์ test เรากำลังให้ definitions ที่จะถูกใช้ใน test เราสามารถให้ bodies ที่ว่างเปล่าสำหรับ methods ที่เราไม่สนใจ หรือใส่ sensing methods ที่สามารถใช้ ได้ในทุก tests ของเรา

เมื่อเราใช้ Definition Completion ใน C หรือ C++ เราจำเป็นต้องสร้าง executable แยกต่างหากสำหรับ tests ที่ใช้ definitions ที่ถูกเติม ถ้าเราไม่ทำ พวกมันจะขัดแย้งกับ definitions จริงตอน link time ข้อเสียอีกอย่างคือตอนนี้เรา มี definitions สองชุดที่แตกต่างกันสำหรับ methods ของคลาส ชุดหนึ่งในไฟล์ source ของ test และอีกชุดใน ไฟล์ source ของ production ซึ่งอาจเป็นภาระในการบำรุงรักษาที่ใหญ่ มันยังอาจทำให้ debuggers สับสนถ้าเราไม่ได้ตั้งค่า สภาพแวดล้อมอย่างถูกต้อง ด้วยเหตุผลเหล่านี้ ผมไม่แนะนำให้ใช้ Definition Completion ยกเว้นในสถานการณ์ dependency ที่แย่ที่สุด แม้กระนั้น ผมแนะนำให้ทำแค่เพื่อทำลาย dependencies เริ่มต้น หลังจากนั้น คุณควรนำคลาสมาอยู่ภายใต้ test โดยเร็วเพื่อให้ definitions ที่ซ้ำกันสามารถถูกลบออกได้

Steps (ขั้นตอน)

ในการใช้ Definition Completion ใน C++ ให้ทำตามขั้นตอนเหล่านี้:

1. ระบุคลาสที่มี definitions ที่คุณต้องการแทนที่

2. ตรวจสอบว่า method definitions อยู่ในไฟล์ source ไม่ใช่ header

3. รวม header ในไฟล์ source test ของคลาสที่คุณกำลังทดสอบ

4. ตรวจสอบว่าไฟล์ source ของคลาสไม่ได้เป็นส่วนหนึ่งของ build

5. Build เพื่อค้นหา methods ที่หายไป

6. เพิ่ม method definitions ในไฟล์ source test จนกว่าคุณจะได้ build ที่สมบูรณ์

Encapsulate Global References (ห่อหุ้ม Global References)

เมื่อคุณพยายามทดสอบโค้ดที่มี dependencies ที่เป็นปัญหากับ globals โดยพื้นฐานคุณมีสามตัวเลือก คุณ สามารถทำให้ globals มีพฤติกรรมแตกต่างภายใต้ test คุณสามารถ link ไปยัง globals ที่แตกต่างกัน หรือคุณสามารถห่อหุ้ม globals เพื่อให้คุณสามารถแยกส่วนต่างๆ ได้มากขึ้น ตัวเลือกสุดท้ายเรียกว่า Encapsulate Global References นี่คือตัวอย่างใน C++:

bool AGG230_activeframe[AGG230_SIZE];
bool AGG230_suspendedframe[AGG230_SIZE];

void AGGController::suspend_frame()
{
    frame_copy(AGG230_suspendedframe,
               AGG230_activeframe);
    clear(AGG230_activeframe);
    flush_frame_buffers();
}

void AGGController::flush_frame_buffers()
{
    for (int n = 0; n < AGG230_SIZE; ++n) {
        AGG230_activeframe[n] = false;
        AGG230_suspendedframe[n] = false;
    }
}

ในตัวอย่างนี้ เรามีโค้ดที่ทำงานกับ global arrays สองสามตัว method suspend_frame จำเป็นต้องเข้าถึง active และ suspended frames เมื่อมองดูครั้งแรก ดูเหมือนว่าเราสามารถทำให้ frames เป็น members ของคลาส AGGController ได้ แต่คลาสอื่นๆ (ไม่ได้แสดง) ก็ใช้ frames เช่นกัน เราจะทำอย่างไร?

ความคิดแรกคือ เราสามารถส่งผ่านพวกมันเป็น parameters ไปยัง method suspend_frame โดยใช้ Parameterize Method ( 383 ) แต่หลังจากนั้น เราจะต้องส่งผ่านพวกมันเป็น parameters ไปยัง methods ใดๆ ที่ suspend_frame เรียกใช้และใช้พวกมันเป็น globals ในกรณีนี้ flush_frame_buffer ก็เป็นตัวการ

ตัวเลือกถัดไปคือการส่งผ่าน frames ทั้งสองเป็น constructor arguments ไปยัง AGGController เราสามารถทำได้ แต่ก็ควรดูที่อื่นๆ ที่พวกมันถูกใช้ด้วย ถ้าดูเหมือนว่าเมื่อใดก็ตามที่เรา ใช้อันหนึ่งเราก็ใช้อีกอันด้วย เราอาจรวมพวกมันเข้าด้วยกัน


ถ้า globals หลายตัวถูกใช้หรือถูกแก้ไขใกล้กัน พวกมันควรอยู่ในคลาสเดียวกัน


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


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


ในตัวอย่างก่อนหน้านี้ ผมคาดว่าเมื่อเวลาผ่านไป method frame_copy และ clear อาจย้ายไปยังคลาสใหม่ที่เราจะสร้าง มีงานที่เหมือนกันระหว่าง suspended frame และ active frame หรือไม่? ดูเหมือนว่ามีในกรณีนี้ function suspend_frame บน AGGController อาจย้ายไปยังคลาสใหม่ได้ตราบเท่าที่มันมีทั้ง array suspended_frame และ active_frame เราจะเรียกคลาสใหม่นี้ว่าอะไร? เราสามารถเรียกมันว่า Frame และบอกว่าแต่ละ frame มี active buffer และ suspended buffer ซึ่งต้องให้เราเปลี่ยนแนวคิดและ เปลี่ยนชื่อตัวแปรบ้าง แต่สิ่งที่เราจะได้กลับมาคือคลาสที่ฉลาดขึ้นซึ่งซ่อนรายละเอียดมากขึ้น


ชื่อคลาสที่คุณหาได้อาจมีคนใช้แล้ว ถ้าเป็นเช่นนั้น ให้พิจารณาว่าคุณสามารถเปลี่ยนชื่อสิ่งที่ใช้ชื่อนั้นได้หรือไม่


นี่คือวิธีที่เราทำ ทีละขั้นตอน

ขั้นแรก เราสร้างคลาสที่มีลักษณะดังนี้:

class Frame
{
public:
    // declare AGG230_SIZE as a constant
    enum { AGG230_SIZE = 256 };

    bool AGG230_activeframe[AGG230_SIZE];
    bool AGG230_suspendedframe[AGG230_SIZE];

};

เราเก็บชื่อของข้อมูลไว้เหมือนเดิมโดยตั้งใจ เพื่อให้ขั้นตอนต่อไปง่ายขึ้น ต่อไป เราประกาศ global instance ของคลาส Frame

Frame  frameForAGG230;

ต่อไป เราคอมเมนต์การประกาศเดิมของข้อมูลและพยายาม build:

// bool AGG230_activeframe[AGG230_SIZE];
// bool AGG230_suspendedframe[AGG230_SIZE];

ณ จุดนี้ เราได้รับ compile errors มากมายบอกเราว่า AGG_activeframe และ AGG230_suspendedframe ไม่มีอยู่แล้ว ขู่เราด้วยผลที่ตามมาอันเลวร้าย ถ้าระบบ build ขี้บ่นพอ มันก็จะจบด้วย การพยายาม link ทิ้งเราไว้กับ unresolved link errors ประมาณ 10 หน้า เราอาจรู้สึกไม่พอใจ แต่เรา คาดหวังทั้งหมดนั้นไว้แล้วใช่มั้ย?

เพื่อผ่านข้อผิดพลาดทั้งหมดนั้น เราสามารถหยุดที่แต่ละจุดและวาง frameForAGG230 . นำหน้าแต่ละ reference ที่เป็นปัญหา

void AGGController::suspend_frame()
{
    frame_copy( frameForAGG230. AGG230_suspendedframe,
                frameForAGG230. AGG230_activeframe);
    clear( frameForAGG20. AGG230_activeframe);
    flush_frame_buffer();
}

เมื่อเราทำเสร็จ เราก็มีโค้ดที่ดูแย่ลง แต่มันจะ compile และทำงานได้ถูกต้อง ดังนั้นมันจึงเป็นการ transformation ที่รักษาพฤติกรรม ตอนนี้เราเสร็จแล้ว เราสามารถส่งผ่าน Frame object ผ่าน constructor ของคลาส AGGController และได้รับการแยกที่เราต้องการเพื่อก้าวต่อไป


การอ้างถึง member ของคลาสแทน global ธรรมดาเป็นแค่ขั้นตอนแรก หลังจากนั้น ให้พิจารณาว่าคุณควรใช้ Introduce Static Setter ( 372 ) หรือ parameterize โค้ดโดยใช้ Parameterize Constructor ( 379 ) หรือ Parameterize Method ( 383 )


ดังนั้น เราได้ introduce คลาสใหม่โดยการเพิ่ม global variables ลงในคลาสใหม่และทำให้มันเป็น public ทำไมเราถึงทำแบบนี้? ถึงอย่างไรเราก็ใช้เวลาคิดว่าจะเรียกคลาสใหม่อะไรและ methods ประเภทไหนที่จะวางบนมัน เราอาจเริ่มต้นด้วยการสร้าง fake Frame object ที่เราสามารถ delegate ไปยังมันใน AGG_Controller และเราสามารถย้ายตรรกะทั้งหมดที่ใช้ตัวแปรเหล่านั้นไปยังคลาส Frame จริง เราสามารถทำได้ แต่มันมากเกินไปที่จะพยายามทั้งหมดในครั้งเดียว แย่กว่านั้น เมื่อเราไม่มี tests และ เรากำลังพยายามทำงานน้อยที่สุดที่จำเป็นเพื่อให้มี tests เข้ามา การปล่อยให้ตรรกะอยู่กับที่มากที่สุดเท่าที่จะเป็นไปได้เป็นสิ่งที่ควรทำ เราควรหลีกเลี่ยงการย้ายมันและพยายามแยกโดยการวาง seams ที่ให้เราเรียก method หนึ่งแทนอีก method หรือเข้าถึงข้อมูลชิ้นหนึ่งแทนอีกชิ้น ต่อมา เมื่อเรามี tests มากขึ้น เราสามารถย้ายพฤติกรรมจากคลาสหนึ่งไปยังอีกคลาสได้อย่างไม่เกรงกลัว

เมื่อเราส่ง frame เข้าไปใน AGGController, เราสามารถเปลี่ยนชื่อเล็กน้อยเพื่อให้ชัดเจนขึ้น นี่คือสถานะสิ้นสุดของการ refactor นี้:

class Frame
{
public:
    enum { BUFFER_SIZE = 256 };
    bool activebuffer[BUFFER_SIZE];
    bool suspendedbuffer[BUFFER_SIZE];
};

Frame  frameForAGG230;

void AGGController::suspend_frame()
{
    frame_copy(frame.suspendedbuffer,
               frame.activebuffer);
    clear(frame.activeframe);
    flush_frame_buffer();
}

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


เมื่อคุณใช้ Encapsulate Global References ให้เริ่มด้วยข้อมูลหรือ methods เล็กๆ Methods ที่มีสาระสำคัญมากขึ้นสามารถย้ายไปยังคลาสใหม่เมื่อมี tests มากขึ้น


ในตัวอย่างก่อนหน้านี้ ผมแสดงวิธีทำ Encapsulate Global References กับ global data คุณสามารถทำสิ่งเดียวกัน กับ non-member functions ในโปรแกรม C++ บ่อยครั้งเมื่อคุณทำงานกับ C API บางตัว คุณจะมีการเรียกใช้ global functions กระจายอยู่ในพื้นที่ของโค้ดที่คุณต้องการทำงาน seam เดียวที่คุณมีคือการ link ของการเรียกใช้ไปยัง function ที่เกี่ยวข้อง คุณสามารถใช้ Link Substitution ( 377 ) เพื่อแยก แต่คุณสามารถจบลงด้วยโค้ดที่มีโครงสร้างดีกว่าถ้าคุณใช้ Encapsulate Global References เพื่อสร้าง seam อื่น นี่คือตัวอย่าง

ในชิ้นส่วนของโค้ดที่เราต้องการนำมาทดสอบ มีการเรียกใช้สอง functions: GetOption(const string optionName) และ setOption(string name, Option option) พวกมันเป็นแค่ free functions ไม่ได้ผูกกับคลาสใดๆ แต่มันถูกใช้อย่างแพร่หลายในโค้ดเช่นนี้:

void ColumnModel::update()
{
    alignRows();
    Option resizeWidth = ::GetOption("ResizeWidth");
    if (resizeWidth.isTrue()) {
        resize();
    }    else {
        resizeToDefault();
    }
}

ในกรณีเช่นนี้ เราสามารถดูที่เทคนิคเก่าๆ เช่น Parameterize Method ( 383 ) และ Extract and Override Getter ( 352 ) แต่ถ้าการเรียกใช้เกิดขึ้นในหลาย methods และหลายคลาส มันจะสะอาดกว่าถ้าใช้ Encapsulate Global References ในการทำเช่นนี้ ให้สร้างคลาสใหม่ดังนี้:

class OptionSource
{
public:
    virtual          ~OptionSource() = 0;
    virtual Option   GetOption(const string& optionName) = 0;
    virtual void     SetOption(const string& optionName,
                               const Option& newOption) = 0;
};

คลาสมี abstract methods สำหรับแต่ละ free functions ที่เราต้องการ ต่อไป สร้าง subclass เพื่อทำ fake สำหรับคลาส ในกรณีนี้ เราสามารถมี map หรือ vector ใน fake ที่ให้เราเก็บชุดของ options ที่จะใช้ระหว่าง tests เราสามารถให้ method add แก่ fake หรือแค่ constructor ที่รับ map—อะไรก็ตามที่สะดวกสำหรับ tests เมื่อเรามี fake แล้ว เราสามารถสร้าง option source ที่แท้จริง:

class  ProductionOptionSource : public OptionSource
{
public:
    Option GetOption(const string& optionName);
    void SetOption(const string& optionName,
                   const Option& newOption) ;
};

Option ProductionOptionSource::GetOption(
        const string& optionName)
{
    ::GetOption(optionName);
}

void ProductionOptionSource::SetOption(
        const string& optionName,
        const Option& newOption)
{
    ::SetOption(optionName, newOption);
}


ในการห่อหุ้ม references ไปยัง free functions ให้สร้าง interface class ที่มี fake และ production subclasses แต่ละ function ใน production code ควรทำแค่ delegate ไปยัง global function


การ refactor นี้เป็นไปด้วยดี เมื่อเรา introduce seam และจบลงด้วยการ delegation ง่ายๆ ไปยัง API function ตอนนี้เราทำอย่างนั้นแล้ว เราสามารถ parameterize คลาสเพื่อรับ OptionSource object เพื่อที่เราจะได้ใช้ fake ใน test และของจริงใน production

ในตัวอย่างก่อนหน้านี้ เราใส่ functions ในคลาสและทำให้มันเป็น virtual เราทำด้วยวิธีอื่นได้มั้ย? ได้ เราสามารถสร้าง free functions ที่ delegate ไปยัง free functions อื่นๆ หรือเพิ่มพวกมันในคลาสใหม่เป็น static functions แต่วิธีใดวิธีหนึ่งนั้นไม่ได้ให้ seams ที่ดีแก่เรา เราจะต้องใช้ link seam ( 36 ) หรือ preprocessing seam ( 33 ) เพื่อแทนที่ implementation หนึ่งด้วยอีกอัน เมื่อเราใช้คลาสและ virtual function approach และ parameterize คลาส seams ที่เรามีจะชัดเจนและจัดการง่าย

Steps (ขั้นตอน)

ในการ Encapsulate Global References ให้ทำตามขั้นตอนเหล่านี้:

1. ระบุ globals ที่คุณต้องการห่อหุ้ม

2. สร้างคลาสที่คุณต้องการอ้างถึงจากคลาสนั้น

3. คัดลอก globals เข้าไปในคลาส ถ้าบางตัวเป็น variables ให้จัดการ initializations ในคลาส

4. คอมเมนต์การประกาศเดิมของ globals

5. ประกาศ global instance ของคลาสใหม่

6. Lean on the Compiler ( 315 ) เพื่อค้นหา unresolved references ทั้งหมดที่อ้างถึง globals เก่า

7. นำชื่อของ global instance ของคลาสใหม่นำหน้าแต่ละ unresolved reference

8. ในที่ที่คุณต้องการใช้ fakes ให้ใช้ Introduce Static Setter ( 372 ) , Parameterize Constructor ( 379 ) , Parameterize Method ( 383 ) หรือ Replace Global Reference with Getter ( 399 )

Expose Static Method (เปิดเผย Static Method)

การทำงานกับคลาสที่ไม่สามารถสร้าง instance ใน test harness ได้นั้นค่อนข้างยุ่งยาก นี่คือเทคนิคที่ผมใช้ ในบางกรณี ถ้าคุณมี method ที่ไม่ใช้ instance data หรือ methods คุณสามารถเปลี่ยนมันเป็น static method เมื่อ มันเป็น static คุณสามารถนำมันมาทดสอบได้โดยไม่ต้องสร้าง instance ของคลาส นี่คือตัวอย่างใน Java

เรามีคลาสที่มี method validate และเราต้องเพิ่มเงื่อนไขการตรวจสอบใหม่ โชคร้ายที่คลาสที่มันอยู่จะสร้าง instance ได้ยากมาก ผมจะไม่ให้คุณเห็นความน่ากลัวของคลาสทั้งหมด แต่ต่อไปนี้คือ method ที่เราต้องเปลี่ยนแปลง:

class RSCWorkflow
{
    ...
    public void validate(Packet packet)
            throws InvalidFlowException {
        if (packet.getOriginator().equals( "MIA")
                || packet.getLength() > MAX_LENGTH
                || !packet.hasValidCheckSum()) {
            throw new InvalidFlowException();
        }
        ...
    }
    ...
}

เราจะทำอย่างไรเพื่อนำ method นี้มาอยู่ภายใต้ test? เมื่อเรามองดูอย่างใกล้ชิด เราจะเห็นว่า method ใช้ methods มากมาย บนคลาส Packet ที่จริงแล้ว การย้าย validate ไปไว้บนคลาส Packet ก็น่าจะสมเหตุสมผล แต่การย้าย method ไม่ใช่สิ่งที่เสี่ยงน้อยที่สุดที่เราสามารถทำได้ตอนนี้ เราจะไม่สามารถ Preserve Signatures ( 312 ) ได้แน่ ถ้าคุณไม่มี automated support ในการย้าย methods บ่อยครั้งที่การมี tests ก่อนก็ดีกว่า Expose Static Method สามารถช่วยคุณทำอย่างนั้นได้ เมื่อมี tests แล้ว คุณสามารถทำการเปลี่ยนแปลงที่คุณต้องการและมีความมั่นใจมากขึ้น ในการย้าย method ทีหลัง


เมื่อคุณทำลาย dependencies โดยไม่มี tests ให้ Preserve Signatures ( 312 ) ของ methods ทุกครั้งที่ทำได้ ถ้าคุณ cut/copy และ paste method signatures ทั้งหมด โอกาสที่จะเกิดข้อผิดพลาดจะมีน้อยลง


โค้ดตรงนี้ไม่ได้ขึ้นอยู่กับ instance variables หรือ methods ใดๆ มันจะหน้าตาเป็นอย่างไรถ้า method validate เป็น public static? ทุกคนในโค้ดสามารถเขียน statement นี้และ validate packet:

RSCWorkflow.validate(packet);

โอกาสที่ใครก็ตามที่สร้างคลาสนี้คงไม่คิดว่าจะมีคนทำให้ method นั้นเป็น static ในสักวัน ยิ่งกว่านั้นเป็น public ด้วย แล้วมันเป็นเรื่องไม่ดีหรือไม่? ไม่จริง การ encapsulation เป็นสิ่งที่ดีสำหรับคลาส แต่ พื้นที่ static ของคลาสนั้นไม่ใช่ส่วนหนึ่งของคลาสจริงๆ ที่จริงแล้ว ในบางภาษา มันเป็นส่วนหนึ่งของคลาสอื่น บางครั้งเรียกว่า metaclass ของคลาส

เมื่อ method เป็น static คุณรู้ว่ามันไม่ได้เข้าถึงข้อมูล private ใดๆ ของคลาส มันเป็นแค่ utility method ถ้าคุณทำให้ method เป็น public คุณสามารถเขียน tests สำหรับมัน Tests เหล่านั้นจะสนับสนุนคุณถ้าคุณเลือกที่จะย้าย method ไปยังคลาสอื่นทีหลัง


Static methods และ data ทำตัวเหมือนกับว่ามันเป็นส่วนหนึ่งของคลาสอื่น Static data อยู่ตลอดอายุของ program ไม่ใช่อายุของ instance และ statics สามารถเข้าถึงได้โดยไม่ต้องมี instance

ส่วน static ของคลาสสามารถมองเห็นได้ว่าเป็น "พื้นที่พัก" สำหรับสิ่งที่ยังไม่เหมาะสมกับคลาสนั้น ถ้าคุณเห็น method ที่ไม่ใช้ instance data ใดๆ การทำให้มันเป็น static เป็นความคิดที่ดีเพื่อให้สังเกตเห็นได้ จนกว่าคุณจะหาว่ามันควรอยู่บนคลาสไหนจริงๆ


นี่คือคลาส RSCWorkflow หลังจากที่เราได้สกัด static method สำหรับ validate

public class RSCWorkflow {
    public void validate(Packet packet)
            throws InvalidFlowException {
         validatePacket(packet);
    }

    public static void validatePacket(Packet packet)
            throws InvalidFlowException {
        if (packet.getOriginator() == "MIA"
                || packet.getLength() <= MAX_LENGTH
                || packet.hasValidCheckSum()) {
            throw new InvalidFlowException();
        }
        ...
    }
    ...
}

ในบางภาษามีวิธีที่ง่ายกว่าสำหรับ Expose Static Method แทนที่จะสกัด static method จาก method เดิม คุณสามารถทำให้ method เดิมเป็น static ได้เลย ถ้า method ถูกใช้โดยคลาสอื่น มันยังสามารถเข้าถึงได้จาก instance ของคลาสของมัน นี่คือตัวอย่าง:

RSCWorkflow workflow = new RCSWorkflow();
...
// static call that looks like a non-static call
workflow.validatePacket(packet);

อย่างไรก็ตาม ในบางภาษา คุณจะได้ compilation warning สำหรับการทำแบบนี้ ทางที่ดีควรพยายามทำให้โค้ดอยู่ในสถานะ ที่ไม่มี compile warnings

ถ้าคุณกังวลว่ามีคนอาจเริ่มใช้ static ในแบบที่จะทำให้เกิด dependency problems ทีหลัง คุณสามารถเปิดเผย static method โดยใช้ access mode ที่ไม่ใช่ public ในภาษาเช่น Java และ C# ซึ่งมี package หรือ internal visibility คุณสามารถจำกัดการเข้าถึง static หรือทำให้มันเป็น protected และเข้าถึงผ่าน testing subclass ใน C++ คุณมีตัวเลือกเดียวกัน: คุณสามารถทำให้ static method เป็น protected หรือใช้ namespace

Steps (ขั้นตอน)

ในการ Expose Static Method ให้ทำตามขั้นตอนเหล่านี้:

1. เขียน test ที่เข้าถึง method ที่คุณต้องการเปิดเผยเป็น public static method ของคลาส

2. สกัด body ของ method ไปยัง static method จำไว้ว่าต้อง Preserve Signatures ( 312 ) คุณจะต้องใช้ชื่อที่แตกต่างสำหรับ method บ่อยครั้งคุณสามารถใช้ชื่อของ parameters เพื่อช่วยคุณคิด ชื่อ method ใหม่ ตัวอย่างเช่น ถ้า method ชื่อ validate รับ Packet คุณสามารถสกัด body ของมันเป็น static method ชื่อ validatePacket

3. Compile

4. ถ้ามีข้อผิดพลาดเกี่ยวกับการเข้าถึง instance data หรือ methods ให้ดูที่ features เหล่านั้นและดูว่าพวกมัน สามารถทำให้เป็น static ได้เช่นกันหรือไม่ ถ้าได้ ให้ทำให้พวกมันเป็น static เพื่อให้ระบบ compile ได้

Extract and Override Call (สกัดและโอเวอร์ไรด์ Call)

ในบางครั้ง dependencies ที่ขัดขวางระหว่างการทดสอบนั้นค่อนข้างเป็น localized เราอาจมี method call เดียว ที่เราต้องแทนที่ ถ้าเราสามารถทำลาย dependency บน method call ได้ เราสามารถป้องกัน side effects ที่แปลกประหลาดใน การทดสอบของเราหรือรับรู้ค่าที่ส่งไปยัง call

ลองดูตัวอย่าง:

public class PageLayout
{
    private int id = 0;
    private List styles;
    private StyleTemplate template;
    ...
    protected void rebindStyles() {
        styles = StyleMaster.formStyles(template, id);
        ...
    }
    ...
}

PageLayout เรียก static function ชื่อ formStyles บนคลาสชื่อ StyleMaster มันกำหนดค่าที่คืนกลับให้กับ instance variable: styles เราจะทำอย่างไรถ้าเราต้องการรับรู้ผ่าน formStyles หรือแยก dependency ของเราบน StyleMaster ? ทางเลือกหนึ่งคือสกัด call ไปยัง method ใหม่และโอเวอร์ไรด์มันใน testing subclass สิ่งนี้เรียกว่า Extract and Override Call

นี่คือโค้ดหลังจากการสกัด:

public class PageLayout
{
    private int id = 0;
    private List styles;
    private StyleTemplate template;
    ...
    protected void rebindStyles() {
        styles = formStyles(template, id);
        ...
    }

     protected List formStyles(StyleTemplate template,
                              int id) {
         return StyleMaster.formStyles(template, id);
    }
    ...
}

ตอนนี้เรามี method formStyles เป็นของเราเองแล้ว เราสามารถโอเวอร์ไรด์มันเพื่อทำลาย dependency เราไม่ต้องการ styles สำหรับสิ่งที่เรากำลังทดสอบตอนนี้ ดังนั้นเราสามารถคืนค่า empty list

public class TestingPageLayout extends PageLayout {
     protected List formStyles(StyleTemplate template,
                             int id) {
        return new ArrayList();
    }
    ...
}

เมื่อเราพัฒนา tests ที่ต้องการ styles ต่างๆ เราสามารถปรับ method นี้เพื่อให้เราสามารถกำหนดค่าที่จะถูกคืนกลับ

Extract and Override Call เป็นการ refactor ที่มีประโยชน์มาก ผมใช้มันบ่อยมาก มันเป็นวิธีที่เหมาะในการทำลาย dependencies บน global variables และ static methods โดยทั่วไป ผมมักจะใช้มันเว้นแต่มีการเรียกใช้หลายครั้งกับ global เดียวกัน ถ้ามีการเรียกใช้หลายครั้ง ผมมักจะใช้ Replace Global Reference with Getter ( 399 ) แทน

ถ้าคุณมี automated refactoring tool Extract and Override Call เป็นเรื่องง่าย คุณสามารถทำได้โดยใช้ Extract Method ( 415 ) refactoring อย่างไรก็ตาม ถ้าคุณไม่มี ให้ใช้ขั้นตอนต่อไปนี้ ซึ่งช่วยให้คุณสามารถสกัด call ได้อย่างปลอดภัย แม้ว่าคุณ จะไม่มี tests

Steps (ขั้นตอน)

ในการ Extract and Override Call ให้ทำตามขั้นตอนเหล่านี้:

1. ระบุ call ที่คุณต้องการสกัด หา declaration ของ method ของมัน คัดลอก method signature เพื่อให้คุณสามารถ Preserve Signatures ( 312 )

2. สร้าง method ใหม่บนคลาสปัจจุบัน ให้ signature ที่คุณคัดลอกมา

3. คัดลอก call ไปยัง method ใหม่และแทนที่ call ด้วยการเรียก method ใหม่

Extract and Override Factory Method (สกัดและโอเวอร์ไรด์ Factory Method)

การสร้าง object ใน constructors อาจเป็นปัญหาที่น่ารำคาญเมื่อคุณต้องการนำคลาสมาอยู่ภายใต้ test บางครั้งงานที่เกิดขึ้น ใน object เหล่านั้นไม่ควรเกิดขึ้นใน test harness ในบางครั้ง คุณแค่อยากได้ sensing object แต่คุณทำไม่ได้เพราะการสร้าง object นั้น hard-coded อยู่ใน constructor


การทำงาน initialization ที่ hard-coded ใน constructors อาจเป็นเรื่องยากมากที่จะจัดการในการทดสอบ


ลองดูตัวอย่าง:

public class WorkflowEngine
{
    public WorkflowEngine () {
        Reader reader
            = new ModelReader(
                AppConfig.getDryConfiguration());

        Persister persister
            = new XMLStore(
                AppConfiguration.getDryConfiguration());

        this.tm = new TransactionManager(reader, persister);
        ...
    }
    ...
}

WorkflowEngine สร้าง TransactionManager ใน constructor ของมัน ถ้าการสร้างอยู่ที่อื่น เราสามารถ introduce การแยกได้ง่ายขึ้น หนึ่งใน ตัวเลือกที่เรามีคือการใช้ Extract and Override Factory Method


Extract and Override Factory Method ค่อนข้างทรงพลัง แต่มันมีประเด็นเฉพาะภาษา ตัวอย่างเช่น คุณไม่สามารถทำใน C++ ได้ C++ ไม่อนุญาตให้ virtual function calls จับคู่กับ functions ใน derived classes Java และภาษาอื่นๆ อนุญาต ใน C++ Supersede Instance Variable และ Extract and Override Getter ( 352 ) เป็นทางเลือกที่ดี ดูตัวอย่างใน Supersede Instance Variable ( 404 ) สำหรับการอภิปรายปัญหานี้


public class WorkflowEngine
{
    public WorkflowEngine () {
        this.tm = makeTransactionManager() ;
        ...
    }
     protected TransactionManager makeTransactionManager() {
         Reader reader
             \= new ModelReader(
                 AppConfiguration.getDryConfiguration());

         Persister persister
             \= new XMLStore(
                 AppConfiguration.getDryConfiguration());

         return new TransactionManager(reader, persister);
     }
    ...
}

เมื่อเรามี factory method นั้นแล้ว เราสามารถ subclass และโอเวอร์ไรด์มันเพื่อให้เราสามารถคืนค่า transaction manager ใหม่ เมื่อใดก็ตามที่เราต้องการ:

public class TestWorkflowEngine extends WorkflowEngine
{
    protected TransactionManager makeTransactionManager() {
        return new FakeTransactionManager();
    }
}

Steps (ขั้นตอน)

ในการ Extract and Override Factory Method ให้ทำตามขั้นตอนเหล่านี้:

1. ระบุการสร้าง object ใน constructor

2. สกัดงานทั้งหมดที่เกี่ยวข้องกับการสร้างลงใน factory method

3. สร้าง testing subclass และโอเวอร์ไรด์ factory method ในนั้นเพื่อหลีกเลี่ยง dependencies กับ types ที่เป็นปัญหาภายใต้ test

Extract and Override Getter (สกัดและโอเวอร์ไรด์ Getter)

Extract and Override Factory Method ( 350 ) เป็นวิธีที่ทรงพลังในการแยก dependencies บน types แต่มันใช้ไม่ได้ในทุกกรณี "รู" ใหญ่ใน การประยุกต์ใช้คือ C++ ใน C++ คุณไม่สามารถเรียก virtual function ใน derived class จาก constructor ของ base class ได้ โชคดีที่มีวิธีแก้สำหรับกรณีที่คุณแค่สร้าง object ใน constructor โดยไม่ได้ ทำงานเพิ่มเติมกับมัน

สาระสำคัญของการ refactor นี้คือการ introduce getter สำหรับ instance variable ที่คุณต้องการแทนที่ด้วย fake object จากนั้นคุณ refactor เพื่อใช้ getter ในทุกที่ในคลาส จากนั้นคุณสามารถ subclass และโอเวอร์ไรด์ getter เพื่อ ให้ object อื่นภายใต้ test

ในตัวอย่างนี้ เราสร้าง transaction manager ใน constructor เราต้องการตั้งค่าเพื่อให้คลาสสามารถใช้ transaction manager นี้ใน production และ sensing ตัวหนึ่งภายใต้ test

นี่คือสิ่งที่เราเริ่มต้นด้วย:

// WorkflowEngine.h
class WorkflowEngine
{
private:
    TransactionManager    *tm;
public:
    WorkflowEngine ();
    ...
}

// WorkflowEngine.cpp
WorkflowEngine::WorkflowEngine()
{
    Reader *reader
        = new ModelReader(
            AppConfig.getDryConfiguration());

    Persister *persister
        = new XMLStore(
            AppConfiguration.getDryConfiguration());

    tm = new TransactionManager(reader, persister);
    ...
}

และนี่คือสิ่งที่เราได้เมื่อเสร็จสิ้น:

// WorkflowEngine.h
class WorkflowEngine
{
private:
    TransactionManager    *tm;

protected:
    TransactionManager    *getTransaction() const;

public:
                           WorkflowEngine ();
    ...
}

// WorkflowEngine.cpp
WorkflowEngine::WorkflowEngine()
:tm (0)
{
    ...
}

TransactionManager *getTransactionManager() const
{
    if (tm == 0) {
        Reader *reader
            = new ModelReader(
                AppConfig.getDryConfiguration());

        Persister *persister
            = new XMLStore(
                AppConfiguration.getDryConfiguration());

        tm = new TransactionManager(reader,persister);
    }
    return tm;
}
...

สิ่งแรกที่เราทำคือ introduce lazy getter ซึ่งเป็น function ที่สร้าง transaction manager เมื่อถูกเรียกครั้งแรก จากนั้นเราแทนที่การใช้งานตัวแปรทั้งหมดด้วยการเรียก getter


lazy getter คือ method ที่ดูเหมือน getter ปกติสำหรับผู้เรียกทั้งหมด ความแตกต่างหลักคือ lazy getters สร้าง object ที่มันควรจะคืนค่าในครั้งแรกที่ถูกเรียก ในการทำเช่นนี้ พวกมันมักจะมีตรรกะที่ดูเหมือนดังนี้ สังเกตว่า instance variable thing ถูก initialize อย่างไร

Thing getThing() {
    if (thing == null) {
        thing = new Thing();
    }
    return thing;
}

Lazy getters ยังถูกใช้ใน Singleton Design Pattern (xx)


เมื่อเรามี getter นั้นแล้ว เราสามารถ subclass และโอเวอร์ไรด์เพื่อใส่ object อื่น:

class TestWorkflowEngine : public WorkflowEngine
{
public:
    TransactionManager  *getTransactionManager()
                         { return &transactionManager; }

    FakeTransactionManager  transactionManager;
};


เมื่อคุณใช้ Extract and Override Getter คุณต้องตระหนักถึงประเด็นอายุของ object เป็นพิเศษ โดยเฉพาะในภาษาที่ไม่มี garbage collection เช่น C++ ตรวจสอบให้แน่ใจว่าคุณลบ testing instance ในแบบที่สอดคล้องกับวิธีที่โค้ดลบ production instance


ใน test เราสามารถเข้าถึง fake transaction manager ได้โดยตรงถ้าต้องการ:

TEST(transactionCount, WorkflowEngine)
{
    auto_ptr<TestWorkflowEngine>  engine(new TestWorkflowEngine);
    engine.run();
    LONGS_EQUAL(0,
        engine.transactionManager.getTransactionCount());
}

ข้อเสียอย่างหนึ่งของ Extract and Override Getter คือมีโอกาสที่ใครบางคนจะใช้ตัวแปรก่อนที่มันจะถูก initialize ด้วยเหตุนี้ จึงควร ตรวจสอบให้แน่ใจว่าโค้ดทั้งหมดในคลาสใช้ getter

Extract and Override Getter ไม่ใช่เทคนิคที่ผมใช้บ่อยมาก เมื่อมีแค่ method เดียวบน object ที่เป็นปัญหา การใช้ Extract and Override Call ( 348 ) นั้นง่ายกว่ามาก แต่ Extract and Override Getter เป็นตัวเลือกที่ดีกว่าเมื่อมี methods ที่เป็นปัญหาหลายตัวบน object เดียวกัน ถ้าคุณสามารถกำจัดปัญหาทั้งหมดเหล่านั้น โดยการสกัด getter และโอเวอร์ไรด์มัน มันคือชัยชนะที่ชัดเจน

Steps (ขั้นตอน)

ในการ Extract and Override Getter ให้ทำตามขั้นตอนเหล่านี้:

1. ระบุ object ที่คุณต้องการ getter

2. สกัดตรรกะทั้งหมดที่จำเป็นในการสร้าง object ลงใน getter

3. แทนที่การใช้งาน object ทั้งหมดด้วยการเรียก getter และ initialize reference ที่ถือ object เป็น null ใน constructors ทั้งหมด

4. เพิ่มตรรกะครั้งแรกใน getter เพื่อให้ object ถูกสร้างและกำหนดให้กับ reference เมื่อใดก็ตามที่ reference เป็น null

5. Subclass คลาสและโอเวอร์ไรด์ getter เพื่อให้ object อื่นสำหรับการทดสอบ

Extract Implementer (สกัด Implementer)

Extract Interface ( 362 ) เป็นเทคนิคที่มีประโยชน์ แต่ส่วนหนึ่งของมันยาก: การตั้งชื่อ ผมมักเจอกรณีที่ผมต้องการสกัด interface แต่ชื่อที่ผมต้องการใช้เป็นชื่อของคลาสอยู่แล้ว ถ้าผมทำงานใน IDE ที่รองรับการ rename classes และ Extract Interface นี่เป็นเรื่องง่ายที่จะจัดการ เมื่อไม่มี ผมมีตัวเลือกสองสามอย่าง:

• ผมสามารถตั้งชื่อที่โง่ๆ ขึ้นมา

• ผมสามารถดู methods ที่ผมต้องการและดูว่ามันเป็น subset ของ public methods บนคลาสหรือไม่ ถ้าใช่ พวกมันอาจแนะนำชื่ออื่นสำหรับ interface ใหม่

สิ่งหนึ่งที่ผมมักจะไม่ทำคือการเติมคำนำหน้า "I" หน้าชื่อคลาสเพื่อสร้างชื่อให้ interface ใหม่ เว้นแต่มันเป็นธรรมเนียมใน code base แล้ว ไม่มีอะไรแย่ไปกว่าการทำงานในพื้นที่โค้ดที่ไม่คุ้นเคย ที่ครึ่งหนึ่งของชื่อ type ขึ้นต้นด้วย I และอีกครึ่งไม่ ทุกครั้งที่พิมพ์ชื่อ type คุณจะผิด


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


เมื่อชื่อของคลาสเหมาะสมกับชื่อของ interface และฉันไม่มี automated refactoring tools ผมใช้ Extract Implementer เพื่อแยกที่ผมต้องการ ในการสกัด implementer ของคลาส เราเปลี่ยนคลาสเป็น interface โดยการ subclass มันและผลัก methods ที่เป็นรูปธรรมทั้งหมดลงไปใน subclass นั้น นี่คือตัวอย่างใน C++:

// ModelNode.h
class ModelNode
{
private:
    list<ModelNode *>    m_interiorNodes;
    list<ModelNode *>    m_exteriorNodes;
    double               m_weight;
    void                 createSpanningLinks();

public:
    void addExteriorNode(ModelNode *newNode);
    void addInternalNode(ModelNode *newNode);
    void colorize();
    ...

};

ขั้นแรกคือการคัดลอก declaration ของคลาส ModelNode ทั้งหมดไปยังไฟล์ header อื่นและเปลี่ยนชื่อของสำเนาเป็น ProductionModelNode นี่คือส่วนหนึ่งของ declaration สำหรับคลาสที่ถูกคัดลอก:

// ProductionModelNode.h
class ProductionModeNode
{
private:
    list<ModelNode *>    m_interiorNodes;
    list<ModelNode *>    m_exteriorNodes;
    double               m_weight;
    void                 createSpanningLinks();
public:
    void addExteriorNode(ModelNode *newNode);
    void addInternalNode(ModelNode *newNode);
    void colorize();
    ...
};

ขั้นตอนต่อไปคือกลับไปที่ header ของ ModelNode และลบ declarations ของตัวแปรและ methods ที่ไม่ใช่ public ทั้งหมด จากนั้นทำให้ methods public ที่เหลือเป็น pure virtual (abstract):

// ModelNode.h
class ModelNode
{
public:
virtual void addExteriorNode(ModelNode *newNode) = 0;
virtual void addInternalNode(ModelNode *newNode) = 0;
virtual void colorize() = 0;
    ...
};

ณ จุดนี้ ModelNode เป็น pure interface แล้ว มันมีแค่ abstract methods เรากำลังทำงานใน C++ ดังนั้นเราควรประกาศ pure virtual destructor และกำหนดมันใน implementation file:

// ModelNode.h
class ModelNode
{
public:
    virtual         ~ModelNode () = 0;
    virtual void     addExteriorNode(ModelNode *newNode) = 0;
    virtual void     addInternalNode(ModelNode *newNode) = 0;
    virtual void     colorize() = 0;
    ...
};

// ModelNode.cpp
ModelNode::~ModelNode()
{}

ตอนนี้เรากลับไปที่คลาส ProductionModelNode และให้มันสืบทอด interface class ใหม่:

#include "ModelNode.h"
class ProductionModelNode : public ModelNode
{
private:
    list<ModelNode *>    m_interiorNodes;
    list<ModelNode *>    m_exteriorNodes;

    double               m_weight;
    void                 createSpanningLinks();

public:
    void addExteriorNode(ModelNode *newNode);
    void addInternalNode(ModelNode *newNode);
    void colorize();
    ...

};

ณ จุดนี้ ProductionModelNode ควร compile ได้เรียบร้อย ถ้าคุณ build ระบบที่เหลือ คุณจะพบที่ที่คนพยายามสร้าง instance ModelNodes คุณสามารถเปลี่ยนพวกมันเพื่อให้สร้าง ProductionModelNodes แทน ในการ refactor นี้ เรากำลังแทนที่การสร้าง object ของคลาส concrete หนึ่งด้วย object ของอีกคลาส ดังนั้นเราจึงไม่ได้ทำให้สถานการณ์ dependency โดยรวมดีขึ้น อย่างไรก็ตาม มันเป็นการดีที่จะดู พื้นที่ของการสร้าง object เหล่านั้นและพยายามหาว่า factory สามารถถูกใช้เพื่อลด dependencies ต่อไปได้หรือไม่

Steps (ขั้นตอน)

ในการ Extract Implementer ให้ทำตามขั้นตอนเหล่านี้:

1. ทำสำเนาของ declaration ของคลาสต้นฉบับ ตั้งชื่อที่แตกต่างกัน การมี naming convention สำหรับคลาสที่คุณสกัดนั้นมีประโยชน์ ผมมักใช้คำนำหน้า Production เพื่อบ่งชี้ว่าคลาสใหม่คือ production code implementer ของ interface

2. เปลี่ยนคลาสต้นฉบับเป็น interface โดยการลบ methods และตัวแปรที่ไม่ใช่ public ทั้งหมด

3. ทำให้ methods public ที่เหลือทั้งหมดเป็น abstract ถ้าคุณทำงานใน C++ ตรวจสอบให้แน่ใจว่าไม่มี methods ที่คุณทำให้ abstract ถูกโอเวอร์ไรด์โดย non-virtual methods

4. ตรวจสอบ imports หรือ file inclusions ทั้งหมดในไฟล์ interface และดูว่าจำเป็นหรือไม่ บ่อยครั้งคุณสามารถลบ หลายอันได้ คุณสามารถ Lean on the Compiler ( 315 ) เพื่อตรวจจับสิ่งเหล่านี้ เพียงแค่ลบทีละอันและ compile ใหม่เพื่อดูว่าจำเป็นหรือไม่

5. ทำให้คลาส production ของคุณ implement interface ใหม่

6. Compile คลาส production เพื่อให้แน่ใจว่า method signatures ทั้งหมดใน interface ถูก implement

7. Compile ระบบที่เหลือเพื่อค้นหาทุกที่ที่มีการสร้าง instance ของคลาสต้นฉบับ แทนที่ การสร้างเหล่านี้ด้วยคลาส production ใหม่

8. Compile ใหม่และทดสอบ

A More Complex Example (ตัวอย่างที่ซับซ้อนมากขึ้น)

Extract Implementer ค่อนข้างง่ายเมื่อคลาสต้นฉบับไม่มีคลาสพ่อแม่หรือคลาสลูกในลำดับชั้นการสืบทอด เมื่อมันมี เราต้องฉลาดขึ้นเล็กน้อย Figure 25.2 แสดง ModelNode อีกครั้ง แต่ใน Java ที่มี superclass และ subclass:

Figure 25.2 ModelNode with superclass and subclass (ที่มี superclass และ subclass)

image

ในดีไซน์นี้ Node , ModelNode , และ LinkageNode เป็นคลาส concrete ทั้งหมด ModelNode ใช้ protected methods จาก Node มันยังให้ methods ที่ถูกใช้โดย subclass ของมัน LinkageNode Extract Implementer ต้องการคลาส concrete ที่สามารถแปลงเป็น interface ได้ หลังจากนั้น คุณจะมี interface และคลาส concrete

นี่คือสิ่งที่เราสามารถทำได้ในสถานการณ์นี้ เราสามารถทำ Extract Implementer บนคลาส Node โดยวางคลาส ProductionNode ไว้ใต้ Node ในลำดับชั้นการสืบทอด เรายังเปลี่ยนความสัมพันธ์การสืบทอดเพื่อให้ ModelNode สืบทอด ProductionNode แทนที่จะเป็น Node Figure 25.3 แสดงให้เห็นว่าดีไซน์มีลักษณะอย่างไรหลังจากนั้น

Figure 25.3 After Extract Implementer on (หลังจาก Extract Implementer บน) Node.

image

ต่อไป เราทำ Extract Implementer บน ModelNode เพราะ ModelNode มี subclass อยู่แล้ว เราจึง introduce ProductionModelNode เข้าไปในลำดับชั้นระหว่าง ModelNode และ LinkageNode เมื่อเราทำเสร็จแล้ว เราสามารถทำให้ ModelNode interface extend Node ดังที่แสดงใน Figure 25.4

Figure 25.4 Extract Implementer on (สกัด Implementer บน) ModelNode

image

เมื่อคุณมีคลาสที่ฝังอยู่ในลำดับชั้นแบบนี้ คุณต้องพิจารณาว่าคุณจะดีกว่าหรือไม่ถ้าใช้ Extract Interface ( 362 ) และเลือกชื่อที่แตกต่างสำหรับ interfaces ของคุณ มันเป็นการ refactor ที่ตรงไปตรงมากว่า

Extract Interface (สกัด Interface)

ในหลายภาษา Extract Interface เป็นหนึ่งในเทคนิคการทำลาย dependency ที่ปลอดภัยที่สุด ถ้าคุณทำผิดขั้นตอน compiler จะบอกคุณทันที ดังนั้น จึงมีโอกาสน้อยมากที่จะเกิด bug สาระสำคัญของมันคือคุณสร้าง interface สำหรับคลาสด้วย declarations สำหรับ methods ทั้งหมดที่คุณต้องการใช้ในบางบริบท เมื่อคุณทำเสร็จแล้ว คุณสามารถ implement interface เพื่อรับรู้หรือแยก โดยส่ง fake object เข้าไปในคลาสที่คุณต้องการทดสอบ

มีสามวิธีในการทำ Extract Interface และข้อควรระวังเล็กน้อยที่ต้องใส่ใจ วิธีแรกคือใช้ automated refactoring support ถ้าคุณ โชคดีพอที่จะมีในสภาพแวดล้อมของคุณ เครื่องมือที่รองรับนี้มักจะให้วิธีเลือก methods บนคลาสและพิมพ์ชื่อของ interface ใหม่ เครื่องมือที่ดีจริงๆ จะถามคุณว่าต้องการให้ค้นหา โค้ดและหาที่ที่สามารถเปลี่ยน references เพื่อใช้ interface ใหม่หรือไม่ เครื่องมือแบบนั้นสามารถช่วยคุณ ประหยัดงานได้มาก

ถ้าคุณไม่มี automated support สำหรับการสกัด interface คุณสามารถใช้วิธีที่สอง: คุณสามารถสกัด มันแบบเพิ่มทีละน้อยโดยใช้ขั้นตอนที่ผมสรุปในส่วนนี้

วิธีที่สามในการสกัด interface คือการ cut/copy และวางหลาย methods จากคลาสพร้อมกันและวาง declarations ของพวกมันใน interface มันไม่ปลอดภัยเท่าสองวิธีแรก แต่มันยังค่อนข้างปลอดภัย และบ่อยครั้งมันเป็น วิธีเดียวที่ใช้ได้จริงในการสกัด interface เมื่อคุณไม่มี automated support และ build ของคุณใช้เวลา นานมาก

มาสกัด interface โดยใช้วิธีที่สองกัน ในระหว่างทาง เราจะพูดถึงสิ่งที่ต้องระวัง

เราต้องสกัด interface เพื่อนำคลาส PaydayTransaction มาอยู่ภายใต้ test Figure 25.5 แสดง PaydayTransaction และ dependency หนึ่งของมัน คลาสชื่อ TransactionLog.

Figure 25.5 PaydayTransaction depending on (ขึ้นอยู่กับ) TransactionLog.

image

เรามี test case:

void testPayday()
{
    Transaction t = new PaydayTransaction(getTestingDatabase());
    t.run();

    assertEquals(getSampleCheck(12),
                 getTestingDatabase().findCheck(12));
}

แต่เราต้องส่ง TransactionLog บางอย่างเพื่อให้มัน compile มาสร้างการเรียกใช้คลาสที่ยังไม่มี FakeTransactionLog

void testPayday()
{
    FakeTransactionLog aLog = new FakeTransactionLog();
    Transaction t = new PaydayTransaction(
                            getTestingDatabase(),
                            aLog);
    t.run();

    assertEquals(getSampleCheck(12),
                 getTestingDatabase().findCheck(12));
}

เพื่อให้โค้ดนี้ compile ได้ เราต้องสกัด interface สำหรับคลาส TransactionLog สร้างคลาสชื่อ FakeTransactionLog implement interface และทำให้ PaydayTransaction รับ FakeTransactionLog

สิ่งแรกที่ต้องทำ: เราสกัด interface เราสร้างคลาสใหม่ที่ว่างเปล่าชื่อ TransactionRecorder ถ้าคุณสงสัยว่าชื่อนั้นมาจากไหน ให้ดูหมายเหตุต่อไปนี้


Interface Naming (การตั้งชื่อ Interface)

Interfaces ค่อนข้างใหม่ในฐานะสิ่งก่อสร้างในการเขียนโปรแกรม Java และภาษา .NET หลายภาษามี ใน C++ คุณต้อง เลียนแบบพวกมันโดยการสร้างคลาสที่มีแต่ pure virtual functions

เมื่อ interfaces ถูก introduce ในภาษาเป็นครั้งแรก บางคนเริ่มตั้งชื่อ interfaces โดยวาง I หน้าชื่อของคลาสที่พวกมันถูกดึงมา ตัวอย่างเช่น ถ้าคุณมีคลาส Account และคุณต้องการ interface คุณสามารถตั้งชื่อมันว่า IAccount ข้อดีของการตั้งชื่อแบบนี้คือคุณไม่ต้องคิดชื่อเมื่อทำการสกัด การตั้งชื่อง่ายเพียงแค่เติมคำนำหน้า ข้อเสียคือคุณลงเอยด้วยโค้ดมากมายที่ ต้องรู้ว่ามันกำลังจัดการกับ interface หรือไม่ ตามหลักแล้ว มันไม่ควรแยกว่าอะไรเป็นอะไร คุณยังลงเอยด้วย code base ที่ชื่อบางชื่อมีคำนำหน้า I และบางชื่อไม่มี การลบ I ถ้าคุณต้องการกลับไปเป็นคลาสปกติกลายเป็นการเปลี่ยนแปลงที่กระจายทั่ว ถ้าคุณไม่ทำการเปลี่ยนแปลง ชื่อ จะอยู่ในโค้ดฐานเป็นคำโกหกเล็กๆ

เมื่อคุณพัฒนาคลาสใหม่ สิ่งที่ง่ายที่สุดคือการสร้างชื่อคลาสที่เรียบง่าย แม้กระทั่งสำหรับ abstractions ขนาดใหญ่ ตัวอย่างเช่น ถ้าเรากำลังเขียนชุดบัญชี เราสามารถเริ่มต้นด้วยคลาสที่เรียกแค่ว่า Account จากนั้นเราสามารถเริ่มเขียน tests เพื่อเพิ่มฟังก์ชันใหม่ ในบางจุด คุณอาจต้องการให้ Account เป็น interface ถ้าคุณทำ คุณสามารถสร้าง subclass ใต้มัน ผลักข้อมูลและ methods ทั้งหมดลงไป และทำให้ Account เป็น interface เมื่อคุณทำอย่างนั้น คุณไม่ต้องเปลี่ยนชื่อ type ของทุก reference เป็น Account ในโค้ดของคุณ

ในกรณีเช่นตัวอย่าง PaydayTransaction ที่เรามีชื่อที่ดีสำหรับ interface ( TransactionLog ) อยู่แล้ว เราสามารถทำสิ่งเดียวกัน ข้อเสียคือการผลักข้อมูลและ methods ลงไปยัง subclass ใหม่ต้องใช้ หลายขั้นตอน แต่เมื่อความเสี่ยงน้อยพอ ผมใช้มันบางครั้ง เทคนิคนี้เรียกว่า Extract Implementer ( 356 )

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


interface TransactionRecorder
{
}

ตอนนี้เราย้อนกลับไปและทำให้ TransactionLog implement interface ใหม่

public class TransactionLog implements TransactionRecorder
{
   ...
}

ต่อไปเราสร้าง FakeTransactionLog เป็นคลาสที่ว่างเปล่าเช่นกัน

public class FakeTransactionLog implements TransactionRecorder
{
}

ทุกอย่างควร compile ได้เพราะสิ่งที่เราทำคือ introduce คลาสใหม่สองสามคลาสและเปลี่ยนคลาสเพื่อให้มัน implement interface ที่ว่างเปล่า

ณ จุดนี้ เราเริ่ม refactor อย่างเต็มที่ เราเปลี่ยน type ของแต่ละ reference ในที่ที่เราต้องการใช้ interface PaydayTransaction ใช้ TransactionLog เราต้องเปลี่ยนมันเพื่อใช้ TransactionRecorder เมื่อเราทำเสร็จแล้ว เมื่อ compile เราจะพบหลายกรณีที่ methods ถูกเรียกจาก TransactionRecorder และเราสามารถกำจัดข้อผิดพลาดทีละอันโดยการเพิ่ม method declarations ใน TransactionRecorder interface และ method definitions ที่ว่างเปล่าใน FakeTransactionLog

นี่คือตัวอย่าง:

public class PaydayTransaction extends Transaction
{
    public PaydayTransaction(PayrollDatabase db,
                             TransactionRecorder log) {
        super(db, log);
    }

    public void run() {
        for(Iterator it = db.getEmployees(); it.hasNext(); ) {
            Employee e = (Employee)it.next();
            if (e.isPayday(date)) {
                e.pay();
            }
        }
        log.saveTransaction(this);
    }
    ...
}

ในกรณีนี้ method เดียวที่เราเรียกบน TransactionRecorder คือ saveTransaction เพราะ TransactionRecorder ยังไม่มี method saveTransaction เราจึงได้ compile error เราสามารถทำให้ test ของเรา compile โดยการเพิ่ม method นั้นเข้าไปใน TransactionRecorder และ FakeTransactionLog

interface TransactionRecorder
{
    void saveTransaction(Transaction transaction);
}

public class FakeTransactionLog implements TransactionRecorder
{
    void saveTransaction(Transaction transaction) {
    }
}

และเราก็เสร็จแล้ว เราไม่ต้องสร้าง TransactionLog จริงใน tests ของเราอีกต่อไป

คุณอาจมองสิ่งนี้แล้วพูดว่า "ก็ยังไม่เสร็จจริงๆ นะ เรายังไม่ได้เพิ่ม method recordError ใน interface และ fake" จริงอยู่ method recordError มีอยู่บน TransactionLog ถ้าเราต้องการสกัด interface ทั้งหมด เราสามารถ introduce มันบน interface ได้เช่นกัน แต่ความจริงคือเรา ไม่ต้องการมันสำหรับ test ถึงแม้ว่าการมี interface ที่ครอบคลุม methods public ทั้งหมดของคลาสจะเป็นสิ่งที่ดี ถ้าเราเดินไปตามเส้นทางนั้น เราอาจทำงานมากกว่าที่จำเป็นเพื่อนำชิ้นส่วนของแอปพลิเคชัน มาทดสอบ ถ้าคุณมุ่งมั่นไปที่ดีไซน์ที่ abstractions หลักบางตัวมี interfaces ที่ครอบคลุม ชุดของ public methods บนคลาสของมัน จำไว้ว่าคุณสามารถไปถึงจุดนั้นได้แบบเพิ่มทีละน้อย


เมื่อคุณสกัด interface คุณไม่จำเป็นต้องสกัด methods public ทั้งหมดบนคลาสที่คุณสกัด Lean on the Compiler ( 315 ) เพื่อหา methods ที่กำลังถูกใช้


Steps (ขั้นตอน)

ในการ Extract Interface ให้ทำตามขั้นตอนเหล่านี้:

1. สร้าง interface ใหม่ด้วยชื่อที่คุณต้องการใช้ ยังไม่ต้องเพิ่ม methods ใดๆ

2. ทำให้คลาสที่คุณสกัด implement interface ซึ่งไม่สามารถทำอะไรพังได้เพราะ interface ยังไม่มี methods แต่เป็นการดีที่จะ compile และรัน test ของคุณเพื่อยืนยัน

3. เปลี่ยนที่ที่คุณต้องการใช้ object เพื่อให้มันใช้ interface แทนคลาสเดิม

4. Compile ระบบและ introduce method declaration ใหม่บน interface สำหรับแต่ละ method use ที่ compiler รายงานเป็น error


Extract Interface และ Non-Virtual Functions

ถ้าคุณมีการเรียกใช้เช่นนี้ในโค้ดของคุณ: bondRegistry.newFixedYield(client) ในหลายภาษา มันยากที่จะบอกว่ามันเป็น static method หรือ virtual หรือ non-virtual instance method ในภาษาที่อนุญาต non-virtual instance methods คุณอาจเจอปัญหา ถ้าคุณสกัด interface และเพิ่ม signature ของ non-virtual methods ของคลาสลงไป โดยทั่วไป ถ้าคุณ คลาสของคุณไม่มี subclasses คุณสามารถทำให้ method เป็น virtual แล้วสกัด interface ทุกอย่างจะโอเค แต่ถ้าคลาสของคุณมี subclasses การดึง method signature ขึ้นไปใน interface สามารถทำลายโค้ดได้ นี่คือตัวอย่างใน C++ เรามีคลาสที่มี non-virtual method:

class BondRegistry
{
public:
    Bond *newFixedYield(Client *client) { ... }
};

และเรามี subclass ที่มี method ที่มีชื่อและ signature เดียวกัน:

class PremiumRegistry : public BondRegistry
{
public:
    Bond *newFixedYield(Client *client) { ... }
};

ถ้าเราสกัด interface จาก BondRegistry:

class BondProvider
{
public:
    virtual Bond *newFixedYield(Client *client) = 0;
};

และให้ BondRegistry implement มัน:

class BondRegistry : public BondProvider { ... };

เราสามารถทำลายโค้ดที่มีลักษณะดังนี้โดยการส่ง PremiumRegistry

void disperse(BondRegistry *registry) {
    ...
    Bond *bond = registry->newFixedYield(existingClient);
    ...
}

ก่อนที่เราจะสกัด interface method BondRegistry's newFixedYield ถูกเรียกเพราะ compile-time type ของตัวแปร registry คือ BondRegistry ถ้าเราทำให้ newFixedYield เป็น virtual ในกระบวนการสกัด interface เราจะเปลี่ยนพฤติกรรม method บน PremiumBondRegistry จะถูกเรียก ใน C++ เมื่อเราทำให้ method เป็น virtual ใน base class methods ที่โอเวอร์ไรด์มันใน subclasses จะกลายเป็น virtual สังเกตว่าเราไม่มีปัญหานี้ใน Java หรือ C# ใน Java instance methods ทั้งหมดเป็น virtual ใน C# สิ่งต่างๆ ปลอดภัยกว่าเพราะการเพิ่ม interface ไม่มีผลต่อการเรียกใช้ non-virtual methods ที่มีอยู่

โดยทั่วไป การสร้าง method ใน derived class ที่มี signature เดียวกับ non-virtual method ใน base ไม่ใช่แนวปฏิบัติที่ดีใน C++ เพราะมันอาจนำไปสู่ความเข้าใจผิด ถ้าคุณต้องการเข้าถึง non-virtual function ผ่าน interface และมันไม่ได้อยู่บนคลาสที่ไม่มี subclasses สิ่งที่ดีที่สุดคือการเพิ่ม virtual method ใหม่ ด้วยชื่อใหม่ method นั้นสามารถ delegate ไปยัง non-virtual หรือ static method คุณแค่ต้องแน่ใจว่า method ทำสิ่งที่ถูกต้องสำหรับ subclasses ทั้งหมดที่อยู่ใต้คลาสที่คุณสกัด


Introduce Instance Delegator (แนะนำ Instance Delegator)

ผู้คนใช้ static methods บนคลาสด้วยหลายเหตุผล หนึ่งในเหตุผลที่พบบ่อยที่สุดคือการ implement Singleton Design Pattern ( 372 ) อีกเหตุผลที่พบบ่อยคือการสร้าง utility classes

Utility classes ค่อนข้างหาได้ง่ายในหลายดีไซน์ พวกมันคือคลาสที่ไม่มี instance variables หรือ instance methods เลย แต่ประกอบด้วยชุดของ static methods และ constants

ผู้คนสร้าง utility classes ด้วยหลายเหตุผล ส่วนใหญ่แล้ว พวกมันถูกสร้างขึ้นเมื่อมันยากที่จะหา abstraction ร่วมสำหรับชุดของ methods คลาส Math ใน Java JDK เป็นตัวอย่างของสิ่งนี้ มันมี static methods สำหรับฟังก์ชันตรีโกณมิติ (cos, sin, tan) และ อีกมากมาย เมื่อนักออกแบบภาษาสร้างภาษาของพวกเขาจาก object "ตลอดทางลง" พวกเขาทำให้แน่ใจว่า primitives ตัวเลขรู้วิธีทำสิ่งเหล่านี้ ตัวอย่างเช่น คุณควรจะสามารถเรียก method sin() บน object 1 หรือ object ตัวเลขอื่นๆ และได้ผลลัพธ์ที่ถูกต้อง ในขณะที่เขียนนี้ Java ไม่รองรับ math methods บน primitive types ดังนั้น utility class จึงเป็นทางออกที่ยุติธรรม แต่มันก็เป็นกรณีพิเศษ ในเกือบทุกกรณี คุณ สามารถใช้คลาสธรรมดาที่มี instance data และ methods ได้

ถ้าคุณมี static methods ในโปรเจกต์ของคุณ โอกาสที่คุณจะไม่เจอปัญหากับพวกมันเว้นแต่มันมี บางอย่างที่ยากต่อการพึ่งพาใน test (คำศัพท์ทางเทคนิคสำหรับสิ่งนี้คือ static cling) ในกรณีเหล่านี้ คุณอาจต้องการใช้ object seam ( 40 ) เพื่อแทนที่พฤติกรรมอื่นเมื่อ static methods ถูกเรียก คุณควรทำอย่างไรในกรณีนี้?

สิ่งหนึ่งที่ทำได้คือเริ่ม introduce delegating instance methods บนคลาส เมื่อคุณทำเช่นนี้ คุณต้อง หาวิธีแทนที่ static calls ด้วย method calls บน object นี่คือตัวอย่าง:

public class BankingServices
{
    public static void updateAccountBalance(int userID,
                                            Money amount) {
        ...
    }
    ...
}

ที่นี่เรามีคลาสที่ไม่มีอะไรนอกจาก static methods ผมแสดงแค่อันเดียวนี่ แต่คุณคงเข้าใจ เราสามารถเพิ่ม instance method ในคลาสแบบนี้และให้มัน delegate ไปยัง static method:

public class BankingServices
{
    public static void updateAccountBalance(int userID,
                                            Money amount) {
        ...
    }

    public void updateBalance(int userID, Money amount) {
        updateAccountBalance(userID, amount);
    }
    ...
}

ในกรณีนี้ เราเพิ่ม instance method ชื่อ updateBalance และให้มัน delegate ไปยัง static method updateAccountBalance

ตอนนี้ในโค้ดที่เรียกใช้ เราสามารถแทนที่ references แบบนี้:

public class SomeClass
{
    public void someMethod() {
        ...
         BankingServices.updateAccountBalance(id, sum);
    }
}

ด้วยสิ่งนี้:

public class SomeClass
{
    public void someMethod( BankingServices services ) {
        ...
         services.updateBalance(id,sum);
    }
    ...
}

สังเกตว่าเราทำสิ่งนี้ได้ก็ต่อเมื่อเราสามารถหาวิธีสร้าง BankingServices object จากภายนอกได้ มันเป็นขั้นตอนการ refactor เพิ่มเติม แต่ในภาษาที่มี type อยู่กับที่ เราสามารถ Lean on the Compiler ( 315 ) เพื่อให้ object เข้ามาอยู่ในที่

Steps (ขั้นตอน)

ในการ Introduce Instance Delegator ให้ทำตามขั้นตอนเหล่านี้:

1. ระบุ static method ที่เป็นปัญหาในการใช้ใน test

2. สร้าง instance method สำหรับ method บนคลาส จำไว้ว่าต้อง Preserve Signatures ( 312 ) ให้ instance method delegate ไปยัง static method

3. หาที่ที่ static methods ถูกใช้ในคลาสที่คุณกำลังทดสอบ ใช้ Parameterize Method ( 383 ) หรือเทคนิคการทำลาย dependency อื่นเพื่อ supply instance ไปยังที่ที่ static method call ถูกทำ

Introduce Static Setter (แนะนำ Static Setter)

บางทีผมอาจเป็นพวกหัวโบราณ แต่ผมไม่ชอบ global mutable data เมื่อผมเยี่ยมทีม มันมักจะเป็นอุปสรรค ที่ชัดเจนที่สุดในการนำส่วนต่างๆ ของระบบเข้าสู่ test harnesses คุณอยากดึงชุดคลาสออกมาใส่ test harness แต่คุณพบว่าบางคลาสต้องถูกตั้งค่าในสถานะเฉพาะเพื่อให้ใช้งานได้เลย เมื่อคุณมี harness พร้อม คุณต้องไล่รายการ globals เพื่อให้แน่ใจว่าแต่ละตัวมีสถานะที่คุณต้องการสำหรับ เงื่อนไขที่คุณต้องการทดสอบ นักฟิสิกส์ควอนตัมไม่ได้ค้นพบ "spooky action at a distance"; ในซอฟต์แวร์ เรามีมันมา หลายปีแล้ว

การบ่นเกี่ยวกับ globals ไม่ได้ช่วยอะไร หลายระบบมีมัน ในบางระบบ พวกมันตรงไปตรงมาและไม่รู้สึกตัว แค่มีคนประกาศตัวแปรไว้ที่ไหนสักแห่ง ในระบบอื่น พวกมันถูกแต่งตัวเป็น singletons ด้วยการยึดมั่นอย่างเคร่งครัดต่อ Singleton Design Pattern ไม่ว่าจะด้วยวิธีใด การได้ fake สำหรับการรับรู้นั้นตรงไปตรงมามาก ถ้าตัวแปรเป็น global ที่ไม่มีความละอาย อยู่นอกคลาสหรือเป็น public static variable คุณสามารถแทนที่ object ได้ ถ้า reference เป็น const หรือ final คุณอาจต้องลบการป้องกันนั้น ทิ้ง comment ในโค้ดบอกว่าคุณทำเพื่อ test และ คนอื่นไม่ควรใช้ประโยชน์จากการเข้าถึงใน production code


Singleton Design Pattern

Singleton Design Pattern เป็น pattern ที่หลายคนใช้เพื่อให้แน่ใจว่ามี instance เดียวของคลาสหนึ่งๆ ใน program มีสามคุณสมบัติที่ singletons ส่วนใหญ่มีร่วมกัน:

1. Constructors ของ singleton class มักจะถูกทำให้เป็น private

2. Static member ของคลาสถือ instance เดียวของคลาสที่จะถูกสร้างใน program

3. Static method ใช้เพื่อให้เข้าถึง instance โดยปกติ method นี้ชื่อ instance

ถึงแม้ว่า singletons จะป้องกันไม่ให้คนสร้าง instance มากกว่าหนึ่งของคลาสใน production code แต่พวกมันก็ ป้องกันไม่ให้คนสร้าง instance มากกว่าหนึ่งของคลาสใน test harness เช่นกัน


การแทนที่ singletons เป็นเพียงงานเพิ่มอีกเล็กน้อย เพิ่ม static setter ใน singleton เพื่อแทนที่ instance จากนั้นทำให้ constructor เป็น protected คุณสามารถ subclass singleton สร้าง object ใหม่ และส่งมันไปยัง setter

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

นี่คือตัวอย่างของ Introduce Static Setter ใน C++:

void MessageRouter::route(Message *message) {
    ...
    Dispatcher *dispatcher
            = ExternalRouter::instance()->getDispatcher();
    if (dispatcher != NULL)
        dispatcher->sendMessage(message);
}

ในคลาส MessageRouter เราใช้ singletons ในหลายที่เพื่อรับ dispatchers คลาส ExternalRouter เป็นหนึ่งใน singletons เหล่านั้น มันใช้ static method ชื่อ instance เพื่อให้เข้าถึง instance เดียวของ ExternalRouter. คลาส ExternalRouter มี getter สำหรับ dispatcher เราสามารถแทนที่ dispatcher ด้วยอีกอันโดยการแทนที่ external router ที่ส่งมอบมัน

นี่คือลักษณะของคลาส ExternalRouter ก่อนที่เราจะ introduce static setter:

class ExternalRouter
{
private:
    static ExternalRouter *_instance;
public:
    static ExternalRouter *instance();
    ...
};

ExternalRouter *ExternalRouter::_instance = 0;

ExternalRouter *ExternalRouter::instance()
{
    if (_instance == 0) {
        _instance = new ExternalRouter;
    }
    return _instance;
}

สังเกตว่า router ถูกสร้างขึ้นในการเรียกครั้งแรกไปยัง method instance เพื่อแทนที่ด้วย router อื่น เราต้องเปลี่ยนสิ่งที่ instance คืนค่า ขั้นแรกคือการ introduce method ใหม่เพื่อแทนที่ instance

void ExternalRouter::setTestingInstance(ExternalRouter *newInstance)
{
    delete _instance;
    _instance = newInstance;
}

แน่นอน สิ่งนี้สมมติว่าเราสามารถสร้าง instance ใหม่ได้ เมื่อคนใช้ singleton pattern พวกเขามักจะ ทำให้ constructor ของคลาสเป็น private เพื่อป้องกันไม่ให้คนสร้าง instance มากกว่าหนึ่ง ถ้าคุณทำให้ constructor เป็น protected คุณสามารถ subclass singleton เพื่อรับรู้หรือแยกและส่ง instance ใหม่ไปยัง method setTestingInstance ในตัวอย่างก่อนหน้านี้ เราจะสร้าง subclass ของ ExternalRouter ชื่อ TestingExternalRouter และโอเวอร์ไรด์ method getDispatcher เพื่อให้มันคืนค่า dispatcher ที่เราต้องการ ซึ่งเป็น fake dispatcher

class TestingExternalRouter : public ExternalRouter
{
public:
    virtual void Dispatcher *getDispatcher() const {
        return new FakeDispatcher;
    }
};

สิ่งนี้อาจดูเหมือนวิธีที่อ้อมค้อมในการแทนที่ dispatcher ใหม่ เราลงเอยด้วยการสร้าง ExternalRouter ใหม่แค่เพื่อแทนที่ dispatchers เราสามารถใช้ทางลัดได้ แต่มันมีข้อแลกเปลี่ยนที่แตกต่างกัน อีกอย่างที่เราสามารถทำได้คือเพิ่ม boolean flag ใน ExternalRouter และให้มันคืนค่า dispatcher ที่แตกต่างเมื่อ flag ถูกตั้ง ใน C++ หรือ C# เราสามารถใช้ conditional compilation เพื่อ เลือก dispatchers เช่นกัน เทคนิคเหล่านี้ใช้ได้ดี แต่มันเป็นการรุกล้ำและอาจยุ่งยากถ้าคุณใช้มัน ทั่วทั้งแอปพลิเคชัน โดยทั่วไป ผมชอบแยกโค้ด production และ test

การใช้ setter method และ protected constructor บน singleton เป็นการรุกล้ำเล็กน้อย แต่มันช่วยให้คุณมี tests ได้ คนจะใช้ public constructor ในทางที่ผิดและสร้าง singleton มากกว่าหนึ่งในระบบ production ได้หรือไม่? ได้ แต่ ในความคิดของฉัน ถ้ามันสำคัญที่จะมี instance เดียวของ object ในระบบ วิธีที่ดีที่สุดในการจัดการคือการ ทำให้แน่ใจว่าทุกคนในทีมเข้าใจข้อจำกัดนั้น


ทางเลือกหนึ่งในการลดการป้องกัน constructor และ subclassing คือการใช้ Extract Interface ( 362 ) บน singleton class และ supply setter ที่รับ object กับ interface นั้น ข้อเสียของสิ่งนี้คือ คุณต้องเปลี่ยน type ของ reference ที่คุณใช้เก็บ singleton ในคลาสและ type ของค่าที่คืนจาก method instance การเปลี่ยนแปลงเหล่านี้อาจซับซ้อน และพวกมันไม่ได้พาเราไปสู่สถานะที่ดีกว่า สถานะ "ที่ดีกว่า" ที่แท้จริง คือการลด references ไปยัง singleton ไปจนถึงจุดที่มันกลายเป็นแค่คลาสปกติ


ในตัวอย่างก่อนหน้านี้ เราแทนที่ singleton โดยใช้ static setter singleton เป็น object ที่ส่งมอบ object อื่น นั่นคือ dispatcher บางครั้งเราเห็น global ชนิดอื่นในระบบ นั่นคือ global factory แทนที่จะเก็บ instance พวกมันส่งมอบ object ใหม่ทุกครั้งที่คุณเรียก static methods ตัวใดตัวหนึ่งของมัน การแทนที่ด้วย object อื่นเพื่อคืนค่าค่อนข้างยุ่งยาก แต่บ่อยครั้งคุณสามารถทำได้โดยให้ factory delegate ไปยัง factory อื่น มาดูตัวอย่างใน Java:

public class RouterFactory
{
    static Router makeRouter() {
        return new EWNRouter();
    }
}

RouterFactory เป็น global factory ที่ตรงไปตรงมา ในขณะนี้ มันไม่อนุญาตให้เราแทนที่ routers ที่มันส่งมอบภายใต้ test แต่เราสามารถปรับมันเพื่อให้ทำได้

interface RouterServer
{
    Router makeRouter();
}

public class RouterFactory implements RouterServer
{
    static Router makeRouter() {
        return server.makeRouter();
    }

    static setServer(RouterServer server) {
        this.server = server;
    }

    static RouterServer server = new RouterServer() {
        public RouterServer makeRouter() {
            return new EWNRouter();
        }
    };
}

ใน test เราสามารถทำสิ่งนี้:

protected void setUp() {
    RouterServer.setServer(new RouterServer() {
        public RouterServer makeRouter() {
            return new FakeRouter();
        }
    });
}

แต่สิ่งสำคัญคือต้องจำไว้ว่าในรูปแบบ static setter เหล่านี้ คุณกำลังแก้ไขสถานะที่พร้อมใช้ กับ tests ทั้งหมด คุณสามารถใช้ tearDown method ใน xUnit testing frameworks เพื่อนำสิ่งต่างๆ กลับสู่สถานะที่รู้จักก่อนที่ tests อื่นจะทำงาน โดย ทั่วไป ผมทำเช่นนั้นเมื่อการใช้สถานะผิดใน test ถัดไปอาจทำให้เข้าใจผิด ถ้าผมกำลังแทนที่ fake MailSender ในทุก tests ของผม การใส่อันอื่นเข้าไปก็ไม่มีความหมายมากนัก ในทางกลับกัน ถ้าผมมี global ที่เก็บสถานะที่ มีผลต่อผลลัพธ์ของระบบ บ่อยครั้งผมทำสิ่งเดียวกันใน method setUp และ tearDown เพื่อให้แน่ใจว่าผมได้ปล่อยให้ทุกอย่างอยู่ในสถานะสะอาด:

protected void setUp() {
    Node.count = 0;
    ...
}

protected void tearDown() {
    Node.count = 0;
}

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

สิ่งหนึ่งที่ควรพิจารณาคือการส่งผ่าน parameter ดูที่คลาสที่ต้องการเข้าถึง global ของคุณและพิจารณา ว่าคุณสามารถให้ superclass ร่วมกันแก่พวกมันได้หรือไม่ ถ้าได้ คุณสามารถส่ง global ให้พวกมันเมื่อสร้างและค่อยๆ ย้ายออกจากการมี globals เลย บ่อยครั้งคนกลัวว่าทุกคลาสในระบบจะต้องใช้ global บางอย่าง บ่อยครั้งคุณจะแปลกใจ ครั้งหนึ่งผมทำงานบน embedded system ที่ encapsulate memory management และ error reporting เป็นคลาส โดยส่ง memory object หรือ error reporter ไปให้ใครก็ตามที่ต้องการ เมื่อเวลาผ่านไป มีการแยกที่สะอาด ระหว่างคลาสที่ต้องการ services เหล่านั้นและคลาสที่ไม่ต้องการ คลาสที่ต้องการ services ก็มีแค่ superclass ร่วมกัน Object ที่ถูกส่งไปทั่วระบบถูกสร้างขึ้นตอนเริ่ม program และมันแทบจะ สังเกตไม่เห็น

Steps (ขั้นตอน)

ในการ Introduce Static Setter ให้ทำตามขั้นตอนเหล่านี้:

1. ลดการป้องกันของ constructor เพื่อให้คุณสามารถสร้าง fake โดย subclassing singleton

2. เพิ่ม static setter ใน singleton class setter ควรรับ reference ไปยัง singleton class ตรวจสอบให้แน่ใจ ว่า setter ทำลาย singleton instance อย่างถูกต้องก่อนที่จะตั้ง object ใหม่

3. ถ้าคุณต้องการเข้าถึง private หรือ protected methods ใน singleton เพื่อตั้งค่ามันอย่างถูกต้องสำหรับการทดสอบ ให้พิจารณา subclassing มันหรือสกัด interface และทำให้ singleton ถือ instance เป็น reference ที่มี type เป็น type ของ interface

Link Substitution (การแทนที่ Link)

Object orientation ทำให้เรามีโอกาสที่ยอดเยี่ยมในการแทนที่ object หนึ่งด้วยอีกอัน ถ้าสองคลาส implement interface เดียวกันหรือมี superclass เดียวกัน คุณสามารถแทนที่อันหนึ่งด้วยอีกอันได้ง่ายๆ โชคร้ายที่คน ที่ทำงานในภาษา procedural เช่น C ไม่มีตัวเลือกนั้น เมื่อคุณมี function แบบนี้ ไม่มีทางที่จะ แทนที่ function หนึ่งด้วยอีกอันตอน compile time นอกจากการใช้ preprocessor:

void account_deposit(int amount);

มีทางเลือกอื่นหรือไม่? ได้ คุณสามารถใช้ Link Substitution เพื่อแทนที่ function หนึ่งด้วยอีกอัน ในการทำเช่นนี้ สร้าง dummy library ที่มี functions ที่มี signatures เดียวกับ functions ที่คุณต้องการ fake ถ้าคุณกำลังรับรู้ คุณต้องตั้งค่ากลไกบางอย่างสำหรับบันทึก notifications และสอบถามพวกมัน คุณสามารถใช้ไฟล์ global variables หรืออะไรก็ตามที่สะดวกภายใต้ test

นี่คือตัวอย่าง:

void account_deposit(int amount)
{
    struct Call *call =
        (struct Call *)calloc(1, sizeof (struct Call));
    call->type = ACC_DEPOSIT;
    call->arg0 = amount;
    append(g_calls, call);
}

ในกรณีนี้ เราสนใจการรับรู้ ดังนั้นเราสร้าง global list ของ calls เพื่อบันทึกทุกครั้งที่ function นี้ (หรืออื่นๆ ที่เรากำลังปลอม) ถูกเรียก ใน test เราสามารถตรวจสอบ list หลังจากที่เราเรียกใช้ชุด object และดูว่า mocked functions ถูกเรียกตามลำดับที่เหมาะสมหรือไม่

ผมไม่เคยลองใช้ Link Substitution กับ C++ classes แต่ผมเดาว่ามันเป็นไปได้ ผมแน่ใจว่า mangled names ที่ C++ compilers สร้างขึ้น ทำให้มันค่อนข้างยาก อย่างไรก็ตาม เมื่อเรียก C functions มันใช้งานได้จริงมาก กรณีที่มีประโยชน์ที่สุดคือ เมื่อปลอม libraries ภายนอก libraries ที่ดีที่สุดในการปลอมคือพวกที่เป็น pure data sinks เป็นส่วนใหญ่: คุณเรียก functions ในนั้น แต่คุณไม่สนใจค่าที่คืนบ่อยนัก ตัวอย่างเช่น graphics libraries มีประโยชน์เป็นพิเศษในการปลอมด้วย Link Substitution

Link Substitution ยังสามารถใช้ใน Java สร้างคลาสที่มีชื่อและ methods เดียวกัน และเปลี่ยน classpath ของคุณเพื่อให้การเรียกใช้ แก้ไปที่คลาสเหล่านั้นแทนที่จะเป็นคลาสที่มี dependencies ที่ไม่ดี

Steps (ขั้นตอน)

ในการใช้ Link Substitution ให้ทำตามขั้นตอนเหล่านี้:

1. ระบุ functions หรือคลาสที่คุณต้องการปลอม

2. สร้าง definitions ทางเลือกสำหรับพวกมัน

3. ปรับ build ของคุณเพื่อให้ definitions ทางเลือกถูกรวมแทนที่จะเป็น production versions

Parameterize Constructor (กำหนด Parameter ใน Constructor)

ถ้าคุณสร้าง object ใน constructor บ่อยครั้งวิธีที่ง่ายที่สุดในการแทนที่มันคือการ externalize การสร้างของมัน สร้าง object นอกคลาส และทำให้ clients ส่งมันเข้าไปใน constructor เป็น parameter นี่คือตัวอย่าง

เราเริ่มต้นด้วยสิ่งนี้:

public class MailChecker
{
    public MailChecker (int checkPeriodSeconds) {
        this.receiver = new MailReceiver();
        this.checkPeriodSeconds = checkPeriodSeconds;
    }
    ...
}

จากนั้นเรา introduce parameter ใหม่ดังนี้:

public class MailChecker
{
    public MailChecker (MailReceiver receiver,
                        int checkPeriodSeconds) {
        this.receiver = receiver;
        this.checkPeriodSeconds = checkPeriodSeconds;
    }
    ...
}

เหตุผลหนึ่งที่คนไม่คิดถึงเทคนิคนี้บ่อยครั้งคือพวกเขาสมมติว่ามันบังคับให้ clients ทุกคนต้องส่ง อาร์กิวเมนต์เพิ่ม อย่างไรก็ตาม คุณสามารถเขียน constructor ที่คง signature เดิมไว้:

public class MailChecker
{
     public MailChecker (int checkPeriodSeconds) {
         this(new MailReceiver(), checkPeriodSeconds);
     }

    public MailChecker (MailReceiver receiver,
                        int checkPeriodSeconds) {
        this.receiver = receiver;
        this.checkPeriodSeconds = checkPeriodSeconds;
    }
    ...

}

ถ้าคุณทำเช่นนั้น คุณสามารถ supply objects ที่แตกต่างสำหรับการทดสอบ และ clients ของคลาสไม่จำเป็นต้องรู้ ความแตกต่าง

มาทำทีละขั้นตอน นี่คือโค้ดดั้งเดิมของเรา:

public class MailChecker
{
    public MailChecker (int checkPeriodSeconds) {
        this.receiver = new MailReceiver();
        this.checkPeriodSeconds = checkPeriodSeconds;
    }
    ...
}

เราทำสำเนาของ constructor:

public class MailChecker
{
    public MailChecker (int checkPeriodSeconds) {
        this.receiver = new MailReceiver();
        this.checkPeriodSeconds = checkPeriodSeconds;
    }

     public MailChecker (int checkPeriodSeconds) {
         this.receiver = new MailReceiver();
         this.checkPeriodSeconds = checkPeriodSeconds;
     }
    ...
}

จากนั้นเราเพิ่ม parameter สำหรับ MailReceiver

public class MailChecker
{
    public MailChecker (int checkPeriodSeconds) {
        this.receiver = new MailReceiver();
        this.checkPeriodSeconds = checkPeriodSeconds;
    }

    public MailChecker ( MailReceiver receiver ,
                        int checkPeriodSeconds) {
        this.receiver = new MailReceiver();
        this.checkPeriodSeconds = checkPeriodSeconds;
    }
    ...
}

ต่อไปเรากำหนด parameter นั้นให้กับ instance variable โดยกำจัด new expression

public class MailChecker
{
    public MailChecker (int checkPeriodSeconds) {
        this.receiver = new MailReceiver();
        this.checkPeriodSeconds = checkPeriodSeconds;
    }

    public MailChecker (MailReceiver receiver,
                        int checkPeriodSeconds) {
        this.receiver = receiver ;
        this.checkPeriodSeconds = checkPeriodSeconds;
    }

    ...
}

ตอนนี้ เรากลับไปที่ constructor เดิมและลบ body ของมัน แทนที่ด้วยการเรียก constructor ใหม่ constructor เดิมใช้ new เพื่อสร้าง parameter ที่ต้องการส่ง

public class MailChecker
{
    public MailChecker (int checkPeriodSeconds) {
        this(new MailReceiver(), checkPeriodSeconds);
    }

    public MailChecker ( MailReceiver receiver ,
                        int checkPeriodSeconds) {
        this.receiver = receiver;
        this.checkPeriodSeconds = checkPeriodSeconds;
    }
    ...
}

มีข้อเสียของเทคนิคนี้หรือไม่? จริงๆ แล้วมี เมื่อเราเพิ่ม parameter ใหม่ใน constructor เรากำลังเปิดประตูให้ dependencies เพิ่มเติมบนคลาสของ parameter ผู้ใช้คลาสสามารถใช้ constructor ใหม่ใน production code และเพิ่ม dependencies ทั่วระบบ อย่างไรก็ตาม โดยทั่วไป นั่นเป็นความกังวล ที่ค่อนข้างเล็ก Parameterize Constructor เป็นการ refactor ที่ง่ายมากและเป็นหนึ่งที่ผมมักจะใช้บ่อย


ในภาษาที่อนุญาต default arguments มีวิธีที่ง่ายกว่าในการทำ Parameterize Constructor เราสามารถ เพิ่ม default argument ใน constructor ที่มีอยู่:

นี่คือ constructor ที่ถูก parameterized ด้วยวิธีนี้ใน C++:

class AssemblyPoint
{
public:
    AssemblyPoint(EquipmentDispatcher *dispatcher
                = new EquipmentDispatcher);
    ...
};

มีข้อเสียเพียงอย่างเดียวเมื่อเราทำใน C++ ไฟล์ header ที่มี class declaration นี้ต้องรวม header สำหรับ EquipmentDispatcher ถ้าไม่ใช่เพราะ constructor call เราอาจสามารถใช้ forward declaration สำหรับ EquipmentDispatcher ได้ ด้วยเหตุนี้ ผมจึงไม่ใช้ default arguments บ่อยนัก


Steps (ขั้นตอน)

ในการ Parameterize Constructor ให้ทำตามขั้นตอนเหล่านี้:

1. ระบุ constructor ที่คุณต้องการ parameterize และทำสำเนาของมัน

2. เพิ่ม parameter ใน constructor สำหรับ object ที่คุณจะแทนที่การสร้าง ลบ object creation และเพิ่มการกำหนดจาก parameter ไปยัง instance variable สำหรับ object

3. ถ้าคุณสามารถเรียก constructor จาก constructor ในภาษาของคุณ ให้ลบ body ของ constructor เก่าและ แทนที่ด้วยการเรียก constructor เก่า เพิ่ม new expression ในการเรียก constructor ใหม่ใน constructor เก่า ถ้าคุณไม่สามารถเรียก constructor จาก constructor อื่นในภาษาของคุณ คุณอาจต้องสกัด duplication ระหว่าง constructors ไปยัง method ใหม่

Parameterize Method (กำหนด Parameter ใน Method)

คุณมี method ที่สร้าง object ภายใน และคุณต้องการแทนที่ object เพื่อรับรู้หรือแยก บ่อยครั้ง วิธีที่ง่ายที่สุดคือการส่ง object จากภายนอก นี่คือตัวอย่างใน C++:

void TestCase::run() {
    delete m_result;
    m_result = new TestResult;
    try {
        setUp();
        runTest(m_result);
    }
    catch (exception& e) {
        result->addFailure(e, this);
    }
    tearDown();
}

ที่นี่เรามี method ที่สร้าง object TestResult เมื่อใดก็ตามที่มันถูกเรียก ถ้าเราต้องการรับรู้หรือแยก เราสามารถส่งมันเป็น parameter

void TestCase::run( TestResult *result ) {
    delete m_result;
    m_result = result ;
    try {
        setUp();
        runTest(m_result);
    }
    catch (exception& e) {
        result->addFailure(e, this);
    }
    tearDown();
}

เราสามารถใช้ forwarding method เล็กๆ ที่คง signature เดิมไว้:

void TestCase::run() {
    run(new TestResult);
}


ใน C++, Java, C#, และภาษาอื่นๆ คุณสามารถมีสอง methods ที่มีชื่อเดียวกันบนคลาส ตราบใดที่ signatures ต่างกัน ในตัวอย่าง เราใช้ประโยชน์จากสิ่งนี้และใช้ชื่อเดียวกันสำหรับ method ใหม่ที่ parameterized และ method เดิม ถึงแม้ว่าสิ่งนี้จะช่วยประหยัดงานบ้าง แต่ในบางครั้งมันอาจทำให้สับสน ทางเลือกคือ การใช้ type ของ parameter ในชื่อของ method ใหม่ ตัวอย่างเช่น ในกรณีนี้ เราสามารถคง run() เป็นชื่อของ method เดิมแต่เรียก method ใหม่ว่า runWithTestResult(TestResult).


เช่นเดียวกับ Parameterize Constructor ( 379 ) , Parameterize Method สามารถทำให้ clients ขึ้นอยู่กับ types ใหม่ที่เคยใช้ในคลาสมาก่อนแต่ไม่ได้ปรากฏ ที่ interface ถ้าผมคิดว่าสิ่งนี้จะเป็นปัญหา ผมจะพิจารณา Extract and Override Factory Method ( 350 ) แทน

Steps (ขั้นตอน)

ในการ Parameterize Method ให้ทำตามขั้นตอนเหล่านี้:

1. ระบุ method ที่คุณต้องการแทนที่และทำสำเนาของมัน

2. เพิ่ม parameter ใน method สำหรับ object ที่คุณจะแทนที่การสร้าง ลบ object creation และเพิ่มการกำหนดจาก parameter ไปยังตัวแปรที่ถือ object

3. ลบ body ของ method ที่คัดลอกมาและเรียก method ที่ parameterized โดยใช้ object creation expression สำหรับ object เดิม

Primitivize Parameter (ปรับ Parameter เป็น Primitive)

โดยทั่วไป วิธีที่ดีที่สุดในการเปลี่ยนแปลงคลาสคือการสร้าง instance ใน test harness เขียน test สำหรับ การเปลี่ยนแปลงที่คุณต้องการทำ จากนั้นทำการเปลี่ยนแปลงเพื่อให้ test ผ่าน แต่บางครั้งปริมาณงานที่คุณ ต้องทำเพื่อนำคลาสมาทดสอบนั้นใหญ่เกินไปอย่างน่าขัน ทีมหนึ่งที่ผมไปเยี่ยมได้รับมรดก legacy system ที่มี domain classes ที่ transitively ขึ้นอยู่กับเกือบทุกคลาสอื่นในระบบ ราวกับว่ายังไม่แย่พอ พวกมันทั้งหมด ถูกผูกกับ persistence framework ด้วย การนำหนึ่งในคลาสเหล่านั้นเข้า test framework อาจทำได้ แต่ทีมคงไม่สามารถก้าวหน้าใน features ได้สักพักถ้าพวกเขาใช้เวลาทั้งหมด ต่อสู้กับ domain classes เพื่อให้ได้การแยกบางอย่าง เราใช้กลยุทธ์นี้ ตัวอย่างถูกเปลี่ยน เพื่อปกป้องผู้บริสุทธิ์

ในเครื่องมือแต่งเพลง track ประกอบด้วย sequences หลายอันของ events ทางดนตรี เราต้องหา "dead time" ในแต่ละ sequence เพื่อที่เราจะเติมมันด้วยรูปแบบดนตรีซ้ำๆ เล็กๆ เราต้องการ method ชื่อ bool Sequence::hasGapFor(Sequence& pattern) const method คืนค่าที่บ่งชี้ว่า pattern สามารถใส่ลงใน sequence ได้หรือไม่

ตามหลักการแล้ว method นี้ควรอยู่บนคลาสชื่อ Sequence แต่ Sequence เป็นหนึ่งในคลาสที่แย่มากที่จะพยายามดูดโลกทั้งใบเข้า test harness ของเราเมื่อเราพยายามสร้างมัน เพื่อ เริ่มเขียน method นั้น เราต้องหาวิธีเขียน test สำหรับมัน สิ่งที่ทำให้มันเป็นไปได้สำหรับเรา คือ sequences มี internal representation ที่สามารถทำให้ง่ายขึ้น ทุก sequence ประกอบด้วย vector ของ events โชคร้ายที่ events มีปัญหาเดียวกันกับ sequences: dependencies ที่แย่มากที่นำไปสู่ build problems โชคดีที่ในการคำนวณนี้ เราต้องการแค่ durations ของแต่ละ event เราสามารถเขียน method อื่นที่จะทำ การคำนวณบน ints เมื่อเรามีมันแล้ว เราสามารถเขียน hasGapFor และให้มันทำงานโดย delegate ไปยัง method อื่น

มาเริ่มเขียน method แรกกัน นี่คือ test สำหรับมัน:

TEST(hasGapFor, Sequence)
{
    vector<unsigned int> baseSequence;
    baseSequence.push_back(1);
    baseSequence.push_back(0);
    baseSequence.push_back(0);

    vector<unsigned int> pattern;
    pattern.push_back(1);
    pattern.push_back(2);

    CHECK(SequenceHasGapFor(baseSequence, pattern));
}

function SequenceHasGapFor เป็นแค่ free function มันไม่ได้เป็นส่วนหนึ่งของคลาสใดๆ แต่มันทำงานบน representation ที่สร้างจาก primitives— ในกรณีนี้คือ unsigned integers ถ้าเราสร้างฟังก์ชันการทำงานของ SequenceHasGapFor ใน test harness เราสามารถเขียน function ที่ค่อนข้างง่ายบน Sequence ที่ delegate ไปยังฟังก์ชันการทำงานใหม่:

bool Sequence::hasGapFor(Sequence& pattern) const
{
    vector<unsigned int> baseRepresentation
            = getDurationsCopy();

    vector<unsigned int> patternRepresentation
            = pattern.getDurationsCopy();

    return SequenceHasGapFor(baseRepresentation,
                             patternRepresentation);
}

function นี้ต้องการอีก function เพื่อรับ array ของ durations ดังนั้นเราจึงเขียนมัน:

vector<unsigned int> Sequence::getDurationsCopy() const
{
    vector<unsigned int> result;
    for (vector<Event>::iterator it = events.begin();
            it != events.end(); ++it) {
        result.push_back(it->duration);
    }
    return result;
}

ณ จุดนี้ เราสามารถเพิ่ม feature ได้ แต่อยู่ในวิธีที่แย่มาก ขอทำรายการสิ่งที่แย่ๆ ทั้งหมด ที่เราทำที่นี่:

1. เปิดเผย internal representation ของ Sequence

2. ทำให้ implementation ของ Sequence เข้าใจยากขึ้นเล็กน้อยโดยการผลักบางส่วนไปไว้ใน free function

3. เขียนโค้ดที่ไม่ได้ทดสอบ (เราไม่สามารถเขียน test สำหรับ getDurationsCopy()) จริงๆ

4. ทำข้อมูลซ้ำในระบบ

5. ยืดเวลาปัญหา เรายังไม่ได้เริ่มทำงานหนักในการทำลาย dependencies ระหว่าง domain classes ของเรา และ infrastructure (นั่นคือสิ่งหนึ่งที่จะสร้างความแตกต่างใหญ่เมื่อเราก้าวไปข้างหน้า และมันยังอยู่ ข้างหน้าเรา)

แม้จะมีข้อเสียทั้งหมดนั้น เราก็สามารถเพิ่ม feature ที่ทดสอบได้ ผมไม่ชอบทำการ refactor นี้ แต่ผม จะใช้มันถ้าผมจนมุม บ่อยครั้งมันเป็นบรรพบุรุษที่ดีของ Sprout Class ( 63 ) เพื่อให้เห็นภาพนี้ ลองนึกถึงการห่อ SequenceHasGapFor ในคลาสชื่อ GapFinder


Primitivize Parameter ( 385 ) ทำให้โค้ดอยู่ในสภาพที่ค่อนข้างแย่ โดยรวมแล้ว มันดีกว่าที่จะเพิ่มโค้ดใหม่ลงในคลาสเดิมหรือใช้ Sprout Class ( 63 ) เพื่อสร้าง abstractions ใหม่ที่สามารถใช้เป็นฐานสำหรับงานต่อไป เวลาเดียวที่ผมใช้ Primitivize Parameter คือเมื่อผมมั่นใจว่าผมจะใช้เวลานำคลาสมาทดสอบทีหลัง ณ จุดนั้น function สามารถถูกพับกลับเข้าไปในคลาสเป็น method จริง


Steps (ขั้นตอน)

ในการ Primitivize Parameter ให้ทำตามขั้นตอนเหล่านี้:

1. พัฒนา free function ที่ทำงานที่คุณต้องทำบนคลาส ในกระบวนการนี้ พัฒนา intermediate representation ที่คุณสามารถใช้ในการทำงาน

2. เพิ่ม function ในคลาสที่สร้าง representation และ delegate ไปยัง function ใหม่

Pull Up Feature (ดึง Feature ขึ้น)

บางครั้งคุณต้องทำงานกับกลุ่มของ methods บนคลาส และ dependencies ที่ป้องกันไม่ให้คุณสร้าง instance ของคลาสนั้นไม่เกี่ยวข้องกับกลุ่มนั้น "ไม่เกี่ยวข้อง" หมายความว่า methods ที่คุณต้องการทำงานด้วยไม่ได้อ้างถึง dependencies ที่ไม่ดีทั้งทางตรงหรือทางอ้อม คุณสามารถทำ Expose Static Method ( 345 ) หรือ Break Out Method Object ( 330 ) ซ้ำๆ แต่นั่นไม่จำเป็นต้องเป็นวิธีที่ตรงที่สุดในการจัดการกับ dependency

ในสถานการณ์นี้ คุณสามารถดึงกลุ่มของ methods หรือ feature ขึ้นไปยัง abstract superclass เมื่อคุณมี abstract superclass นั้นแล้ว คุณสามารถ subclass มันและสร้าง instances ของ subclass ใน tests ของคุณ นี่คือตัวอย่าง:

public class Scheduler
{
    private List items;

    public void updateScheduleItem(ScheduleItem item)
            throws SchedulingException {
        try {
            validate(item);
        }
        catch (ConflictException e) {
            throw new SchedulingException(e);
        }
        ...
    }

    private void validate(ScheduleItem item)
            throws ConflictException {
        // make calls to a database
        ...
    }

    public int getDeadtime() {
        int result = 0;
        for (Iterator it = items.iterator(); it.hasNext(); ) {
            ScheduleItem item = (ScheduleItem)it.next();
            if (item.getType() != ScheduleItem.TRANSIENT
                    && notShared(item)) {
                result += item.getSetupTime() + clockTime();
            }
            if (item.getType() != ScheduleItem.TRANSIENT) {
                result += item.finishingTime();
            }
            else {
                result += getStandardFinish(item);
            }
        }
        return result;
    }
}

สมมติว่าเราต้องการปรับเปลี่ยน getDeadTime แต่เราไม่สนใจ updateScheduleItem คงจะดีถ้าไม่ต้องจัดการกับ dependency บน database เลย เราสามารถลองใช้ Expose Static Method ( 345 ) แต่เรากำลังใช้ features ที่ไม่ใช่ static หลายอย่างของคลาส Scheduler Break Out Method Object ( 330 ) เป็นอีกความเป็นไปได้ แต่นี่เป็น method ที่ค่อนข้างเล็ก และ dependencies เหล่านั้นบน methods และ fields อื่นของ คลาสจะทำให้งานซับซ้อนเกินกว่าที่เราต้องการเพียงเพื่อนำ method มาทดสอบ

ทางเลือกอื่นคือการดึง method ที่เราสนใจขึ้นไปยัง superclass เมื่อเราทำเช่นนั้น เราสามารถปล่อย dependencies ที่ไม่ดี ไว้ในคลาสนี้ ที่ซึ่งพวกมันจะไม่ขวางทาง tests ของเรา นี่คือลักษณะของคลาส หลังจากนั้น:

public class Scheduler extends SchedulingServices
{
    public void updateScheduleItem(ScheduleItem item)
            throws SchedulingException {
        ...
    }

    private void validate(ScheduleItem item)
            throws ConflictException {
        // make calls to the database
        ...
    }
    ...
}

เราได้ดึง getDeadtime (feature ที่เราต้องการทดสอบ) และ features ทั้งหมดที่มันใช้ไปยัง abstract class

public abstract class SchedulingServices
{
     protected List items;

     protected boolean notShared(ScheduleItem item) {
         ...
     }

     protected  int getClockTime() {
         ...
     }

     protected int getStandardFinish(ScheduleItem item) {
         ...
     }

     public int getDeadtime() {
         int result = 0;
         for (Iterator it = items.iterator(); it.hasNext(); ) {
             ScheduleItem item = (ScheduleItem)it.next();
             if (item.getType() != ScheduleItem.TRANSIENT
                     && notShared(item)) {
                 result += item.getSetupTime() + clockTime();
             }
             if (item.getType() != ScheduleItem.TRANSIENT) {
                 result += item.finishingTime();
             }
             else {
                 result += getStandardFinish(item);
             }
         }
         return result;
     }
    ...
}

ตอนนี้เราสามารถสร้าง testing subclass ที่ให้เราเข้าถึง methods เหล่านั้นใน test harness:

public class TestingSchedulingServices extends SchedulingServices
{
    public TestingSchedulingServices() {
    }

    public void addItem(ScheduleItem item) {
        items.add(item);
    }
}

import junit.framework.*;

class SchedulingServicesTest extends TestCase
{
    public void testGetDeadTime() {
        TestingSchedulingServices services
            = new TestingSchedulingServices();
        services.addItem(new ScheduleItem("a",
                         10, 20, ScheduleItem.BASIC));
        assertEquals(2, services.getDeadtime());
    }
    ...
}

ดังนั้นสิ่งที่เราทำคือดึง methods ที่เราต้องการทดสอบขึ้นไปยัง abstract superclass และสร้าง concrete subclass ที่เราสามารถใช้ทดสอบพวกมัน นี่เป็นสิ่งที่ดีหรือไม่? จากมุมมองของดีไซน์ มันน้อยกว่าอุดมคติ เราได้ กระจายชุดของ features ข้ามสองคลาสแค่เพื่อทำให้ทดสอบง่ายขึ้น การกระจายอาจทำให้สับสนถ้า ความสัมพันธ์ระหว่าง features ในแต่ละคลาสไม่แข็งแรงมาก และนั่นก็เป็นกรณีนี้ เรามี Scheduler ซึ่งรับผิดชอบในการอัปเดต scheduling items และ SchedulingServices ซึ่งรับผิดชอบหลายสิ่ง รวมถึงการหา default times สำหรับ items และการคำนวณ dead time การจัดโครงสร้างที่ดีกว่าคือให้ Scheduler delegate ไปยัง validator object ที่รู้วิธีคุยกับ database แต่ถ้าขั้นตอนนั้นดูเสี่ยงเกินไปที่จะทำ ทันทีหรือมี dependencies ที่ไม่ดีอื่นๆ การดึง features ขึ้นเป็นขั้นตอนแรกที่ดี ถ้าคุณ Preserve Signatures ( 312 ) และ Lean on the Compiler ( 315 ), มันเสี่ยงน้อยกว่ามาก เราสามารถย้ายไปสู่ delegation ทีหลังเมื่อมี tests มากขึ้น

Steps (ขั้นตอน)

ในการทำ Pull Up Feature ให้ทำตามขั้นตอนเหล่านี้:

1. ระบุ methods ที่คุณต้องการดึงขึ้น

2. สร้าง abstract superclass สำหรับคลาสที่มี methods

3. คัดลอก methods ไปยัง superclass และ compile

4. คัดลอก reference ที่หายไปแต่ละอันที่ compiler แจ้งเตือนคุณไปยัง superclass ใหม่ จำไว้ว่าต้อง Preserve Signatures ( 312 ) เมื่อคุณทำ เพื่อลดโอกาสของข้อผิดพลาด

5. เมื่อทั้งสองคลาส compile สำเร็จ ให้สร้าง subclass สำหรับ abstract class และเพิ่ม methods ที่คุณต้องการ เพื่อให้สามารถตั้งค่าใน tests ของคุณ


คุณอาจสงสัยว่าทำไมเราถึงทำให้ superclass เป็น abstract ผมชอบทำให้มัน abstract เพื่อให้โค้ดเข้าใจง่ายขึ้น มันดีมากที่สามารถมองโค้ดในแอปพลิเคชันและรู้ว่าทุก concrete class กำลังถูกใช้ ถ้าคุณค้นหาโค้ดและพบ concrete classes ที่ไม่ได้ถูกสร้าง instance ที่ไหน พวกมันอาจดูเหมือน "dead code"


Push Down Dependency (ผลัก Dependency ลง)

บางคลาสมี dependencies ที่เป็นปัญหาเพียงไม่กี่อัน ถ้า dependencies อยู่ใน method calls เพียงไม่กี่ครั้ง คุณสามารถใช้ Subclass and Override Method ( 401 ) เพื่อกำจัดมันเมื่อคุณเขียน tests แต่ถ้า dependencies แพร่หลาย Subclass and Override Method อาจใช้ไม่ได้ คุณอาจต้องใช้ Extract Interface ( 362 ) หลายครั้งเพื่อลบ dependencies บน type ที่เฉพาะเจาะจง Push Down Dependency เป็นอีกทางเลือกหนึ่ง เทคนิคนี้ช่วยให้คุณแยก dependencies ที่เป็นปัญหาออกจากคลาสที่เหลือ ทำให้ ทำงานด้วยใน test harness ได้ง่ายขึ้น

เมื่อคุณใช้ Push Down Dependency คุณทำให้คลาสปัจจุบันของคุณเป็น abstract จากนั้นคุณสร้าง subclass ที่จะเป็นคลาส production ใหม่ของคุณ และคุณผลัก dependencies ที่เป็นปัญหาทั้งหมดลงไปในคลาสนั้น ณ จุดนั้นคุณสามารถ subclass คลาสเดิมของคุณเพื่อทำให้ methods ของมันพร้อมใช้งานสำหรับการทดสอบ นี่คือตัวอย่างใน C++:

class OffMarketTradeValidator : public TradeValidator
{
private:
    Trade& trade;
    bool flag;

    void showMessage() {
        int status  = AfxMessageBox(makeMessage(),
                                    MB_ABORTRETRYIGNORE);
        if (status == IDRETRY) {
            SubmitDialog dlg(this,
                    "Press okay if this is a valid trade");
            dlg.DoModal();
            if (dlg.wasSubmitted()) {
                g_dispatcher.undoLastSubmission();
                flag = true;
            }
        }
        else
        if (status == IDABORT) {
            flag = false;
        }
    }

public:
    OffMarketTradeValidator(Trade& trade)
    : trade(trade), flag(false)
    {}

    bool isValid() const {
        if (inRange(trade.getDate())
                && validDestination(trade.destination)
                && inHours(trade) {
            flag = true;
        }
        showMessage();
        return flag;
    }
    ...
};

ถ้าเราต้องเปลี่ยนแปลงตรรกะการตรวจสอบของเรา เราอาจมีปัญหาถ้าเราไม่ต้องการ link functions และคลาสที่เฉพาะเจาะจงกับ UI เข้าไปใน test harness ของเรา Push Down Dependency เป็นตัวเลือกที่ดีในกรณีนี้

นี่คือลักษณะของโค้ดหลังจาก Push Down Dependency:

class OffMarketTradeValidator : public TradeValidator
{
protected:
    Trade& trade;
    bool flag;
    virtual void showMessage() = 0;

public:
    OffMarketTradeValidator(Trade& trade)
    : trade(trade), flag(false) {}

    bool isValid() const {
        if (inRange(trade.getDate())
                && validDestination(trade.destination)
                && inHours(trade) {
            flag = true;
        }
        showMessage();
        return flag;
    }
    ...
};

class WindowsOffMarketTradeValidator
        : public OffMarketTradeValidator
{
protected:
    virtual void showMessage() {
        int status  = AfxMessageBox(makeMessage(),
                          MB_ABORTRETRYIGNORE);
        if (status == IDRETRY) {
            SubmitDialog dlg(this,
                "Press okay if this is a valid trade");
            dlg.DoModal();
            if (dlg.wasSubmitted()) {
                g_dispatcher.undoLastSubmission();
                flag = true;
            }
        }
        else
        if (status == IDABORT) {
            flag = false;
        }
    }
    ...
};

เมื่อเรามีงานที่เฉพาะกับ UI ถูกผลักลงไปใน subclass ใหม่ ( WindowsOffMarketValidator) เราสามารถสร้าง subclass อีกอันสำหรับการทดสอบ สิ่งที่ต้องทำคือทำให้พฤติกรรม showMessage เป็น null:

class TestingOffMarketTradeValidator
        : public OffMarketTradeValidator
{
protected:
    virtual void showMessage() {}
};

ตอนนี้เรามีคลาสที่เราสามารถทดสอบได้โดยไม่มี dependencies ใดๆ กับ UI การใช้ inheritance ในลักษณะนี้ เหมาะสมหรือไม่? ไม่ แต่มันช่วยให้เรานำตรรกะส่วนหนึ่งของคลาสมาทดสอบได้ เมื่อเรามี tests สำหรับ OffMarketTradeValidator, เราสามารถเริ่มทำความสะอาดตรรกะ retry และดึงมันขึ้นมาจาก WindowsOffMarketTradeValidator เมื่อเหลือเพียงการเรียกใช้ UI เราสามารถย้ายไปสู่การ delegate พวกมันไปยังคลาสใหม่ คลาสใหม่นั้นจะจบลงด้วยการถือ dependencies ต่อ UI เพียงอย่างเดียว

Steps (ขั้นตอน)

ในการ Push Down Dependency ให้ทำตามขั้นตอนเหล่านี้:

1. พยายาม build คลาสที่มีปัญหา dependency ใน test harness ของคุณ

2. ระบุว่า dependencies ใดสร้างปัญหาใน build

3. สร้าง subclass ใหม่ที่มีชื่อที่สื่อถึงสภาพแวดล้อมเฉพาะของ dependencies เหล่านั้น

4. คัดลอก instance variables และ methods ที่มี dependencies ที่ไม่ดีไปยัง subclass ใหม่ ระวังในการ รักษา signatures ทำให้ methods เป็น protected และ abstract ในคลาสเดิมของคุณ และทำให้คลาสเดิมของคุณ เป็น abstract

5. สร้าง testing subclass และเปลี่ยน test ของคุณเพื่อให้คุณพยายามสร้าง instance ของมัน

6. Build tests ของคุณเพื่อยืนยันว่าคุณสามารถสร้าง instance ของคลาสใหม่ได้

Replace Function with Function Pointer (แทนที่ Function ด้วย Function Pointer)

เมื่อคุณต้องการทำลาย dependencies ในภาษา procedural คุณไม่มีตัวเลือกมากเท่าใน ภาษา object-oriented คุณไม่สามารถใช้ Encapsulate Global References ( 339 ) หรือ Subclass and Override Method ( 401 ) ตัวเลือกทั้งหมดนั้นปิด คุณสามารถใช้ Link Substitution ( 377 ) หรือ Definition Completion ( 337 ) แต่บ่อยครั้งมันมากเกินไปสำหรับการทำลาย dependency เล็กๆ Replace Function with Function Pointer เป็นทางเลือกหนึ่งในภาษาที่รองรับ function pointers ภาษาที่รู้จักดีที่สุดที่มีการรองรับนี้คือ C

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

เริ่มจากสิ่งแรกก่อน มาดู function pointer ในสภาพแวดล้อมธรรมชาติของมัน ตัวอย่างต่อไปนี้แสดง การประกาศ function pointers สองสามตัวใน C และการเรียกผ่านพวกมัน:

struct base_operations
{
    double (*project)(double,double);
    double (*maximize)(double,double);
};

double default_projection(double first, double second) {
    return second;
}

double maximize(double first, double second) {
    return first + second;
}

void init_ops(struct base_operations *operations) {
    operations->project = default_projection;
    operations->maximize = default_maximize;
}

void run_tesselation(struct node *base,
                     struct base_operations *operations) {
    double value = operations->project(base.first, base.second);
    ...
}

ด้วย function pointers คุณสามารถทำ object-based programming อย่างง่าย แต่พวกมันมีประโยชน์แค่ไหนเมื่อทำลาย dependencies? พิจารณาสถานการณ์นี้:

คุณมีแอปพลิเคชันเครือข่ายที่เก็บข้อมูล packet ใน online database คุณโต้ตอบกับ database ผ่านการเรียกใช้ที่ดูเหมือนนี้:

void db_store(
    struct receive_record *record,
    struct time_stamp receive_time);
struct receive_record * db_retrieve(time_stamp search_time);

เราสามารถใช้ Link Substitution ( 377 ) เพื่อ link ไปยัง bodies ใหม่สำหรับ functions เหล่านี้ แต่บางครั้ง Link Substitution ทำให้เกิดการเปลี่ยนแปลง build ที่ไม่เล็กน้อย เราอาจต้องแยก libraries เพื่อแยก functions ที่เราต้องการ ปลอม ที่สำคัญกว่านั้น seams ที่เราได้จาก Link Substitution ไม่ใช่ประเภทที่คุณต้องการใช้ประโยชน์เพื่อเปลี่ยนพฤติกรรมใน production code ถ้าคุณต้องการนำโค้ดมาทดสอบ และให้ความยืดหยุ่น เช่น เปลี่ยนประเภทของ database ที่โค้ดของคุณสามารถคุยด้วย Replace Function with Function Pointer สามารถมีประโยชน์ มาดูขั้นตอนกัน:

อันดับแรกเราหา declaration ของ function ที่เราต้องการแทนที่

// db.h
void db_store(struct receive_record *record,
              struct time_stamp receive_time);

จากนั้นเราประกาศ function pointer ที่มีชื่อเดียวกัน

// db.h
void db_store(struct receive_record *record,
              struct time_stamp receive_time);

void (*db_store)(struct receive_record *record,
                 struct time_stamp receive_time);

ตอนนี้เราเปลี่ยนชื่อ declaration เดิม

// db.h
void db_store_production(struct receive_record *record,
                         struct time_stamp receive_time);

void (*db_store)(struct receive_record *record,
                 struct time_stamp receive_time);

จากนั้นเรา initialize pointer ในไฟล์ C source:

// main.c
extern void db_store_production(
                struct receive_record *record,
                struct time_stamp receive_time);

void initializeEnvironment() {
    db_store = db_store_production;
    ...
}

int main(int ac, char **av) {
    initializeEnvironment();
    ...
}

ตอนนี้เราหา definition ของฟังก์ชัน db_store และเปลี่ยนชื่อมันเป็น db_store_production

// db.c
void db_store_production(
         struct receive_record *record,
         struct time_stamp receive_time) {
    ...
}

ตอนนี้เราสามารถ compile และทดสอบได้

ด้วย function pointers tests สามารถให้ definitions ทางเลือกสำหรับการรับรู้หรือแยก


Replace Function with Function Pointer เป็นวิธีที่ดีในการทำลาย dependencies ข้อดีอย่างหนึ่งคือมันเกิดขึ้นที่ compile time ทั้งหมด ดังนั้นจึงมีผลกระทบน้อยที่สุดต่อระบบ build ของคุณ อย่างไรก็ตาม ถ้าคุณใช้เทคนิคนี้ใน C ให้พิจารณาอัปเกรดเป็น C++ เพื่อให้คุณสามารถใช้ประโยชน์จาก seams อื่นๆ ที่ C++ มีให้ ในขณะที่เขียนนี้ คอมไพเลอร์ C หลายตัวมี switches ที่ให้คุณทำการคอมไพล์ C และ C++ แบบผสมได้ เมื่อใช้คุณสมบัตินี้ คุณสามารถ ย้ายโปรเจกต์ C ของคุณไปยัง C++ อย่างช้าๆ โดยใช้เฉพาะไฟล์ที่คุณต้องการทำลาย dependencies ก่อน


Steps (ขั้นตอน)

ในการใช้ Replace Function with Function Pointer ให้ทำดังนี้:

1. หา declarations ของ functions ที่คุณต้องการแทนที่

2. สร้าง function pointers ที่มีชื่อเดียวกันก่อนแต่ละ function declaration

3. เปลี่ยนชื่อ function declarations เดิมเพื่อให้ชื่อไม่เหมือนกับ function pointers ที่คุณเพิ่งประกาศ

4. Initialize pointers ไปยังที่อยู่ของ functions เก่าในไฟล์ C

5. รัน build เพื่อหา bodies ของ functions เก่า เปลี่ยนชื่อเป็นชื่อ function ใหม่

Replace Global Reference with Getter (แทนที่ Global Reference ด้วย Getter)

Global variables อาจเป็นปัญหาจริงๆ เมื่อคุณต้องการทำงานกับชิ้นส่วนของโค้ดอย่างอิสระ ผมจะพูดแค่นั้น เกี่ยวกับเรื่องนี้ ผมมีคำบ่นที่ค่อนข้างสมบูรณ์เกี่ยวกับ globals ในบทนำของ Introduce Static Setter ( 372 ) ผมจะไม่กล่าวซ้ำที่นี่

วิธีหนึ่งในการผ่าน dependencies บน globals ในคลาสคือการ introduce getters สำหรับแต่ละตัวในคลาส เมื่อคุณมี getters แล้ว คุณสามารถ Subclass and Override Method ( 401 ) เพื่อให้ getters คืนค่าที่เหมาะสม ในบางกรณี คุณอาจไปไกลถึงการใช้ Extract Interface ( 362 ) เพื่อทำลาย dependencies บนคลาสของ global นี่คือตัวอย่างใน Java:

public class RegisterSale
{
    public void addItem(Barcode code) {
        Item newItem =
            Inventory.getInventory().itemForBarcode(code);
        items.add(newItem);
    }
    ...
}

ในโค้ดนี้ คลาส Inventory ถูกเข้าถึงเป็น global "เดี๋ยวนะ?" ผมได้ยินคุณพูด "global? มันแค่เรียก static method บนคลาส" สำหรับวัตถุประสงค์ของเรา นั่นนับเป็น global ใน Java ตัวคลาสเองเป็น global object และดูเหมือนว่ามันต้อง อ้างถึงสถานะบางอย่างเพื่อให้สามารถทำงานได้ (คืนค่า item objects จาก barcodes) เราสามารถผ่านสิ่งนี้ ด้วย Replace Global Reference with Getter ? มาลองกัน

อันดับแรกเราเขียน getter สังเกตว่าเราทำให้มันเป็น protected เพื่อให้เราสามารถโอเวอร์ไรด์มันภายใต้ test

public class RegisterSale
{
    public void addItem(Barcode code) {
        Item newItem = Inventory.getInventory().itemForBarcode(code);
        items.add(newItem);
    }

    protected Inventory getInventory() {
        return Inventory.getInventory();
    }
    ...
}

จากนั้นเราแทนที่การเข้าถึง global ทุกครั้งด้วย getter

public class  RegisterSale
{
    public void addItem(Barcode code) {
        Item newItem = getInventory().itemForBarcode(code);
        items.add(newItem);
    }

    protected Inventory getInventory() {
        return Inventory.getInventory();
    }
    ...
}

ตอนนี้เราสามารถสร้าง subclass ของ Inventory ที่เราสามารถใช้ใน test ได้ เพราะ Inventory เป็น singleton เราต้องทำให้ constructor ของมันเป็น protected แทนที่จะเป็น private หลังจากนั้น เราสามารถ subclass มันแบบนี้และใส่ตรรกะใดๆ ที่เราต้องการใช้เพื่อแปลง barcodes เป็น items ใน test

public class FakeInventory extends Inventory
{
    public Item itemForBarcode(Barcode code) {
        ...
    }
    ...
}

ตอนนี้เราสามารถเขียนคลาสที่เราจะใช้ใน test

class TestingRegisterSale extends RegisterSale
{
    Inventory inventory = new FakeInventory();

    protected Inventory getInventory() {
        return inventory;
    }
}

Steps (ขั้นตอน)

ในการ Replace Global Reference with Getter ให้ทำดังนี้:

1. ระบุ global reference ที่คุณต้องการแทนที่

2. เขียน getter สำหรับ global reference ตรวจสอบให้แน่ใจว่าการป้องกันการเข้าถึงของ method หลวมพอสำหรับคุณ ที่จะโอเวอร์ไรด์ getter ใน subclass

3. แทนที่ references ไปยัง global ด้วยการเรียก getter

4. สร้าง testing subclass และโอเวอร์ไรด์ getter

Subclass and Override Method (Subclass และโอเวอร์ไรด์ Method)

Subclass and Override Method เป็นเทคนิคหลักในการทำลาย dependencies ในโปรแกรม object-oriented ที่จริงแล้ว เทคนิคอื่นๆ ในการทำลาย dependency ในบทนี้หลายเทคนิคล้วนเป็นรูปแบบของมัน

แนวคิดหลักของ Subclass and Override Method คือคุณสามารถใช้ inheritance ในบริบทของ test เพื่อทำให้พฤติกรรมที่คุณไม่สนใจเป็น null หรือเข้าถึง พฤติกรรมที่คุณสนใจ

มาดู method ในแอปพลิเคชันเล็กๆ:

class MessageForwarder
{
    private Message createForwardMessage(Session session,
                                         Message message)
                    throws MessagingException, IOException {
         MimeMessage forward = new MimeMessage (session);
        forward.setFrom (getFromAddress (message));
        forward.setReplyTo (
                new Address [] {
                    new InternetAddress (listAddress) });
        forward.addRecipients (Message.RecipientType.TO,
                               listAddress);
        forward.addRecipients (Message.RecipientType.BCC,
                               getMailListAddresses ());
        forward.setSubject (
                transformedSubject (message.getSubject ()));
        forward.setSentDate (message.getSentDate ());
        forward.addHeader (LOOP_HEADER, listAddress);
        buildForwardContent(message, forward);

        return forward;
    }
    ...
}

MessageForwarder มี methods จำนวนมากที่ไม่ได้แสดงไว้ หนึ่งใน public methods เรียก private method นี้ createForwardMessage เพื่อสร้าง message ใหม่ สมมติว่าเราไม่ต้องการ dependency บนคลาส MimeMessage เมื่อเราทดสอบ มันใช้ตัวแปรชื่อ session, และเราจะไม่มี session จริงเมื่อเราทดสอบ ถ้าเราต้องการแยก dependency บน MimeMessage เราสามารถทำให้ createForwardMessage เป็น protected และโอเวอร์ไรด์มันใน subclass ใหม่ที่เราสร้างสำหรับการทดสอบ:

class TestingMessageForwarder extends MessageForwarder
{
    protected Message createForwardMessage(Session session,
                                           Message message) {
        Message forward = new FakeMessage(message);
        return forward;
    }
    ...
}

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

ใน production code เราสร้าง instance MessageForwarders ใน tests เราสร้าง instance TestingMessageForwarders เราสามารถแยกได้ด้วยการปรับเปลี่ยนเล็กน้อยใน production code สิ่งที่เราทำคือเปลี่ยนขอบเขตของ method จาก private เป็น protected

โดยทั่วไป การจัดโครงสร้างในคลาสจะกำหนดว่าคุณสามารถใช้ inheritance เพื่อแยก dependencies ได้ดีแค่ไหน บางครั้งคุณมี dependency ที่คุณต้องการกำจัดซึ่งถูกแยกอยู่ใน method เล็กๆ ในบางครั้ง คุณต้องโอเวอร์ไรด์ method ที่ใหญ่กว่าเพื่อแยก dependency

Subclass and Override Method เป็นเทคนิคที่ทรงพลัง แต่คุณต้องระวัง ในตัวอย่างก่อนหน้านี้ ผมสามารถคืนค่า empty message โดยไม่มี subject, from address และอื่นๆ แต่นั่นจะสมเหตุสมผลก็ต่อเมื่อฉันกำลังทดสอบข้อเท็จจริงที่ว่าผมสามารถรับ message จากที่หนึ่งไปอีกที่หนึ่งในซอฟต์แวร์และไม่สนใจว่าเนื้อหาและการระบุที่อยู่จริงคืออะไร

สำหรับผม การเขียนโปรแกรมเป็นเรื่องที่มองเห็นเป็นภาพ ส่วนใหญ่ ผมเห็นภาพต่างๆ ในใจเมื่อผมทำงาน และพวกมันช่วยผม ตัดสินใจระหว่างทางเลือก เป็นที่น่าเสียดายที่ไม่มีภาพใดเป็น UML จริงๆ แต่มันช่วยผมได้

ภาพหนึ่งที่ผมนึกถึงบ่อยคือสิ่งที่ผมเรียกว่า paper view ผมมอง method และเริ่มเห็นทุกวิธีที่ผมสามารถจัดกลุ่ม statements และ expressions สำหรับ snippet เล็กๆ ใน method ที่ผมระบุได้ ผมตระหนักว่าถ้าผมสามารถสกัดมันไปยัง method ผมสามารถแทนที่มันด้วย สิ่งอื่นระหว่างการทดสอบ มันเหมือนกับว่าผมวางแผ่นกระดาษโปร่งแสงบนแผ่นที่มีโค้ด แผ่นใหม่สามารถมีโค้ดชิ้นอื่นสำหรับ snippet ที่ผมต้องการแทนที่ กองกระดาษคือสิ่งที่ผม ทดสอบ และ methods ที่ผมเห็นผ่านแผ่นบนสุดคือ methods ที่สามารถถูก execute เมื่อผมทดสอบ Figure 25.6 เป็นความพยายามที่จะแสดง paper view ของคลาส

Figure 25.6 TestingAccount superimposed on (ซ้อนทับบน) Account.

image

paper view ช่วยให้ผมเห็นสิ่งที่เป็นไปได้ แต่เมื่อผมเริ่มใช้ Subclass and Override Method ผมพยายามโอเวอร์ไรด์ methods ที่มีอยู่แล้ว หลังจากทั้งหมด เป้าหมายคือการมี tests และการสกัด methods โดยไม่มี tests อาจมีความเสี่ยงในบางครั้ง

Steps (ขั้นตอน)

ในการ Subclass and Override Method ให้ทำดังนี้:

1. ระบุ dependencies ที่คุณต้องการแยกหรือที่ที่คุณต้องการรับรู้ พยายามหา ชุด methods ที่เล็กที่สุดที่คุณสามารถโอเวอร์ไรด์เพื่อบรรลุเป้าหมาย

2. ทำให้แต่ละ method สามารถโอเวอร์ไรด์ได้ วิธีการทำแตกต่างกันไปในแต่ละภาษาโปรแกรม ใน C++ methods ต้อง ถูกทำให้เป็น virtual ถ้ายังไม่ใช่ ใน Java methods ต้องถูกทำให้ไม่เป็น final ในภาษา .NET หลายภาษา คุณต้องทำให้ method สามารถโอเวอร์ไรด์ได้อย่างชัดเจนเช่นกัน

3. ถ้าภาษาของคุณต้องการ ให้ปรับการมองเห็นของ methods ที่คุณจะโอเวอร์ไรด์เพื่อให้พวกมันสามารถ ถูกโอเวอร์ไรด์ใน subclass ใน Java และ C# methods ต้องมี visibility อย่างน้อย protected เพื่อที่จะถูกโอเวอร์ไรด์ใน subclasses ใน C++ methods สามารถคงเป็น private และยังถูกโอเวอร์ไรด์ใน subclasses ได้

4. สร้าง subclass ที่โอเวอร์ไรด์ methods ตรวจสอบว่าคุณสามารถ build มันใน test harness ได้

Supersede Instance Variable (แทนที่ Instance Variable)

การสร้าง object ใน constructors อาจเป็นปัญหา โดยเฉพาะเมื่อมันยากที่จะพึ่งพา object เหล่านั้นใน test ในกรณีส่วนใหญ่ เราสามารถใช้ Extract and Override Factory Method ( 350 ) เพื่อผ่านปัญหานี้ อย่างไรก็ตาม ในภาษาที่ไม่อนุญาตการโอเวอร์ไรด์ virtual function calls ใน constructors เราต้องดูตัวเลือกอื่น หนึ่งในนั้นคือ Supersede Instance Variable

นี่คือตัวอย่างที่แสดงปัญหา virtual function ใน C++:

class Pager
{
public:
    Pager() {
        reset();
        formConnection();
    }

    virtual void formConnection() {
        assert(state == READY);
        // nasty code that talks to hardware here
        ...
    }

    void sendMessage(const std::string& address,
                     const std::string& message) {
        formConnection();
        ...
    }
    ...
};

ในตัวอย่างนี้ method formConnection ถูกเรียกใน constructor ไม่มีอะไรผิดปกติกับ constructors ที่ delegate งานไปยัง functions อื่น แต่มีบางอย่างที่ทำให้เข้าใจผิดเล็กน้อยเกี่ยวกับโค้ดนี้ formConnection ถูกประกาศเป็น virtual method ดังนั้นดูเหมือนว่าเราสามารถ Subclass and Override Method ( 401 ) แต่อย่าเพิ่งด่วนสรุป มาลองกัน:

class TestingPager : public Pager
{
public:
    virtual void formConnection() {
    }
};

TEST(messaging,Pager)
{
    TestingPager pager;
    pager.sendMessage("5551212",
                      "Hey, wanna go to a party? XXXOOO");
    LONGS_EQUAL(OKAY, pager.getStatus());
}

เมื่อเราโอเวอร์ไรด์ virtual function ใน C++ เรากำลังแทนที่พฤติกรรมของ function นั้นใน derived classes เหมือนที่เราคาดหวัง แต่มีข้อยกเว้นอย่างหนึ่ง เมื่อมีการเรียก virtual function ใน constructor ภาษา ไม่อนุญาตให้โอเวอร์ไรด์ ในตัวอย่างนี้หมายความว่าเมื่อ sendMessage ถูกเรียก TestingPager::formConnection ถูกใช้ และนั่นก็เยี่ยม: เราไม่ได้ต้องการส่งข้อความเจ้าชู้ไปยังผู้ให้บริการข้อมูลจริงๆ แต่ โชคร้ายที่เราทำไปแล้ว เมื่อเราสร้าง TestingPager, Page::formConnection ถูกเรียกระหว่าง initialization เพราะ C++ ไม่อนุญาตให้โอเวอร์ไรด์ใน constructor

C++ มีกฎข้อนี้เพราะ constructor calls ไปยัง virtual functions ที่ถูกโอเวอร์ไรด์อาจไม่ปลอดภัย ลองนึกภาพสถานการณ์นี้:

class A
{
public:
    A() {
        someMethod();
    }

    virtual void someMethod() {
    }
};

class B : public A
{
    C *c;
public:

    B() {
        c = new C;
    }

    virtual void someMethod() {
        c.doSomething();
    }
};

ที่นี่เรามี B's someMethod โอเวอร์ไรด์ A แต่จำลำดับการเรียก constructor เมื่อเราสร้าง B, A constructor ถูกเรียกก่อน B ดังนั้น A constructor เรียก someMethod และ someMethod ถูกโอเวอร์ไรด์ ดังนั้นตัวที่อยู่ใน B ถูกใช้ มันพยายามเรียก doSomething บน reference ของ type C แต่เดาอะไร? มันไม่เคยถูก initialize เพราะ B constructor ยังไม่ถูกเรียก

C++ ป้องกันสิ่งนี้ไม่ให้เกิดขึ้น ภาษาอื่นยืดหยุ่นกว่า ตัวอย่างเช่น โอเวอร์ไรด์ methods สามารถถูกเรียก จาก constructors ใน Java แต่ผมไม่แนะนำให้ทำใน production code

ใน C++ กลไกป้องกันเล็กๆ นี้ป้องกันเราจากการแทนที่พฤติกรรมใน constructors โชคดีที่เรามี วิธีอื่นๆ อีกสองสามวิธี ถ้า object ที่คุณกำลังแทนที่ไม่ถูกใช้ใน constructor คุณสามารถใช้ Extract and Override Getter ( 352 ) เพื่อทำลาย dependency ถ้าคุณใช้ object แต่คุณต้องแน่ใจว่าคุณสามารถแทนที่มันก่อนที่ method อื่นจะ ถูกเรียก คุณสามารถใช้ Supersede Instance Variable นี่คือตัวอย่าง:

BlendingPen::BlendingPen()
{
    setName("BlendingPen");
    m_param = ParameterFactory::createParameter(
                      "cm", "Fade", "Aspect Alter");
    m_param->addChoice("blend");
    m_param->addChoice("add");
    m_param->addChoice("filter");

    setParamByName("cm", "blend");
}

ในกรณีนี้ constructor กำลังสร้าง parameter ผ่าน factory เราสามารถใช้ Introduce Static Setter ( 372 ) เพื่อควบคุม object ถัดไปที่ factory จะคืนค่า แต่นั่นเป็นการรุกล้ำพอสมควร ถ้าเราไม่รังเกียจ การเพิ่ม method พิเศษให้กับคลาส เราสามารถแทนที่ parameter ที่เราสร้างใน constructor:

void BlendingPen::supersedeParameter(Parameter *newParameter)
{

    delete m_param;

    m_param = newParameter;

}

ใน tests เราสามารถสร้าง pens ตามที่เราต้องการและเรียก supersedeParameter เมื่อเราต้องการใส่ sensing object

เมื่อมองเผินๆ Supersede Instance Variable ดูเหมือนวิธีที่แย่ในการได้ sensing object แต่ใน C++ เมื่อ Parameterize Constructor ( 379 ) ยุ่งยากเกินไปเพราะตรรกะที่พัวพันใน constructor Supersede Instance Variable ( 404 ) อาจเป็นทางเลือกที่ดีที่สุด ในภาษาที่อนุญาต virtual calls ใน constructors Extract and Override Factory Method ( 350 ) มักจะเป็นตัวเลือกที่ดีกว่า


โดยทั่วไป การให้ setters ที่เปลี่ยน base objects ที่ object ใช้เป็นแนวปฏิบัติที่ไม่ดี Setters เหล่านั้น อนุญาตให้ clients เปลี่ยนแปลงพฤติกรรมของ object อย่างรุนแรงในช่วงอายุของมัน เมื่อมีคน ทำการเปลี่ยนแปลงเหล่านั้น คุณต้องรู้ประวัติของ object นั้นเพื่อเข้าใจว่าเกิดอะไรขึ้นเมื่อคุณเรียก method หนึ่งของมัน เมื่อคุณไม่มี setters โค้ดจะเข้าใจง่ายขึ้น


สิ่งที่ดีอย่างหนึ่งเกี่ยวกับการใช้คำว่า supersede เป็นคำนำหน้า method คือมันค่อนข้างสวยและไม่ธรรมดา ถ้าคุณกังวลว่าคนจะ ใช้ methods superseding ใน production code คุณสามารถค้นหาอย่างรวดเร็วเพื่อให้แน่ใจว่าพวกมันไม่ได้ใช้

Steps (ขั้นตอน)

ในการ Supersede Instance Variable ให้ทำตามขั้นตอนเหล่านี้:

1. ระบุ instance variable ที่คุณต้องการแทนที่

2. สร้าง method ชื่อ supersedeXXX โดยที่ XXX คือชื่อของตัวแปรที่คุณต้องการแทนที่

3. ใน method ให้เขียนโค้ดที่จำเป็นเพื่อทำลาย instance ก่อนหน้าของตัวแปรและตั้งค่า เป็นค่าใหม่ ถ้าตัวแปรเป็น reference ให้ตรวจสอบว่าไม่มี references อื่นในคลาสไปยัง object ที่มันชี้ไป ถ้ามี คุณอาจมีงานเพิ่มเติมใน method superseding เพื่อให้แน่ใจว่า การแทนที่ object ปลอดภัยและให้ผลที่ถูกต้อง

Template Redefinition (การนิยาม Template ใหม่)

เทคนิคการทำลาย dependency หลายอย่างในบทนี้อาศัยกลไก object-oriented หลัก เช่น interface และ implementation inheritance คุณสมบัติภาษาใหม่บางอย่างให้ทางเลือกเพิ่มเติม ตัวอย่างเช่น ถ้าภาษา มี generics และวิธีการสร้าง type aliasing คุณสามารถทำลาย dependencies โดยใช้เทคนิคที่เรียกว่า Template Redefinition นี่คือตัวอย่างใน C++:

// AsyncReceptionPort.h

class AsyncReceptionPort
{
private:
    CSocket m_socket;
    Packet m_packet;
    int m_segmentSize;
    ...

public:
         AsyncReceptionPort();
    void Run();
    ...
};

// AsynchReceptionPort.cpp

void AsyncReceptionPort::Run() {
    for(int n = 0; n < m_segmentSize; ++n) {
        int bufferSize = m_bufferMax;
        if (n = m_segmentSize - 1)
            bufferSize = m_remainingSize;
        m_socket.receive(m_receiveBuffer, bufferSize);
        m_packet.mark();
        m_packet.append(m_receiveBuffer,bufferSize);
        m_packet.pack();
    }
    m_packet.finalize();
}

ถ้าเรามีโค้ดแบบนี้และเราต้องการเปลี่ยนตรรกะใน method เราจะเจอความจริงที่ว่าเรา ไม่สามารถเรียก method ใน test harness ได้โดยไม่ต้องส่งอะไรบางอย่างผ่าน socket ใน C++ เราสามารถหลีกเลี่ยงสิ่งนี้ได้ทั้งหมด โดยทำให้ AsyncReceptionPort เป็น template แทนที่จะเป็นคลาสปกติ นี่คือลักษณะของโค้ดหลังการเปลี่ยนแปลง

// AsynchReceptionPort.h

template<typename SOCKET > class AsyncReceptionPortImpl
{
private:
     SOCKET   m_socket;
    Packet m_packet;
    int m_segmentSize;
    ...

public:
    AsyncReceptionPortImpl();
    void Run();
    ...
};

template<typename SOCKET>
void AsyncReceptionPortImpl<SOCKET>::Run() {
    for(int n = 0; n < m_segmentSize; ++n) {
        int bufferSize = m_bufferMax;
        if (n = m_segmentSize - 1)
            bufferSize = m_remainingSize;
        m_socket.receive(m_receiveBuffer, bufferSize);
        m_packet.mark();
        m_packet.append(m_receiveBuffer,bufferSize);
        m_packet.pack();
    }
    m_packet.finalize();
}

typedef AsyncReceptionPortImpl<CSocket> AsyncReceptionPort;

เมื่อเรามีการเปลี่ยนแปลงนี้ เราสามารถสร้าง instance ของ template ด้วย type ที่แตกต่างในไฟล์ test:

// TestAsynchReceptionPort.cpp

#include "AsyncReceptionPort.h"

class FakeSocket
{
public:
    void receive(char *, int size) { ... }
};

TEST(Run,AsyncReceptionPort)
{
    AsyncReceptionPortImpl<FakeSocket> port;
    ...
}

สิ่งที่เจ๋งที่สุดเกี่ยวกับเทคนิคนี้คือความจริงที่ว่าเราสามารถใช้ typedef เพื่อหลีกเลี่ยงการเปลี่ยน references ทั่วทั้ง code base ของเรา ถ้าไม่มี typedef เราจะต้องแทนที่ทุก reference ถึง AsyncReceptionPort ด้วย AsyncReceptionPort<CSocket> มันจะเป็นงานที่น่าเบื่อมาก แต่ง่ายกว่าที่คิด เราสามารถ Lean on the Compiler ( 315 ) เพื่อให้แน่ใจว่าเราเปลี่ยน references ที่ถูกต้องทั้งหมด ในภาษาที่มี generics แต่ไม่มี type-aliasing mechanism เช่น typedef คุณจะต้อง Lean on the Compiler

ใน C++ คุณสามารถใช้เทคนิคนี้เพื่อให้ definitions ทางเลือกของ methods แทนที่จะเป็น data แต่มัน เล็กน้อยยุ่งเหยิง กฎของ C++ กำหนดให้คุณต้องมี template parameter ดังนั้นคุณสามารถเลือกตัวแปรและทำให้ type ของมันเป็น template parameter แบบสุ่ม หรือ introduce ตัวแปรใหม่แค่เพื่อให้คลาส parameterized ด้วย type บางอย่าง—แต่ผมจะ ทำอย่างนั้นเป็นทางเลือกสุดท้ายเท่านั้น ผมจะดูอย่างระมัดระวังเพื่อดูว่าผมสามารถใช้เทคนิคที่ใช้ inheritance-based ก่อน


Template Redefinition ใน C++ มีข้อเสียหลักหนึ่งข้อ โค้ดที่เคยอยู่ใน implementation files จะย้ายไปที่ headers เมื่อคุณ templatize มัน ซึ่งสามารถเพิ่ม dependencies ในระบบ ผู้ใช้ template จะถูกบังคับให้ compile ใหม่เมื่อใดก็ตามที่ template code ถูกเปลี่ยนแปลง

โดยทั่วไป ผมเอนเอียงไปทางการใช้เทคนิคที่ใช้ inheritance-based สำหรับการทำลาย dependencies ใน C++ อย่างไรก็ตาม Template Redefinition สามารถมีประโยชน์เมื่อ dependencies ที่คุณต้องการทำลายอยู่ใน templatized code อยู่แล้ว นี่คือตัวอย่าง:

template<typename ArcContact> class CollaborationManager
{
    ...
    ContactManager<ArcContact> m_contactManager;
    ...
};

ถ้าเราต้องการทำลาย dependency บน m_contactManager เราไม่สามารถใช้ Extract Interface ( 362 ) บนมันได้ง่ายๆ เพราะวิธีที่เราใช้ templates ที่นี่ อย่างไรก็ตาม เราสามารถ parameterize template แตกต่างกัน:

template<typename ArcContactManager> class CollaborationManager
{
    ...
    ArcContactManager m_contactManager;
    ...
};


Steps (ขั้นตอน)

นี่คือคำอธิบายวิธีการทำ Template Redefinition ใน C++ ขั้นตอนอาจแตกต่างในภาษาอื่นที่รองรับ generics แต่คำอธิบายนี้ให้รสชาติของ เทคนิค:

1. ระบุ features ที่คุณต้องการแทนที่ในคลาสที่คุณต้องทดสอบ

2. เปลี่ยนคลาสเป็น template โดย parameterizing ด้วยตัวแปรที่คุณต้องการแทนที่และคัดลอก method bodies ขึ้นไปยัง header

3. ตั้งชื่ออื่นให้ template วิธีหนึ่งในการทำคือการต่อท้ายชื่อเดิมด้วย Impl

4. เพิ่ม typedef statement หลังจาก template definition โดยกำหนด template ด้วยอาร์กิวเมนต์เดิมโดยใช้ ชื่อคลาสเดิม

5. ในไฟล์ test รวม template definition และสร้าง instance ของ template ด้วย types ใหม่ที่จะแทนที่ types ที่คุณต้องการแทนที่สำหรับ test

Text Redefinition (การนิยามข้อความใหม่)

ภาษาที่ถูกตีความรุ่นใหม่บางภาษาให้วิธีที่ยอดเยี่ยมในการทำลาย dependencies เมื่อพวกมันถูกตีความ methods สามารถถูก redefine ได้ทันที นี่คือตัวอย่างในภาษา Ruby:

# Account.rb
class Account
    def report_deposit(value)
        ...
    end

    def deposit(value)
        @balance += value
        report_deposit(value)
    end

    def withdraw(value)
        @balance -= value
    end
end

ถ้าเราไม่ต้องการให้ report_deposit ทำงานภายใต้ test เราสามารถ redefine มันในไฟล์ test และวาง tests หลังการ redefinition:

# AccountTest.rb
require "runit/testcase"
require "Account"

class Account
    def report_deposit(value)
    end
end

# tests start here
class AccountTest < RUNIT::TestCase
    ...
end

สิ่งสำคัญที่ต้องสังเกตคือเราไม่ได้ redefine ทั้งคลาส Account ที่นี่—แค่ method report_deposit ตัวแปล Ruby ตีความทุกบรรทัดในไฟล์ Ruby เป็นคำสั่งที่ executable class Account statement เปิด definition ของคลาส Account เพื่อให้สามารถเพิ่ม definitions เพิ่มเติมเข้าไปได้ def report_deposit(value) statement เริ่มกระบวนการเพิ่ม definition ให้กับคลาสที่เปิดอยู่ ตัวแปล Ruby ไม่สนใจว่ามี definition ของ method นั้นอยู่แล้วหรือไม่ ถ้ามี มันก็แค่แทนที่มัน


Text Redefinition ใน Ruby มีข้อเสียอย่างหนึ่ง method ใหม่แทนที่ method เก่าจนกว่าโปรแกรมจะสิ้นสุด ซึ่งอาจทำให้เกิดปัญหาได้ถ้า คุณลืมว่า method หนึ่งๆ ถูก redefine โดย test ก่อนหน้านี้

เราสามารถทำ Text Redefinition ใน C และ C++ ได้เช่นกัน โดยใช้ preprocessor เพื่อดูตัวอย่างวิธีการทำ ดูที่ Preprocessing Seam ( 33 ) ใน Chapter 4 , The Seam Model


Steps (ขั้นตอน)

ในการใช้ Text Redefinition ใน Ruby ให้ทำตามขั้นตอนเหล่านี้:

1. ระบุคลาสที่มี definitions ที่คุณต้องการแทนที่

2. เพิ่ม require clause ด้วยชื่อของโมดูลที่มีคลาสนั้นที่ด้านบนของไฟล์ source test

3. ให้ definitions ทางเลือกที่ด้านบนของไฟล์ source test สำหรับแต่ละ method ที่คุณต้องการแทนที่