2010-05-28 21 views
6

Przeszukałem odpowiedzi na podobne tematy na stronie SO, ale nie mogłem znaleźć satysfakcjonującej odpowiedzi. Ponieważ wiem, że jest to dość duży temat, postaram się być bardziej szczegółowy.Jak napisać elastyczny modułowy program z dobrymi możliwościami interakcji pomiędzy modułami?

Chcę napisać program, który przetwarza pliki. Przetwarzanie jest nietrywialne, więc najlepszym sposobem jest podzielenie różnych faz na samodzielne moduły, które następnie byłyby używane w razie potrzeby (ponieważ czasami będę zainteresowany tylko wyjściem modułu A, czasami potrzebowałbym mocy wyjściowej pięciu innych modułów, itp.). Chodzi o to, że potrzebuję modułów do współpracy, ponieważ wyjście jednego może być wejściem drugiego. I potrzebuję, żeby było SZYBKO. Ponadto chcę uniknąć wykonywania pewnych operacji więcej niż jeden raz (jeśli moduł A tworzy pewne dane, które następnie muszą być przetwarzane przez moduł B i C, nie chcę uruchamiać modułu A dwa razy, aby utworzyć dane wejściowe dla modułów B, C) .

Informacje, które moduły muszą udostępniać, to przede wszystkim bloki danych binarnych i/lub przesunięcia w przetwarzanych plikach. Zadanie głównego programu byłoby dość proste - wystarczy przeanalizować argumenty, uruchomić wymagane moduły (i być może podać jakieś wyniki, czy powinno to być zadaniem modułów?).

Nie potrzebuję modułów do załadowania w czasie wykonywania. Całkiem dobrze jest mieć biblioteki z plikiem .h i przekompilować program za każdym razem, gdy pojawi się nowy moduł lub jakiś moduł zostanie zaktualizowany. Pomysł na moduły jest tutaj głównie ze względu na czytelność kodu, utrzymanie i możliwość posiadania większej liczby osób pracujących nad różnymi modułami bez potrzeby posiadania jakiegoś wcześniej zdefiniowanego interfejsu lub czegoś podobnego (z drugiej strony, pewne "wytyczne", jak pisać moduły byłyby prawdopodobnie wymagane, wiem o tym). Możemy założyć, że przetwarzanie pliku jest operacją tylko do odczytu, oryginalny plik nie jest zmieniany.

Czy ktoś może wskazać mi w dobrym kierunku, jak to zrobić w C++? Wszelkie rady są mile widziane (linki, tutoriale, książki w formacie pdf ...).

+3

To pytanie jest w zasadzie " jak napisać modułowy kod "? Ponieważ _all_ kod powinien być modułowy, nie ma tu nic konkretnego o C++ ani o twojej konkretnej domenie problemowej. a odpowiedź brzmi "stosując umiejętności, talent i doświadczenie". –

Odpowiedz

2

Wygląda to bardzo podobnie do architektury wtyczki. Polecam zacząć (nieformalny) Wykres przepływu danych w celu zidentyfikowania:

  • w jaki sposób te dane procesowe bloki
  • jakie dane muszą być przesyłane
  • jakie wyniki wrócić z jednego bloku do drugiego (dane/kody błędów/wyjątki)

Dzięki tym informacjom można rozpocząć tworzenie ogólnych interfejsów, które pozwalają na powiązanie z innymi interfejsami w czasie wykonywania.Następnie dodałabym funkcję fabryczną do każdego modułu, aby zażądać od niego rzeczywistego obiektu przetwarzania. I Do not nie polecam, aby obiekty przetwarzania bezpośrednio z interfejsu modułu, ale zwrócić obiekt fabryki, w którym obiekty przetwarzania mogą być pobierane. Te obiekty przetwarzania są następnie wykorzystywane do budowania całego łańcucha przetwarzania.

uproszczony schemat wyglądałby następująco:

struct Processor 
{ 
    void doSomething(Data); 
}; 

struct Module 
{ 
    string name(); 
    Processor* getProcessor(WhichDoIWant); 
    deleteprocessor(Processor*); 
}; 

z mojego umysłu te wzory są częściej pojawiają się:

  • funkcji fabrycznego: aby uzyskać obiektów z modułów
  • kompozytowych & & dekorator: tworzenie łańcucha przetwarzania
+0

Dziękuję za odpowiedź, podejście do wzoru fabryki wygląda dobrze! – PeterK

+1

Wdrożenie fabryki wygląda jednak nie tak. Użyj RAII i przestań pytać klienta o zwrócenie jego 'Procesora' do' Module': wiemy, że zapomni! –

+0

@Matthieu M. nawet jeśli nie było metody usuwania, strona klienta musi wykonać usunięcie, ponieważ obiekty nie mogą przejść na wartość, ale tylko na wskaźnik. Zatem RAII nie zapobiega żadnym uszkodzeniom w tym momencie. Powodem posiadania metody usuwania jest większa swoboda w przypadku wdrożenia w fabryce i nieużywanie nowych do budowy obiektu. Używam tego wzoru w jednym projekcie, w którym niektóre fabryki tworzą obiekty na żądanie, podczas gdy inne zwracają wskaźniki do singletonów lub obiektów z puli. – Rudi

2

Zastanawiam się, czy C++ jest odpowiednim poziomem do przemyślenia w tym celu. Z mojego doświadczenia wynika, że ​​zawsze przydały się osobne programy, które są połączone ze sobą w filozofii systemu UNIX.

Jeśli twoje dane nie są zbyt duże, istnieje wiele korzyści w dzieleniu. Najpierw uzyskasz możliwość testowania każdej fazy przetwarzania niezależnie, uruchamiasz jeden program i przekierowujesz wyjście do pliku: możesz łatwo sprawdzić wynik. Następnie możesz korzystać z wielu podstawowych systemów, nawet jeśli każdy z twoich programów ma jeden wątek, a zatem o wiele łatwiejsze do utworzenia i debugowania. Korzystasz również z synchronizacji systemu operacyjnego za pomocą przewodów między programami. Może niektóre z twoich programów mogłyby zostać wykonane przy użyciu już istniejących programów użytkowych?

Twój ostateczny program stworzy klej do zebrania wszystkich twoich narzędzi w jeden program, pipetowania danych z programu do innego (nie ma więcej plików w tym czasie) i replikowania ich zgodnie z wymaganiami dla wszystkich twoich obliczeń.

+0

Zapomniałem powiedzieć, że jestem związany z systemem operacyjnym Windows. Naprawdę chcę tylko jednego programu, a nie zestawu programów, które mogłyby współpracować (ponieważ jest całkiem możliwe, że moduły, które utworzę, nie będą używane tylko w mojej aplikacji, ale także w innych). W każdym razie, dziękuję za odpowiedź. – PeterK

+0

Istnieją biblioteki dla rurociągów niezależne od systemu operacyjnego (a dokładniej, abstrakty). –

+0

Bycie związanym z Windows nie jest ograniczeniem do tworzenia kilku programów i łączenia ich w pary. Nawet Windows może to zrobić doskonale! –

1

To naprawdę wydaje się dość banalne, więc przypuszczam, że brakuje nam pewnych wymagań.

Użyj Memoization, aby uniknąć liczenia wyniku więcej niż jeden raz. Należy to zrobić w ramach.

Można użyć schematu blokowego do określenia sposobu przekazywania informacji z jednego modułu do drugiego ... ale najprostszym sposobem jest, aby każdy moduł bezpośrednio wywoływał te, od których zależą. Przy zapamiętywaniu nie kosztuje wiele, ponieważ jeśli zostało już obliczone, wszystko w porządku.

Ponieważ musisz mieć możliwość uruchamiania o dowolnym module, musisz podać im identyfikatory i zarejestrować je gdzieś w sposób umożliwiający ich sprawdzenie w czasie wykonywania. Można to zrobić na dwa sposoby.

  • Przykład: Otrzymujesz unikalny przykład tego modułu i go wykonujesz.
  • Fabryka: Tworzysz żądany moduł, wykonujesz go i wyrzucasz.

Wadą metody Exemplar jest, że jeśli dwa razy uruchomić moduł, będziesz nie być począwszy od czystego stanu, ale od państwa, że ​​ostatni (prawdopodobnie nie), wykonanie go w lewo. Na to memoization może być postrzegana jako zaleta, ale jeśli się nie powiodło, wynik nie jest obliczany (urgh), więc zalecałbym przeciw niemu.

Jak się masz ...?

Zacznijmy od fabryki.

class Module; 
class Result; 

class Organizer 
{ 
public: 
    void AddModule(std::string id, const Module& module); 
    void RemoveModule(const std::string& id); 

    const Result* GetResult(const std::string& id) const; 

private: 
    typedef std::map< std::string, std::shared_ptr<const Module> > ModulesType; 
    typedef std::map< std::string, std::shared_ptr<const Result> > ResultsType; 

    ModulesType mModules; 
    mutable ResultsType mResults; // Memoization 
}; 

To naprawdę bardzo podstawowy interfejs. Jednakże, ponieważ chcemy nowej instancji modułu za każdym razem, gdy wywoływamy Organizer (aby uniknąć problemu ponownego wejścia), potrzebujemy pracować nad naszym interfejsem Module.

class Module 
{ 
public: 
    typedef std::auto_ptr<const Result> ResultPointer; 

    virtual ~Module() {}    // it's a base class 
    virtual Module* Clone() const = 0; // traditional cloning concept 

    virtual ResultPointer Execute(const Organizer& organizer) = 0; 
}; // class Module 

A teraz, to proste:

// Organizer implementation 
const Result* Organizer::GetResult(const std::string& id) 
{ 
    ResultsType::const_iterator res = mResults.find(id); 

    // Memoized ? 
    if (res != mResults.end()) return *(it->second); 

    // Need to compute it 
    // Look module up 
    ModulesType::const_iterator mod = mModules.find(id); 
    if (mod != mModules.end()) return 0; 

    // Create a throw away clone 
    std::auto_ptr<Module> module(it->second->Clone()); 

    // Compute 
    std::shared_ptr<const Result> result(module->Execute(*this).release()); 
    if (!result.get()) return 0; 

    // Store result as part of the Memoization thingy 
    mResults[id] = result; 

    return result.get(); 
} 

i Przykład prosty moduł/Wynik:

struct FooResult: Result { FooResult(int r): mResult(r) {} int mResult; }; 

struct FooModule: Module 
{ 
    virtual FooModule* Clone() const { return new FooModule(*this); } 

    virtual ResultPointer Execute(const Organizer& organizer) 
    { 
    // check that the file has the correct format 
    if(!organizer.GetResult("CheckModule")) return ResultPointer(); 

    return ResultPointer(new FooResult(42)); 
    } 
}; 

iz głównym:

#include "project/organizer.h" 
#include "project/foo.h" 
#include "project/bar.h" 


int main(int argc, char* argv[]) 
{ 
    Organizer org; 

    org.AddModule("FooModule", FooModule()); 
    org.AddModule("BarModule", BarModule()); 

    for (int i = 1; i < argc; ++i) 
    { 
    const Result* result = org.GetResult(argv[i]); 
    if (result) result->print(); 
    else std::cout << "Error while playing: " << argv[i] << "\n"; 
    } 
    return 0; 
}