2012-04-30 9 views
5

Jestem mocno wierzy w następującej filozofii projektowania:Projekt lepszy interfejs API przekazać struct z jednej klasy do innej klasy

1> Usługi powinny być realizowane możliwie jak najbliżej, gdzie są przechowywane dane.

2> Getter i Setter są źli i powinni być ostrożnie wykorzystywani.

Raczej nie argumentuję powyżej dwóch argumentów i zakładam, że mają krawędzie.

Oto wyzwanie, przed którym stoję obecnie. Mam dwie klasy (tj. AComputer i A), gdzie program AComputer udostępnia niektóre usługi dla A i A zawiera wszystkie podstawowe elementy danych.

Fakt: Nie można łączyć AComputer wewnątrz A z powodu projektu systemu. Wiedziałem, że złamało to mój punkt 1> gdzie obliczenia powinny pozostać przy danych.

Przekazując dane od A do AComputer, musimy przekazać 10 (około) poszczególnych parametrów i dlatego lepiej jest zaprojektować strukturę, aby to zrobić, w przeciwnym razie lista konstruktorów stanie się szalona. Większość danych przechowywanych w AComputer to kopie bezpośrednie przechowywane w A. Zdecydowaliśmy się przechowywać te dane wewnątrz AComputer, ponieważ inne funkcje w AComputer również potrzebują tych zmiennych.

Oto jest pytanie (Pytam dla najlepszych praktyk rozważa konserwacja api & modyfikacji):

1> Gdzie powinniśmy zdefiniować pass-strukturę PassData?

2> Czy należy zapewnić program pobierający/ustawiający dla struct PassData?

Podałem przykładowy kod, jak pokazano poniżej, aby zilustrować moje pytanie w szczegółach. Najlepiej, gdy znajdę prawdziwy open source, który rozwiązał ten sam problem, abym mógł się z niego uczyć.

Jeśli przyjrzeć się prywatnemu PassData m_data; zdefiniowanemu w klasie AComputer, robię to celowo. Innymi słowy, jeśli zmienimy podstawową implementację AComputer, możemy zastąpić PassData m_data; pojedynczymi zmiennymi lub czymś innym, ale NIE łamiemy interfejsu PassData. Tak więc w tym projekcie NIE dostarczam programu pobierającego/ustawiającego dla struktury PassData.

Dziękuję

class AComputer 
{ 
public: 
    struct PassData 
    { // int type just used as an illustration. Real data has different types, 
     // such as double, data, string, enum, etc. 
     // Note: they are not exact copies of variables from A but derived from them 
     int m_v1; 
     // from m_v1 to m_v10 
     //... 
     int m_v10; 
    }; 

    // it is better to store the passed-in data since other functions also need it. 
    AComputer(const PassData& pd) : m_data(pd) {} 

    int GetCombinedValue() const 
    { /* This function returns a value based the passed-in struct of pd */ } 

private: 
    PassData m_data;  
}; 

class A 
{ 
private: 
    int m_i1; 
    // from m_i1 to m_i10 
    // ... 
    int m_i10; 
    // from m_i11 to m_i20 
    // ... 
    int m_i20; 

    boost::shared_ptr<AComputer> m_pAComputer; 

public: 
    A() 
    { 
     AComputer::PassData aData; 
     // populate aData ... 
     m_pAComputer = boost::shared_ptr<AComputer>(new AComputer(aData)); 
    } 

    int GetCombinedValue() const 
    { 
     return m_pAComputer->GetCombinedValue(); 
    } 
}; 
+0

Czy wszystkie argumenty są naprawdę wszystkie "int" (lub wszystkie typy)? Jeśli tak, myślę, że po prostu przekazałbym 'std :: vector '. Jeśli chcesz uzyskać dostęp do nich po nazwie, zdefiniowałbym enum w "AComputer", który podaje nazwy dla indeksów dolnych, więc możesz użyć 'argument [m_i1]' zamiast 'argument [0]'. –

+0

Dobra uwaga. Argumenty są pełne różnych typów, takich jak int, double, string, date, itp. Zaktualizuję mój OP, aby wyjaśnić zamieszanie. – q0987

+3

Co ?! Masz dwie filozofie projektowania i z jakiegoś powodu po prostu * masz *, aby złamać numer 1. Teraz pytasz nas, czy to jest dobry pomysł, aby złamać numer 2? Za czym tęskniłem? –

Odpowiedz

0

Można rozważyć refaktoryzacji użyć obiektu Pattern - jedynym celem tego obiektu byłaby zawierać parametry wywołania metody. Więcej szczegółów: http://sourcemaking.com/refactoring/introduce-parameter-object

+0

Mam podobne pomysły w PO. Jak widać, wprowadziliśmy klasę "PassData", która służy do przekazywania danych grupowych od A do AComputer. Według mnie link nie zawiera nowych informacji, które mogą poprawić obecny kod. -Thx – q0987

11

myślę, że jest lepiej wyjaśnić kilka punktów przed startem, powiedziałeś:

Jeśli spojrzeć na prywatnej PassData m_data; zdefiniowane w klasie AComputer, I zrobić to celowo. Innymi słowy, jeśli zmienimy podstawową implementację AComputer na , możemy zastąpić PassData m_data; z pojedynczych zmiennych lub czymś innym, ale NIE naruszającym interfejsu PassData.

To nie prawda, PassData jest częścią twojego interfejsu! Nie możesz zastąpić PassData bez zerwania kodu klienta, ponieważ potrzebujesz PassData w konstruktorze AComputer. PassData nie jest szczegółami implementacji, ale jest czystym interfejsem.

Drugi punkt, który wymaga wyjaśnienia:

2> Getter i Setter są złe i należy stosować ostrożnie.

Prawidłowo! Ale powinieneś wiedzieć, że POD (struktura zwykłego-starego-danych) jest jeszcze gorsza. Jedyną zaletą korzystania z POD zamiast klasy z programem pobierającym i ustawiającym jest to, że zapisujesz problem z pisaniem funkcji. Ale prawdziwy problem jest wciąż otwarty, interfejs twojej klasy jest zbyt kłopotliwy i będzie bardzo trudny do utrzymania.

Design jest zawsze kompromisem pomiędzy różnymi wymaganiami:

fałszywe poczucie elastyczności

Twoja biblioteka jest rozprowadzany i dużo kodu wykorzystuje swoją klasę. W takim przypadku zmiana PassData będzie dramatyczna. Jeśli możesz zapłacić niewielką cenę w czasie wykonywania, możesz sprawić, że interfejs będzie elastyczny. Na przykład konstruktor acomputer będą:

AComputer(const std::map<std::string,boost::any>& PassData); 

Wystarczy popatrzeć na boost :: dowolny here. Możesz również podać factory dla mapy, aby pomóc użytkownikowi w łatwym tworzeniu mapy.

Pro

  • Jeśli nie wymagane polu dowolny kod jest więcej zmian.

Wady

  • Mała cena wykonawcze.
  • Utrata kontroli bezpieczeństwa typu kompilatora.
  • Jeśli twoja funkcja wymaga innego obowiązkowego pola, nadal masz kłopoty. Kod klienta zostanie skompilowany, ale nie będzie zachowywał się poprawnie.

Ogólnie rzecz biorąc to rozwiązanie nie jest dobre, na samym końcu jest po prostu fantazyjną wersją oryginału.

Strategia Wzór

struct CalculateCombinedValueInterface 
{ 
    int GetCombinedValue()=0; 
    virtual ~CalculateCombinedValueInterface(){} 
}; 

class CalculateCombinedValueFirst : CalculateCombinedValueInterface 
{ 
    public: 
     CalculateCombinedValueFirst(int first):first_(first){} 
     int GetCombinedValue(); //your implementation here 
    private: 
     //I used one field but you get the idea 
     int first_; 
}; 

Kod klienta będą:

CalculateCombinedValueFirst* values = new CalculateCombinedValueFirst(42); 

boost::shared_ptr<CalculateCombinedValueInterface> data(values); 

Teraz, jeśli masz zamiar zmodyfikować kod, nie należy dotykać już wdrożony interfejs. Rozwiązaniem obiektowym jest nowa klasa dziedzicząca z klasy abstrakcyjnej.

class CalculateCombinedValueSecond : CalculateCombinedValueInterface 
{ 
    public: 
     CalculateCombinedValueFirst(int first,double second) 
      :first_(first),second_(second){} 
     int GetCombinedValue(); //your implementation here 
    private: 
     int first_; 
     double second_; 
}; 

Klient podejmie decyzję o aktualizacji do nowej klasy lub pozostaniu przy istniejącej wersji.

Pro

  • Poprawa interfejs bez kodu klienckiego przerwa.
  • Nie dotykasz istniejącego kodu, ale wprowadzasz nowe funkcje do nowego pliku.
  • Możesz chcieć użyć template method design pattern, jeśli potrzebujesz mniejszej kontroli ziarnistości.

Wady

  • Overhead z użyciem funkcji wirtualnych (w zasadzie kilka pikosekundy!)
  • Nie można przełamać istniejący kod. Musisz pozostawić istniejący interfejs nietknięty i dodać nową klasę, aby modelować inne zachowanie.

Liczba parametrów

Jeśli masz zestaw wejścia dziesięć parametrów w jednej funkcji, to jest bardzo prawdopodobne, że wartości te są logicznie powiązane. Możesz zebrać niektóre z tych wartości na zajęciach. Te klasy mogą być łączone w innej klasie, która będzie stanowiła wejście twojej funkcji. Fakt, że masz 10 (lub więcej!) Członków danych w klasie, powinien dzwonić.

The single responsibility principle powiedział:

Nigdy nie powinno być więcej niż jeden powód klasą zmienić.

Następstwem tej zasady jest: Twoja klasa musi być mała. Jeśli twoja klasa ma 20 członków, jest bardzo prawdopodobne, że znajdziesz wiele powodów, aby to zmienić.

Wnioski

Po podaniu interfejs (wszelkiego rodzaju interfejsu) do klienta nie można go zmienić (dobrym przykładem są wszystkie funkcje potępiać w C++, że kompilatory potrzeba wdrożenia przez lata). Zwróć uwagę na interfejs, który udostępniasz nawet niejawnym interfejsem. W twoim przykładzie PassData nie jest szczegółami implementacji, ale jest częścią interfejsu klasy.

Liczba parametrów jest sygnałem, że projekt musi zostać sprawdzony. To bardzo trudna zmiana w dużej klasie. Twoje klasy powinny być małe i zależne od innych klas tylko za pośrednictwem interfejsu (klasa abstrakcyjna w slangu C++).

Jeśli klasa jest:

1) małe i tylko z jednego powodu, aby zostać zmieniona

2) pochodzący z klasy abstrakcyjnej

3) inne zajęcia odnoszą się do niego za pomocą wskaźnika do Klasa abstrakcyjna

Twój kod może być łatwo zmieniony (ale musi już zostać zachowany interfejs).

Jeśli nie spełnisz wszystkich tych wymagań, będziesz miał kłopoty.

UWAGA: UWAGA: wymagania 2) i 3) mogą ulec zmianie, jeśli zamiast dostarczać dynamiczne polimorfiny, projekt korzysta z polimorfów statycznych.

+0

pozwól mi odpowiedzieć na twoje pytania jeden po drugim. Point1> Jeśli postanowię zmienić wewnętrzną implementację 'AComputer', nie muszę zmieniać' PassData'. Metoda1> zmiana/zamiana definicji zmiennej 'm_data' (tzn. Może wskazywać na nową strukturę). Metoda2> Podałeś dobry przykład udostępniania nowych funkcji bez naruszania istniejącego kodu. Podobnie możemy wprowadzić "PassDataV2" tutaj bez łamania istniejącego interfejsu. To jest główny powód, dla którego powiedziałem: "Nie wiem, czy konieczne jest pobieranie/ustawianie dla PassData". 'AComputer' ma dwa poziomy, aby dostosować się do nowych zmian. – q0987

+0

Point2> Całkowicie się z tobą zgadzam. W rzeczywistości kluczową kwestią jest to, że struktura przekazywania danych jest ogromna (10 ~ 20). Klasa służy do obliczania złożonego modelu matematycznego, który wymaga tak wielu różnych parametrów, aby mógł on działać. Głównym problemem jest to, jak/gdzie powinniśmy zdefiniować "PassData" dla dobrych praktyk i utrzymania kodu. -Dziękuję, – q0987

0

W normalnej klasy design, wszystko funkcja członek mają ten wskaźnik przekazany jako parametr niejawny, tak aby mogły uzyskać dostęp do danych użytkowników:

// Regular class 
class SomeClass 
{ 
public: 
    // will be name-mangled by the compiler as something like: 
    // void SomeClass_getValue(const SomeClass*) const; 
    void getValue() const 
    { 
     return value_; // actually: return this->value_; 
    } 

private: 
    int value_; 
}; 

Należy naśladować tego jak najwięcej. Jeśli z jakiegoś powodu nie możesz scalić klas AC i A w jedną czystą klasę, najlepszym rozwiązaniem byłoby pozwolić programowi AComputer przenieść wskaźnik do A jako członka danych. W każdej funkcji członka AComputer będziesz musiał użyć funkcji pobierania/ustawiania dla A, aby uzyskać dostęp do odpowiednich elementów danych.

class AComputer 
{ 
public: 
    AComputer(A* a): p_(a) {} 

    // this will be mangled by the compiler to something like 
    // AComputer_GetCombinedValue(const Acomputer*) const; 
    int GetCombinedValue() const 
    { 
     // in a normal class it would be: return m_i1 + m_i2 + ... 
     // which would actually be: return this->m_i1 + this->m_i12 + ... 
     // the code below actually is: return this->p_->m_i1 + this->p_->m_i2 + ... 
     return p_->get_i1() + p_->get_i2() + ...  
    } 

private: 
    class A; 
    A* p_; 
}; 

class A 
{ 
public: 
    // setters and getters 

private: 
    // data only, NO pointer to AComputer object 
} 

Więc w efekcie utworzono dodatkowy poziom pośredni, który tworzy iluzję użytkownikom że acomputer i A są częścią tej samej abstrakcji.

+0

Dokładnie tak wygląda kod źródłowy. Klasa kończy się 20 ~ 40 funkcjami, które działają wyłącznie w celu zapewnienia getter/setter. Też rozważałem używanie znajomego, ale przyjaciel wprowadza problem sprzężenia klas. – q0987

+0

@ q0987 Jeśli weźmiesz pod uwagę "AComputer" pod względem interfory i "A" pod względem implementacji, to jest to najlepsze, co możesz zrobić. Utworzenie kolejnej klasy PassData po prostu przesłania twoje intencje, ponieważ wtedy masz jeszcze inny poziom pośrednictwa. Twórcy starszego kodu powinni zintegrować klasy. – TemplateRex

+0

Twoja proponowana metoda wprowadza zależność kołową i staram się jej unikać. Również bez wprowadzenia PassData konstruktor otrzyma parametry ~ 10 pass-in. – q0987

0

Używanie PassData zamiast 10 argumentów jest dobre, jeśli masz pełną kontrolę nad wszystkimi klientami AComputer. Ma dwie zalety: musisz wprowadzić mniej zmian, gdy dodajesz kolejne dane do przekazania, a możesz użyć przypisania do członków struktury na stronie wywołującej, aby wyjaśnić znaczenie każdego "argumentu".

Jednakże, jeśli inne osoby będą używać AComputer, używanie PassData ma poważną wadę. Bez tego, po dodaniu 11 argumentu do konstruktora AComputer, kompilator wykryje błąd dla użytkowników, którzy nie zaktualizowali faktycznej listy argumentów. Jeśli dodasz 11 element do PassData, kompilator po cichu zaakceptuje strukturę, w której nowy członek jest śmieci lub, w najlepszym przypadku, zero.

Moim zdaniem, jeśli użyjesz PassData, posiadanie modułów pobierających i ustawiających będzie przesadą. "Standardy kodowania C++" Sutter i Alexandresku zgadzają się z tym. Tytuł pozycji nr 41 brzmi: "spraw, aby członkowie danych byli prywatni, , z wyjątkiem zachowawczych agregatów (struktur C-style)" (nacisk jest mój).