Làm cách nào để chỉ định điều kiện tiên quyết (LSP) trong giao diện trong C #?


11

Hãy nói rằng chúng ta có giao diện sau -

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

Điều kiện tiên quyết là ConnectionString phải được đặt / intialized trước khi bất kỳ phương thức nào có thể được chạy.

Điều kiện tiên quyết này có thể đạt được phần nào bằng cách chuyển một kết nốiString thông qua một hàm tạo nếu IDatabase là một lớp trừu tượng hoặc cụ thể -

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
}

Ngoài ra, chúng ta có thể tạo ConnectionString một tham số cho mỗi phương thức, nhưng có vẻ tệ hơn là chỉ tạo một lớp trừu tượng -

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

Câu hỏi -

  1. Có cách nào để xác định điều kiện tiên quyết này trong chính giao diện không? Đó là một "hợp đồng" hợp lệ, vì vậy tôi tự hỏi liệu có một tính năng hoặc mẫu ngôn ngữ nào cho việc này không (giải pháp lớp trừu tượng là một hack imo bên cạnh nhu cầu tạo hai loại - một giao diện và một lớp trừu tượng - mỗi lần cái này là cần thiết)
  2. Đây là một sự tò mò về mặt lý thuyết - Điều kiện tiên quyết này có thực sự rơi vào định nghĩa của điều kiện tiên quyết như trong bối cảnh của LSP không?

2
"LSP" các bạn đang nói về nguyên tắc thay thế Liskov? Nguyên tắc "nếu nó giống như con vịt nhưng cần pin thì nó không phải là con vịt"? Bởi vì như tôi thấy, việc vi phạm ISP và SRP thậm chí có thể là OCP nhưng không thực sự là LSP.
Sebastien

2
Để bạn biết, toàn bộ khái niệm "ConnectionString phải được đặt / intialized trước khi có thể chạy bất kỳ phương thức nào" là một ví dụ về blog ghép nối tạm thời.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling và nên tránh, nếu khả thi.
Richiban

Seemann thực sự là một fan hâm mộ lớn của Tóm tắt Factory.
Adrian Iftode

Câu trả lời:


10
  1. Đúng. Từ .Net 4.0 trở lên, Microsoft cung cấp Hợp đồng mã . Đây có thể được sử dụng để xác định các điều kiện tiên quyết trong mẫu Contract.Requires( ConnectionString != null );. Tuy nhiên, để làm cho giao diện này hoạt động, bạn vẫn sẽ cần một lớp trình trợ giúp IDatabaseContract, được gắn vào IDatabasevà điều kiện tiên quyết cần được xác định cho mọi phương thức riêng biệt của giao diện nơi nó sẽ giữ. Xem ở đây cho một ví dụ rộng rãi cho các giao diện.

  2. , LSP liên quan đến cả hai phần cú pháp và ngữ nghĩa của hợp đồng.


Tôi không nghĩ bạn có thể sử dụng Hợp đồng mã trong giao diện. Ví dụ bạn cung cấp cho thấy chúng đang được sử dụng trong các lớp. Các lớp thực hiện theo một giao diện, nhưng bản thân giao diện đó không chứa thông tin Hợp đồng mã (thật đáng xấu hổ. Đó sẽ là nơi lý tưởng để đặt nó).
Robert Harvey

1
@RobertHarvey: vâng, bạn đúng. Về mặt kỹ thuật, bạn cần một lớp thứ hai, tất nhiên, nhưng sau khi được xác định, hợp đồng sẽ tự động hoạt động cho mỗi lần thực hiện giao diện.
Doc Brown

21

Kết nối và truy vấn là hai mối quan tâm riêng biệt. Như vậy, chúng nên có hai giao diện riêng biệt.

interface IDatabaseConnection
{
    IDatabase Connect(string connectionString);
}

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

Điều này vừa đảm bảo IDatabasesẽ được kết nối khi sử dụng và khiến máy khách không phụ thuộc vào giao diện mà nó không cần.


Có thể rõ ràng hơn về "đây là mô hình thực thi các điều kiện tiên quyết thông qua các loại"
Caleth

@Caleth: đây không phải là "mô hình chung về thực thi các điều kiện tiên quyết". Đây là một giải pháp cho yêu cầu cụ thể này để đảm bảo kết nối xảy ra trước mọi thứ khác. Các điều kiện tiên quyết khác sẽ cần các giải pháp khác nhau (như giải pháp tôi đã đề cập trong câu trả lời của mình). Tôi muốn thêm vào yêu cầu này, tôi rõ ràng thích đề xuất của Euphoric hơn yêu cầu của tôi, vì nó đơn giản hơn nhiều và không cần thêm bất kỳ thành phần bên thứ ba nào.
Doc Brown

Việc mua lại cụ thể rằng một cái gì đó xảy ra trước khi một cái gì đó khác được áp dụng rộng rãi. Tôi cũng nghĩ rằng câu trả lời của bạn phù hợp hơn với câu hỏi này , nhưng câu trả lời này có thể được cải thiện
Caleth

1
Câu trả lời này hoàn toàn bỏ lỡ điểm. Các IDatabasegiao diện định nghĩa một đối tượng có khả năng thiết lập một kết nối đến một cơ sở dữ liệu và sau đó thực hiện các truy vấn tùy ý. Nó đối tượng đóng vai trò là ranh giới giữa cơ sở dữ liệu và phần còn lại của mã. Như vậy, đối tượng này phải duy trì trạng thái (như giao dịch) có thể ảnh hưởng đến hành vi của các truy vấn. Đưa chúng vào cùng một lớp là rất thực tế.
jpmc26

4
@ jpmc26 Không có sự phản đối nào của bạn có ý nghĩa, vì trạng thái có thể được duy trì trong lớp triển khai IDatabase. Nó cũng có thể tham chiếu lớp cha đã tạo ra nó, do đó có quyền truy cập vào toàn bộ trạng thái cơ sở dữ liệu.
Euphoric

5

Hãy lùi lại một bước và nhìn vào bức tranh lớn hơn ở đây.

Là gì IDatabase's trách nhiệm?

Nó có một vài hoạt động khác nhau:

  • Phân tích chuỗi kết nối
  • Mở kết nối với cơ sở dữ liệu (hệ thống bên ngoài)
  • Gửi tin nhắn đến cơ sở dữ liệu; các thông báo ra lệnh cho cơ sở dữ liệu thay đổi trạng thái của nó
  • Nhận phản hồi từ cơ sở dữ liệu và chuyển đổi chúng thành định dạng mà người gọi có thể sử dụng
  • Đóng kết nối

Nhìn vào danh sách này, bạn có thể nghĩ, "Điều này có vi phạm SRP không?" Nhưng tôi không nghĩ rằng nó làm. Tất cả các hoạt động là một phần của một khái niệm gắn kết, duy nhất: quản lý một kết nối trạng thái với cơ sở dữ liệu (một hệ thống bên ngoài) . Nó thiết lập kết nối, nó theo dõi trạng thái hiện tại của kết nối (đặc biệt là liên quan đến các hoạt động được thực hiện trên các kết nối khác), nó báo hiệu khi nào thực hiện trạng thái hiện tại của kết nối, v.v. Theo nghĩa này, nó hoạt động như một API ẩn nhiều chi tiết triển khai mà hầu hết người gọi sẽ không quan tâm. Ví dụ: nó có sử dụng HTTP, ổ cắm, đường ống, TCP, HTTPS tùy chỉnh không? Gọi mã không quan tâm; nó chỉ muốn gửi tin nhắn và nhận được phản hồi. Đây là một ví dụ tốt về đóng gói.

Chúng tôi có chắc không Chúng ta không thể tách ra một số các hoạt động này? Có thể, nhưng không có lợi ích. Nếu bạn cố tách chúng ra, bạn vẫn sẽ cần một đối tượng trung tâm giữ kết nối mở và / hoặc quản lý trạng thái hiện tại. Tất cả các hoạt động khác được kết hợp chặt chẽ với cùng một trạng thái và nếu bạn cố tách chúng ra, cuối cùng chúng sẽ ủy thác trở lại đối tượng kết nối. Các hoạt động này được kết hợp một cách tự nhiênhợp lý với nhà nước và không có cách nào để tách chúng ra. Decoupling là tuyệt vời khi chúng ta có thể làm điều đó, nhưng trong trường hợp này, chúng ta thực sự không thể. Ít nhất không phải không có một giao thức không trạng thái rất khác để nói chuyện với DB và điều đó thực sự sẽ khiến các vấn đề rất quan trọng như tuân thủ ACID trở nên khó khăn hơn nhiều. Ngoài ra, trong quá trình cố gắng tách các hoạt động này khỏi kết nối, bạn sẽ buộc phải tiết lộ chi tiết về giao thức mà người gọi không quan tâm, vì bạn sẽ cần một cách gửi một loại tin nhắn "tùy ý" đến cơ sở dữ liệu.

Lưu ý rằng thực tế chúng ta đang xử lý một giao thức trạng thái khá nghiêm ngặt quy định thay thế cuối cùng của bạn (truyền chuỗi kết nối dưới dạng tham số).

Chúng ta có thực sự cần chuỗi kết nối được thiết lập không?

Đúng. Bạn không thể mở kết nối cho đến khi bạn có chuỗi kết nối và bạn không thể làm gì với giao thức cho đến khi bạn mở kết nối. Vì vậy, thật vô nghĩa khi có một đối tượng kết nối mà không có một đối tượng.

Làm thế nào để chúng ta giải quyết vấn đề yêu cầu chuỗi kết nối?

Vấn đề chúng tôi đang cố gắng giải quyết là chúng tôi muốn đối tượng luôn ở trạng thái có thể sử dụng được. Loại thực thể nào được sử dụng để quản lý trạng thái trong các ngôn ngữ OO? Đối tượng , không phải giao diện. Giao diện không có nhà nước để quản lý. Bởi vì vấn đề bạn đang cố gắng giải quyết là vấn đề quản lý nhà nước, giao diện không thực sự phù hợp ở đây. Một lớp trừu tượng là tự nhiên hơn nhiều. Vì vậy, sử dụng một lớp trừu tượng với một nhà xây dựng.

Bạn cũng có thể muốn xem xét thực sự mở kết nối trong khi xây dựng, vì kết nối cũng vô dụng trước khi nó được mở. Điều đó sẽ yêu cầu một protected Openphương thức trừu tượng vì quá trình mở kết nối có thể là cơ sở dữ liệu cụ thể. Nó cũng là một ý tưởng tốt để làm cho thuộc ConnectionStringtính chỉ đọc trong trường hợp này, vì thay đổi chuỗi kết nối sau khi kết nối mở sẽ là vô nghĩa. (Thành thật mà nói, tôi sẽ làm cho nó chỉ đọc bằng mọi cách. Nếu bạn muốn kết nối với một chuỗi khác, hãy tạo một đối tượng khác.)

Chúng ta có cần một giao diện nào không?

Giao diện chỉ định các tin nhắn khả dụng bạn có thể gửi qua kết nối và các loại phản hồi bạn có thể nhận lại có thể hữu ích. Điều này sẽ cho phép chúng ta viết mã thực thi các hoạt động này nhưng không được kết hợp với logic mở kết nối. Nhưng đó là vấn đề: quản lý kết nối không phải là một phần của giao diện, "Tôi có thể gửi tin nhắn nào và tôi có thể lấy lại tin nhắn nào từ / cơ sở dữ liệu?", Vì vậy, chuỗi kết nối thậm chí không phải là một phần của điều đó giao diện.

Nếu chúng ta đi theo lộ trình này, mã của chúng ta có thể trông giống như thế này:

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
}

Sẽ đánh giá cao nếu downvoter sẽ giải thích lý do của họ không đồng ý.
jpmc26

Đồng ý, lại: downvoter. Đây là giải pháp chính xác. Chuỗi kết nối phải được cung cấp trong hàm tạo cho lớp cụ thể / trừu tượng. Công việc lộn xộn của việc mở / đóng kết nối không phải là mối quan tâm của mã sử dụng đối tượng này và nên duy trì nội bộ cho chính lớp đó. Tôi sẽ lập luận rằng Openphương pháp nên là privatevà bạn nên để lộ một thuộc tính được bảo vệ Connectiontạo ra kết nối và kết nối. Hoặc phơi bày một OpenConnectionphương pháp được bảo vệ .
Greg Burghardt

Giải pháp này khá thanh lịch và thiết kế rất tốt. Nhưng tôi nghĩ rằng một số lý do đằng sau các quyết định thiết kế là sai. Chủ yếu trong một vài đoạn đầu về SRP. Nó vi phạm SRP ngay cả khi được giải thích trong "Trách nhiệm của IDatabase là gì?". Trách nhiệm như đã thấy đối với SRP không chỉ là những thứ mà một lớp học hoặc quản lý. Nó cũng là "diễn viên" hoặc "lý do để thay đổi". Và tôi nghĩ rằng nó vi phạm SRP vì "Nhận phản hồi từ cơ sở dữ liệu và chuyển đổi chúng thành định dạng mà người gọi có thể sử dụng" có lý do rất khác để thay đổi so với "Phân tích chuỗi kết nối".
Sebastien

Tôi vẫn nêu lên điều này.
Sebastien

1
Và BTW, RẮN không phải là phúc âm. Chắc chắn rằng họ rất quan trọng để ghi nhớ khi thiết kế một giải pháp. Nhưng bạn CÓ THỂ vi phạm chúng nếu bạn biết TẠI SAO bạn làm điều đó, CÁCH NÀO sẽ ảnh hưởng đến giải pháp của bạn và CÁCH khắc phục mọi thứ bằng cách tái cấu trúc nếu nó khiến bạn gặp rắc rối. Vì vậy, tôi nghĩ ngay cả khi giải pháp được đề cập ở trên vi phạm SRP thì đây vẫn là giải pháp tốt nhất.
Sebastien

0

Tôi thực sự không thấy lý do để có một giao diện ở đây. Lớp cơ sở dữ liệu của bạn là dành riêng cho SQL và thực sự chỉ cung cấp cho bạn một cách thuận tiện / an toàn để đảm bảo bạn không truy vấn trên một kết nối không được mở đúng cách. Nếu bạn nhấn mạnh vào một giao diện, đây là cách tôi sẽ làm.

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();
    }
}

Việc sử dụng có thể trông như thế này:

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");
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.