Powiedzmy, że mamy następujący interfejs -
interface IDatabase {
string ConnectionString{get;set;}
void ExecuteNoQuery(string sql);
void ExecuteNoQuery(string[] sql);
//Various other methods all requiring ConnectionString to be set
}
Warunkiem jest ustawienie / zainicjowanie ConnectionString przed uruchomieniem dowolnej metody.
Ten warunek można w pewnym stopniu osiągnąć, przekazując połączenieString za pośrednictwem konstruktora, jeśli baza danych IDatabela była klasą abstrakcyjną lub konkretną -
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
}
Alternatywnie możemy utworzyć parametr connectionString dla każdej metody, ale wygląda to gorzej niż tylko tworzenie klasy abstrakcyjnej -
interface IDatabase {
void ExecuteNoQuery(string connectionString, string sql);
void ExecuteNoQuery(string connectionString, string[] sql);
//Various other methods all with the connectionString parameter
}
Pytania -
- Czy istnieje sposób na określenie tego warunku wstępnego w samym interfejsie? Jest to ważna „umowa”, więc zastanawiam się, czy jest w tym jakaś cecha językowa lub wzorzec (rozwiązanie klasy abstrakcyjnej to raczej hack imo oprócz potrzeby tworzenia dwóch typów - interfejsu i klasy abstrakcyjnej - za każdym razem to jest potrzebne)
- Jest to bardziej teoretyczna ciekawość - czy ten warunek wstępny rzeczywiście mieści się w definicji warunku wstępnego, jak w kontekście LSP?
c#
solid
liskov-substitution
Achilles
źródło
źródło
Odpowiedzi:
Tak. Począwszy od .Net 4.0 w górę, Microsoft zapewnia umowy na kod . Można ich użyć do zdefiniowania warunków wstępnych w formularzu
Contract.Requires( ConnectionString != null );
. Jednak, aby zadziałało to w przypadku interfejsu, nadal będziesz potrzebować klasy pomocniczejIDatabaseContract
, do której się przyłączyIDatabase
, i konieczne jest zdefiniowanie warunków wstępnych dla każdej metody interfejsu, w której będzie on przechowywany. Zobacz tutaj obszerny przykład interfejsów.Tak , LSP zajmuje się zarówno składniowymi, jak i semantycznymi częściami kontraktu.
źródło
Łączenie i zapytania to dwie osobne kwestie. Jako takie powinny mieć dwa oddzielne interfejsy.
Zapewnia to
IDatabase
połączenie, gdy zostanie użyte, i sprawia, że klient nie zależy od interfejsu, którego nie potrzebuje.źródło
IDatabase
Interfejs definiuje obiekt zdolny do nawiązywania połączenia z bazą danych, a następnie wykonywanie dowolnych zapytań. Jest to obiekt, który działa jako granica między bazą danych a resztą kodu. Jako taki, obiekt ten musi utrzymywać stan (taki jak transakcja), który może wpływać na zachowanie zapytań. Umieszczenie ich w tej samej klasie jest bardzo praktyczne.Cofnijmy się o krok i spójrzmy na większy obraz tutaj.
Jaka jest
IDatabase
odpowiedzialność?Ma kilka różnych operacji:
Patrząc na tę listę, możesz pomyśleć: „Czy to nie narusza SRP?” Ale nie sądzę, że tak. Wszystkie operacje są częścią jednej, spójnej koncepcji: zarządzaj stanowym połączeniem z bazą danych (system zewnętrzny) . Ustanawia połączenie, śledzi bieżący stan połączenia (w szczególności w odniesieniu do operacji wykonywanych na innych połączeniach), sygnalizuje, kiedy zatwierdzić bieżący stan połączenia itp. W tym sensie działa jako interfejs API który ukrywa wiele szczegółów implementacji, o które większość rozmówców nie będzie się troszczyć. Na przykład, czy używa HTTP, gniazd, potoków, niestandardowego TCP, HTTPS? Wywołanie kodu nie ma znaczenia; chce tylko wysyłać wiadomości i uzyskiwać odpowiedzi. To dobry przykład enkapsulacji.
Jesteśmy pewni? Czy nie możemy podzielić niektórych z tych operacji? Może, ale nie ma korzyści. Jeśli spróbujesz je rozdzielić, nadal będziesz potrzebować centralnego obiektu, który utrzymuje połączenie otwarte i / lub zarządza obecnym stanem. Wszystkie pozostałe operacje są silnie sprzężone z tym samym stanem, a jeśli spróbujesz je rozdzielić, w końcu i tak delegują się z powrotem do obiektu połączenia. Operacje te są naturalnie i logicznie powiązane ze stanem i nie ma sposobu, aby je rozdzielić. Oddzielenie jest świetne, kiedy możemy to zrobić, ale w tym przypadku tak naprawdę nie możemy. Przynajmniej nie bez bardzo odmiennego, bezstanowego protokołu do rozmowy z DB, co w rzeczywistości znacznie utrudniłoby bardzo ważne problemy, takie jak zgodność z ACID. Ponadto, w trakcie próby oddzielenia tych operacji od połączenia, będziesz zmuszony ujawnić szczegółowe informacje na temat protokołu, na który nie zwracają uwagi osoby dzwoniące, ponieważ będziesz potrzebować sposobu wysłania pewnego rodzaju „arbitralnej” wiadomości do bazy danych.
Zauważ, że fakt, że mamy do czynienia z protokołem stanowym, całkiem solidnie wyklucza twoją ostatnią alternatywę (przekazanie ciągu połączenia jako parametru).
Czy naprawdę potrzebujemy ustawić parametry połączenia?
Tak. Nie możesz otworzyć połączenia, dopóki nie masz ciągu połączenia i nie możesz nic zrobić z protokołem, dopóki nie otworzysz połączenia. Nie ma więc sensu mieć obiektu połączenia bez niego.
Jak rozwiązać problem wymaganego ciągu połączenia?
Problem, który próbujemy rozwiązać, polega na tym, że chcemy, aby obiekt znajdował się w użytecznym stanie przez cały czas. Jakiego rodzaju podmiotu używa się do zarządzania stanem w językach OO? Obiekty , a nie interfejsy. Interfejsy nie mają stanu do zarządzania. Ponieważ problem, który próbujesz rozwiązać, to problem zarządzania stanem, interfejs nie jest tutaj naprawdę odpowiedni. Klasa abstrakcyjna jest znacznie bardziej naturalna. Użyj więc abstrakcyjnej klasy z konstruktorem.
Możesz także rozważyć otwarcie połączenia również podczas konstruktora, ponieważ połączenie jest również bezużyteczne przed jego otwarciem. Wymagałoby to abstrakcyjnej
protected Open
metody, ponieważ proces otwierania połączenia może być specyficzny dla bazy danych. Byłoby również dobrym pomysłem, abyConnectionString
właściwość była odczytywana tylko w tym przypadku, ponieważ zmiana ciągu połączenia po otwarciu połączenia byłaby bez znaczenia. (Szczerze mówiąc, i tak uczynię to tylko do odczytu. Jeśli chcesz połączenie z innym ciągiem, stwórz inny obiekt.)Czy w ogóle potrzebujemy interfejsu?
Przydatny może być interfejs, który określa dostępne wiadomości, które można wysyłać przez połączenie, oraz typy odpowiedzi, które można uzyskać. Pozwoliłoby nam to napisać kod, który wykonuje te operacje, ale nie jest powiązany z logiką otwierania połączenia. Ale o to chodzi: zarządzanie połączeniem nie jest częścią interfejsu „Jakie wiadomości mogę wysłać i jakie wiadomości mogę odzyskać do / z bazy danych?”, Więc ciąg połączenia nie powinien nawet być częścią tego berło.
Jeśli pójdziemy tą drogą, nasz kod może wyglądać mniej więcej tak:
źródło
Open
metoda powinna byćprivate
i należy ujawnić chronionąConnection
właściwość, która tworzy połączenie i łączy się. Lub ujawnij chronionąOpenConnection
metodę.Naprawdę nie widzę powodu, by mieć tutaj interfejs. Twoja klasa bazy danych jest specyficzna dla SQL i naprawdę zapewnia wygodny / bezpieczny sposób upewnienia się, że nie pytasz o połączenie, które nie zostało poprawnie otwarte. Jeśli jednak nalegasz na interfejs, oto jak bym to zrobił.
Użycie może wyglądać następująco:
źródło