สำนวน "ดำเนินรอบ" คืออะไร?


151

"Execute Around" นี่คืออะไรสำนวน (หรือคล้ายกัน) ฉันเคยได้ยินเกี่ยวกับ? ทำไมฉันถึงใช้มันและทำไมฉันถึงไม่ต้องการใช้มัน?


9
ฉันไม่ได้สังเกตว่ามันเป็นคุณ มิฉะนั้นฉันอาจจะเหน็บแนมในคำตอบของฉันมากขึ้น)
Jon Skeet

1
ดังนั้นนี่เป็นมุมมองที่ถูกต้องใช่ไหม ถ้าไม่มันแตกต่างกันอย่างไร
ลูคัส

คำตอบ:


147

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

public interface InputStreamAction
{
    void useStream(InputStream stream) throws IOException;
}

// Somewhere else    

public void executeWithFile(String filename, InputStreamAction action)
    throws IOException
{
    InputStream stream = new FileInputStream(filename);
    try {
        action.useStream(stream);
    } finally {
        stream.close();
    }
}

// Calling it
executeWithFile("filename.txt", new InputStreamAction()
{
    public void useStream(InputStream stream) throws IOException
    {
        // Code to use the stream goes here
    }
});

// Calling it with Java 8 Lambda Expression:
executeWithFile("filename.txt", s -> System.out.println(s.read()));

// Or with Java 8 Method reference:
executeWithFile("filename.txt", ClassName::methodName);

รหัสโทรไม่จำเป็นต้องกังวลเกี่ยวกับด้านเปิด / สะอาดขึ้น - executeWithFileมันจะได้รับการดูแลโดย

นี่คือความเจ็บปวดตรงไปตรงมาใน Java เพราะการปิดมีคำพูดมากเริ่มต้นด้วยการแสดงออกแลมบ์ดาของ Java 8 สามารถดำเนินการเช่นเดียวกับในภาษาอื่น ๆ (เช่นการแสดงออก C # lambda หรือ Groovy) และกรณีพิเศษนี้ถูกจัดการตั้งแต่ Java 7 ด้วยtry-with-resourcesและAutoClosableสตรีม

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


4
มันกำหนดไว้แล้ว Finalizers ใน Java ไม่ได้ถูกเรียกอย่างถูกต้อง ตามที่ฉันพูดในย่อหน้าสุดท้ายมันไม่เพียง แต่ใช้สำหรับการจัดสรรทรัพยากรและการล้างข้อมูล อาจไม่จำเป็นต้องสร้างวัตถุใหม่เลย โดยทั่วไปคือ "การกำหนดค่าเริ่มต้นและการลบ" แต่นั่นอาจไม่ใช่การจัดสรรทรัพยากร
Jon Skeet

3
มันเหมือนกับใน C ที่คุณมีฟังก์ชั่นที่คุณผ่านในตัวชี้ฟังก์ชั่นเพื่อทำงานบางอย่าง?
พอลทอมบลิน

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

8
@ ฟิล: ฉันคิดว่ามันเป็นเรื่องของการศึกษาระดับปริญญา คลาสภายในที่ไม่ระบุชื่อของ Java มีการเข้าถึงสภาพแวดล้อมโดยรอบในแง่ที่ จำกัดดังนั้นในขณะที่พวกเขาไม่ได้ปิด "เต็ม" พวกเขากำลังปิด "จำกัด " ฉันจะพูด แน่นอนฉันจะชอบที่จะเห็นการปิดที่เหมาะสมใน Java แม้ว่าการตรวจสอบ (ต่อ)
จอนสกีต

4
Java 7 เพิ่มลองกับทรัพยากรและ Java 8 เพิ่มแลมบ์ดา ฉันรู้ว่านี่เป็นคำถาม / คำตอบเก่า ๆ แต่ฉันอยากจะชี้ให้ใครเห็นคำถามนี้ในอีกห้าปีครึ่ง เครื่องมือภาษาทั้งสองนี้จะช่วยแก้ปัญหารูปแบบนี้ถูกคิดค้นเพื่อแก้ไข

45

Execute Around สำนวนที่ใช้เมื่อคุณพบว่าตัวเองต้องทำอะไรเช่นนี้:

//... chunk of init/preparation code ...
task A
//... chunk of cleanup/finishing code ...

//... chunk of identical init/preparation code ...
task B
//... chunk of identical cleanup/finishing code ...

//... chunk of identical init/preparation code ...
task C
//... chunk of identical cleanup/finishing code ...

//... and so on.

เพื่อหลีกเลี่ยงการทำซ้ำรหัสซ้ำซ้อนทั้งหมดนี้ซึ่งจะถูกดำเนินการ "รอบ" งานจริงของคุณเสมอคุณจะต้องสร้างคลาสที่ดูแลโดยอัตโนมัติ:

//pseudo-code:
class DoTask()
{
    do(task T)
    {
        // .. chunk of prep code
        // execute task T
        // .. chunk of cleanup code
    }
};

DoTask.do(task A)
DoTask.do(task B)
DoTask.do(task C)

สำนวนนี้ย้ายโค้ดซ้ำซ้อนที่ซับซ้อนทั้งหมดไว้ในที่เดียวและทำให้โปรแกรมหลักของคุณอ่านได้ง่ายขึ้น (และสามารถบำรุงรักษาได้!)

ลองดูที่ โพสต์นี้เพื่อดูตัวอย่าง C # และบทความนี้สำหรับตัวอย่าง C ++


7

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

Java ไม่ใช่ภาษาที่ฉันเลือกที่จะทำสิ่งนี้มันมีสไตล์มากกว่าที่จะผ่านการปิด (หรือการแสดงออกแลมบ์ดา) เป็นอาร์กิวเมนต์ แม้ว่าวัตถุจะเทียบเท่ากับการปิดเทียบเท่ากับการปิด

สำหรับผมแล้วดูเหมือนว่า Execute Around Method นั้นเหมือนกับInversion of Control (Dependency Injection) ที่คุณสามารถปรับเปลี่ยน Ad Hoc ได้ทุกครั้งที่คุณเรียกเมธอด

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


7

ฉันเห็นว่าคุณมีแท็ก Java ที่นี่ดังนั้นฉันจะใช้ Java เป็นตัวอย่างแม้ว่ารูปแบบจะไม่เฉพาะแพลตฟอร์ม

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

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

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

คุณอาจคุ้นเคยกับกรณีทั่วไปบางอย่างใน Java หนึ่งคือตัวกรอง servlet อีกข้อหนึ่งคือ AOP เกี่ยวกับคำแนะนำ ที่สามคือคลาส xxxTemplate ต่างๆใน Spring ในแต่ละกรณีคุณมีออบเจกต์ wrapper ซึ่งโค้ด "ที่น่าสนใจ" ของคุณ (พูดคำค้นหา JDBC และการประมวลผลชุดผลลัพธ์) วัตถุห่อหุ้มทำส่วน "ก่อน" เรียกใช้รหัสที่น่าสนใจจากนั้นทำส่วน "หลัง"


7

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

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

และหลังจากนั้น:

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

กระดาษไม่ได้สำรวจว่าทำไมถึงไม่ใช้สำนวนนี้ แต่อธิบายว่าทำไมสำนวนนั้นง่ายต่อการเข้าใจผิดโดยปราศจากความช่วยเหลือระดับภาษา:

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

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


จุดที่ดีสีฟ้า ฉันได้แก้ไขและขยายคำตอบของฉันเพื่อให้เป็นคำตอบที่มีอยู่ในตัวของมันเอง ขอบคุณที่แนะนำสิ่งนี้
เบ็น Liblit

4

ฉันจะพยายามอธิบายเช่นเดียวกับฉันอายุสี่ขวบ:

ตัวอย่างที่ 1

ซานต้ามาถึงเมือง รหัสเอลฟ์ของเขาสิ่งที่พวกเขาต้องการด้านหลังของเขาและถ้าพวกเขาเปลี่ยนสิ่งต่าง ๆ จะได้รับซ้ำ:

  1. รับกระดาษห่อของขวัญ
  2. รับซูเปอร์แฟมิ
  3. ห่อมัน

หรือสิ่งนี้:

  1. รับกระดาษห่อของขวัญ
  2. รับตุ๊กตาบาร์บี้
  3. ห่อมัน

.... โฆษณาคลื่นไส้ล้านครั้งด้วยของขวัญที่แตกต่างกันนับล้าน: สังเกตว่าสิ่งเดียวที่แตกต่างคือขั้นตอนที่ 2 ถ้าขั้นตอนที่สองเป็นสิ่งเดียวที่แตกต่างกันแล้วทำไมซานต้าจึงทำรหัสซ้ำนั่นคือทำไมเขาจึงทำซ้ำขั้นตอน 1 และ 3 หนึ่งล้านครั้ง? ของขวัญนับล้านหมายความว่าเขาทำซ้ำขั้นตอนที่ 1 และ 3 ล้านครั้งโดยไม่จำเป็น

ดำเนินการรอบ ๆ ช่วยในการแก้ปัญหาดังกล่าว และช่วยกำจัดรหัส ขั้นตอนที่ 1 และ 3 นั้นคงที่โดยทั่วไปทำให้ขั้นตอนที่ 2 เป็นส่วนเดียวที่เปลี่ยนแปลง

ตัวอย่างที่ 2

หากคุณยังไม่เข้าใจนี่เป็นอีกตัวอย่าง: ลองนึกถึงแซนวิช: ขนมปังจากด้านนอกจะเหมือนเดิมเสมอ แต่สิ่งที่เปลี่ยนแปลงภายในขึ้นอยู่กับประเภทของแซนวิชที่คุณเลือก (.eg แฮม, ชีส, แยมเนยถั่ว ฯลฯ ) ด้านนอกของขนมปังอยู่ด้านนอกเสมอและคุณไม่จำเป็นต้องทำซ้ำหลายพันครั้งสำหรับทรายทุกประเภทที่คุณกำลังสร้าง

ตอนนี้ถ้าคุณอ่านคำอธิบายข้างต้นบางทีคุณอาจเข้าใจได้ง่ายขึ้น ฉันหวังว่าคำอธิบายนี้จะช่วยคุณได้


+ สำหรับจินตนาการ: D
เซอร์ Hedgehog

3

นี้ทำให้ผมนึกถึงรูปแบบการออกแบบกลยุทธ์ โปรดสังเกตว่าลิงก์ที่ฉันชี้ไปยังมีรหัส Java สำหรับรูปแบบ

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

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

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


0

หากคุณต้องการสำนวนแรง ๆ นี่คือ:

//-- the target class
class Resource { 
    def open () { // sensitive operation }
    def close () { // sensitive operation }
    //-- target method
    def doWork() { println "working";} }

//-- the execute around code
def static use (closure) {
    def res = new Resource();
    try { 
        res.open();
        closure(res)
    } finally {
        res.close();
    }
}

//-- using the code
Resource.use { res -> res.doWork(); }

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