Εισαγωγή στον Συμπεριφορικό Προγραμματισμό με το React: αίτηση, αναμονή και αποκλεισμός

Το Behavioral Programming (BP) είναι ένα παράδειγμα που επινοήθηκε στο άρθρο του 2012 από τους David Harel, Assaf Marron και Gera Weiss.

Άμεσα από την περίληψη:

Ο προγραμματισμός συμπεριφοράς απλοποιεί το καθήκον της αντιμετώπισης της κάτω προδιαγραφής και των αντικρουόμενων απαιτήσεων, επιτρέποντας την προσθήκη λειτουργικών μονάδων λογισμικού που όχι μόνο μπορούν να προσθέσουν αλλά και να τροποποιήσουν υπάρχουσες συμπεριφορές .

Έννοιες υψηλού επιπέδου

Θα εξηγήσω πρώτα τις έννοιες υψηλού επιπέδου χρησιμοποιώντας ένα παράδειγμα δύο στοιχείων React MoviesListκαι MoviesCount. Το ένα εμφανίζει μια λίστα ταινιών, ενώ το άλλο πόσες ταινίες υπάρχουν. Τότε θα ερευνήσω πώς λειτουργεί ακριβώς ο προγραμματισμός συμπεριφοράς.

Και τα δύο στοιχεία λαμβάνουν δεδομένα από την ίδια διεύθυνση URL HTTP. Αναπτύχθηκαν από δύο διαφορετικές ομάδες σε έναν μεγάλο οργανισμό. Όταν αποδίδουμε και τα δύο στοιχεία σε μια σελίδα, έχουμε ένα πρόβλημα καθώς εκτελούν το ίδιο αίτημα:

Λίγο γνωρίζαμε ότι αυτά είναι στοιχεία συμπεριφοράς . Αυτό σημαίνει ότι μπορούμε να κάνουμε κάτι αρκετά έξυπνο για να αποφύγουμε την ενεργοποίηση και των δύο αιτημάτων:

const MoviesCountFromList = withBehavior([ function* () { // block FETCH_COUNT from happening yield { block: ['FETCH_COUNT'] } }, function* () { // wait for FETCH_LIST, requested by the other // MoviesList component, and derive the count const response = yield { wait: ['FETCH_LIST'] } this.setState({ count: response.length }) }])(MoviesCount)

Στο παραπάνω παράδειγμα, μπήκαμε μέσα στο MoviesCountστοιχείο. Εμείς περιμέναμε και ζήτησε κάτι να συμβεί. Και, πιο μοναδικά στον προγραμματισμό συμπεριφοράς, αποκλείσαμε επίσης κάτι να συμβεί.

Επειδή προσπαθούσαμε να αποφύγουμε την ενεργοποίηση και των δύο αιτημάτων, αποκλείσαμε την FETCH_COUNTενεργοποίηση του συμβάντος (καθώς τα ίδια δεδομένα είχαν ήδη ληφθεί από το FETCH_LISTσυμβάν).

Η προσθήκη λειτουργικότητας σε υπάρχοντα στοιχεία χωρίς να τροποποιηθεί ο κώδικάς τους είναι η καινοτομία του παραδείγματος συμπεριφοράς προγραμματισμού.

Διαισθητικά, αυτό μπορεί να επιτρέψει τη δημιουργία πιο επαναχρησιμοποιήσιμων στοιχείων.

Στο υπόλοιπο άρθρο, θα αναλύσω λεπτομερέστερα πώς λειτουργεί ο προγραμματισμός συμπεριφοράς (BP), ειδικά στο πλαίσιο του React .

Επανεξέταση της ροής προγραμματισμού

Για να επιτύχουμε την παραπάνω λειτουργικότητα, πρέπει να σκεφτούμε σχετικά με τον προγραμματισμό συμπεριφορών λίγο διαφορετικά. Συγκεκριμένα, τα γεγονότα παίζουν καθοριστικό ρόλο στην ενορχήστρωση του συγχρονισμού μεταξύ των διαφόρων συμπεριφορών που ορίζουμε για τα συστατικά μας.

const addHotThreeTimes = behavior( function* () { yield { request: ['ADD_HOT'] } yield { request: ['ADD_HOT'] } yield { request: ['ADD_HOT'] } })
const addColdThreeTimes = behavior( function* () { yield { request: ['ADD_COLD'] } yield { request: ['ADD_COLD'] } yield { request: ['ADD_COLD'] } })
run( addHotThreeTimes, addColdThreeTimes)

Όταν εκτελούμε τον παραπάνω κώδικα, λαμβάνουμε μια λίστα με τα ζητούμενα συμβάντα:

ADD_HOTADD_HOTADD_HOTADD_COLDADD_COLDADD_COLD

Όπως αναμενόταν, η πρώτη συμπεριφορά εκτελείται. Μόλις ολοκληρωθεί, η δεύτερη συμπεριφορά συνεχίζεται. Ωστόσο, οι νέες προδιαγραφές για το στοιχείο μας απαιτούν να αλλάξουμε τη σειρά με την οποία ενεργοποιούνται και τα δύο συμβάντα. Αντί να ενεργοποιούνται ADD_HOTτρεις φορές και μετά ADD_COLDτρεις φορές, θέλουμε να παρεμβάλλονται και να ενεργοποιούνται ADD_COLDαμέσως μετά από ένα ADD_HOT. Αυτό θα διατηρήσει τη θερμοκρασία κάπως σταθερή.

...
const interleave = behavior( function* () { while (true) { // wait for ADD_HOT while blocking ADD_COLD yield { wait: ['ADD_HOT'], block: ['ADD_COLD'] }
 // wait for ADD_COLD while blocking ADD_HOT yield { wait: ['ADD_COLD'], block: ['ADD_HOT'] } } })
run( addHotThreeTimes, addColdThreeTimes, interleave)

Στο παραπάνω παράδειγμα, παρουσιάζουμε μια νέα συμπεριφορά interleave που κάνει ακριβώς αυτό που χρειαζόμαστε.

ADD_HOTADD_COLDADD_HOTADD_COLDADD_HOTADD_COLD

Αλλάξαμε τη σειρά πότε εκτελούνται τα πράγματα, χωρίς να χρειάζεται να τροποποιήσουμε τον κώδικα των ήδη γραπτών συμπεριφορών.

Η διαδικασία συνοψίζεται στο παρακάτω γράφημα.

Οι βασικές έννοιες αυτού του τρόπου προγραμματισμού είναι οι τελεστές αιτήματος , αναμονής και αποκλεισμού . Η σημασιολογία για αυτούς τους χειριστές είναι:

  • Αίτημα ενός συμβάντος: πρόταση να εξεταστεί το συμβάν για ενεργοποίηση και ζήτημα ειδοποίησης όταν ενεργοποιείται
  • Αναμονή για ένα συμβάν: χωρίς να προτείνει την ενεργοποίησή του, ζητώντας να ειδοποιηθείτε όταν ενεργοποιείται το συμβάν
  • Αποκλεισμός ενός συμβάντος: απαγόρευση της ενεργοποίησης του συμβάντος, απαίτηση βέτο για άλλα β-νήματα.

Κάθε νήμα b (νήμα συμπεριφοράς) ζει μόνο του και δεν γνωρίζει άλλα νήματα. Όλοι όμως είναι συνυφασμένοι στο χρόνο εκτέλεσης, κάτι που τους επιτρέπει να αλληλεπιδρούν μεταξύ τους με έναν πολύ νέο τρόπο.

Η σύνταξη της γεννήτριας είναι απαραίτητη για τη λειτουργία ενός προγράμματος συμπεριφοράς. Πρέπει να ελέγξουμε πότε να προχωρήσουμε στην επόμενη δήλωση απόδοσης.

Επιστροφή στο React

Πώς μπορούν αυτές οι έννοιες BP να χρησιμοποιηθούν στο πλαίσιο του React;

Αποδεικνύεται ότι μέσω στοιχείων υψηλής τάξης (HOC), μπορείτε να προσθέσετε αυτό το ιδίωμα συμπεριφοράς σε υπάρχοντα στοιχεία με πολύ διαισθητικό τρόπο:

class CommentsCount extends React.Component { render() { return {this.state.commentsCount} }}
const FetchCommentsCount = withBehavior([ function* () { yield { request: ['FETCH_COMMENTS_COUNT']} const comments = yield fetchComments() yield { request: ['FETCH_COMMENTS_COUNT_SUCCESS']} this.setState({ commentsCount: comments.length }) },])(CommentsCount)

Εδώ χρησιμοποιούμε withBehavior, από τη βιβλιοθήκη νήματος, για να δημιουργήσουμε CommentsCountένα στοιχείο συμπεριφοράς. Συγκεκριμένα, το κάνουμε να πάρει τα σχόλια και να εμφανίσει τα δεδομένα μόλις τα δεδομένα είναι έτοιμα.

Για απλά στοιχεία, αυτό μπορεί να μην είναι τόσο αλλαγή παιχνιδιού. Αλλά ας φανταστούμε πιο περίπλοκα στοιχεία, με πολλή λογική και άλλα συστατικά μέσα τους.

Θα μπορούσαμε να φανταστούμε ολόκληρο τον ιστότοπο του Netflix ως /> component:

When we use this component in our app, we’d like to interact with it. Specifically, when a movie is clicked, we don’t want to start the movie immediately, but instead we want to make an HTTP request, show other data about the movie, and then start the movie.

Without changing code inside the /> component, I’d argue that this would be impossible to achieve without it being a behavioral component.

Instead let’s imagine that /> was developed using behavioral programming:

const NetflixWithMovieInfo = withBehavior([ function* () { // First, block the MOVIE_START from happening // within until a new // FETCH_MOVIE_INFO_SUCCESS event has been requested. // The yield statement below can be read as: // wait for FETCH_MOVIE_INFO_SUCCESS while blocking MOVIE_START yield { wait: ['FETCH_MOVIE_INFO_SUCCESS'], block: ['MOVIE_START'] } }, function* () { // Here we wait for MOVIE_CLICKED, which is // triggered within , and we fetch our // movie info. Once that's done we request a new event // which the earlier behavior is waiting upon const movie = yield { wait: ['MOVIE_CLICKED'] } const movieInfo = yield fetchMovieInfo(movie) yield { request: ['FETCH_MOVIE_INFO_SUCCESS'], payload: movieInfo } }])(Netflix)

Above we’ve created a new NetflixWithMovieInfo component which modifies the behavior of the /> component (again, without changing its source code). The addition of the above behaviors makes it so that MOVIE_CLICKED will not tr igger MOVIE_START immediately.

Instead, it uses a combination of “waiting while blocking”: a wait and a block can be defined within a single yield statement.

The picture above describes, more in detail, what is happening within our behavioral components. Each little box within the components is a yield statement. Each vertical dashed arrow represents a behavior (aka b-thread).

Internally, the behavioral implementation will start by looking at all the yield statements of all b-threads at the current synchronization point, depicted using an horizontal yellow line. It will only continue to the next yield statement within a b-thread if no events in other b-threads are blocking it.

Since nothing is blocking MOVIE_CLICKED , it will be requested. We can then continue to the next yield statement for the Netflix behavior. At the next synch point, the b-thread on the far right, which is waiting for MOVIE_CLICKED, will proceed to its next yield statement.

The middle behavior that is waiting-and-blocking does not proceed. FETCH_MOVIE_INFO_SUCCESS was not requested by other b-threads, so it still waits-and-blocks. The next synchronization point will look something like this:

As before, we will look at all the yield statement at this synchronization point. This time, however, we cannot request MOVIE_START because there’s another b-thread that is blocking it (the black yield statement). The Netflix component will therefore not start the movie.

FETCH_MOVIE_INFO_SUCCESS on the far right, however, is free to be requested. This will unblock MOVIE_START at the next synch point.

All this in practice allowed us to change the order of things happening within other components, without directly modifying their code. We were able to block certain events from firing until other conditions were met in other components.

This changes the way we might think of programming: not necessarily a set of statements executed in order, but rather an interleaving of yield statements all synchronized through specific event semantics.

Here’s a simple animation depicting the way b-threads are executed and interwoven at runtime.

Programming without changing old code

There is another way we can understand this programming idiom. We can compare the way we currently program as specifications change, versus how it would be done with behavioral programming.

In the above caption, we imagine how behavior may be added to a non-behavioral program. We start with a program described only using three black rectangles (on the left).

As specifications change, we realize we need to modify the program and add new behavior in various sections of the program, depicted as newly added colored rectangles. We continue doing this as requirements for our software change.

Every addition of behavior requires us to change code that was written, which possibly litters the old behavior with bugs. Furthermore, if the program we are changing is part of various other modules used by different people, we might be introducing unwanted behavior to their software. Finally, it may not be possible to change specific programs as they might be distributed as libraries with licensed source code.

In the above figure, we see how the same program-modifications can be achieved using behavioral programming idioms. We still start with our three rectangles on the left as we did before. But as new specifications arise, we don’t modify them. Instead we add new b-threads, represented as columns.

The resulting program is the same, although constructed in a very different way. One of the advantages of the behavioral approach is that we don’t have to modify old code as requirements change.

You can also imagine developing each b-thread in parallel, possibly by different people in a large organization, since they do not directly depend on each other.

The benefit of this approach also seems to be with packaging: we can change the behavior of a library without needing to access or modify its source-code.

APIs not only as props, but as events

Currently, the only way for a React component to communicate with the outside world is via props (apart from the Context API).

By making a component behavioral, instead of using props, we tell the outside world about when things happen within the component by yielding events.

To allow other developers to interact with the behavior of a component, we must therefore document the events that it requests, the events it waits for, and finally the events it blocks.

Events become the new API.

For instance, in a non-behavioral Counter component, we tell the outside world when the counter is incremented and what the current count is, using an onIncrement prop:

class Counter extends React.Component { state = { currentCount: 0 } handleClick = () => { this.setState(prevState => ({ currentCount: prevState.currentCount + 1 }), () => { this.props.onIncrement(this.state.currentCount) }) } render() { {this.state.currentCount} + }}
 console.log(currentCount) }/>

What if we want to do something else before the counter’s state gets incremented? Indeed we could add a new prop such as onBeforeIncrement, but the point is that we don’t want to add props and refactor code every time a new specific arises.

If we transform it into a behavioral component we can avoid refactoring when new specifications emerge:

class Counter extends React.Component { state = { currentCount: 0 } handleClick = () => { bp.event('CLICKED_INCREMENT') } render() { {this.state.currentCount} + }}
const BehavioralCounter = withBehavior([ function* () { yield { wait: ['CLICKED_INCREMENT'] } yield { request: ['UPDATE_CURRENT_COUNT'] }
 this.setState(prevState => ({ currentCount: prevState.currentCount + 1 }), () => { this.props.onIncrement(this.state.currentCount) }) }])(Counter)

Notice how we moved the logic for when the state is updated inside a b-thread. Furthermore, before the update actually takes place, a new event UPDATE_CURRENT_COUNT is requested.

This effectively allows other b-threads to block the update from happening.

Components can also be encapsulated and shared as different packages, and users can add behavior as they see fit.

// package-name: movies-listexport const function MoviesList() { ...}
// package-name: movies-list-with-paginationexport const MoviesListWithPagination = pipe( withBehavior(addPagination))(MoviesList)
// package-name: movies-list-with-pagination-logicexport const MoviesListWithDifferentPaginationLogic = pipe( withBehavior(changePaginationLogic))(MoviesListWithPagination)

Again this is different from simply enhancing a component, as a regular HOC would do. We can block certain things from happening in the components we extend from, effectively modifying their behavior.

Conclusion

This new programming idiom might feel uncomfortable at first, but it seems to alleviate a prominent issue we have when using UI components: it is hard to reuse components, because they don’t blend with the environment they were put into.

In the future, perhaps using these behavioral concepts, we will be able to add new behavior to apps by simply mounting new components. Stuff like this will be possible:

Additionally, events don’t need to pollute the whole app and can be broadcast only within a specific environment.

Thanks for reading! If you’re interested in an actual implementation of behavioral programming, please see my current work in progress library that works with React: //github.com/lmatteis/b-thread. The Behavioral Programming homepage also contains various implementations.

For more information on this exciting new concept, I suggest you read the scientific papers on Behavioral Programming or check some of my other articles on the subject.