Sunday, March 11, 2018
TDD in a sound bite
Test Driven Development is a discipline that asserts that you should not implement functionality until you have demonstrated that you can document the requirements in code.
Monday, February 5, 2018
Events in Evolution
During a discussion today on the DDD/CQRS slack channel, I remembered Rinat Abdullin's discussion of evolving process managers, and it led to the following thinking on events and boundaries.
Let us begin with out simplified matching service; the company profits by matching buyers and sellers. To begin, the scenario we will use is that Alice, Bob, and Charlie place sell orders, and David places a buy order. When our domain expert Madhav receives this information, he uses his knowledge of the domain and recognizes that David's order should be matched with Bob's.
Let's rephrase this, using the ideas provided by Rinat, and focusing on the design of Madhav's interface to the matching process. Alice, Bob, Charlie, and David have placed their orders. Madhav's interface at this point shows the four unmatched orders; he reviews them, decides that Bob and David's orders match, and sends a message describing this decision to the system. When that decision reaches the projection, it updates the representation, and now shows two unmatched orders from Alice and Charlie, and the matched orders from Bob and David.
Repeating the exercise: if we consider the inputs of the system, then we see that Madhav's decision comes after four inputs.
So the system uses that information to build Madhav's view of the system. When Madhav reports his decision to the system, it rebuilds his view from five inputs:
With all five inputs represented in the view, Madhav can see his earlier decision to match the orders has been captured and persisted, so he doesn't need to repeat that work.
At this point in the demonstration, we don't have any intelligence built into the model. It's just capturing data; Udi Dahan might say that all we have here is a database. The database collects inputs; Madhav's interface is built by a simple function - given this collection of inputs, show that on the screen.
Now we start a new exercise, following the program suggested by Rinat; we learn from Madhav that within the business of matching, there are a lot of easy cases where we might be able to automate the decision. We're not trying to solve the whole problem yet; in this stage our ambitions are very small: we just want the interface to provide recommendations to Madhav for review. Ignore all the hard problems, don't do anything real, just highlight a possibility. We aren't changing the process at all - the inputs are the same that we saw before.
We might iterate a number of times on this, getting feedback from Madhav on the quality of the recommendations, until he announces that the recommendations are sufficiently reliable that they could be used to reduce his workload.
Now we start a new exercise, where we introduce time as an element. If there is a recommended match available, and Madhav does not override the recommendation within 10 seconds, then the system should automatically match the order.
We're introducing time into the model, and we want to do that with some care. In 1998, John Carmack told us
This teaches us that we should be thinking about introducing a new input into our process flow
Let's review the example, with this in mind. The orders arrive as before, and there are no significant changes to what we have seen before
But treating time as an input introduces an extra row before the match is made
While that seems simple enough, something arbitrary has crept in. For example, why would time be an input in only 10 second bursts?
Or perhaps it's a better separation of concerns to use a scheduler
And we start to notice that things are getting complicated; what's worse, they are getting complicated in a way that Madhav didn't care about when he was doing the work by hand.
What's happening here is that we've confused "inputs" with "things that we should remember". We need to remember orders, we need to remember matches -- we saw that when Madhav was doing the work. But we don't need to remember time, or scheduling; those are just plumbing constructs we introduced to allow Madhav to intercede before the automation made an error.
Inputs and thing we should remember were the same when our system was just a database. No, that's not quite right; they weren't the same, they were different things that looked the same. They were different things that happened to have the same representation because all of the complicated stuff was outside of the system. They diverged when we started making the system more intelligent.
With this revised idea, we have two different ways of thinking about the after situation; we can consider its inputs
Or instead we can think about its things to remember
"Inputs" and "Things to Remember" are really really lousy names, and those spellings don't really lend themselves well to the ubiquitous language of domain modeling. To remain in consistent alignment with the literate, we should use the more common terminology: commands and events.
In the design described so far, we happen to have an alignment of commands and events that is one to one. To illustrate, we can think of the work thus far as an enumeration of database transactions, that look something like:
In the next exercise, consider what would happen if we tried to introduce as an invariant that there should be no easily matched items (following the rules we were previously taught by Madhav) left unmatched. In other words, when David's order arrives, it should be immediately matched with Bob's, rather than waiting 10 seconds. That means that one command (the input of David's order) produces two different events; one to capture the observation of David's order, the other to capture the decision made by the automation as a proxy for Madhav.
The motivation for treating these as two separate events is this: it most closely aligns with the events we were generating when Madhav was making all of the decisions himself. Whether we use Madhav in the loop making the decisions, or simply reviewing scheduled decisions, or leaving all of the decisions to the automation, the captured list of events is the same. That in turn means that these different variations in implementation here do not impact the other implementations at all. We're limiting the impact of the change by ensuring the the observable results are consistent in all three cases.
Let us begin with out simplified matching service; the company profits by matching buyers and sellers. To begin, the scenario we will use is that Alice, Bob, and Charlie place sell orders, and David places a buy order. When our domain expert Madhav receives this information, he uses his knowledge of the domain and recognizes that David's order should be matched with Bob's.
Let's rephrase this, using the ideas provided by Rinat, and focusing on the design of Madhav's interface to the matching process. Alice, Bob, Charlie, and David have placed their orders. Madhav's interface at this point shows the four unmatched orders; he reviews them, decides that Bob and David's orders match, and sends a message describing this decision to the system. When that decision reaches the projection, it updates the representation, and now shows two unmatched orders from Alice and Charlie, and the matched orders from Bob and David.
Repeating the exercise: if we consider the inputs of the system, then we see that Madhav's decision comes after four inputs.
So the system uses that information to build Madhav's view of the system. When Madhav reports his decision to the system, it rebuilds his view from five inputs:
With all five inputs represented in the view, Madhav can see his earlier decision to match the orders has been captured and persisted, so he doesn't need to repeat that work.
At this point in the demonstration, we don't have any intelligence built into the model. It's just capturing data; Udi Dahan might say that all we have here is a database. The database collects inputs; Madhav's interface is built by a simple function - given this collection of inputs, show that on the screen.
Now we start a new exercise, following the program suggested by Rinat; we learn from Madhav that within the business of matching, there are a lot of easy cases where we might be able to automate the decision. We're not trying to solve the whole problem yet; in this stage our ambitions are very small: we just want the interface to provide recommendations to Madhav for review. Ignore all the hard problems, don't do anything real, just highlight a possibility. We aren't changing the process at all - the inputs are the same that we saw before.
We might iterate a number of times on this, getting feedback from Madhav on the quality of the recommendations, until he announces that the recommendations are sufficiently reliable that they could be used to reduce his workload.
Now we start a new exercise, where we introduce time as an element. If there is a recommended match available, and Madhav does not override the recommendation within 10 seconds, then the system should automatically match the order.
We're introducing time into the model, and we want to do that with some care. In 1998, John Carmack told us
If you don't consider time an input value, think about it until you do -- it is an important concept
This teaches us that we should be thinking about introducing a new input into our process flow
Let's review the example, with this in mind. The orders arrive as before, and there are no significant changes to what we have seen before
But treating time as an input introduces an extra row before the match is made
While that seems simple enough, something arbitrary has crept in. For example, why would time be an input in only 10 second bursts?
Or perhaps it's a better separation of concerns to use a scheduler
And we start to notice that things are getting complicated; what's worse, they are getting complicated in a way that Madhav didn't care about when he was doing the work by hand.
What's happening here is that we've confused "inputs" with "things that we should remember". We need to remember orders, we need to remember matches -- we saw that when Madhav was doing the work. But we don't need to remember time, or scheduling; those are just plumbing constructs we introduced to allow Madhav to intercede before the automation made an error.
Inputs and thing we should remember were the same when our system was just a database. No, that's not quite right; they weren't the same, they were different things that looked the same. They were different things that happened to have the same representation because all of the complicated stuff was outside of the system. They diverged when we started making the system more intelligent.
With this revised idea, we have two different ways of thinking about the after situation; we can consider its inputs
Or instead we can think about its things to remember
"Inputs" and "Things to Remember" are really really lousy names, and those spellings don't really lend themselves well to the ubiquitous language of domain modeling. To remain in consistent alignment with the literate, we should use the more common terminology: commands and events.
In the design described so far, we happen to have an alignment of commands and events that is one to one. To illustrate, we can think of the work thus far as an enumeration of database transactions, that look something like:
In the next exercise, consider what would happen if we tried to introduce as an invariant that there should be no easily matched items (following the rules we were previously taught by Madhav) left unmatched. In other words, when David's order arrives, it should be immediately matched with Bob's, rather than waiting 10 seconds. That means that one command (the input of David's order) produces two different events; one to capture the observation of David's order, the other to capture the decision made by the automation as a proxy for Madhav.
The motivation for treating these as two separate events is this: it most closely aligns with the events we were generating when Madhav was making all of the decisions himself. Whether we use Madhav in the loop making the decisions, or simply reviewing scheduled decisions, or leaving all of the decisions to the automation, the captured list of events is the same. That in turn means that these different variations in implementation here do not impact the other implementations at all. We're limiting the impact of the change by ensuring the the observable results are consistent in all three cases.
Tuesday, January 16, 2018
Events are messages that describe state, not behavior
I have felt, for some time now, that the literature explains event sourcing poorly.
The basic plot, that current state is just a left fold over previous behaviors, is fine, so far as it goes. But it rather assumes that the audience is clear on what "previous behaviors" means.
And I certainly haven't been.
In many, perhaps even most, domain models can be thought of as state machines:
Cargo begins its life cycle when it is booked, and and our delivery document changes state when the cargo is received, and when an itinerary is selected, it gets loaded in port, the itinerary changes, each of these messages arrives and changes the state of the delivery document.
So we can think of the entire process as a state machine; each message from the outside world causes the model to follow a transition edge out of one state node and into another.
If we choose to save the state of the delivery document, so that we can resume work on it later, there are three approaches we might take
We call the patches "events", and they have domain specific semantics; but they are fundamentally dumb documents that decouple the representation of state from the model that generated it.
This decoupling is important, because it allows us to change the model without changing the semantics of the persisted representation.
To demonstrate this, let's imaging a simple trade matching application. Buy and Sell orders come in from different customers, and the model is responsible for pairing them up. There might be elaborate rules in place for deciding how matches work, but to save the headache of working them out we'll instead focus our attention on a batch of buy and sell orders that can be paired arbitrarily -- the actual selects are going to be determined by the model's internal tie breaker strategy.
So we'll image that a new burst of messages appear, at some new price -- we don't need to worry about any earlier orders. The burst begins...
After things have settled down, we restart the service. That means that the in memory state is lost, as has to be recovered from what was written to the persistent store. We now get an additional burst of messages.
Using a first in, first out tiebreaker, we would expect to see pairs (A,1), (B,2), (C,3), and (D,4). If we were using a last in, first out tiebreaker, we would presumably see (D,1), (E,2), (C,3), (B,4).
But what happens if, during the shutdown, we switch from FIFO to LIFO? During the first phase, we see (A,1) matched, as before. After that, we should see (D,2), (E,3), (C,4).
In order to achieve that outcome, the model in the second phase needs knowledge of the (A,1) match before the shutdown. But it can only know about that match if there is evidence of the match written to the persistent store. Without that knowledge, the LIFO strategy would expect that (D,1) were already matched, and would in turn produce (C,2), (E, 3), and (A,4). The last of these conflicts with the original (A,1) match. In other words, we're in a corrupted state.
Writing the entire document to the event store works just fine, we read a representation that suggests that A and 1 are unavailable, and the domain model can proceed accordingly. Writing the sequence of patches works, because when we fold the patches together we get the state document. It's only the middle case, where we wrote out representations that implied a particular model, that we got into trouble.
The middle approach is not wrong, by any means. The LMAX architecture worked this way; they would write the input messages to a journal, and in the event of a failure they would recover by starting a copy of the same model. The replacement of the model behavior happened off hours.
Similarly, if you have the list of inputs and the old behavior, you can recover current state in memory, and then write out a representation of that state that will allow a new model to pick up from where the old left off.
Not wrong, but different. One approach or the other might be more suitable for your unique collection of operational constraints.
The basic plot, that current state is just a left fold over previous behaviors, is fine, so far as it goes. But it rather assumes that the audience is clear on what "previous behaviors" means.
And I certainly haven't been.
In many, perhaps even most, domain models can be thought of as state machines:
Cargo begins its life cycle when it is booked, and and our delivery document changes state when the cargo is received, and when an itinerary is selected, it gets loaded in port, the itinerary changes, each of these messages arrives and changes the state of the delivery document.
So we can think of the entire process as a state machine; each message from the outside world causes the model to follow a transition edge out of one state node and into another.
If we choose to save the state of the delivery document, so that we can resume work on it later, there are three approaches we might take
- We could simply save the entire delivery document as is
- We could save the sequence of messages that we received
- We could save a sequence of patch documents that describe the differences between states
We call the patches "events", and they have domain specific semantics; but they are fundamentally dumb documents that decouple the representation of state from the model that generated it.
This decoupling is important, because it allows us to change the model without changing the semantics of the persisted representation.
To demonstrate this, let's imaging a simple trade matching application. Buy and Sell orders come in from different customers, and the model is responsible for pairing them up. There might be elaborate rules in place for deciding how matches work, but to save the headache of working them out we'll instead focus our attention on a batch of buy and sell orders that can be paired arbitrarily -- the actual selects are going to be determined by the model's internal tie breaker strategy.
So we'll image that a new burst of messages appear, at some new price -- we don't need to worry about any earlier orders. The burst begins...
After things have settled down, we restart the service. That means that the in memory state is lost, as has to be recovered from what was written to the persistent store. We now get an additional burst of messages.
Using a first in, first out tiebreaker, we would expect to see pairs (A,1), (B,2), (C,3), and (D,4). If we were using a last in, first out tiebreaker, we would presumably see (D,1), (E,2), (C,3), (B,4).
But what happens if, during the shutdown, we switch from FIFO to LIFO? During the first phase, we see (A,1) matched, as before. After that, we should see (D,2), (E,3), (C,4).
In order to achieve that outcome, the model in the second phase needs knowledge of the (A,1) match before the shutdown. But it can only know about that match if there is evidence of the match written to the persistent store. Without that knowledge, the LIFO strategy would expect that (D,1) were already matched, and would in turn produce (C,2), (E, 3), and (A,4). The last of these conflicts with the original (A,1) match. In other words, we're in a corrupted state.
Writing the entire document to the event store works just fine, we read a representation that suggests that A and 1 are unavailable, and the domain model can proceed accordingly. Writing the sequence of patches works, because when we fold the patches together we get the state document. It's only the middle case, where we wrote out representations that implied a particular model, that we got into trouble.
The middle approach is not wrong, by any means. The LMAX architecture worked this way; they would write the input messages to a journal, and in the event of a failure they would recover by starting a copy of the same model. The replacement of the model behavior happened off hours.
Similarly, if you have the list of inputs and the old behavior, you can recover current state in memory, and then write out a representation of that state that will allow a new model to pick up from where the old left off.
Not wrong, but different. One approach or the other might be more suitable for your unique collection of operational constraints.
Monday, January 15, 2018
Command messages in REST
Kevin Swiber, of SIREN fame, just taught me a nifty trick for describing commands to a resource
Background: commands are semantically unsafe operations, as described in the HTTP specification. They are messages that expect to induce a change of state on the server, they invalidate caches, and so on.
In considering a design that conforms to the REST architectural style, it's useful to ask "how did we do it on the web?" We had URI, and HTTP, and HTML; those elements alone were enough to support catastrophic success.
Now, in HTML, the only readily available link for unsafe operations was the form, which supported only GET and POST. When there's only one road, you take it: servers would send HTML representations that would include Forms, that would describe to the client how to create the command message.
Notice that this is very much a hypermedia approach - if the server wishes to communicate that the command should not be sent, it simply removes the form from the representation. Elements that are no longer necessary can be removed from the form. New optional elements can be added by simply including a new input control and providing a reasonable default value.
What this teaches us is that a POST message with an application/x-www-form-urlencoded payload is a perfectly satisfactory way of modeling a command sent to a resource.
But I've got to admit, that doesn't feel much like "REST".
So here's another way of thinking about it.
HTTP Patch affords the modification of resources. We tend to think about the payload as a list of changes to make; but another way of thinking about it is that the payload describes a list of commands. For example, application/json-patch+json is a document that describes operations; add, remove, and so on.
First trick: any resource that supports PATCH application/json-patch+json can just as easily provide the same functionality via POST application/json-patch+json. The semantics of PATCH are more specific than those of POST, but they don't fundamentally conflict with each other.
Second trick: application/json-patch+json is just a representation of a sequence of operations; with those operations taken specifically from the vocabulary of manipulating a json document. We don't particularly want to be borrowing those semantics unless they happen to be a particularly good fit for our domain; we likely have our own names for operations, particular parameters, and so on. So we instead might choose our own patch document representation; and write it up so that clients can be coded against it.
Third trick: these media types don't have any magical properties; they are just rules for taking semantic meaning and encoding that meaning as bytes, and then reversing the process at the other end. We can use any byte representation we like, provided that the client and the server agree on the rules being used. We could use text/plain, or application/json. Alas, both of those representations suffer from the problem that we need additional out of band communication to ensure that both ends of the conversation understand the meaning the same way.
Fourth trick: if our command message can be represented as a collection of key/value pairs (likely the case, if we account for hierarchical keys), then application/x-www-form-urlencoding is another possible representation that we might use. It shares the same problem we just saw: the client and server need to agree on semantics. But with one important difference: we already possess a standard format with facilities for describing application/x-www-form-urlencoded representations in band: HTML.
So, yeah -- it's REST.
That still doesn't mean "easy"; the client still has to know what it wants to do, how to find the right form to do it, how to find the fields it wants to set before submitting the form. In much the same way that JSON Patch puts semantics on top of JSON, you need something that describes your domain semantics on top of HTML (or whatever other hypermedia representation you prefer).
An advantage in this approach is that it allows us to take advantage of the cache invalidation semantics of POST; which is to say, because we are sending the message to be handled by "the" resource, as opposed to some other resource that understands the semantics of a specific command, the caches are able to invalidate the representations that are actually being changed, rather than invalidating an irrelevant side channel.
Background: commands are semantically unsafe operations, as described in the HTTP specification. They are messages that expect to induce a change of state on the server, they invalidate caches, and so on.
In considering a design that conforms to the REST architectural style, it's useful to ask "how did we do it on the web?" We had URI, and HTTP, and HTML; those elements alone were enough to support catastrophic success.
Now, in HTML, the only readily available link for unsafe operations was the form, which supported only GET and POST. When there's only one road, you take it: servers would send HTML representations that would include Forms, that would describe to the client how to create the command message.
Notice that this is very much a hypermedia approach - if the server wishes to communicate that the command should not be sent, it simply removes the form from the representation. Elements that are no longer necessary can be removed from the form. New optional elements can be added by simply including a new input control and providing a reasonable default value.
What this teaches us is that a POST message with an application/x-www-form-urlencoded payload is a perfectly satisfactory way of modeling a command sent to a resource.
But I've got to admit, that doesn't feel much like "REST".
So here's another way of thinking about it.
HTTP Patch affords the modification of resources. We tend to think about the payload as a list of changes to make; but another way of thinking about it is that the payload describes a list of commands. For example, application/json-patch+json is a document that describes operations; add, remove, and so on.
First trick: any resource that supports PATCH application/json-patch+json can just as easily provide the same functionality via POST application/json-patch+json. The semantics of PATCH are more specific than those of POST, but they don't fundamentally conflict with each other.
Second trick: application/json-patch+json is just a representation of a sequence of operations; with those operations taken specifically from the vocabulary of manipulating a json document. We don't particularly want to be borrowing those semantics unless they happen to be a particularly good fit for our domain; we likely have our own names for operations, particular parameters, and so on. So we instead might choose our own patch document representation; and write it up so that clients can be coded against it.
Third trick: these media types don't have any magical properties; they are just rules for taking semantic meaning and encoding that meaning as bytes, and then reversing the process at the other end. We can use any byte representation we like, provided that the client and the server agree on the rules being used. We could use text/plain, or application/json. Alas, both of those representations suffer from the problem that we need additional out of band communication to ensure that both ends of the conversation understand the meaning the same way.
Fourth trick: if our command message can be represented as a collection of key/value pairs (likely the case, if we account for hierarchical keys), then application/x-www-form-urlencoding is another possible representation that we might use. It shares the same problem we just saw: the client and server need to agree on semantics. But with one important difference: we already possess a standard format with facilities for describing application/x-www-form-urlencoded representations in band: HTML.
So, yeah -- it's REST.
That still doesn't mean "easy"; the client still has to know what it wants to do, how to find the right form to do it, how to find the fields it wants to set before submitting the form. In much the same way that JSON Patch puts semantics on top of JSON, you need something that describes your domain semantics on top of HTML (or whatever other hypermedia representation you prefer).
An advantage in this approach is that it allows us to take advantage of the cache invalidation semantics of POST; which is to say, because we are sending the message to be handled by "the" resource, as opposed to some other resource that understands the semantics of a specific command, the caches are able to invalidate the representations that are actually being changed, rather than invalidating an irrelevant side channel.
Thursday, January 4, 2018
Property based testing: Mars Rover
My go to TDD exercise has been the mars rover kata. Trying a property test first approach to it has been a struggle. Compared with an example driven approach, there seems to be a whole lot of front loaded pain without an obvious payoff.
I reached out to Paul Holser to see if I was missing a bet, and of course the discussion turned to what sorts of properties I had discovered in the design.
First property: for all inputs, the output is an ordered collection of representations of rover positions. If we are checking within the domain boundary, this is pretty trivial -- we can use the type system to enforce this constraint. If we are checking from outside of the boundary, then we're checking that the lines of text/bytes that have been returned to us can be parsed; three entries per represe, the first two entries are coordinates/integers, the last entry is a member of the set of cardinal directions.
Second property: for all inputs, the number of representations in the output equals the number of rovers described by the input. Rovers are conserved.
Third property: for any inputs, you get the same output each time you run it.
Fourth property: for any pair of inputs, reversing the order of execution does not change the outputs.
In other words, its a pure function. I don't actually know the battery of tests that you should run to establish this fact.
A rover with an empty instruction set remains at the same coordinates in the same orientation. This gives us our fifth property: for all inputs, if we replace a single rover's instructions with an empty instruction set (thereby creating a different input), the corresponding output will have the same position. Note that this indirectly demonstrates that the order of the outputs matches the order of the inputs.
This property would not apply if the rovers moved other than by consuming their own instructions. For example, if one rover were to push another rover out of the way, then the property would not hold.
The above identity leads to two additional properties, both variations of different paths, same destination.
The sixth property takes advantage of the identity to allow us to establish that the behavior of the rovers is consistent with their programs being executed in the right order. So for all inputs, we can select any rover, and create a new program in the following way: the positions an instructions of the rovers before the selection in the list are copied directly, the positions of the remaining rovers are copied with empty instructions. We get the corresponding output, and then build a second program -- the output positions are taken as inputs, with empty instruction sets assigned to the rovers before the selected rover, and the original instruction sets assigned to the remainder. The property to be checked is that the output of this second program matches the original that we started with.
The seventh property is similar; instead of breaking up the original program along neat rover boundaries, we can also split the selected rover's instructions into two pieces, running part of the instruction set with the rovers ahead of it, then running the remaining instructions with the subsequent rovers.
Both of these are basically establishing that we're dealing with a stateless process; the next collection of positions depends only on the current collection of positions and the next instruction.
Up until this point, we haven't really been looking at the semantics of the output at all; we've got a list of coordinates and orientations, but all we've done is check for equality.
Putting this another way, the simplest thing that could possibly work is still to ignore the instruction sets entirely, and simply return the original positions and orientations in each case.
The plateau is rectangular, and presumably the boundary effects are equivalent on all sides. If you flip the rectangle upside down -- exchanging north for south and east for west -- you get the same configuration of rovers in a different coordinate system. Right and Left have the same effect on orientation that they did before the flip. Any interactions with the boundary will still appear at the same points in the evaluations of the rovers.
This yields our eighth property: for any input, if we flip the input position, compute the output, and then flip the output, we should get the same answer that we would get from running the original output as is.
There is a similar result for quarter rotations, if you adjust the input so that the plateau itself is square.
Rotations are mostly immune to boundary effects and collisions, so it's probably reasonable to start there.
One assumption that we probably need to make is that all valid inputs have rovers that start within the region of the specified plateau. The problem statement doesn't address that point. I don't think property based testing helps much here - running the checks can only tell you if the test and implementation made compatible assumptions, not correct assumptions.
Property nine: for all inputs, we can select any rover and replace it's original instruction set with one that has all of the move instructions removed. When we run this program:
This establishes that we've got four-symmetry, that left and right cancel each other out, and that rotations preserve position.
Property ten: for all inputs, we can select any rover and replace it's original instruction set with a one that elides all of the left and right instructions. When we run this program, the orientation of the selected rover will be unchanged.
EDIT: come to think of it, any single instruction leaves at least two of the three properties unchanged; the remaining property changes by at most one unit. The complication of the move instruction is that you need the orientation to know which property is changing in which direction.
We can introduce into this domain the concept of a displacement - we're in a taxicab geometry. The number of moves in an instruction set establishes an upper bound on the displacement that can be achieved; we know that the rover will be found somewhere in the circle.
Property eleven: for all inputs, we can measure the displacement of the output coordinates from the input coordinates. For each rover, the displacement will be less than or equal to the count of moves in its instruction set.
Furthermore, if the circle doesn't reach the boundary of the plateau, then we don't need to worry about boundary effects for that rover. Taking the initial positions of the rovers, and the size of the bounding circles allows us to compute a bounding box; if the box doesn't reach the plateau boundaries, then the simulation is completely free of boundary effects.
If the plateau is large enough to enclose the bounding box at two different locations, then we can establish twelfth property - that of translation symmetry in the following way. Given any valid input, we can compute the extents of a bounding box that encloses the possible programs of all rovers. Choose a displacement in positive x and y. Compute the dimensions of a plateau by adding the displacement to the extent of the bounding box. Create two inputs using this plateau: for the first, compute the initial positions of the rovers such that the lower left corner of the bounding box is at the origin; for the second, compute initial positions of the rovers such that the upper right corner of the bounding box is at the upper right corner of the plateau. Note that the relative displacment of the input coordinates of the corresponding rovers should be the same. Compute outputs for both programs; the displacements of the corresponding outputs should all match.
The thirteenth property also plays with bounding boxes to establish this property - that if the bounding circles of two rovers don't overlap, then they don't have any interference effects. The check goes something like this: first you run a single program with all of the rovers participating. Then you run a program that isolates each rover on the same plateau, and confirm that the final positions of the isolated rovers matches that predicted by the single program containing all of the rovers.
It's not clear how you transform input with overlapping bounding circles into one that doesn't have them. Do you move the rovers? shrink the bounding circles? skip examples that don't match the criteria?
But in the main, it seems to be a very powerful pattern to use an input as a seed from which you can compute an input that has the properties you want to verify -- using an identity transformation wherever possible.
It's not so clear to me that these constraints are driving the design in any useful way. For example, we don't have any properties that establish that left and right are correctly oriented. We don't have any tests that actually demonstrate that the rover has moved.
I reached out to Paul Holser to see if I was missing a bet, and of course the discussion turned to what sorts of properties I had discovered in the design.
First property: for all inputs, the output is an ordered collection of representations of rover positions. If we are checking within the domain boundary, this is pretty trivial -- we can use the type system to enforce this constraint. If we are checking from outside of the boundary, then we're checking that the lines of text/bytes that have been returned to us can be parsed; three entries per represe, the first two entries are coordinates/integers, the last entry is a member of the set of cardinal directions.
Second property: for all inputs, the number of representations in the output equals the number of rovers described by the input. Rovers are conserved.
Third property: for any inputs, you get the same output each time you run it.
Fourth property: for any pair of inputs, reversing the order of execution does not change the outputs.
In other words, its a pure function. I don't actually know the battery of tests that you should run to establish this fact.
A rover with an empty instruction set remains at the same coordinates in the same orientation. This gives us our fifth property: for all inputs, if we replace a single rover's instructions with an empty instruction set (thereby creating a different input), the corresponding output will have the same position. Note that this indirectly demonstrates that the order of the outputs matches the order of the inputs.
This property would not apply if the rovers moved other than by consuming their own instructions. For example, if one rover were to push another rover out of the way, then the property would not hold.
The above identity leads to two additional properties, both variations of different paths, same destination.
The sixth property takes advantage of the identity to allow us to establish that the behavior of the rovers is consistent with their programs being executed in the right order. So for all inputs, we can select any rover, and create a new program in the following way: the positions an instructions of the rovers before the selection in the list are copied directly, the positions of the remaining rovers are copied with empty instructions. We get the corresponding output, and then build a second program -- the output positions are taken as inputs, with empty instruction sets assigned to the rovers before the selected rover, and the original instruction sets assigned to the remainder. The property to be checked is that the output of this second program matches the original that we started with.
The seventh property is similar; instead of breaking up the original program along neat rover boundaries, we can also split the selected rover's instructions into two pieces, running part of the instruction set with the rovers ahead of it, then running the remaining instructions with the subsequent rovers.
Both of these are basically establishing that we're dealing with a stateless process; the next collection of positions depends only on the current collection of positions and the next instruction.
Up until this point, we haven't really been looking at the semantics of the output at all; we've got a list of coordinates and orientations, but all we've done is check for equality.
Putting this another way, the simplest thing that could possibly work is still to ignore the instruction sets entirely, and simply return the original positions and orientations in each case.
The plateau is rectangular, and presumably the boundary effects are equivalent on all sides. If you flip the rectangle upside down -- exchanging north for south and east for west -- you get the same configuration of rovers in a different coordinate system. Right and Left have the same effect on orientation that they did before the flip. Any interactions with the boundary will still appear at the same points in the evaluations of the rovers.
This yields our eighth property: for any input, if we flip the input position, compute the output, and then flip the output, we should get the same answer that we would get from running the original output as is.
There is a similar result for quarter rotations, if you adjust the input so that the plateau itself is square.
Rotations are mostly immune to boundary effects and collisions, so it's probably reasonable to start there.
One assumption that we probably need to make is that all valid inputs have rovers that start within the region of the specified plateau. The problem statement doesn't address that point. I don't think property based testing helps much here - running the checks can only tell you if the test and implementation made compatible assumptions, not correct assumptions.
Property nine: for all inputs, we can select any rover and replace it's original instruction set with one that has all of the move instructions removed. When we run this program:
- The coordinates of the output should match those of the input
- The orientation of the output will match that of the input if, and only if, the difference in the count of left instructions and the count of right instructions is a multiple of four.
This establishes that we've got four-symmetry, that left and right cancel each other out, and that rotations preserve position.
Property ten: for all inputs, we can select any rover and replace it's original instruction set with a one that elides all of the left and right instructions. When we run this program, the orientation of the selected rover will be unchanged.
EDIT: come to think of it, any single instruction leaves at least two of the three properties unchanged; the remaining property changes by at most one unit. The complication of the move instruction is that you need the orientation to know which property is changing in which direction.
We can introduce into this domain the concept of a displacement - we're in a taxicab geometry. The number of moves in an instruction set establishes an upper bound on the displacement that can be achieved; we know that the rover will be found somewhere in the circle.
Property eleven: for all inputs, we can measure the displacement of the output coordinates from the input coordinates. For each rover, the displacement will be less than or equal to the count of moves in its instruction set.
Furthermore, if the circle doesn't reach the boundary of the plateau, then we don't need to worry about boundary effects for that rover. Taking the initial positions of the rovers, and the size of the bounding circles allows us to compute a bounding box; if the box doesn't reach the plateau boundaries, then the simulation is completely free of boundary effects.
If the plateau is large enough to enclose the bounding box at two different locations, then we can establish twelfth property - that of translation symmetry in the following way. Given any valid input, we can compute the extents of a bounding box that encloses the possible programs of all rovers. Choose a displacement in positive x and y. Compute the dimensions of a plateau by adding the displacement to the extent of the bounding box. Create two inputs using this plateau: for the first, compute the initial positions of the rovers such that the lower left corner of the bounding box is at the origin; for the second, compute initial positions of the rovers such that the upper right corner of the bounding box is at the upper right corner of the plateau. Note that the relative displacment of the input coordinates of the corresponding rovers should be the same. Compute outputs for both programs; the displacements of the corresponding outputs should all match.
The thirteenth property also plays with bounding boxes to establish this property - that if the bounding circles of two rovers don't overlap, then they don't have any interference effects. The check goes something like this: first you run a single program with all of the rovers participating. Then you run a program that isolates each rover on the same plateau, and confirm that the final positions of the isolated rovers matches that predicted by the single program containing all of the rovers.
It's not clear how you transform input with overlapping bounding circles into one that doesn't have them. Do you move the rovers? shrink the bounding circles? skip examples that don't match the criteria?
But in the main, it seems to be a very powerful pattern to use an input as a seed from which you can compute an input that has the properties you want to verify -- using an identity transformation wherever possible.
It's not so clear to me that these constraints are driving the design in any useful way. For example, we don't have any properties that establish that left and right are correctly oriented. We don't have any tests that actually demonstrate that the rover has moved.
Tuesday, January 2, 2018
Property based testing: thoughts of a novice.
when i write tests, i do it because writing tests lets me make more features faster. that's the only reason.— Michael D. Hill (@GeePawHill) April 11, 2017
Writing tests first forces you to think about the problem you're solving. Writing property-based tests forces you to think way harder.— Jessica Kerr (@jessitron) April 25, 2013
The tension between these two ideas drives me nuts. Thinking way harder means that I'm not delivering features faster.
Example based testing is straight forward; choose an input, hard code the output, remove the duplication, repeat until you have no more examples that produce the wrong output. You may even be able to estimate the minimum required number of tests just by thinking about the cyclomatic complexity of the problem.
But this in turn means that you can't easily judge "complete" just from looking at the demonstrated examples. As Scott Wlaschin points out, a finite suite of example tests can be beaten by a pathological implementation that is fitted to the suite.
Property based tests handle this concern better -- they explore the domain, rather than just sampling it. That's a lie, of course; what property based tests are really doing is sampling a lot of the domain -- enough that the pathological fake is more expensive than just solving the problem.
My most startling test result, ever, came about from a property test which revealed that I had completely failed to recognize that the properties that I thought would hold were not consistent with the implementation I had chosen.
But it didn't come from randomly exploring the space, but rather choosing an example from the domain that I recognized was "weird". Fortunately, there were lots of weird values in the domain, and they all demonstrated that my implementation didn't support the property in question. So I got
"lucky" without having to write four billion tests.
I'm not at all sold on the idea of using a random input generator to find a needle in the haystack.
Friday, December 22, 2017
Injection: a refactoring exercise.
Yesterday, I needed to extend a program that had a broken design.
The existing program works fine; but it has hard wired into it an identifier for a specific database host. That host name is used during execution to create the URL string that is passed to JDBC, by merging the hard coded host with a configured database name.
An additional constraint is that I need to have the old and new versions of the program running in parallel, in the same environment. We're going to run the two reports in parallel for a time, and then decommission the legacy.
So, how to dig my way out? Here's the outline of how I did it....
As is often the case, I rely on Parnas -- the legacy implementation is the result of a decision that I want to change. Therefore, I begin by creating a boundary around that decision.
In this case, the code in question was computing a new string from the hard coded literal and a member variable. So I applied an "extract method" refactoring to give me a member function that promised to provide the string that I needed. The method simply returned the calculated string, as before.
I then created a new type; the configuration model already included DatabaseName as an explicit concept, but it didn't have an understanding for the URL. So I introduced a DatabaseUrl type, that was simply an alias for a string.
I then inserted that type into the middle of the method -- so we compute the string, use the string to create a new DatabaseUrl, then use the DatabaseUrl to yield the string to return.
Next, I apply extract method again, creating a function that would convert the DatabaseName to the DatabaseUrl.
Next, I take DatabaseUrl, which had been a local variable, and make it a private member of the object; it now has the same lifetime and scope as DatabaseName, the only different is that it is being initialized at a different point in the program.
Next, I copy the logic to initialize the DatabaseURL to the method where the DatabaseName was initialized. I now have a redundant copy, which I can remove -- the query that produces the string representation of the url when I need it is just a toString() on the member value
At this point, the only place where the DatabaseName is used in in the initializer, so I can demote that to a local variable in the method, and inline it away.
I don't think we should be using the old method on the API any more, so I mark it as deprecated.
Reviewing at this point -- through a sequence of refactorings, I've managed to extend the existing API, adding the new method I need to it, while at the same time ensure that my existing tests are exercising that code (because the implementation of the old API feeds into the new one).
The legacy implementation includes a factory, which when passed the configuration (captured within a Properties object) returns the interpreted configuration. So the new piece I need is a factory with the same signature, that understands the new configuration.
But before I can think about creating a new factory implementation, I have to address a second problem -- I was sloppy ensuring the integrity of the composition root. The factory I need to replace is allocated behind two different default constructors.
So I need to perform a second round of refactoring. The member variable for the factory that I need to replace is too specific, as it names the implementation directly. So the spelling of that type needs to be changed to the interface. Fortunately, the interface I need is already available, so I can just replace it.
Then we delete the assignment at the member variable declaration. At this point, everything goes red, because we aren't assigning a final field. Next, we fix the compilation problem by allocating a new instance of the factory in the constructor body.
Next, we work our way back up the stack to the composition root. There are a couple possibilities; you can either develop a completely new path from the root to the assignment so that the factory can be passed along, and then with everything wired up remove the duplication, or you can create new signatures, but play criss cross -- the existing implementation gets a new signature, and a new method appears which creates a default factory and then routes to the real implementation.
I prefer the latter course; you only really need to preserve the signatures that are part of the public API. Anything that is private to the module you can simply change, watching for local compile errors.
At the end of the game, I have the starting position I'm waiting for: me legacy implementation, with the creation of the default factory at the composition root.
Now I'm ready to make the easy change - implement the new factory, clone the composition root, and replace the legacy factory with the new implementation.
The existing program works fine; but it has hard wired into it an identifier for a specific database host. That host name is used during execution to create the URL string that is passed to JDBC, by merging the hard coded host with a configured database name.
An additional constraint is that I need to have the old and new versions of the program running in parallel, in the same environment. We're going to run the two reports in parallel for a time, and then decommission the legacy.
So, how to dig my way out? Here's the outline of how I did it....
for each desired change, make the change easy (warning: this may be hard), then make the easy change— Kent Beck (@KentBeck) September 25, 2012
As is often the case, I rely on Parnas -- the legacy implementation is the result of a decision that I want to change. Therefore, I begin by creating a boundary around that decision.
In this case, the code in question was computing a new string from the hard coded literal and a member variable. So I applied an "extract method" refactoring to give me a member function that promised to provide the string that I needed. The method simply returned the calculated string, as before.
I then created a new type; the configuration model already included DatabaseName as an explicit concept, but it didn't have an understanding for the URL. So I introduced a DatabaseUrl type, that was simply an alias for a string.
I then inserted that type into the middle of the method -- so we compute the string, use the string to create a new DatabaseUrl, then use the DatabaseUrl to yield the string to return.
Next, I apply extract method again, creating a function that would convert the DatabaseName to the DatabaseUrl.
Next, I take DatabaseUrl, which had been a local variable, and make it a private member of the object; it now has the same lifetime and scope as DatabaseName, the only different is that it is being initialized at a different point in the program.
Next, I copy the logic to initialize the DatabaseURL to the method where the DatabaseName was initialized. I now have a redundant copy, which I can remove -- the query that produces the string representation of the url when I need it is just a toString() on the member value
At this point, the only place where the DatabaseName is used in in the initializer, so I can demote that to a local variable in the method, and inline it away.
I don't think we should be using the old method on the API any more, so I mark it as deprecated.
Reviewing at this point -- through a sequence of refactorings, I've managed to extend the existing API, adding the new method I need to it, while at the same time ensure that my existing tests are exercising that code (because the implementation of the old API feeds into the new one).
The legacy implementation includes a factory, which when passed the configuration (captured within a Properties object) returns the interpreted configuration. So the new piece I need is a factory with the same signature, that understands the new configuration.
But before I can think about creating a new factory implementation, I have to address a second problem -- I was sloppy ensuring the integrity of the composition root. The factory I need to replace is allocated behind two different default constructors.
So I need to perform a second round of refactoring. The member variable for the factory that I need to replace is too specific, as it names the implementation directly. So the spelling of that type needs to be changed to the interface. Fortunately, the interface I need is already available, so I can just replace it.
Then we delete the assignment at the member variable declaration. At this point, everything goes red, because we aren't assigning a final field. Next, we fix the compilation problem by allocating a new instance of the factory in the constructor body.
Next, we work our way back up the stack to the composition root. There are a couple possibilities; you can either develop a completely new path from the root to the assignment so that the factory can be passed along, and then with everything wired up remove the duplication, or you can create new signatures, but play criss cross -- the existing implementation gets a new signature, and a new method appears which creates a default factory and then routes to the real implementation.
I prefer the latter course; you only really need to preserve the signatures that are part of the public API. Anything that is private to the module you can simply change, watching for local compile errors.
At the end of the game, I have the starting position I'm waiting for: me legacy implementation, with the creation of the default factory at the composition root.
Now I'm ready to make the easy change - implement the new factory, clone the composition root, and replace the legacy factory with the new implementation.
Subscribe to:
Posts (Atom)