ไม่ทำให้สมาชิกสาธารณะเสมือนหรือเป็นนามธรรม - จริงเหรอ?


20

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

ตัวอย่างเช่นเขาถือว่าคลาสเช่นนี้ไม่ได้ออกแบบมาอย่างดี:

public abstract class PublicAbstractOrVirtual
{
  public abstract void Method1(string argument);

  public virtual void Method2(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    // default implementation
  }
}

เขากล่าวว่า

  • นักพัฒนาของคลาสที่ได้รับที่ใช้Method1และการแทนที่Method2มีการทำซ้ำการตรวจสอบอาร์กิวเมนต์
  • ในกรณีที่ผู้พัฒนาคลาสพื้นฐานตัดสินใจที่จะเพิ่มบางสิ่งรอบ ๆ ส่วนที่กำหนดเองได้Method1หรือMethod2ใหม่กว่าเขาจะไม่สามารถทำได้

เพื่อนร่วมงานของฉันเสนอวิธีการนี้แทน:

public abstract class ProtectedAbstractOrVirtual
{
  public void Method1(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    this.Method1Core(argument);
  }

  public void Method2(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    this.Method2Core(argument);
  }

  protected abstract void Method1Core(string argument);

  protected virtual void Method2Core(string argument)
  {
    // default implementation
  }
}

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

แต่ฉันไม่เห็นว่านี่เป็นแนวทางการออกแบบ แม้แต่ Microsoft ก็ไม่ทำตามมันแค่ดูที่Streamคลาสเพื่อตรวจสอบสิ่งนี้

คุณคิดอย่างไรกับแนวทางนี้ มันสมเหตุสมผลหรือไม่หรือคุณคิดว่ามันซับซ้อนเกินไป API?


5
ทำให้วิธีการvirtual ช่วยให้เอาชนะทางเลือก วิธีการของคุณอาจเป็นแบบสาธารณะเนื่องจากอาจไม่ถูกแทนที่ ทำให้วิธีการabstract บังคับให้คุณแทนที่พวกเขา; พวกเขาควรจะเป็นprotectedเพราะพวกเขาไม่ได้มีประโยชน์โดยเฉพาะอย่างยิ่งในpublicบริบท
Robert Harvey

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

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

8
@GregBurghardt: เสียงเหมือนเพื่อนร่วมงานของ OP ที่แสดงให้เห็นเสมอที่จะใช้รูปแบบวิธีการเทมเพลตโดยไม่คำนึงถึงหากมีความจำเป็นหรือไม่ นั่นเป็นรูปแบบที่มากเกินไปโดยทั่วไป - หากมีค้อนไม่ช้าก็เร็วปัญหาทุกอย่างจะเริ่มดูเหมือนเล็บ ;-)
Doc Brown

3
@PeterPerot: ฉันไม่เคยมีปัญหาในการเริ่มต้นด้วย DTO แบบง่าย ๆ ที่มีเพียงฟิลด์สาธารณะและเมื่อ DTO ดังกล่าวกลายเป็นที่ต้องการสมาชิกที่มีตรรกะทางธุรกิจ สิ่งที่แตกต่างอย่างแน่นอนเมื่อทำงานเป็นผู้ขายห้องสมุดและต้องดูแลไม่เปลี่ยน API สาธารณะแล้วแม้แต่เปลี่ยนเขตข้อมูลสาธารณะเป็นทรัพย์สินสาธารณะที่มีชื่อเดียวกันอาจทำให้เกิดปัญหา
Doc Brown

คำตอบ:


30

คำพูด

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

กำลังผสมสาเหตุและผลกระทบ มันทำให้สมมติฐานที่ว่าทุกวิธีการ overrideable ต้องมีการตรวจสอบอาร์กิวเมนต์ที่ไม่สามารถปรับแต่งได้ แต่มันค่อนข้างเป็นไปในทางตรงกันข้าม:

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

แต่มีจำนวนมากตัวอย่างที่มันทำให้รู้สึกดีที่จะมีวิธีการที่ประชาชนเสมือนเนื่องจากไม่มีคงไม่ใช่การปรับแต่งส่วน: ดูที่วิธีการมาตรฐานเหมือนToStringหรือEqualsหรือGetHashCode- มันจะปรับปรุงการออกแบบของobjectระดับที่จะมีเหล่านี้ไม่ได้เป็นของประชาชนและ เสมือนจริงในเวลาเดียวกัน? ฉันไม่คิดอย่างนั้น

หรือในแง่ของรหัสของคุณเอง: เมื่อรหัสในคลาสฐานสุดท้ายและจงใจมีลักษณะเช่นนี้

 public void Method1(string argument)
 {
    // nothing to validate here, all strings including null allowed
    this.Method1Core(argument);
 }

มีการแยกระหว่างนี้Method1และMethod1Coreสิ่งที่ซับซ้อนเท่านั้นโดยไม่มีเหตุผลชัดเจน


1
ในกรณีของวิธีการที่ไมโครซอฟท์ได้ทำดีกว่าก็ไม่ใช่เสมือนและแนะนำวิธีการที่แม่แบบเสมือนToString() ToStringCore()เหตุผล: เพราะเหตุนี้: ToString()- หมายเหตุผู้รับมรดก พวกเขาระบุว่าToString()ไม่ควรกลับเป็นโมฆะ ToString() => ToStringCore() ?? string.Emptyพวกเขาจะได้ข่มขู่ความต้องการนี้โดยการดำเนินการ
Peter Perot

9
@PeterPerot: แนวทางที่คุณเชื่อมโยงไปยังแนะนำไม่ให้กลับมาstring.Emptyคุณสังเกตเห็น? และแนะนำสิ่งอื่น ๆ อีกมากมายที่ไม่สามารถบังคับใช้ในโค้ดโดยแนะนำToStringCoreวิธีการเช่นนั้น ToStringดังนั้นเทคนิคนี้อาจจะไม่ได้เป็นเครื่องมือที่เหมาะสมสำหรับ
Doc Brown

3
@Theraot: แน่นอนว่าเราสามารถหาเหตุผลหรือข้อโต้แย้งว่าทำไม ToString และ Equals หรือ GetHashcode อาจได้รับการออกแบบที่แตกต่างกัน แต่วันนี้พวกเขาเป็นอย่างที่พวกเขาเป็น (อย่างน้อยฉันคิดว่าการออกแบบของพวกเขาดีพอสำหรับเป็นตัวอย่างที่ดี)
Doc Brown

1
@PeterPerot "ฉันเห็นรหัสเป็นจำนวนมากเช่นanyObjectIGot.ToString()แทนที่จะเป็นanyObjectIGot?.ToString()" - สิ่งนี้เกี่ยวข้องกันอย่างไร ToStringCore()วิธีการของคุณจะป้องกันไม่ให้มีการส่งคืนสตริง null แต่ก็ยังคงส่งNullReferenceExceptionหากวัตถุเป็นโมฆะ
IMil

1
@ PeterPerot ฉันไม่ได้พยายามที่จะถ่ายทอดการโต้แย้งจากผู้มีอำนาจ ไม่มากที่ Microsoft ใช้public virtualแต่มีหลายกรณีที่public virtualใช้ได้ เราสามารถโต้เถียงสำหรับชิ้นส่วนที่ไม่สามารถปรับแต่งได้ที่ว่างเปล่าว่าเป็นข้อพิสูจน์รหัสในอนาคต ... แต่นั่นไม่ได้ผล การย้อนกลับและเปลี่ยนแปลงมันอาจทำลายประเภทที่ได้รับมา ดังนั้นจึงไม่ได้รับอะไรเลย
ธีระโรจน์

6

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

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

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

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


1
Doวิธีคำนำหน้าเป็นทางเลือกหนึ่ง Microsoft มักใช้postfix วิธีการหลัก
Peter Perot

@Peter Perot ฉันไม่เคยเห็นคำนำหน้า Core ในเนื้อหาใด ๆ ของ Microsoft แต่อาจเป็นเพราะฉันไม่ได้ให้ความสนใจมากในช่วงนี้ ฉันสงสัยว่าพวกเขาเริ่มทำสิ่งนี้เมื่อไม่นานมานี้เพื่อโปรโมต Core moniker เพื่อสร้างชื่อให้กับ. NET Core
Martin Maat

ไม่เป็นหมวกอันเก่า: BindingList. นอกจากนี้ฉันพบข้อเสนอแนะที่ไหนสักแห่งบางทีแนวทางการออกแบบกรอบงานหรือคล้ายกัน และเป็นความpostfix ;-)
Peter Perot

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

2

ใน C ++ สิ่งนี้เรียกว่าnon-virtual interface pattern (NVI) (กาลครั้งหนึ่งนานมานี้เรียกว่าวิธีเทมเพลตซึ่งทำให้สับสน แต่บทความเก่า ๆ บางฉบับมีคำศัพท์นั้น) NVI ได้รับการส่งเสริมโดย Herb Sutter ผู้เขียนเกี่ยวกับเรื่องนี้อย่างน้อยสองสามครั้ง ผมคิดว่าคนแรกคือที่นี่

ถ้าผมจำได้อย่างถูกต้องหลักฐานเป็นที่เรียนมาไม่ควรเปลี่ยนสิ่งที่ชั้นฐานไม่ แต่วิธีการมันไม่ได้

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

ในตัวอย่างง่ายๆสิ่งนี้มักทำให้เดือดร้อนจากการย้ายสาธารณะที่เพิ่งมอบหมายงานทั้งหมดให้กับ ReallyDoTheMove เสมือนส่วนตัวดังนั้นดูเหมือนว่าจะไม่มีค่าใช้จ่ายมากนัก

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

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

ฉันไม่ทราบถึงความแตกต่างระหว่าง C # และ C ++ ที่จะเปลี่ยนมุมมองของการออกแบบคลาสนี้


หาดี! ตอนนี้ฉันจำได้ว่าฉันพบว่าโพสต์ลิงก์ที่ 2 ของคุณชี้ไปที่ (หรือสำเนา) ย้อนกลับไปในยุค 2000 ฉันจำได้ว่าฉันกำลังค้นหาหลักฐานเพิ่มเติมเกี่ยวกับการอ้างสิทธิ์ของเพื่อนร่วมงานและฉันไม่พบสิ่งใดในบริบท C # แต่เป็นภาษา C ++ นี้. คือ. มัน! :-) แต่กลับมาใน C # land ดูเหมือนว่ารูปแบบนี้ไม่ค่อยได้ใช้เท่าไหร่ บางทีคนอาจจะรู้ว่าการเพิ่มฟังก์ชั่นพื้นฐานในภายหลังอาจทำลายคลาสที่ได้รับเช่นกันและการใช้ TMP หรือ NVIP อย่างเข้มงวดแทนวิธีเสมือนสาธารณะไม่สมเหตุสมผล
Peter Perot

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