Chiến lược thực hiện tốt để đóng gói dữ liệu chia sẻ trong một đường ống phần mềm


13

Tôi đang làm việc để bao thanh toán lại các khía cạnh nhất định của một dịch vụ web hiện có. Cách các API dịch vụ được triển khai là bằng cách có một loại "đường ống xử lý", trong đó có các tác vụ được thực hiện theo trình tự. Không có gì đáng ngạc nhiên, các tác vụ sau này có thể cần thông tin được tính toán bởi các tác vụ trước đó và hiện tại cách thức này được thực hiện bằng cách thêm các trường vào lớp "trạng thái đường ống".

Tôi đã suy nghĩ (và hy vọng?) Rằng có một cách tốt hơn để chia sẻ thông tin giữa các bước đường ống hơn là có một đối tượng dữ liệu với hàng triệu trường, một số trong đó có ý nghĩa đối với một số bước xử lý chứ không phải cho các bước khác. Sẽ là một nỗi đau lớn để làm cho lớp này an toàn (tôi không biết liệu nó có khả thi hay không), không có cách nào để lý giải về sự bất biến của nó (và có khả năng là nó không có).

Tôi đã xem qua cuốn sách mẫu thiết kế Gang of Four để tìm cảm hứng, nhưng tôi không cảm thấy như có một giải pháp trong đó (Memento có phần giống với tinh thần, nhưng không hoàn toàn). Tôi cũng đã tìm kiếm trực tuyến, nhưng lần thứ hai bạn tìm kiếm "đường ống" hoặc "quy trình công việc" bạn sẽ bị ngập trong thông tin về các đường dẫn Unix, hoặc các công cụ và khung công việc độc quyền.

Câu hỏi của tôi là - làm thế nào bạn sẽ tiếp cận vấn đề ghi lại trạng thái thực thi của một đường ống xử lý phần mềm, để các tác vụ sau này có thể sử dụng thông tin được tính toán bởi những cái trước đó? Tôi đoán sự khác biệt lớn với các ống Unix là bạn không quan tâm đến đầu ra của nhiệm vụ ngay trước đó.


Theo yêu cầu, một số mã giả để minh họa trường hợp sử dụng của tôi:

Đối tượng "bối cảnh đường ống" có một loạt các trường mà các bước đường ống khác nhau có thể cư trú / đọc:

public class PipelineCtx {
    ... // fields
    public Foo getFoo() { return this.foo; }
    public void setFoo(Foo aFoo) { this.foo = aFoo; }
    public Bar getBar() { return this.bar; }
    public void setBar(Bar aBar) { this.bar = aBar; }
    ... // more methods
}

Mỗi bước của đường ống cũng là một đối tượng:

public abstract class PipelineStep {
    public abstract PipelineCtx doWork(PipelineCtx ctx);
}

public class BarStep extends PipelineStep {
    @Override
    public PipelineCtx doWork(PipelieCtx ctx) {
        // do work based on the stuff in ctx
        Bar theBar = ...; // compute it
        ctx.setBar(theBar);

        return ctx;
    }
}

Tương tự như vậy đối với một giả thuyết FooStep, có thể cần Bar được tính toán bởi BarStep trước nó, cùng với các dữ liệu khác. Và sau đó chúng ta có lệnh gọi API thực sự:

public class BlahOperation extends ProprietaryWebServiceApiBase {
    public BlahResponse handle(BlahRequest request) {
        PipelineCtx ctx = PipelineCtx.from(request);

        // some steps happen here
        // ...

        BarStep barStep = new BarStep();
        barStep.doWork(crx);

        // some more steps maybe
        // ...

        FooStep fooStep = new FooStep();
        fooStep.doWork(ctx);

        // final steps ...

        return BlahResponse.from(ctx);
    }
}

6
không đăng bài chéo nhưng gắn cờ cho một mod để di chuyển
ratchet freak

1
Sẽ tiếp tục, tôi đoán tôi nên dành nhiều thời gian hơn để làm quen với các quy tắc. Cảm ơn!
RuslanD

1
Bạn đang tránh bất kỳ lưu trữ dữ liệu liên tục cho việc thực hiện của bạn, hoặc có bất cứ điều gì để lấy tại thời điểm này?
CokoBWare

1
Xin chào RuslanD và chào mừng! Điều này thực sự phù hợp với Lập trình viên hơn Stack Overflow, vì vậy chúng tôi đã xóa phiên bản SO. Hãy ghi nhớ những gì @ratchetfreak đã đề cập, bạn có thể gắn cờ cho sự chú ý kiểm duyệt và yêu cầu một câu hỏi được di chuyển đến một trang web phù hợp hơn, không cần phải đăng bài. Nguyên tắc lựa chọn giữa hai trang web là Lập trình viên dành cho các vấn đề bạn gặp phải khi bạn đứng trước bảng trắng thiết kế dự án của bạn và Stack Overflow dành cho các vấn đề kỹ thuật hơn (ví dụ: các vấn đề triển khai). Để biết thêm chi tiết, xem Câu hỏi thường gặp của chúng tôi .
yannis

1
Nếu bạn thay đổi kiến ​​trúc thành DAG xử lý (biểu đồ chu kỳ có hướng) thay vì đường ống, bạn có thể vượt qua kết quả của các bước trước đó một cách rõ ràng.
Patrick

Câu trả lời:


4

Lý do chính để sử dụng một thiết kế đường ống là bạn muốn tách rời các giai đoạn. Vì một giai đoạn có thể được sử dụng trong nhiều đường ống (như các công cụ shell Unix) hoặc bởi vì bạn có được một số lợi ích mở rộng (nghĩa là bạn có thể dễ dàng chuyển từ kiến ​​trúc một nút sang kiến ​​trúc nhiều nút).

Trong cả hai trường hợp, mỗi giai đoạn trong đường ống cần phải được cung cấp mọi thứ mà nó cần để thực hiện công việc của mình. Không có lý do gì mà bạn không thể sử dụng một cửa hàng bên ngoài (ví dụ: cơ sở dữ liệu), nhưng trong hầu hết các trường hợp, tốt hơn hết là truyền dữ liệu từ giai đoạn này sang giai đoạn khác.

Tuy nhiên, điều đó không có nghĩa là bạn phải hoặc nên vượt qua một đối tượng tin nhắn lớn với mọi trường có thể (mặc dù xem bên dưới). Thay vào đó, mỗi giai đoạn trong đường ống nên xác định giao diện cho các thông điệp đầu vào và đầu ra của nó, chỉ xác định dữ liệu mà giai đoạn đó cần.

Sau đó, bạn có rất nhiều sự linh hoạt trong cách bạn thực hiện các đối tượng tin nhắn thực tế của bạn. Một cách tiếp cận là sử dụng một đối tượng dữ liệu khổng lồ thực hiện tất cả các giao diện cần thiết. Một cách khác là tạo các lớp bao bọc xung quanh một cách đơn giản Map. Một cách khác là tạo một lớp bao bọc xung quanh cơ sở dữ liệu.


1

Có một vài suy nghĩ nảy ra trong đầu, đầu tiên là tôi không có đủ thông tin.

  • Có phải mỗi bước tạo ra dữ liệu được sử dụng ngoài đường ống, hoặc chúng ta chỉ quan tâm đến kết quả của giai đoạn cuối?
  • Có nhiều mối quan tâm dữ liệu lớn? I E. mối quan tâm bộ nhớ, mối quan tâm tốc độ, vv

Các câu trả lời có thể sẽ khiến tôi suy nghĩ kỹ hơn về thiết kế, tuy nhiên dựa trên những gì bạn nói có 2 cách tiếp cận tôi có thể sẽ xem xét đầu tiên.

Cấu trúc mỗi giai đoạn như là đối tượng riêng của nó. Giai đoạn thứ n sẽ có các giai đoạn từ 1 đến n dưới dạng danh sách các đại biểu. Mỗi giai đoạn đóng gói dữ liệu và xử lý dữ liệu; giảm độ phức tạp tổng thể và các trường trong mỗi đối tượng. Bạn cũng có thể có các giai đoạn sau truy cập dữ liệu khi cần từ các giai đoạn trước đó bằng cách duyệt qua các đại biểu. Bạn vẫn có sự kết hợp khá chặt chẽ giữa tất cả các đối tượng bởi vì đó là kết quả của các giai đoạn (nghĩa là tất cả các thành phần) rất quan trọng, nhưng nó giảm đáng kể và mỗi giai đoạn / đối tượng có thể dễ đọc và dễ hiểu hơn. Bạn có thể làm cho chuỗi đó an toàn bằng cách làm cho danh sách đại biểu trở nên lười biếng và sử dụng hàng đợi an toàn của luồng để điền vào danh sách đại biểu trong mỗi đối tượng khi cần.

Ngoài ra, tôi có thể sẽ làm một cái gì đó tương tự như những gì bạn đang làm. Một đối tượng dữ liệu lớn đi qua các chức năng đại diện cho từng giai đoạn. Điều này thường nhanh hơn và trọng lượng nhẹ hơn, nhưng phức tạp hơn và dễ bị lỗi vì nó chỉ là một đống lớn các thuộc tính dữ liệu. Rõ ràng là không an toàn chủ đề.

Thành thật tôi đã thực hiện một lần sau thường xuyên hơn cho ETL và một số vấn đề tương tự khác. Tôi đã tập trung vào hiệu suất vì lượng dữ liệu hơn là khả năng bảo trì. Ngoài ra, chúng là một lần sẽ không được sử dụng lại.


1

Điều này trông giống như một mô hình chuỗi trong GoF.

Một điểm khởi đầu tốt sẽ là xem xét chuỗi commons làm gì .

Một kỹ thuật phổ biến để tổ chức thực hiện các luồng xử lý phức tạp là mẫu "Chuỗi trách nhiệm", như được mô tả (trong số nhiều nơi khác) trong cuốn sách mẫu thiết kế "Gang of Four" cổ điển. Mặc dù các hợp đồng API cơ bản cần có để thực hiện patten thiết kế này cực kỳ đơn giản, nhưng thật hữu ích khi có một API cơ sở tạo điều kiện cho việc sử dụng mẫu và (quan trọng hơn) khuyến khích thành phần thực thi lệnh từ nhiều nguồn khác nhau.

Để đạt được điều đó, API Chuỗi mô hình hóa một tính toán dưới dạng một loạt các "lệnh" có thể được kết hợp thành một "chuỗi". API cho một lệnh bao gồm một phương thức duy nhất ( execute()), được truyền tham số "bối cảnh" có chứa trạng thái động của tính toán và giá trị trả về của nó là một boolean xác định xem có xử lý được chuỗi hiện tại hay không ( đúng) hoặc liệu việc xử lý có nên được ủy quyền cho lệnh tiếp theo trong chuỗi không (false).

Sự trừu tượng hóa "bối cảnh" được thiết kế để tách biệt các triển khai lệnh khỏi môi trường mà chúng đang chạy (chẳng hạn như một lệnh có thể được sử dụng trong Servlet hoặc Portlet, mà không bị ràng buộc trực tiếp với các hợp đồng API của một trong hai môi trường này). Đối với các lệnh cần phân bổ tài nguyên trước khi ủy quyền và sau đó giải phóng chúng khi trả về (ngay cả khi lệnh được ủy nhiệm ném ngoại lệ), phần mở rộng "bộ lọc" thành "lệnh" cung cấp postprocess()phương thức cho việc dọn dẹp này. Cuối cùng, các lệnh có thể được lưu trữ và tra cứu trong một "danh mục" để cho phép trì hoãn quyết định về lệnh nào (hoặc chuỗi) thực sự được thực thi.

Để tối đa hóa tính hữu ích của API mẫu của Chuỗi trách nhiệm, các hợp đồng giao diện cơ bản được xác định theo cách không phụ thuộc khác với JDK thích hợp. Việc triển khai lớp cơ sở tiện lợi của các API này được cung cấp, cũng như các triển khai chuyên biệt hơn (nhưng tùy chọn) cho môi trường web (ví dụ: servlets và portlets).

Do việc triển khai lệnh được thiết kế để phù hợp với các khuyến nghị này, nên có thể sử dụng API chuỗi trách nhiệm trong "bộ điều khiển phía trước" của khung ứng dụng web (như Struts), nhưng cũng có thể sử dụng nó trong kinh doanh logic và các lớp kiên trì để mô hình hóa các yêu cầu tính toán phức tạp thông qua thành phần. Ngoài ra, việc tách một tính toán thành các lệnh rời rạc hoạt động trong bối cảnh mục đích chung cho phép tạo ra các lệnh dễ dàng hơn để kiểm tra đơn vị, bởi vì tác động của việc thực hiện lệnh có thể được đo trực tiếp bằng cách quan sát các thay đổi trạng thái tương ứng trong bối cảnh được cung cấp ...


0

Một giải pháp đầu tiên tôi có thể tưởng tượng là làm cho các bước rõ ràng. Mỗi người trong số họ trở thành một đối tượng có thể xử lý một phần dữ liệu và truyền nó đến đối tượng xử lý tiếp theo. Mỗi quy trình tạo ra một sản phẩm mới (lý tưởng là bất biến), do đó không có sự tương tác giữa các quy trình và sau đó không có rủi ro do chia sẻ dữ liệu. Nếu một số quy trình tốn nhiều thời gian hơn một số quy trình khác, bạn có thể đặt một số bộ đệm giữa hai quy trình. Nếu bạn khai thác chính xác một trình lập lịch biểu cho đa luồng, nó sẽ phân bổ nhiều nguồn tài nguyên hơn để xóa bộ đệm.

Một giải pháp thứ hai có thể là nghĩ "thông điệp" thay vì đường ống, có thể bằng một khung chuyên dụng. Sau đó, bạn có một số "diễn viên" nhận tin nhắn từ các diễn viên khác và gửi tin nhắn khác cho các diễn viên khác. Bạn sắp xếp các diễn viên của mình theo một đường ống dẫn và cung cấp dữ liệu chính của bạn cho một diễn viên đầu tiên khởi xướng chuỗi. Không có chia sẻ dữ liệu vì việc chia sẻ được thay thế bằng việc gửi tin nhắn. Tôi biết mô hình diễn viên của Scala có thể được sử dụng trong Java, vì không có gì cụ thể về Scala ở đây, nhưng tôi chưa bao giờ sử dụng nó trong chương trình Java.

Các giải pháp tương tự nhau và bạn có thể thực hiện cái thứ hai với cái thứ nhất. Về cơ bản, các khái niệm chính là để đối phó với dữ liệu bất biến để tránh các vấn đề truyền thống do chia sẻ dữ liệu và tạo các thực thể rõ ràng và độc lập đại diện cho các quy trình trong đường ống của bạn. Nếu bạn đáp ứng các điều kiện này, bạn có thể dễ dàng tạo các đường ống rõ ràng, đơn giản và sử dụng chúng trong một chương trình song song.


Này, tôi đã cập nhật câu hỏi của mình với một số mã giả - thực tế chúng tôi có các bước rõ ràng.
RuslanD
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.