วิธีสร้างและใช้งานอินเทอร์เฟซสำหรับการดำเนินการที่บางครั้งไม่พร้อมกันเท่านั้น

สมมติว่าฉันมีคลาสกว่า 100 คลาสที่ใช้อินเทอร์เฟซทั่วไปด้วยวิธี "คำนวณ" คลาสบางคลาสจะดำเนินการแบบอะซิงก์ (เช่น อ่านไฟล์) และคลาสอื่น ๆ ที่ใช้อินเทอร์เฟซเดียวกันจะรันโค้ดที่ซิงค์ (เช่น การเพิ่มตัวเลขสองตัว) วิธีที่ดีในการเขียนโค้ดนี้ เพื่อการบำรุงรักษาและเพื่อประสิทธิภาพคืออะไร

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

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

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

using System.IO;
using System.Threading.Tasks;

namespace TestApp
{
    interface IBlackBox
    {
        Task<string> PullText();
    }

    sealed class MyAsyncBlackBox : IBlackBox
    {
        public async Task<string> PullText()
        {
            using (var reader = File.OpenText("Words.txt"))
            {
                return await reader.ReadToEndAsync();
            }
        }
    }

    sealed class MyCachedBlackBox : IBlackBox
    {
        public Task<string> PullText()
        {
            return Task.Run(() => "hello world");
        }
    }
}

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


person cat_in_hat    schedule 01.04.2019    source แหล่งที่มา
comment
ฉันคิดว่าคุณมีความคิดที่ถูกต้องโดยทำให้อินเทอร์เฟซส่งคืนงาน แต่การใช้งานแบบซิงโครนัสของคุณไม่ถูกต้อง อย่าใช้ Task.Run(...) ให้ใช้ Task.FromResult(...); ไลบรารีของคุณไม่ควรใช้ Task.Run(...) เพราะนั่นจะใช้เธรดใหม่ (ค่อนข้างซับซ้อนกว่านั้น) แทนที่จะทำงานบนเธรดที่ดำเนินการอยู่ในปัจจุบัน   -  person Nelson    schedule 01.04.2019
comment
ทำไมคุณถึงคิดว่าคุณต้องมีคุณสมบัติบูลีนหนึ่งรายการเพื่อบอกผู้โทรว่าจะโทรด้วยวิธีใด   -  person Enigmativity    schedule 01.04.2019
comment
ฉันไม่ต้องการบูลีน นั่นเป็นแนวคิดหนึ่งที่จะหลีกเลี่ยงการรวมโค้ดไว้ใน Task   -  person cat_in_hat    schedule 01.04.2019
comment
ความคิดเห็นจาก @Nelson ดูเหมือนจะเป็นคำตอบที่ดีที่สุด   -  person cat_in_hat    schedule 02.04.2019


คำตอบ (3)


นี่เป็นสถานการณ์ทั่วไปกับ อินเทอร์เฟซ หากคุณมีสัญญาที่ต้องระบุ task สำหรับ Async Await Pattern และเราจำเป็นต้องติดตั้ง Task นั้นใน อินเทอร์เฟซ

สมมติว่าผู้โทรจะใช้ await คุณสามารถวาง async และส่งคืน Task ได้

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

การใช้งานทั่วไป

มาตรฐาน async

public async Task<string> PullText()
{
   using (var reader = File.OpenText("Words.txt"))
   {
      return await reader.ReadToEndAsync();
   }
}

ส่งคืน Task สำหรับงาน CPU ที่ผูกไว้ (บันทึกข้อยกเว้นและวางไว้บน Task)

public Task<string> PullText()
{
   try
   {
      return Task.Run(() => DoCpuWork());
   }
   catch (Exception e)
   {
      return Task.FromException<string>(e);
   }
}

มีประสิทธิภาพน้อยลงเล็กน้อยเนื่องจากเรากำลังวางท่อ IAsyncStateMachine

public async Task<string> PullText()
{
    return await Task.Run(() => DoCpuWork());
}

ส่งคืน เสร็จสมบูรณ์ Task ด้วยผลลัพธ์แบบง่าย (บันทึกข้อยกเว้นและวางไว้บน Task)

public Task<string> PullText()
{
   try
   {
      // simplified example
      return Task.FromResult("someString");
   }
   catch (Exception e)
   {
      return Task.FromException<string>(e);
   }
}

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

#pragma warning disable 1998
public async Task<string> PullText()()
#pragma warning restore 1998
{
    return Task.Run(() => "hello world");
}

และ

#pragma warning disable 1998
public async Task<string> PullText()()
#pragma warning restore 1998
{
    return Task.FromResult("someString");
}

หมายเหตุ การจัดการข้างต้นทั้งหมดด้วยการส่งคืน Task<T> จากเมธอด หากใครต้องการส่งคืน Task คุณสามารถใช้ประโยชน์จาก Task.CompletedTask; ด้วยซีแมนทิกส์ข้อผิดพลาดแบบเดียวกับข้างต้น

person TheGeneral    schedule 01.04.2019
comment
เห็นด้วยกับคำตอบนี้โดยทั่วไป แต่ไม่ใช่การใช้งาน Task.Run IMO Task.Run ควรใช้เท่าที่จำเป็นโดยโค้ด การบริโภค เท่านั้น กล่าวคือ เมื่อเรียกจากเธรด UI แต่ไม่ใช่จากเธรดพูลเธรด - person Stephen Cleary; 01.04.2019
comment
ขอบคุณ @StephenCleary ฉันซื้อหนังสือของคุณแล้ว! - person cat_in_hat; 23.10.2019

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

if(thisObject is IAscyncAble) {
  ... call the ansync request.
}
person alwayslearning    schedule 01.04.2019

ฉันลงเอยด้วยการใช้รหัสต่อไปนี้:

using System.IO;
using System.Threading.Tasks;

namespace TestApp
{
    interface IBlackBox // interface for both sync and async execution
    {
        Task<string> PullText();
    }

    sealed class MyAsyncBlackBox : IBlackBox
    {
        public async Task<string> PullText()
        {
            using (var reader = File.OpenText("Words.txt"))
            {
                return await reader.ReadToEndAsync();
            }
        }
    }

    sealed class MyCachedBlackBox : IBlackBox
    {
        public Task<string> PullText() // notice no 'async' keyword
        {
            return Task.FromResult("hello world");
        }
    }
}
person cat_in_hat    schedule 02.04.2019