2012-02-10 13 views
7

Jakie są plusy i minusy używania niejawnych interfejsów (przypadki 2 i 3, szablony) w porównaniu do używania jawnych interfejsów (Przypadek 1, wskaźnik do klasy abstrakcyjnej) w poniższym przykładzie?niejawne vs jawne interfejsy

Kod, który nie zmienia:

class CoolClass 
{ 
public: 
    virtual void doSomethingCool() = 0; 
    virtual void worthless() = 0; 
}; 

class CoolA : public CoolClass 
{ 
public: 
    virtual void doSomethingCool() 
    { /* Do cool stuff that an A would do */ } 

    virtual void worthless() 
    { /* Worthless, but must be implemented */ } 
}; 

class CoolB : public CoolClass 
{ 
public: 
    virtual void doSomethingCool() 
    { /* Do cool stuff that a B would do */ } 

    virtual void worthless() 
    { /* Worthless, but must be implemented */ } 
}; 

Przypadek 1: non-matrycy klasy, która pobiera wskaźnik bazowy klasy, który zapewnia wyraźny interfejs:

class CoolClassUser 
{ 
public: 
    void useCoolClass(CoolClass * coolClass) 
    { coolClass.doSomethingCool(); } 
}; 

int main() 
{ 
    CoolClass * c1 = new CoolA; 
    CoolClass * c2 = new CoolB; 

    CoolClassUser user; 
    user.useCoolClass(c1); 
    user.useCoolClass(c2); 

    return 0; 
} 

Przypadek 2: szablonowa klasa, której typ szablonu zapewnia domyślny interfejs:

template <typename T> 
class CoolClassUser 
{ 
public: 
    void useCoolClass(T * coolClass) 
    { coolClass->doSomethingCool(); } 
}; 

int main() 
{ 
    CoolClass * c1 = new CoolA; 
    CoolClass * c2 = new CoolB; 

    CoolClassUser<CoolClass> user; 
    user.useCoolClass(c1); 
    user.useCoolClass(c2); 

    return 0; 
} 

Przypadek 3: klasa szablonowa, której szablon jedli typ zapewnia niejawny interfejs (tym razem nie powstałymi z CoolClass:

class RandomClass 
{ 
public: 
    void doSomethingCool() 
    { /* Do cool stuff that a RandomClass would do */ } 

    // I don't have to implement worthless()! Na na na na na! 
}; 

template <typename T> 
class CoolClassUser 
{ 
public: 
    void useCoolClass(T * coolClass) 
    { coolClass->doSomethingCool(); } 
}; 

int main() 
{ 
    RandomClass * c1 = new RandomClass; 
    RandomClass * c2 = new RandomClass; 

    CoolClassUser<RandomClass> user; 
    user.useCoolClass(c1); 
    user.useCoolClass(c2); 

    return 0; 
} 

Case 1 wymaga, że ​​obiekt jest przekazany do useCoolClass() być dzieckiem CoolClass (i wdrożenia bezwartościowe ()). Natomiast przypadki 2 i 3 zajmą dowolną klasę, która ma funkcję doSomethingCool().

Jeśli użytkownicy kodu były zawsze drobnym podklasy CoolClass, następnie Przypadek 1 ma intuicyjny sens, ponieważ CoolClassUser będzie zawsze oczekiwał wdrożenie CoolClass. Załóżmy jednak, że ten kod będzie częścią struktury interfejsu API, więc nie mogę przewidzieć, czy użytkownicy będą chcieli podklasę CoolClass lub przetasować własną klasę, która ma funkcję doSomethingCool().

Niektóre pokrewne posty:

https://stackoverflow.com/a/7264550/635125

https://stackoverflow.com/a/7264689/635125

https://stackoverflow.com/a/8009872/635125

+0

Twoje Case 1 i Case 2 nie będą się kompilować. Inicjalizacja wskaźnika przebiega w niewłaściwy sposób. – Novelocrat

+0

@ Naprawiono wadliwą rodzinę. –

Odpowiedz

3

Niektóre czynniki, które przyszło mi do głowy, dlaczego można preferować Przypadek 1:

  • Jeśli CoolClass nie jest czystym interfejsem, tzn. Część implementacji jest również dziedziczona (chociaż możesz podać ją również dla przypadku 2/3, np. w postaci klasy bazowej);
  • jeśli istnieją powody, dla których wprowadzono CoolClassUser w postaci binarnej zamiast nagłówka (i to nie tylko ochrona, ale może to być również rozmiar kodu, kontrola zasobów, scentralizowana obsługa błędów itp.);
  • jeśli chcesz przechowywać wskaźniki i używać ich później, wtedy również Przypadek 1 wydaje się lepszy: (a) łatwiej jest przechowywać je wszystkie w tym samym pojemniku, i (b) musisz zapisać rzeczywisty typ danych jako cóż, aw przypadku 2/3 rozwiązaniem, które przychodzi na myśl, jest przekonwertowanie go na interfejs "jawny" (np. Przypadek 1) za pomocą opakowania szablonu.

Powody Case 2/3 może być preferrable:

  • jeśli później zdecydować, że worthless() jest coś wart, i rozpocząć korzystanie z niego, w przypadku 2 dostaniesz kompilacji błędów dla klas gdzie nie jest zaimplementowany. W przypadku 1 nic nie będzie przypominać o tym, aby zaimplementować te funkcje w rzeczywistości, z wyjątkiem może błędów w czasie wykonywania, jeśli jesteś (nie) szczęśliwy.
  • Case2/3 może mieć nieco lepszą wydajność, ale kosztem większego rozmiaru kodu.

W niektórych przypadkach może to być wyłącznie kwestia osobistych preferencji, zarówno użytkownika, jak i użytkownika.

1

Należy pamiętać, że w przypadkach # 2 i # 3 zależy to od parametrów szablonu, co oznacza, że ​​koder w czasie wywołania będzie musiał prawidłowo utworzyć instancję argumentu szablonu o poprawnym typie. Zależnie od tego, w jaki sposób będą używane funkcje, może powstać problem, w którym chcesz utworzyć abstrakcyjny interfejs dla użytkownika, bez konieczności martwienia się o typ przekazywanego obiektu ... np. "Uchwyt" lub inny wskaźnik do obiektu pochodnego, który używa polimorfizmu do przekazania obiektu z jednej funkcji API do drugiej. Na przykład:

class abstract_base_class; 

abtract_base_class* get_handle(); 
void do_something_with_handle(abstract_base_class* handle); 
void do_something_else_with_handle(abstract_base_class* handle); 
//... more API functions 

Teraz Twój ramy API może przekazać obiekt z powrotem do użytkownika w kodzie, a oni nie muszą wiedzieć, co to jest obiekt ... tylko trzeba wiedzieć, że opisuje jakiś typ interfejsu, który możesz publicznie ujawnić gdzieś w nagłówku. Ale nie będą musieli wiedzieć nic o "wnętrzności" obiektu, który im przekazałeś. Możesz dać im wskaźnik do jakiegoś typu pochodnego, który kontrolujesz implementację. Wystarczy podać szablony dla najbardziej ogólnych typów funkcji w swoim interfejsie API. W przeciwnym razie konieczne jest utworzenie instancji dla funkcji, które są przeznaczone tylko do pobrania abstract_base_class*, tylko po to, aby utworzyć więcej kodu dla użytkownika.