Làm thế nào để dễ dàng duy trì mã hướng sự kiện?


16

Khi sử dụng một thành phần dựa trên sự kiện tôi thường cảm thấy đau ở giai đoạn bảo trì.

Vì mã thực thi được phân chia xung quanh nên có thể khá khó để tìm ra phần nào sẽ là phần mã sẽ tham gia vào thời gian chạy.

Điều này có thể dẫn đến các vấn đề tinh vi và khó gỡ lỗi khi ai đó thêm một số trình xử lý sự kiện mới.

Chỉnh sửa từ nhận xét: Ngay cả với một số thực tiễn tốt trên tàu, như có một xe buýt sự kiện rộng ứng dụng và người xử lý ủy thác kinh doanh cho phần khác của ứng dụng, có một thời điểm mã bắt đầu trở nên khó đọc vì có rất nhiều xử lý đã đăng ký từ nhiều nơi khác nhau (đặc biệt đúng khi có xe buýt).

Sau đó, sơ đồ trình tự bắt đầu xem xét phức tạp, dành thời gian để tìm hiểu những gì đang xảy ra và phiên gỡ lỗi trở nên lộn xộn (điểm dừng trên trình quản lý trình xử lý trong khi lặp trên trình xử lý, đặc biệt vui mừng với trình xử lý async và một số bộ lọc trên đầu trang).

//////////////
Ví dụ

Tôi có một dịch vụ đang lấy một số dữ liệu trên máy chủ. Trên máy khách, chúng tôi có một thành phần cơ bản đang gọi dịch vụ này bằng cách gọi lại. Để cung cấp điểm mở rộng cho người dùng thành phần và để tránh ghép nối giữa các thành phần khác nhau, chúng tôi sẽ thực hiện một số sự kiện: một sự kiện trước khi truy vấn được gửi, một khi câu trả lời trở lại và một sự kiện khác trong trường hợp thất bại. Chúng tôi có một bộ trình xử lý cơ bản được đăng ký trước, cung cấp hành vi mặc định của thành phần.

Bây giờ người dùng của thành phần (và chúng tôi cũng là người dùng của thành phần) có thể thêm một số trình xử lý để thực hiện một số thay đổi về hành vi (sửa đổi truy vấn, nhật ký, phân tích dữ liệu, lọc dữ liệu, tạo khối dữ liệu, hoạt hình ưa thích UI, chuỗi nhiều truy vấn tuần tự , bất cứ điều gì). Vì vậy, một số trình xử lý phải được thực thi trước / sau một số trình xử lý khác và chúng được đăng ký từ rất nhiều điểm nhập khác nhau trong ứng dụng.

Sau một thời gian, có thể xảy ra việc một tá hoặc nhiều trình xử lý được đăng ký và làm việc với điều đó có thể tẻ nhạt và nguy hiểm.

Thiết kế này nổi lên vì sử dụng sự kế thừa đã bắt đầu là một mớ hỗn độn. Hệ thống sự kiện được sử dụng tại một loại sáng tác mà bạn chưa biết những gì sẽ là vật liệu tổng hợp của bạn.

Kết thúc ví dụ
//////////////

Vì vậy, tôi tự hỏi làm thế nào những người khác đang giải quyết loại mã này. Cả khi viết và đọc nó.

Bạn có bất kỳ phương pháp hoặc công cụ nào cho phép bạn viết và duy trì mã như vậy mà không phải chịu nhiều đau đớn không?


Ý bạn là, bên cạnh việc tái cấu trúc logic từ các trình xử lý sự kiện?
Telastyn

Tài liệu những gì diễn ra.

@Telastyn, tôi không chắc chắn hiểu đầy đủ ý của bạn bằng cách 'bên cạnh việc tái cấu trúc logic từ các trình xử lý sự kiện'.
Guillaume

@Thorbjoern: xem cập nhật của tôi.
Guillaume

3
Có vẻ như bạn không sử dụng đúng công cụ cho công việc? Ý tôi là, nếu thứ tự quan trọng, thì bạn không nên sử dụng các sự kiện đơn giản cho các phần đó của ứng dụng ngay từ đầu. Về cơ bản nếu có những hạn chế về thứ tự các sự kiện trong một hệ thống xe buýt thông thường, thì đó không phải là một hệ thống xe buýt thông thường nữa, nhưng bạn vẫn đang sử dụng nó như vậy. Điều đó có nghĩa là bạn cần giải quyết vấn đề đó bằng cách thêm một số logic bổ sung để xử lý đơn hàng. Ít nhất, nếu tôi hiểu chính xác vấn đề của bạn ..
stijn

Câu trả lời:


7

Tôi đã thấy rằng việc xử lý các sự kiện bằng cách sử dụng một chồng các sự kiện nội bộ (cụ thể hơn là hàng đợi LIFO với việc loại bỏ tùy ý) giúp đơn giản hóa đáng kể việc lập trình theo hướng sự kiện. Nó cho phép bạn phân chia việc xử lý một "sự kiện bên ngoài" thành một số "sự kiện nội bộ" nhỏ hơn, với trạng thái được xác định rõ ở giữa. Để biết thêm thông tin, xem câu trả lời của tôi cho câu hỏi này .

Ở đây tôi trình bày một ví dụ đơn giản được giải quyết bằng mẫu này.

Giả sử bạn đang sử dụng đối tượng A để thực hiện một số dịch vụ và bạn gọi lại để thông báo cho bạn khi hoàn thành. Tuy nhiên, A là như vậy sau khi gọi lại cuộc gọi của bạn, nó có thể cần thực hiện thêm một số công việc. Một mối nguy hiểm phát sinh khi, trong cuộc gọi lại đó, bạn quyết định rằng bạn không cần A nữa và bạn phá hủy nó bằng cách này hay cách khác. Nhưng bạn đang được gọi từ A - nếu A, sau khi bạn gọi lại, không thể nhận ra rằng nó đã bị hủy một cách an toàn, một sự cố có thể xảy ra khi nó cố gắng thực hiện công việc còn lại.

LƯU Ý: Đúng là bạn có thể thực hiện "hủy diệt" theo một cách khác, như giảm số lần từ chối, nhưng điều đó chỉ dẫn đến các trạng thái trung gian, và thêm mã và lỗi khi xử lý chúng; tốt hơn là A chỉ dừng hoạt động hoàn toàn sau khi bạn không cần nó nữa ngoài việc tiếp tục ở một số trạng thái trung gian.

Trong mẫu của tôi, A chỉ cần lên lịch cho công việc tiếp theo cần thực hiện bằng cách đẩy một sự kiện nội bộ (công việc) vào hàng đợi LIFO của vòng lặp sự kiện, sau đó tiến hành gọi lại và quay lại vòng lặp sự kiện ngay lập tức . Đoạn mã này không còn là mối nguy hiểm nữa, vì A vừa trở về. Bây giờ, nếu cuộc gọi lại không phá hủy A, công việc được đẩy cuối cùng sẽ được thực hiện bởi vòng lặp sự kiện để thực hiện công việc phụ của nó (sau khi thực hiện cuộc gọi lại và tất cả các công việc được đẩy của nó, theo cách đệ quy). Mặt khác, nếu hàm gọi lại phá hủy A, hàm hủy hoặc hàm deinit của A có thể loại bỏ công việc được đẩy ra khỏi ngăn xếp sự kiện, ngầm ngăn chặn việc thực hiện công việc được đẩy.


7

Tôi nghĩ rằng đăng nhập thích hợp có thể giúp khá lớn. Đảm bảo rằng mọi sự kiện được ném / xử lý được ghi lại ở đâu đó (bạn có thể sử dụng các khung ghi nhật ký cho việc này). Khi bạn gỡ lỗi, bạn có thể tham khảo nhật ký để xem thứ tự thực hiện chính xác mã của mình khi xảy ra lỗi. Thường thì điều này sẽ thực sự giúp thu hẹp nguyên nhân của vấn đề.


Vâng, đó là một lời khuyên hữu ích. Số lượng nhật ký được sản xuất sau đó có thể đáng sợ (tôi có thể nhớ rằng đã có một thời gian khó khăn để tìm ra nguyên nhân của một chu kỳ trong quá trình xử lý sự kiện của một hình thức rất phức tạp.)
Guillaume

Có thể một giải pháp là ghi nhật ký thông tin thú vị vào một tệp nhật ký riêng. Ngoài ra, bạn có thể chỉ định UUID cho từng sự kiện, do đó bạn có thể theo dõi từng sự kiện dễ dàng hơn. Bạn cũng có thể viết một số công cụ báo cáo cho phép bạn trích xuất thông tin cụ thể từ các tệp nhật ký. (Hoặc cách khác, sử dụng các công tắc trong mã để ghi thông tin khác nhau vào các tệp nhật ký khác nhau).
Giorgio

3

Vì vậy, tôi tự hỏi làm thế nào những người khác đang giải quyết loại mã này. Cả khi viết và đọc nó.

Mô hình lập trình hướng sự kiện đơn giản hóa mã hóa ở một mức độ nào đó. Nó có lẽ đã phát triển như một sự thay thế cho các câu lệnh Chọn (hoặc trường hợp) lớn được sử dụng trong các ngôn ngữ cũ và trở nên phổ biến trong các môi trường phát triển Visual sớm như VB 3 (Đừng trích dẫn tôi về lịch sử, tôi đã không kiểm tra nó)!

Mô hình trở thành nỗi đau nếu chuỗi sự kiện quan trọng và khi 1 hành động kinh doanh được chia thành nhiều sự kiện. Phong cách của quá trình này vi phạm lợi ích của phương pháp này. Bằng mọi giá, hãy cố gắng làm cho mã hành động được gói gọn trong sự kiện tương ứng và không nêu ra các sự kiện từ bên trong các sự kiện. Điều đó sau đó trở nên tồi tệ hơn nhiều so với Spaghetti kết quả từ GoTo.

Đôi khi các nhà phát triển mong muốn cung cấp chức năng GUI yêu cầu sự phụ thuộc sự kiện như vậy, nhưng thực sự không có sự thay thế thực sự nào đơn giản hơn đáng kể.

Điểm mấu chốt ở đây là kỹ thuật không tệ nếu được sử dụng một cách khôn ngoan.


Có bất kỳ thiết kế thay thế nào của họ để tránh 'tệ hơn Spaghetti' không?
Guillaume

Tôi không chắc chắn có một cách dễ dàng mà không cần đơn giản hóa hành vi GUI dự kiến, nhưng nếu bạn liệt kê tất cả các tương tác người dùng của bạn với một cửa sổ / trang và để xác định chính xác chương trình của bạn sẽ làm gì, bạn có thể bắt đầu nhóm mã vào một nơi và trực tiếp một số sự kiện cho một nhóm các phương thức / phụ trách chịu trách nhiệm xử lý thực sự của yêu cầu. Ngoài ra, việc tách mã chỉ phục vụ GUI khỏi mã xử lý back end có thể giúp ích.
NoChance

Nhu cầu đăng thông báo này xuất phát từ việc tái cấu trúc khi tôi chuyển từ địa ngục thừa kế sang một thứ gì đó dựa trên sự kiện. Về mặt nào đó thì nó thực sự tốt hơn, nhưng ở một khía cạnh khác thì nó khá tệ ... Vì tôi đã gặp một số rắc rối với "sự kiện diễn ra" trong một số GUI, tôi tự hỏi có thể làm gì để cải thiện việc bảo trì như vậy sự kiện mã cắt lát.
Guillaume

Lập trình hướng sự kiện cũ hơn nhiều so với VB; nó đã có mặt trong GUI SunTools và trước đó tôi dường như nhớ rằng nó đã được tích hợp vào ngôn ngữ Simula.
kevin cline

@Guillaume, tôi đoán bạn quá kỹ thuật dịch vụ. Mô tả ở trên của tôi trên thực tế dựa trên các sự kiện GUI. Bạn có (thực sự) cần loại xử lý này?
NoChance

3

Tôi muốn cập nhật câu trả lời này vì tôi đã nhận được một số khoảnh khắc eureka kể từ sau khi "làm phẳng" và "làm phẳng" các luồng điều khiển và đã hình thành một số suy nghĩ mới về chủ đề này.

Tác dụng phụ phức tạp so với dòng điều khiển phức tạp

Những gì tôi đã tìm thấy là bộ não của tôi có thể chịu được các tác dụng phụ phức tạp hoặc các luồng điều khiển giống như đồ thị phức tạp như bạn thường thấy khi xử lý sự kiện, nhưng không phải là kết hợp của cả hai.

Tôi có thể dễ dàng suy luận về mã gây ra 4 tác dụng phụ khác nhau nếu chúng được áp dụng với luồng điều khiển rất đơn giản, giống như mã tuần tự for vòng lặp. Bộ não của tôi có thể chịu đựng một vòng tuần tự thay đổi kích thước và định vị lại các yếu tố, hoạt hình chúng, vẽ lại chúng và cập nhật một số loại trạng thái phụ trợ. Điều đó đủ dễ hiểu.

Tôi cũng có thể hiểu một luồng điều khiển phức tạp như bạn có thể nhận được với các sự kiện xếp tầng hoặc duyệt qua cấu trúc dữ liệu giống như biểu đồ phức tạp nếu chỉ có một hiệu ứng phụ rất đơn giản xảy ra trong quy trình khi thứ tự không quan trọng một chút, như đánh dấu các phần tử được xử lý theo kiểu trì hoãn trong một vòng tuần tự đơn giản.

Nơi tôi bị lạc và bối rối và choáng ngợp là khi bạn có các luồng điều khiển phức tạp gây ra các tác dụng phụ phức tạp. Trong trường hợp đó, luồng điều khiển phức tạp gây khó khăn cho việc dự đoán trước nơi bạn sẽ kết thúc trong khi các tác dụng phụ phức tạp làm cho khó dự đoán chính xác điều gì sẽ xảy ra do kết quả và theo thứ tự. Vì vậy, sự kết hợp của hai điều này khiến nó trở nên khó chịu ở đâu, ngay cả khi mã hoạt động hoàn toàn tốt ngay bây giờ, thật đáng sợ khi thay đổi nó mà không sợ gây ra tác dụng phụ không mong muốn.

Các luồng điều khiển phức tạp có xu hướng gây khó khăn cho việc suy luận về thời điểm / nơi mọi thứ sẽ xảy ra. Điều đó chỉ thực sự gây đau đầu nếu các luồng kiểm soát phức tạp này đang kích hoạt một sự kết hợp phức tạp của các tác dụng phụ trong đó điều quan trọng là phải hiểu khi nào / nơi xảy ra, như tác dụng phụ có một số loại phụ thuộc trật tự xảy ra trước một điều tiếp theo.

Đơn giản hóa luồng điều khiển hoặc tác dụng phụ

Vậy bạn sẽ làm gì khi gặp phải tình huống trên rất khó hiểu? Chiến lược là đơn giản hóa dòng điều khiển hoặc tác dụng phụ.

Một chiến lược được áp dụng rộng rãi để đơn giản hóa các tác dụng phụ là ưu tiên xử lý hoãn lại. Sử dụng một sự kiện thay đổi kích thước GUI làm ví dụ, sự cám dỗ thông thường có thể là áp dụng lại bố cục GUI, định vị lại và thay đổi kích thước các widget con, kích hoạt một tầng khác của các ứng dụng bố trí và thay đổi kích thước và định vị lại hệ thống phân cấp, cùng với việc sơn lại các điều khiển, có thể kích hoạt một số điều khiển các sự kiện duy nhất cho các widget có hành vi thay đổi kích thước tùy chỉnh sẽ kích hoạt nhiều sự kiện hơn dẫn đến ai biết ở đâu, v.v. Thay vì cố gắng thực hiện tất cả trong một lần hoặc bằng cách spam hàng đợi sự kiện, một giải pháp khả thi là hạ xuống hệ thống phân cấp widget và đánh dấu những vật dụng cần bố trí của chúng để được cập nhật. Sau đó, trong một lần vượt qua sau đó, có một luồng điều khiển tuần tự đơn giản, áp dụng lại tất cả các bố trí cho các vật dụng cần nó. Sau đó, bạn có thể đánh dấu những vật dụng cần phải được sơn lại. Một lần nữa trong một lần trì hoãn tuần tự với luồng điều khiển đơn giản, sơn lại các vật dụng được đánh dấu là cần phải được vẽ lại.

Điều này có tác dụng vừa đơn giản hóa luồng điều khiển và tác dụng phụ do luồng điều khiển trở nên đơn giản hóa do nó không xếp tầng các sự kiện đệ quy trong quá trình truyền đồ thị. Thay vào đó, các tầng xảy ra trong vòng tuần tự bị trì hoãn mà sau đó có thể được xử lý trong một vòng tuần tự bị trì hoãn khác. Các tác dụng phụ trở nên đơn giản khi nó được tính từ, trong các luồng điều khiển giống như đồ thị phức tạp hơn, tất cả những gì chúng ta đang làm chỉ đơn giản là đánh dấu những gì cần xử lý bằng các vòng lặp liên tục bị trì hoãn gây ra các tác dụng phụ phức tạp hơn.

Điều này không đi kèm với một số chi phí xử lý nhưng sau đó có thể mở ra các cánh cửa để thực hiện các đường chuyền bị hoãn này song song, có khả năng cho phép bạn có được một giải pháp thậm chí hiệu quả hơn so với khi bạn bắt đầu nếu hiệu suất là vấn đề đáng lo ngại. Nói chung, hiệu suất không phải là mối quan tâm trong hầu hết các trường hợp. Quan trọng nhất, trong khi điều này có vẻ như là một sự khác biệt, tôi đã thấy nó dễ dàng hơn nhiều để lý do. Nó giúp dễ dàng hơn nhiều để dự đoán những gì đang xảy ra và khi nào, và tôi không thể đánh giá quá cao giá trị có thể có để dễ dàng hiểu được những gì đang diễn ra.


2

Những gì đã làm việc cho tôi là làm cho mỗi sự kiện đứng riêng, mà không cần tham khảo các sự kiện khác. Nếu chúng đến không đồng bộ, bạn không trình tự, vì vậy cố gắng tìm hiểu điều gì xảy ra theo thứ tự là vô nghĩa, bên cạnh đó là không thể.

Những gì bạn làm cuối cùng là một loạt các cấu trúc dữ liệu đang được đọc và sửa đổi và được tạo và xóa bởi một tá các luồng không theo thứ tự cụ thể. Bạn đã phải thực hiện lập trình đa luồng rất phù hợp, điều này không dễ dàng. Bạn cũng đã phải suy nghĩ đa luồng, như trong "Với sự kiện này, tôi sẽ xem xét dữ liệu tôi có ngay lập tức, không quan tâm đến những gì nó là một phần triệu giây trước đó, mà không quan tâm đến những gì vừa thay đổi nó và không quan tâm đến việc 100 chủ đề đang chờ tôi phát hành khóa sẽ làm gì với nó. Sau đó, tôi sẽ thực hiện các thay đổi của mình dựa trên điều này ngay cả và những gì tôi thấy. Sau đó, tôi đã hoàn thành. "

Một điều tôi thấy mình đang làm là quét một Bộ sưu tập cụ thể và đảm bảo rằng cả tham chiếu và chính bộ sưu tập (nếu không phải là chủ đề an toàn) được khóa chính xác và được đồng bộ hóa chính xác với dữ liệu khác. Càng nhiều sự kiện được thêm vào, công việc này càng phát triển. Nhưng nếu tôi đã theo dõi các mối quan hệ giữa các sự kiện, rằng việc vặt sẽ phát triển nhanh hơn rất nhiều. Ngoài ra, đôi khi rất nhiều khóa có thể được phân lập theo phương thức riêng của nó, thực sự làm cho mã đơn giản hơn.

Đối xử với mỗi luồng như một thực thể hoàn toàn độc lập là khó khăn (vì đa luồng lõi cứng) nhưng có thể thực hiện được. "Khả năng mở rộng" có thể là từ tôi đang tìm kiếm. Hai lần nhiều sự kiện chỉ mất gấp đôi công việc và có thể chỉ gấp 1,5 lần. Cố gắng phối hợp các sự kiện không đồng bộ hơn sẽ chôn vùi bạn nhanh chóng.


Tôi đã cập nhật câu hỏi của tôi. Bạn hoàn toàn đúng về trình tự và công cụ không đồng bộ. Làm thế nào bạn sẽ xử lý một trường hợp trong đó sự kiện X phải được thực hiện sau khi tất cả các sự kiện khác được xử lý? Có vẻ như tôi phải tạo một trình quản lý Handlers phức tạp hơn, nhưng tôi cũng muốn tránh để lộ quá nhiều sự phức tạp cho người dùng.
Guillaume

Trong bộ dữ liệu của bạn, có một công tắc và một số trường được đặt khi sự kiện X được đọc. Khi tất cả những người khác được xử lý, bạn kiểm tra công tắc và biết rằng bạn phải xử lý X và bạn có dữ liệu. Việc chuyển đổi và dữ liệu nên tự đứng trên thực tế. Khi được đặt, bạn nên nghĩ, "Tôi phải làm công việc này" chứ không phải "Tôi phải giao cho X." Vấn đề tiếp theo: làm thế nào để bạn biết các sự kiện được thực hiện? và nếu bạn nhận được 2 sự kiện X trở lên thì sao? Trường hợp xấu nhất, bạn có thể chạy một chuỗi bảo trì lặp để kiểm tra tình huống và có thể tự mình thực hiện. (Không có đầu vào trong 3 giây? Công tắc X được đặt? Sau đó chạy mã tắt máy.
RalphChapin

2

Có vẻ như bạn đang tìm kiếm các Máy móc Nhà nước & Hoạt động hướng sự kiện .

Tuy nhiên, bạn cũng có thể muốn xem Mẫu quy trình đánh dấu máy tính trạng thái .

Ở đây bạn là một tổng quan ngắn về thực hiện máy trạng thái. Một quy trình làm việc của máy trạng thái bao gồm các trạng thái. Mỗi tiểu bang bao gồm một hoặc nhiều trình xử lý sự kiện. Mỗi trình xử lý sự kiện phải chứa một độ trễ hoặc IEventActivity là hoạt động đầu tiên. Mỗi trình xử lý sự kiện cũng có thể chứa một hoạt động SetStateActivity được sử dụng để chuyển từ trạng thái này sang trạng thái khác.

Mỗi quy trình làm việc của máy trạng thái có hai thuộc tính: InitialStateName và CompleteedStateName. Khi một phiên bản của dòng công việc máy trạng thái được tạo, nó sẽ được đưa vào thuộc tính InitialStateName. Khi máy trạng thái đạt đến thuộc tính CompleteedStateName, nó sẽ hoàn thành thực thi.


2
Trong khi về mặt lý thuyết có thể trả lời câu hỏi, tốt hơn là nên bao gồm các phần thiết yếu của câu trả lời ở đây và cung cấp liên kết để tham khảo.
Thomas Owens

Nhưng nếu bạn có hàng tá trình xử lý được gắn vào mỗi trạng thái, bạn sẽ không ổn khi cố gắng hiểu điều gì đang xảy ra ... Và mọi thành phần dựa trên sự kiện có thể không được mô tả như một máy trạng thái.
Guillaume

1

Mã hướng sự kiện không phải là vấn đề thực sự. Tôi thực sự không có vấn đề gì sau logic trong mã được điều khiển, trong đó gọi lại được xác định rõ ràng hoặc gọi lại trong dòng được sử dụng. Ví dụ, các cuộc gọi lại kiểu trình tạo trong Tornado rất dễ thực hiện.

Những gì thực sự khó để gỡ lỗi là các lệnh gọi hàm được tạo động. Mẫu (chống?) Mà tôi sẽ gọi là Nhà máy gọi lại từ Địa ngục. Tuy nhiên, loại nhà máy chức năng này cũng khó gỡ lỗi trong dòng chảy truyền thống.

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.