2010-01-26 16 views
9

Pracuję nad klasą, która obsługuje interakcję ze zdalnym procesem, który może ale nie musi być dostępny; w rzeczywistości w większości przypadków tak nie będzie. Jeśli nie jest, obiekt tej klasy nie ma celu w życiu i musi odejść.Konfiguracja połączenia sieciowego w konstruktorze: dobry czy zły?

Czy mniej brzydki:

  1. Uchwyt konfiguracji połączenia w konstruktorze, rzuca wyjątek, jeśli proces nie ma.
  2. Ustaw konfigurację połączenia w oddzielnej metodzie connect(), zwracając kod błędu, jeśli procesu nie ma.

W opcji 1) kod wywołujący będzie oczywiście musiał zawinąć jego instancję tej klasy i wszystko inne, co zajmuje się nią w bloku try(). W opcji 2 może po prostu sprawdzić wartość zwracaną przez connect() i zwrócić (zniszczyć obiekt), jeśli się nie udało, ale jest to mniej zgodne z RAII,

Podobnie, jeśli mam opcję 1), czy jest to lepiej rzucić jedną z klas std :: exception, wyprowadzić z niej swoją własną klasę wyjątków, przetasować własną podrzędną klasę wyjątków lub po prostu rzucić ciąg? Chciałbym podać pewne wskazanie awarii, która wydaje się wykluczać pierwszą z nich.

Edytowany w celu wyjaśnienia: Zdalny proces jest na tym samym komputerze, więc jest mało prawdopodobne, że zablokuje się połączenie ::connect().

Odpowiedz

6

Uważam, że źle jest wykonać blokowanieconnect() w konstruktorze, ponieważ charakter blokowania nie jest czymś, czego zwykle oczekuje od skonstruowania obiektu. Tak więc użytkownicy Twojej klasy mogą być zdezorientowani przez tę funkcjonalność.

Jeśli chodzi o wyjątki, myślę, że generalnie najlepsze (ale także najwięcej pracy) jest wyprowadzenie nowej klasy ze std :: exception. To umożliwia łapaczowi wykonanie działania dla tego określonego typu wyjątku za pomocą instrukcji catch (const myexception &e) {...}, a także zrobić jedną rzecz dla wszystkich wyjątków za pomocą catch (const std::exception &e) {...}.

Zobacz powiązane pytanie: How much work should be done in a constructor?

+1

Istnieją oczywiście wyjątki, gdy konstruktorzy blokujący mają sens, na przykład blokady typu RAII. Oczywiście ważna jest dokumentacja i odpowiednie nazewnictwo. –

+0

+1 Wykonując operacje blokowania w oddzielnej metodzie łączenia, pozostawiasz otwarte drzwi dla metody "anuluj", aby anulować tę operację. –

0

pójdę z drugim, ponieważ wierzę, że konstruktor nie powinien robić cokolwiek innego niż zainicjować członków prywatnych. Poza tym łatwiej radzić sobie z awariami (takimi jak brak połączenia). W zależności od tego, co dokładnie zamierzasz zrobić, możesz utrzymać obiekt przy życiu i wywołać metodę connect, gdy jej potrzebujesz, minimalizując potrzebę tworzenia kolejnego obiektu.

Co do wyjątków, należy utworzyć własne. Umożliwi to wywołującemu podjęcie określonych akcji przywracania w razie potrzeby.

0

Nie łącz się z konstruktorem, konstruktor, który blokuje, jest nieoczekiwany i ma zły interfejs API.

Napisz metodę łączenia i oznacz swoją klasę jako nieodczytywalną. Jeśli już polegasz na połączonych instancjach, ustaw konstruktor jako prywatny i napisz statyczną metodę fabryczną, aby uzyskać wstępnie połączone instancje.

1

Jeśli połączenie zajęłoby dużo czasu, rozsądniej jest umieścić kod w innej metodzie. Mimo to możesz (i powinieneś) użyć wyjątków, aby poinformować dzwoniącego, czy twoja metoda connect() powiodła się, zamiast zwracać kody błędów.

Bardziej wskazane jest również tworzenie nowej klasy wyjątków, pochodzącej z std::exception, zamiast rzucania zwykłych danych lub nawet wyrzucania innych wyjątków STL. Możesz również wyprowadzić swoją klasę wyjątków z bardziej szczegółowego opisu błędu (na przykład pochodzącego z std::runtime_error), ale to podejście jest mniej powszechne.

1

Myślę, że opcja 1 to lepsze podejście, ale trzeba się zastanowić, jak można by tego oczekiwać od konsumenta klasy? Tylko to, że je podłączyli, jest wystarczająco dobre, aby połączyć się (opcja 1) lub fakt, że powinni mieć możliwość połączenia się z Connect(), gdy są dobrze przygotowani (opcja 2)?

RAII obsługuje także zasadę DRY (nie powtarzaj się). Jednak z Opcją 1 musisz zapewnić, że obsługa Wyjątków jest na miejscu i nie dostaniesz się do warunków wyścigowych. Jak wiadomo, jeśli w konstruktorze istnieje wyjątek, destruktor nie zostanie wezwany do oczyszczenia. Możesz również zmieniać dowolne funkcje statyczne, ponieważ będziesz potrzebował również blokady wokół nich - prowadząc cię spiralną ścieżką.

Jeśli jeszcze nie widziałeś this post, to dobrze się czyta.

0

Pod pojęciem umysłu RAII, czyż nie jest to z definicji dobre? Akwizycja to inicjalizacja.

3

Jeśli chodzi o rzucanie wyjątków, doskonale nadaje się do tworzenia własnych klas. Jako hipotetyczny użytkownik wolałabym, gdyby pochodziły ze std :: exception, lub std :: runtime_error (który pozwala ci przekazać ciąg błędu do ctor).

Użytkownicy, którzy chcą może złapać typ pochodnej, ale wspólna idiom:

try { 
    operation_that_might_throw(); 
    } catch (std::exception& e) { 
    cerr << "Caught exception: " << e.what() << endl; 
    } 

będzie pracować dla nowych typów wyjątków, jak również czegokolwiek rzucony przez C++ starcie. Jest to w zasadzie Rule of Least Surprise.

0

Jeśli obiekt połączenia jest w rzeczywistości niedziałający, jeśli połączenie nie powiedzie się, nie ma sensu, aby obiekt istniał, jeśli wszystkie jego inne metody zawsze będą działały bezczynnie lub będą zgłaszać wyjątki. Z tego powodu chciałbym wykonać połączenie w konstruktorze i nie powiedzie się, wyrzucając wyjątek (pochodzący z std::exception), jeśli ta metoda się nie powiedzie.

Jednak masz rację, że klienci klasy mogą potrzebować świadomości, że konstruktor może zablokować lub zawieść. Z tego powodu mógłbym uczynić konstruktor prywatnym i zastosować statyczną metodę fabryk (nazwany idiom konstruktora), aby klienci musieli wykonać jawne wywołanie MakeConnection.

Klient nadal jest odpowiedzialny za sprawdzenie, czy połączenie nie jest dla niego śmiertelne lub czy może obsłużyć tryb offline. W pierwszym przypadku może posiadać połączenie według wartości i pozwolić, aby jakakolwiek awaria połączenia była przekazywana klientom; w tym ostatnim może być właścicielem obiektu za pomocą wskaźnika, najlepiej "inteligentnego". W tym ostatnim przypadku może podjąć próbę skonstruowania posiadanego połączenia w swoim konstruktorze lub może odroczyć go, dopóki nie będzie potrzebny.

E.g. (ostrzeżenie: kod całkowicie nietestowany)

class Connection 
{ 
    Connection(); // Actually make the connection, may throw 
    // ... 

public: 
    static Connection MakeConnection() { return Connection(); } 

    // ... 
}; 

Oto klasa, która wymaga działającego połączenia.

class MustHaveConnection 
{ 
public: 
    // You can't create a MustHaveConnection if `MakeConnection` fails 
    MustHaveConnection() 
     : _connection(Connection::MakeConnection()) 
    { 
    } 

private: 
    Connection _connection; 
}; 

Oto klasa, która może pracować bez niej.

class OptionalConnection 
{ 
public: 
    // You can create a OptionalConnectionif `MakeConnection` fails 
    // 'offline' mode can be determined by whether _connection is NULL 
    OptionalConnection() 
    { 
     try 
     { 
      _connection.reset(new Connection(Connection::MakeConnection())); 
     } 
     catch (const std::exception&) 
     { 
      // Failure *is* an option, it would be better to capture a more 
      // specific exception if possible. 
     } 
    } 

    OptionalConnection(const OptionalConnection&); 
    OptionalConnection& operator=(const OptionalConnection&); 

private: 
    std::auto_ptr<Connection> _connection; 
} 

I wreszcie taki, który tworzy jeden na żądanie, i proponuje wyjątki dla dzwoniącego.

class OnDemandConnection 
{ 
public: 
    OnDemandConnection() 
    { 
    } 

    OnDemandConnection(const OnDemandConnection&); 
    OnDemandConnection& operator=(const OnDemandConnection&); 

    // Propgates exceptions to caller 
    void UseConnection() 
    { 
     if (_connection.get() == NULL) 
      _connection.reset(new Connection(Connection::MakeConnection())); 

     // do something with _connection 
    } 

private: 
    std::auto_ptr<Connection> _connection; 
} 
0

Inną rzeczą, która była niejasna w moim oryginalnego postu jest to, że kod klienta nie ma żadnej interakcji z tego obiektu po jego podłączeniu. Klient uruchamia własny wątek, a gdy obiekt jest tworzony i podłączony, klient wywołuje na nim jedną metodę, która działa przez cały proces nadrzędny. Po zakończeniu tego procesu (z dowolnego powodu) obiekt rozłącza się i wątek klienta wychodzi. Jeśli zdalny proces nie był dostępny, wątek jest natychmiast zamykany. Zatem posiadanie niepołączonego obiektu leżącego wokół nie stanowi problemu.

znalazłem kolejny powód, aby nie zrobić połączenia w konstruktorze: oznacza to, że albo trzeba obsłużyć przerywaniem w de structor lub mają oddzielną disconnect() połączenia bez oddzielnego connect() rozmowy, która pachnie zabawny. Rozproszenie nie jest trywialne i może blokować lub rzucać, więc robienie tego w destruktorze jest prawdopodobnie mniej niż idealne.

Powiązane problemy