จะระบุ precondition (LSP) ในอินเตอร์เฟสใน C # ได้อย่างไร?


11

สมมติว่าเรามีส่วนต่อไปนี้ -

interface IDatabase { 
    string ConnectionString{get;set;}
    void ExecuteNoQuery(string sql);
    void ExecuteNoQuery(string[] sql);
    //Various other methods all requiring ConnectionString to be set
}

เงื่อนไขก่อนกำหนดคือ ConnectionString จะต้องตั้งค่า / intialized ก่อนที่จะสามารถเรียกใช้วิธีการใด ๆ

เงื่อนไขนี้สามารถทำได้ค่อนข้างโดยผ่านการเชื่อมต่อสายผ่านตัวสร้างถ้า IDatabase เป็นระดับนามธรรมหรือคอนกรีต -

abstract class Database { 
    public string ConnectionString{get;set;}
    public Database(string connectionString){ ConnectionString = connectionString;}

    public void ExecuteNoQuery(string sql);
    public void ExecuteNoQuery(string[] sql);
    //Various other methods all requiring ConnectionString to be set
}

อีกทางเลือกหนึ่งเราสามารถสร้างการเชื่อมต่อพารามิเตอร์สำหรับแต่ละวิธี แต่มันดูแย่กว่าแค่การสร้างระดับนามธรรม -

interface IDatabase { 
    void ExecuteNoQuery(string connectionString, string sql);
    void ExecuteNoQuery(string connectionString, string[] sql);
    //Various other methods all with the connectionString parameter
}

คำถาม -

  1. มีวิธีการระบุสิ่งที่จำเป็นต้องมีในอินเทอร์เฟซของตัวเองหรือไม่? มันเป็น "สัญญา" ที่ถูกต้องดังนั้นฉันจึงสงสัยว่ามีคุณสมบัติภาษาหรือรูปแบบสำหรับสิ่งนี้หรือไม่ (การแก้ปัญหาในระดับนามธรรมเป็นมากกว่าการแฮ็ก IMO นอกเหนือจากความต้องการในการสร้างสองประเภท - อินเตอร์เฟซและคลาสนามธรรม - ทุกครั้ง สิ่งนี้จำเป็น)
  2. นี่เป็นมากกว่าความอยากรู้อยากเห็นทางทฤษฎี - เงื่อนไขนี้จริง ๆ แล้วตกอยู่ในคำนิยามของเงื่อนไขก่อนหน้านี้ในบริบทของ LSP หรือไม่?

2
โดย "LSP" พวกคุณกำลังพูดถึงหลักการทดแทน Liskov? "ถ้ามันต้มตุ๋นเหมือนเป็ด แต่ต้องการแบตเตอรี่มันไม่ใช่เป็ด" หลักการ? เพราะอย่างที่ฉันเห็นมันเป็นการละเมิด ISP และ SRP มากกว่าเดิมแม้กระทั่ง OCP แต่ไม่ใช่ LSP จริงๆ
เซบาสเตียน

2
เพียงเพื่อให้คุณทราบแนวคิดทั้งหมดของ "ConnectionString จะต้องตั้งค่า / intialized ก่อนที่วิธีการใด ๆ ที่สามารถเรียกใช้" เป็นตัวอย่างของการมีเพศสัมพันธ์ชั่วคราวblog.ploeh.dk/2011/05/24/DesignSmellTemporalCouplingและควรหลีกเลี่ยงหาก เป็นไปได้
Richiban

Seemann เป็นแฟนตัวยงของ Abstract Factory จริงๆ
Adrian Iftode

คำตอบ:


10
  1. ใช่. จากสุทธิ 4.0 ขึ้นไปข้างบนไมโครซอฟท์ให้สัญญารหัส Contract.Requires( ConnectionString != null );เหล่านี้สามารถนำมาใช้ในการกำหนดปัจจัยพื้นฐานในรูปแบบ อย่างไรก็ตามในการทำให้งานนี้เป็นอินเทอร์เฟซคุณจะยังคงต้องการคลาสตัวช่วยIDatabaseContractซึ่งติดอยู่IDatabaseและจำเป็นต้องกำหนดเงื่อนไขเบื้องต้นสำหรับแต่ละเมธอดของอินเตอร์เฟสของคุณที่จะเก็บ ดูที่นี่สำหรับตัวอย่างมากมายสำหรับอินเตอร์เฟส

  2. ใช่ LSP เกี่ยวข้องกับส่วนของประโยคและความหมายของสัญญา


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

1
@ RobertHarvey: ใช่คุณพูดถูก ในทางเทคนิคแล้วคุณต้องมีชั้นที่สองแน่นอน แต่เมื่อกำหนดไว้แล้วสัญญาจะทำงานโดยอัตโนมัติสำหรับการใช้งานอินเทอร์เฟซทุกครั้ง
Doc Brown

21

การเชื่อมต่อและสอบถามเป็นสองข้อกังวลแยกกัน ดังนั้นควรมีสองอินเตอร์เฟสแยกกัน

interface IDatabaseConnection
{
    IDatabase Connect(string connectionString);
}

interface IDatabase
{
    public void ExecuteNoQuery(string sql);
    public void ExecuteNoQuery(string[] sql);
}

ทั้งสองนี้ทำให้มั่นใจได้ว่าIDatabaseจะเชื่อมต่อเมื่อใช้และทำให้ลูกค้าไม่ได้ขึ้นอยู่กับอินเตอร์เฟสที่ไม่ต้องการ


อาจมีความชัดเจนมากขึ้นเกี่ยวกับ "นี่เป็นรูปแบบของการบังคับใช้เงื่อนไขเบื้องต้นผ่านประเภท"
Caleth

@Caleth: นี่ไม่ใช่ "รูปแบบทั่วไปของการบังคับใช้เงื่อนไขเบื้องต้น" นี่เป็นวิธีแก้ปัญหาสำหรับข้อกำหนดเฉพาะนี้เพื่อให้แน่ใจว่าการเชื่อมต่อจะเกิดขึ้นก่อนสิ่งอื่นใด เงื่อนไขอื่น ๆ จะต้องใช้วิธีแก้ไขปัญหาที่แตกต่างกัน (เช่นที่ฉันกล่าวถึงในคำตอบของฉัน) ฉันต้องการเพิ่มสำหรับข้อกำหนดนี้ฉันต้องการคำแนะนำของ Euphoric อย่างชัดเจนเพราะมันง่ายกว่ามากและไม่ต้องการองค์ประกอบของบุคคลที่สามเพิ่มเติม
Doc Brown

requrement เฉพาะที่บางสิ่งบางอย่างเกิดขึ้นก่อนสิ่งอื่นเป็นอย่างกว้างขวางใช้ ฉันคิดว่าคำตอบของคุณเหมาะกับคำถามนี้ดีกว่าแต่คำตอบนี้สามารถปรับปรุงได้
Caleth

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

4
@ jpmc26 ไม่มีข้อคัดค้านใด ๆ ของคุณที่เหมาะสมเนื่องจากสามารถรักษาสถานะไว้ในคลาสที่ใช้งาน IDatabase ได้ นอกจากนี้ยังสามารถอ้างอิงคลาสพาเรนต์ที่สร้างขึ้นซึ่งทำให้สามารถเข้าถึงสถานะฐานข้อมูลทั้งหมดได้
ร่าเริง

5

ลองย้อนกลับไปดูภาพใหญ่ที่นี่

อะไรคือIDatabaseความรับผิดชอบ 's?

มันมีการดำเนินการที่แตกต่างกันไม่กี่:

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

เมื่อดูรายการนี้คุณอาจกำลังคิดว่า "นี่ไม่ได้ละเมิด SRP หรือไม่" แต่ฉันไม่คิดว่ามันจะทำ ทั้งหมดของการดำเนินงานเป็นส่วนหนึ่งของเดี่ยวแนวคิดเหนียว: การจัดการการเชื่อมต่อ stateful ฐานข้อมูล (ระบบภายนอก) มันสร้างการเชื่อมต่อมันติดตามสถานะปัจจุบันของการเชื่อมต่อ (โดยเฉพาะอย่างยิ่งในการดำเนินการกับการเชื่อมต่ออื่น ๆ ) มันส่งสัญญาณเมื่อมีการกระทำสถานะปัจจุบันของการเชื่อมต่อ ฯลฯ ในแง่นี้มันทำหน้าที่เป็น API ที่ซ่อนรายละเอียดการใช้งานจำนวนมากที่ผู้โทรส่วนใหญ่ไม่สนใจ ตัวอย่างเช่นใช้ HTTP, ซ็อกเก็ต, ไพพ์, TCP แบบกำหนดเอง, HTTPS หรือไม่ รหัสการโทรไม่สนใจ มันแค่ต้องการส่งข้อความและรับการตอบกลับ นี่เป็นตัวอย่างที่ดีของการห่อหุ้ม

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

โปรดทราบว่าความจริงที่เรากำลังเผชิญกับโปรโตคอล stateful สวยออกกฎทางเลือกสุดท้ายของคุณ (ผ่านสตริงการเชื่อมต่อเป็นพารามิเตอร์)

เราต้องการตั้งค่าสตริงการเชื่อมต่อหรือไม่?

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

เราจะแก้ปัญหาในการกำหนดสตริงการเชื่อมต่อได้อย่างไร?

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

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

เราต้องการอินเทอร์เฟซเลยหรือไม่?

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

หากเราไปเส้นทางนี้รหัสของเราอาจมีลักษณะเช่นนี้:

interface IDatabase {
    void ExecuteNoQuery(string sql);
    void ExecuteNoQuery(string[] sql);
    //Various other methods all requiring ConnectionString to be set
}

abstract class ConnectionStringDatabase : IDatabase { 

    public string ConnectionString { get; }

    public Database(string connectionString) {
        this.ConnectionString = connectionString;
        this.Open();
    }

    protected abstract void Open();

    public abstract void ExecuteNoQuery(string sql);
    public abstract void ExecuteNoQuery(string[] sql);
    //Various other methods all requiring ConnectionString to be set
}

จะขอบคุณถ้าผู้ลงคะแนนจะอธิบายเหตุผลของพวกเขาสำหรับการไม่เห็นด้วย
jpmc26

ตกลงกันอีกครั้ง: ผู้ลงคะแนน นี่คือทางออกที่ถูกต้อง สตริงการเชื่อมต่อควรมีอยู่ในตัวสร้างไปยังคลาสคอนกรีต / นามธรรม ธุรกิจที่ยุ่งเหยิงในการเปิด / ปิดการเชื่อมต่อนั้นไม่ได้เป็นเรื่องของรหัสที่ใช้วัตถุนี้และควรจะยังคงอยู่ภายในคลาสนั้น ฉันจะยืนยันว่าOpenวิธีการควรเป็นprivateและคุณควรเปิดเผยConnectionคุณสมบัติที่มีการป้องกันที่สร้างการเชื่อมต่อและการเชื่อมต่อ หรือเปิดเผยOpenConnectionวิธีการป้องกัน
เกร็ก Burghardt

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

ยังฉันโหวตขึ้นนี้
เซบาสเตียน

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

0

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

public interface IDatabase : IDisposable
{
    string ConnectionString { get; }
    void ExecuteNoQuery(string sql);
    void ExecuteNoQuery(string[] sql);
    //Various other methods all requiring ConnectionString to be set
}

public class SqlDatabase : IDatabase
{
    public string ConnectionString { get; }
    SqlConnection sqlConnection;
    SqlTransaction sqlTransaction; // optional

    public SqlDatabase(string connectionStr)
    {
        if (String.IsNullOrEmpty(connectionStr)) throw new ArgumentException("connectionStr empty");
        ConnectionString = connectionStr;
        instantiateSqlProps();
    }

    private void instantiateSqlProps()
    {
        sqlConnection.Open();
        sqlTransaction = sqlConnection.BeginTransaction();
    }

    public void ExecuteNoQuery(string sql) { /*run query*/ }
    public void ExecuteNoQuery(string[] sql) { /*run query*/ }

    public void Dispose()
    {
        sqlTransaction.Commit();
        sqlConnection.Dispose();
    }

    public void Commit()
    {
        Dispose();
        instantiateSqlProps();
    }
}

การใช้งานอาจมีลักษณะเช่นนี้:

using (IDatabase dbase = new SqlDatabase("Data Source = servername; Initial Catalog = MyDb; Integrated Security = True"))
{
    dbase.ExecuteNoQuery("delete from dbo.Invoices");
    dbase.ExecuteNoQuery("delete from dbo.Customers");
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.