2010-05-28 6 views
6

Tôi đã đi qua câu trả lời về các chủ đề tương tự ở đây trên SO nhưng could't tìm thấy một câu trả lời thỏa mãn. Vì tôi biết đây là một chủ đề khá lớn, tôi sẽ cố gắng cụ thể hơn.Làm thế nào để viết một chương trình mô-đun linh hoạt với khả năng tương tác tốt giữa các mô-đun?

Tôi muốn viết chương trình xử lý tệp. Việc xử lý là không cần thiết, vì vậy cách tốt nhất là chia các giai đoạn khác nhau thành các mô-đun độc lập mà sau đó sẽ được sử dụng khi cần thiết (vì đôi khi tôi sẽ chỉ quan tâm đến đầu ra của mô-đun A, đôi khi tôi sẽ cần đầu ra của năm mô-đun khác, v.v.). Vấn đề là, tôi cần các mô-đun hợp tác, bởi vì đầu ra của một cái có thể là đầu vào của cái khác. Và tôi cần nó nhanh. Hơn nữa tôi muốn tránh thực hiện xử lý nhất định nhiều lần (nếu module A tạo ra một số dữ liệu cần xử lý bằng mô-đun B và C, tôi không muốn chạy mô-đun A hai lần để tạo đầu vào cho mô-đun B, C) .

Thông tin mà các mô-đun cần chia sẻ chủ yếu sẽ là các khối dữ liệu nhị phân và/hoặc bù trừ vào các tệp được xử lý. Nhiệm vụ của chương trình chính sẽ khá đơn giản - chỉ cần phân tích các đối số, chạy các mô-đun cần thiết (và có thể cung cấp một số đầu ra, hoặc đây có phải là nhiệm vụ của các mô-đun không?).

Tôi không cần mô-đun được tải khi chạy. Nó hoàn toàn tốt để có libs với một tập tin .h và biên dịch lại chương trình mỗi khi có một mô-đun mới hoặc một số mô-đun được cập nhật. Ý tưởng mô-đun ở đây chủ yếu là do khả năng đọc mã, duy trì và có thể có nhiều người làm việc trên các mô-đun khác nhau mà không cần phải có một số giao diện được xác định trước hoặc bất kỳ điều gì (mặt khác, một số "nguyên tắc" về cách viết mô-đun có lẽ sẽ được yêu cầu, tôi biết điều đó). Chúng ta có thể giả định rằng việc xử lý tệp là một hoạt động chỉ đọc, tệp gốc không bị thay đổi.

Ai đó có thể chỉ cho tôi một hướng tốt về cách thực hiện điều này trong C++? Mọi lời khuyên đều được chào đón (liên kết, hướng dẫn, sách pdf ...).

+3

Câu hỏi này là về cơ bản " làm cách nào để viết mã mô-đun "? Vì _all_ code nên được mô-đun, không có gì đặc biệt về C++ ở đây, hoặc về miền vấn đề cụ thể của bạn. và câu trả lời là "bằng cách áp dụng kỹ năng, tài năng và kinh nghiệm". –

Trả lời

2

Điều này trông rất giống với kiến ​​trúc plugin. Tôi khuyên bạn nên bắt đầu với một (không chính thức) biểu đồ luồng dữ liệu để xác định:

  • cách những dữ liệu khối quá trình
  • cần những dữ liệu được chuyển giao
  • gì kết quả trở lại từ một khối khác (dữ liệu/mã lỗi/ngoại lệ)

Với những thông tin này bạn có thể bắt đầu xây dựng giao diện chung, cho phép liên kết với các giao diện khác trong thời gian chạy.Sau đó, tôi sẽ thêm một chức năng nhà máy cho mỗi mô-đun để yêu cầu đối tượng xử lý thực sự ra khỏi nó. Tôi không khuyên bạn nên lấy các đối tượng xử lý trực tiếp ra khỏi giao diện mô-đun, nhưng để trả về một đối tượng nhà máy, trong đó các đối tượng xử lý được lấy ra. Các đối tượng xử lý này sau đó được sử dụng để xây dựng toàn bộ chuỗi xử lý.

Một phác thảo đơn giản đi sẽ trông như thế này:

struct Processor 
{ 
    void doSomething(Data); 
}; 

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

Out of tâm trí tôi những mô hình có thể sẽ xuất hiện:

  • chức năng nhà máy: để có được các đối tượng từ các module
  • composit & & trang trí: tạo thành chuỗi xử lý
+0

Cảm ơn bạn đã trả lời, cách tiếp cận mẫu nhà máy có vẻ tốt! – PeterK

+1

Việc thực hiện của nhà máy có vẻ sai mặc dù. Sử dụng RAII và ngừng yêu cầu khách hàng trả lại 'Processor' cho 'Module': chúng ta biết anh ta sẽ quên! –

+0

@Matthieu M. ngay cả khi không có phương pháp xóa, phía máy khách phải thực hiện xóa, vì các đối tượng không thể chuyển cho mỗi giá trị, nhưng chỉ cho mỗi con trỏ. Vì vậy, RAII không ngăn chặn bất kỳ thiệt hại vào thời điểm này. Lý do để có phương pháp xóa là có nhiều tự do hơn cho việc triển khai nhà máy và không bị buộc phải sử dụng mới cho việc xây dựng đối tượng. Tôi sử dụng mô hình này trong một dự án mà một số nhà máy tạo ra các đối tượng theo yêu cầu, trong khi những người khác lại đưa con trỏ đến các đơn hoặc đối tượng từ một hồ bơi. – Rudi

2

Tôi tự hỏi liệu C++ có đúng mức để suy nghĩ về mục đích này không. Theo kinh nghiệm của tôi, nó đã luôn luôn được chứng minh hữu ích để có các chương trình riêng biệt được đường ống với nhau, trong triết lý UNIX.

Nếu dữ liệu của bạn không quá lớn, có nhiều lợi thế khi tách. Đầu tiên bạn có khả năng kiểm tra từng giai đoạn xử lý của bạn một cách độc lập, bạn chạy một chương trình chuyển hướng đầu ra tới một tệp: bạn có thể dễ dàng kiểm tra kết quả. Sau đó, bạn tận dụng lợi thế của nhiều hệ thống lõi ngay cả khi mỗi chương trình của bạn là một luồng duy nhất, và do đó dễ dàng hơn nhiều để tạo và gỡ lỗi. Và bạn cũng tận dụng lợi thế của việc đồng bộ hóa hệ điều hành bằng cách sử dụng các đường ống giữa các chương trình của bạn. Có lẽ một số chương trình của bạn có thể được thực hiện bằng các chương trình tiện ích đã tồn tại?

Chương trình cuối cùng của bạn sẽ tạo keo để thu thập tất cả các tiện ích của bạn vào một chương trình duy nhất, chuyển dữ liệu từ chương trình này sang chương trình khác (không có tệp nào tại thời điểm này) và sao chép nó theo yêu cầu cho tất cả các tính toán của bạn.

+0

Quên để nói rằng tôi đang bị ràng buộc với hệ điều hành Windows. Và tôi thực sự chỉ muốn một chương trình, không phải là một tập hợp các chương trình sẽ hoạt động cùng nhau (vì có thể các mô-đun tôi tạo sẽ không chỉ được sử dụng trong ứng dụng của tôi, mà còn trong các ứng dụng khác). Dù sao, cảm ơn câu trả lời của bạn. – PeterK

+0

Có thư viện cho đường ống độc lập với hệ điều hành (hoặc chính xác hơn, trừu tượng hóa nó). –

+0

Bị ràng buộc với Windows không phải là trình hiển thị cho việc tạo một số chương trình và liên kết chúng với nhau. Ngay cả Windows cũng có thể thực hiện điều này một cách hoàn hảo! –

1

Điều này thực sự có vẻ khá tầm thường, vì vậy tôi cho rằng chúng tôi bỏ lỡ một số yêu cầu.

Sử dụng Memoization để tránh tính toán kết quả nhiều lần. Điều này nên được thực hiện trong khuôn khổ.

Bạn có thể sử dụng một số sơ đồ để xác định cách chuyển thông tin từ mô-đun này sang mô-đun khác ... nhưng cách đơn giản nhất là có mỗi mô-đun trực tiếp gọi những mô-đun mà họ phụ thuộc. Với việc ghi nhớ nó không tốn nhiều tiền vì nếu nó đã được tính toán, bạn ổn.

Vì bạn cần để có thể khởi chạy về bất kỳ mô-đun nào, bạn cần cung cấp cho họ ID và đăng ký chúng ở đâu đó với cách tìm kiếm chúng khi chạy. Có hai cách để làm điều này.

  • Ví dụ: Bạn nhận được mẫu duy nhất của loại mô-đun này và thực thi nó.
  • Nhà máy: Bạn tạo mô-đun thuộc loại được yêu cầu, thực thi và loại bỏ nó đi.

Mặt trái của phương pháp Exemplar là nếu bạn thực thi mô-đun hai lần, bạn sẽ không bắt đầu từ trạng thái sạch nhưng từ trạng thái thực thi cuối cùng (có thể không thành công). có thể được coi là một lợi thế, nhưng nếu nó thất bại kết quả là không tính (urgh), vì vậy tôi sẽ khuyên bạn nên chống lại nó.

Vậy làm thế nào để bạn ...?

Hãy bắt đầu với nhà máy.

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 
}; 

Giao diện rất cơ bản thực sự. Tuy nhiên, vì chúng tôi muốn có một phiên bản mới của mô-đun mỗi lần chúng tôi gọi số Organizer (để tránh vấn đề về tái sử dụng), chúng tôi cần phải làm việc trên giao diện Module của chúng tôi.

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 

Và bây giờ, thật dễ dàng:

// 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(); 
} 

Và một Module đơn giản/example quả:

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)); 
    } 
}; 

Và từ chính:

#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; 
}