Cofnijmy się o krok i spójrzmy na większy obraz tutaj.
Jaka jest IDatabase
odpowiedzialność?
Ma kilka różnych operacji:
- Analizuj parametry połączenia
- Otwórz połączenie z bazą danych (system zewnętrzny)
- Wysyłaj wiadomości do bazy danych; komunikaty nakazują bazie danych zmienić jej stan
- Otrzymuj odpowiedzi z bazy danych i przekształcaj je w format, z którego może korzystać osoba dzwoniąca
- Zamknij połączenie
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, aby ConnectionString
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:
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
}