Last active
September 2, 2019 23:41
-
-
Save sergueif/c0ffb30363f25c48ac3ca30de126cfed to your computer and use it in GitHub Desktop.
Genuenly asking: What are some versatile patterns for making imperative code more functional/testable?
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# can arbitrary imperative code be chopped up into a loop with a functional step? | |
# "comm" indicates some communication to an external computer or otherwise side-effecty step | |
# order of them presumably matters | |
# code is made up. It's an arbitrary collection of steps and "if" statements | |
# BUT! it's very similar to actual code I found at the office. :) #java | |
# Note: I think the following transformation is harder in typed langs. | |
# I wish not to deal with that at the moment. Will accept any chance no matter how verbose or clunky. | |
# Note/Constraint: I'm particularly hoping for a tactic that keeps the code in 1-2 modules, | |
# not spreading the conditional logic across many classes. | |
# I’m willing to try (even if the result is silly), because if/else statements in the before case | |
# would usually be unit-tested/exercised with mocks like `when/thenReturn`, | |
# whereas the extracted logic can become boolean expressions and be tested/exercised with simple `assertEquals` | |
#before | |
class Responder | |
def respond(input) | |
if (input.foo?) | |
bar = @peerA.commA(input.foo) | |
if (bar.isBarEnough?) | |
baz = @peerB.commB(bar) | |
if (baz.stuff?) | |
qux = @peerA.commC() | |
quz = @peerC.commD(qux) | |
return [qux, quz] | |
end | |
return [baz, nil] | |
else | |
quz = @peerC.commD(nil) | |
return [nil, quz] | |
end | |
end | |
return nil | |
end | |
end | |
#after | |
class Responder | |
def respond(input) | |
situation = Situation.new(input) | |
# I get the danger of infinite loop here, | |
# but figure that can be worked around and it's worth it | |
while !situation.resolved? | |
next_step = situation.next_step | |
case next_step.type | |
when "A": | |
bar = @peerA.commA(next_step.args) | |
situation.stepAperformed(bar) | |
when "B": | |
baz = @peerB.commB(next_step.args) | |
situation.stepBperformed(baz) | |
when "C": | |
qux = @peerA.commC(next_step.args) | |
situation.stepCperformed(qux) | |
when "D": | |
quz = @peerC.commD(next_step.args) | |
situation.stepDperformed(qux) | |
end | |
end | |
return situation.final_value | |
end | |
# part of the whole point of this is that | |
# this class is far easier to test than the "before" example | |
class Situation | |
def next_step | |
#... | |
end | |
end | |
end | |
### Solution 1 | |
# After a conversation, it occured this is a state machine and can be coded like this | |
def Responder2 | |
def respond(input) | |
sit = Situation.new(input, @peerA, @peerB, @peerC) | |
until sit.is_over? | |
step = sit.next_step | |
step.execute | |
end | |
sit.final_value | |
end | |
class Situation < Struct.new(:input, :peerA, :peerB, :peerC) | |
def is_over? | |
defined?(@resolution) | |
end | |
def final_value | |
@resolution | |
end | |
def step_a_happened(bar); @bar = bar; end | |
def step_d_happened(quz); @quz = quz; end | |
def step_b_happened(baz); @baz = baz; end | |
def step_c_happened(qux); @qux = qux; end | |
def resolve(value); @resolution = value; end | |
def next_step | |
if !input.foo? | |
return Resolution.new(self, nil) | |
end | |
!defined?(@bar) and return StepA.new(self, @peerA, @input.foo) | |
if @bar.isBarEnough? | |
!defined?(@baz) and return StepB.new(self, @peerB, @bar) | |
if @baz.stuff? | |
!defined?(@qux) and return StepC.new(self, @peerA) | |
!defined?(@quz) and return StepD.new(self, @peerC, @qux) | |
return Resolution.new(self, [@qux, @quz]) | |
else | |
return Resolution.new(self, [@baz, nil]) | |
end | |
else | |
!defined?(@quz) and return StepD.new(self, @peerC, nil) | |
return Resolution.new(self, [nil, quz]) | |
end | |
end | |
end | |
end | |
class Resolution < Struct.new(:situation, :value) | |
def execute | |
@situation.resolve(@value) | |
end | |
end | |
class StepA < Struct.new(:situation, :peer, :input_a) | |
def execute | |
bar = @peer.commA(@input_a) | |
@situation.step_a_happened(bar) | |
end | |
end | |
class StepD < Struct.new(:situation, :peer, :input_d) | |
def execute | |
quz = @peer.commD(@input_d) | |
@situation.step_d_happened(quz) | |
end | |
end | |
class StepB < Struct.new(:situation, :peer, :input_b) | |
def execute | |
baz = @peer.commB(@input_b) | |
@situation.step_b_happened(baz) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment