สำหรับถ้า antipattern


9

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

foreach (string filename in Directory.GetFiles("."))
{
    if (filename.Equals("desktop.ini", StringComparison.OrdinalIgnoreCase))
    {
        return new StreamReader(filename);
    }
}

คำถามที่ 1:

เป็นเพราะreturn new StreamReader(filename);ภายในfor loop? หรือความจริงที่ว่าคุณไม่จำเป็นต้องforวนซ้ำในกรณีนี้

ในฐานะผู้เขียนบล็อกชี้ให้เห็นว่ารุ่นที่บ้าน้อยกว่านี้คือ:

if (File.Exists("desktop.ini"))
{
    return new StreamReader("desktop.ini");
} 

ทั้งประสบจากสภาพการแข่งขันเพราะถ้าไฟล์ถูกลบออกก่อนที่จะสร้างของคุณจะได้รับStreamReaderFile­Not­Found­Exception

คำถามที่ 2:

ในการแก้ไขตัวอย่างที่สองคุณจะเขียนใหม่โดยไม่มีคำสั่ง if และล้อมรอบStreamReaderด้วยบล็อก try-catch แทนและถ้ามันโยนFile­Not­Found­Exceptionคุณจัดการมันในcatchบล็อกตามนั้นหรือไม่



1
@ มอน - ขอบคุณ แต่ฉันไม่ได้ทำคะแนนเพิ่มเติมฉันแค่พยายามที่จะเข้าใจสิ่งที่บทความพูดและวิธีที่ฉันจะแก้ไขมัน

3
ฉันจะไม่ให้ความคิดนี้มาก ผู้โพสต์บล็อกกำลังสร้างรหัสงี่เง่าที่ไม่มีใครเขียนให้ชื่อและเรียกว่ารูปแบบการต่อต้าน นี่คืออีกรูปแบบหนึ่ง: "ฉันเรียกสิ่งนี้ว่ารูปแบบการมอบหมายที่ไม่มีจุดหมาย: x = x; ฉันเห็นสิ่งนี้ถูกทำอยู่ตลอดเวลาดังนั้นโง่ฉันคิดว่าฉันจะเขียนบล็อกเกี่ยวกับเรื่องนี้"
Martin Maat

4
To fix the second example, would you re-write it without the if statement, and instead surround the StreamReader with a try-catch block, and if it throws a File­Not­Found­Exception you handle it in the catch block accordingly?- ใช่นั่นคือสิ่งที่ฉันจะทำ การแก้ปัญหาสภาพการแข่งขันมีความสำคัญมากกว่าความคิดบางอย่างของ "ข้อยกเว้นในฐานะการควบคุมการไหล" และมันแก้ปัญหาได้อย่างสวยงามและเรียบร้อย
Robert Harvey

1
จะทำอย่างไรถ้าไฟล์ใดไม่ผ่านเกณฑ์ที่กำหนด? มันกลับมาnullไหม โดยปกติคุณสามารถใช้ LINQ เพื่อล้างรหัสที่ดูเหมือนรหัสตัวอย่างของคุณ: return Directory.GetFiles(".").FirstOrDefault(fileName => fileName.Equals("desktop.ini", StringComparison.OrdinalIgnoreCase))?.Select(fileName => new StreamReader(filename)); สังเกต?.ผู้ดำเนินการระหว่างการเรียก LINQ สองครั้ง ผู้คนอาจโต้แย้งว่าการสร้างวัตถุเช่นนี้ไม่ใช่การใช้งาน LINQ ที่เหมาะสมที่สุด แต่ฉันคิดว่ามันไม่เป็นไร นี่ไม่ใช่คำตอบสำหรับคำถามของคุณ แต่จะพูดนอกเรื่องในส่วนหนึ่งของมัน
Panzercrisis

คำตอบ:


7

นี่คือ antipattern ตามที่ใช้ในรูปแบบของ:

loop over a set of values
   if current value meets a condition
       do something with value
   end
end

และสามารถถูกแทนที่ด้วย

do something with value

ตัวอย่างคลาสสิกของรหัสนี้คือ:

for (var i=0; i < 5; i++)
{
    switch (i)
        case 1:
            doSomethingWith(1);
            break;
        case 2:
            doSomethingWith(2);
            break;
        case 3:
            doSomethingWith(4);
            break;
        case 4:
            doSomethingWith(4);
            break;
    }
}

เมื่อสิ่งต่อไปนี้ใช้ได้ผล:

doSomethingWith(1);
doSomethingWith(2);
doSomethingWith(3);
doSomethingWith(4);

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

นั่นเป็นสาเหตุที่เป็นรูปแบบการต่อต้าน: ใช้รูปแบบ "วนรอบและทดสอบ" และใช้ในทางที่ผิด

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

ปัญหาเกี่ยวกับรหัสนี้:

if (File.Exists("desktop.ini"))
{
    return new StreamReader("desktop.ini");
}

คือในช่วงเวลาระหว่างFile.ExistsและStreamReaderพยายามเปิดไฟล์นั้นเธรดหรือกระบวนการอื่นสามารถลบไฟล์ได้ ดังนั้นคุณจะได้รับการยกเว้น ดังนั้นข้อยกเว้นนั้นจะต้องได้รับการปกป้องผ่านบางสิ่งเช่น:

try
{
    return new StreamReader("desktop.ini");
}
catch (File­Not­Found­Exception)
{
    return null; // or whatever
}

@Flater ยกจุดดีหรือไม่ ตัวเองเป็นปฏิปักษ์หรือไม่ เราใช้ข้อยกเว้นเป็นโฟลว์ควบคุมหรือไม่

หากรหัสอ่านบางอย่างเช่น:

try
{
    if (!File.Exists("desktop.ini")
    {
        throw new IniFileMissingException();
        return new StreamReader("desktop.ini");
    }
}
catch (IniFileMissingException)
{
    return null;
}

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

แน่นอนสิ่งที่เราต้องการจริงๆเป็นวิธีที่สง่างามกว่าในการสร้างกระแส สิ่งที่ต้องการ:

return TryCreateStream("desktop.ini", out var stream) ? stream : null;

และฉันขอแนะนำให้ห่อtry catchรหัสนั้นในวิธีการยูทิลิตี้เช่นนี้ถ้าคุณพบว่าตัวเองใช้รหัสนี้มาก


1
@ Flater ฉันมีแนวโน้มที่จะยอมรับว่ามันไม่เหมาะ (แม้ว่าฉันจะโต้แย้งมันเป็นตัวอย่างของรูปแบบการต่อต้านที่คำตอบพูดถึง) รหัสควรแสดงเจตนาไม่ใช่กลไก ดังนั้นฉันชอบบางอย่างของ Scala Tryเช่นreturn Try(() => new StreamReader("desktop.ini")).OrElse(null);แต่ C # ไม่รองรับโครงสร้างนั้น (โดยไม่ต้องใช้ห้องสมุดบุคคลที่ 3) ดังนั้นเราต้องทำงานกับเวอร์ชัน clunky
David Arno

1
@ Flater ฉันเห็นด้วยอย่างสมบูรณ์ว่าข้อยกเว้นควรเป็นพิเศษ แต่พวกเขาไม่ค่อยมี ตัวอย่างเช่นไฟล์ที่ไม่มีอยู่นั้นแทบจะไม่ได้รับการยกเว้น แต่File.Openจะทิ้งไฟล์ไว้หากไม่พบไฟล์ เนื่องจากเรามีข้อยกเว้นที่ไม่มีข้อยกเว้นถูกโยนทิ้งการวางไว้เป็นส่วนหนึ่งของโฟลว์การควบคุมจึงเป็นสิ่งจำเป็น
David Arno

1
@Flater: ตัวอย่างโค้ดที่สองเป็นวิธีที่เหมาะสมในการเปิดไฟล์ มีอะไรอีกที่แนะนำสภาพการแข่งขัน แม้ว่าคุณจะตรวจสอบว่ามีไฟล์อยู่ระหว่างการตรวจสอบนั้นและเมื่อคุณเปิดมันจริงๆบางสิ่งที่อาจทำให้ไฟล์นั้นไม่พร้อมใช้งานสำหรับคุณและการโทร Open () จะระเบิด ดังนั้นคุณต้องพร้อมสำหรับข้อยกเว้นอยู่ดี ดังนั้นคุณอาจไม่ต้องกังวลเรื่องการตรวจสอบและลองเปิดมันดู ข้อยกเว้นเป็นเครื่องมือที่ช่วยให้เราทำสิ่งต่างๆเสร็จสิ้นและการใช้งานของพวกเขาไม่ควรถูกมองว่าเป็นหลักศาสนา
whatsisname

1
@Flater: วิธีใด ๆ ที่จะหลีกเลี่ยงการลอง / จับการดำเนินงานของไฟล์จะทำให้เกิดสภาวะการแข่งขัน ระบบไฟล์ที่คุณต้องการเขียนสามารถใช้งานได้ทันทีก่อนที่คุณจะโทรไปที่ File สร้างการแสดงผลการตรวจสอบที่ไม่ถูกต้อง
whatsisname

1
@ Flater " คุณพิสูจน์ได้อย่างมีประสิทธิภาพว่าการเรียกใช้เมธอดทุกประเภทต้องการ return type ... ซึ่งพยายามที่จะสร้างข้อยกเว้นในวิธีที่แตกต่างกัน " แก้ไข. แม้ว่าการประดิษฐ์นั้นไม่ใช่ของฉัน มันถูกใช้ในภาษาที่ใช้งานได้หลายปี พวกเขามักหลีกเลี่ยงทั้ง "ข้อยกเว้นเป็นปัญหาการควบคุมการไหล" (ซึ่งคุณพลุกพล่านเรียกร้องต่อต้านรูปแบบในลมหายใจแล้วยืนยันในความโปรดปรานของในครั้งต่อไป) โดยใช้ประเภทสหภาพเช่นในกรณีนี้เราจะใช้Maybe<Stream>ประเภท ที่ส่งคืนnothingหากไฟล์ไม่มีอยู่และสตรีมหากมี
David Arno

1

คำถามที่ 1: (นี่คือ for-loop antipattern)

ใช่เพราะคุณไม่จำเป็นต้องค้นหารายการที่เก็บไว้ในระบบย่อยที่สามารถสืบค้นได้

ในนามธรรมระบบไฟล์เช่นฐานข้อมูลสามารถตอบกลับการสืบค้นได้ เมื่อมีปฏิสัมพันธ์กับระบบย่อยที่สืบค้นได้เรามีตัวเลือกพื้นฐานว่าเราต้องการให้ระบบย่อยดังกล่าวระบุเนื้อหาให้เราเพื่อที่เราจะทำการจับคู่นอกระบบย่อยหรือเพื่อใช้ความสามารถในการสืบค้นของระบบย่อย

มาทำเป็นช่วงเวลาที่คุณค้นหาระเบียนในฐานข้อมูลแทนไฟล์ในไดเรกทอรีในระบบไฟล์ คุณอยากเห็นไหม

SELECT * FROM SomeTable;

และจากนั้นในลูป (เช่นใน C #) บนเคอร์เซอร์ที่ส่งคืนเพื่อค้นหา ID = 100 หรือให้ระบบย่อยที่สืบค้นได้ทำในสิ่งที่ทำได้เพื่อค้นหาสิ่งที่คุณต้องการอย่างแท้จริง

SELECT * FROM SomeTable WHERE ID = 100;

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


คำถามที่ 2: เมื่อต้องการแก้ไขตัวอย่างที่สองคุณจะเขียนใหม่โดยไม่มีคำสั่ง if และล้อมรอบ StreamReader ด้วยบล็อก try-catch แทนและถ้ามันโยน FileNotFoundException คุณจัดการกับ catch catch ตามนั้นหรือไม่

ใช่เพราะนั่นเป็นเพียงวิธีที่ API นั้นทำงาน - มันไม่ใช่ทางเลือกของเราเพราะนี่เป็นฟังก์ชั่นห้องสมุด หากการตรวจสอบก่อนการโทรไม่มีค่าเพิ่มเติมเราต้องใช้การลอง / จับต่อไปเนื่องจาก (1) ข้อผิดพลาดอื่น ๆ ที่เกินกว่า FileNotFound สามารถเกิดขึ้นได้และ (2) สภาพการแข่งขัน


0

คำถามที่ 1:

เป็นเพราะการคืน StreamReader ใหม่ (ชื่อไฟล์); ภายในสำหรับวง? หรือความจริงที่ว่าคุณไม่ต้องการลูปสำหรับกรณีนี้

สตรีมไม่มีอะไรเกี่ยวข้องกับมัน รูปแบบการต่อต้านเกิดขึ้นเนื่องจากความขัดแย้งระหว่างเจตนาforeachและif:

วัตถุประสงค์ของการforeachคืออะไร?

ฉันคิดว่าคำตอบของคุณจะเป็นเช่น: "ฉันต้องการรันโค้ดบางส่วนซ้ำ ๆ "

คุณคาดว่าจะประมวลผลไฟล์กี่ไฟล์?

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

นี่คือการยืนยันโดยความจริงที่ว่าคุณจะคืนค่าทันที คุณไม่สนใจเกี่ยวกับนัดที่สองแม้ว่ามันจะมีอยู่จริง


มีสถานการณ์ที่นี่ไม่ใช่รูปแบบการต่อต้าน

  • หากคุณดูในไดเรกทอรีย่อย ( Directory.GetFiles(".", SearchOption.AllDirectories)) ก็เป็นไปได้ที่จะค้นหามากกว่าหนึ่งไฟล์ที่มีชื่อไฟล์เดียวกัน (รวมถึงนามสกุล)
  • หากคุณค้นหาชื่อไฟล์ที่ตรงกันบางส่วน (เช่นทุกไฟล์ที่ชื่อขึ้นต้นด้วย"Test_"หรือทุก"*.zip"ไฟล์

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


คำถามที่ 2:

ในการแก้ไขตัวอย่างที่สองคุณจะเขียนใหม่โดยไม่มีคำสั่ง if และแทนที่ StreamReader ด้วยบล็อก try-catch แทนและถ้ามันโยน FileNotFoundException คุณจัดการมันใน catch block ตามนั้นหรือไม่

ข้อยกเว้นมีราคาแพง พวกเขาไม่ควรใช้แทนตรรกะการไหลที่เหมาะสม มีข้อยกเว้นตามชื่อของพวกเขาแนะนำสถานการณ์พิเศษ

ifสำหรับเหตุผลที่คุณไม่ควรเอา

ตามคำตอบนี้ใน SoftwareEngineering.SE :

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

สรุปโดยย่อสำหรับสาเหตุโดยทั่วไปเป็นรูปแบบการต่อต้าน:

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

อ่านการสนทนาที่wiki ของ Wardเพื่อดูข้อมูลเชิงลึกเพิ่มเติม

ไม่ว่าคุณจะต้องการสรุปในการลอง / จับขึ้นอยู่กับสถานการณ์ของคุณ:

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

ไม่มีสิ่งใดเป็นเรื่องของ "ใช้งานได้เสมอ" เพื่อพิสูจน์ประเด็นของฉัน:

การศึกษาแยกได้พิสูจน์ว่าคุณมีโอกาสน้อยที่จะได้รับบาดเจ็บเมื่อคุณสวมหมวกนิรภัย, แว่นตานิรภัยและเสื้อกันกระสุน

เหตุใดเราจึงไม่สวมอุปกรณ์ความปลอดภัยนี้ตลอดเวลา

คำตอบง่ายๆคือเพราะมีข้อเสียในการสวมใส่:

  • ค่าใช้จ่ายเงิน
  • มันทำให้การเคลื่อนไหวของคุณยุ่งยากมากขึ้น
  • มันค่อนข้างอบอุ่นที่จะสวมใส่

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

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

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

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

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

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

อัปเดต - จากความคิดเห็นที่ฉันเขียนไปยังคำตอบอื่น ๆ เนื่องจากฉันคิดว่ามันเป็นข้อพิจารณาที่เกี่ยวข้องสำหรับคุณเช่นกัน:

มันบานพับมากในบริบทโดยรอบ

  • หากการเปิดตัวของการ StreamReader จะนำหน้าด้วยif(!File.Exists) File.Create()แล้วกรณีที่ไม่มีแฟ้มเมื่อเปิด StreamReader ย่อมเป็นพิเศษ
  • ถ้าชื่อไฟล์ที่ถูกเลือกจากรายชื่อของไฟล์ที่มีอยู่ขาดอย่างฉับพลันของมันเป็นอีกครั้งที่ได้รับการยกเว้น
  • หากคุณทำงานกับสตริงที่คุณยังไม่ได้ทดสอบกับไดเรกทอรีจริงๆ แล้วกรณีที่ไม่มีของไฟล์ที่เป็นผลเชิงตรรกะที่ดีเลิศและดังนั้นจึงไม่ได้รับการยกเว้น
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.