I don't think I agree with using objects as the public interface to an algorithm, especially if its primary purpose is to apply some transformation over the input (i.e. a natural fit for a function). The examples given have a bit of a two-phase initialization smell: initialize some internal state, then run the algorithm: steps that are never performed separately.
Objects as an algorithm internal state might be a better approach: the caller doesn't have to worry about the algorithm's internal state, they only need to call the external function entry point (and you'll destroy or otherwise forget the internal state at the end of the function, rather than putting the burden on the caller).
, which, hopefully, resets exactly the right amount of state to work just fine.
There’s also the ‘fun’ of sharing an ‘algorithm’ between threads. That will teach you that this ‘algorithm’ is more a set combining function arguments with function state, and a method that uses them to compute a result. Next, if you’re smart, you’ll separate the arguments from the state.
⇒ if you do something like this, use that builder pattern.
Pendulum is slowly turning back to "Everything's an Object". Time to decorate Jabba sections in that CV and remember to drop in an obvious reference to GoF/Fowler/UncleBob writeups.
The traversal example is a bit weird. There's a simple solution to what the author wanted. Replace `print(root.value)` with `yield root.value`. Now you've got a simple algorithm where the caller can decide what to do with the values. And it nicely resembles every kind of iterating things in python. Instead, the object idea makes the algorithm specific to one single use case.
Same with the second example: You can write this as a filter on a function which reads the data. You get a generic reader and a generic filter which can be tested separately.
Twisting the code to fit it into the strategy pattern and preserving some hidden state just to make that possible seems silly to me. Is the class really worth writing instead of composable / reusable / testable / streamable:
def data_reader(fin):
fin.readline()
for line in fin:
yield [int(val) for val in line.split()]
def fill_zero(source):
last_day = 0
for next_day, value in source:
while last_day < next_day - 1:
yield 0
last_day += 1
yield value
last_day = next_day
As far as the general principle goes, an iterator object is a great example of "algorithms as objects" as opposed to "algorithms as monolithic functions".
From the code simplicity POV they're very different though. Sure, an iterator is an object here internally. But continue that way and you'll get to the "objects are closures / closures are objects" discussion.
I see the fact that Python has a nice syntax for writing generators as incidental to the main point of "algorithms as objects". In Rust you'd (currently) explicitly write a struct and its next() method, but its still the exact same idea.
the argument that text books teach us algorithms in form of single function with inputs and outputs is quite weak, there's a very simple solution - break up the large function into smaller functions.
i'll always prefer a set of isolated functions with explicit argument and return declarations over a class implementation with methods because:
- argument list tells me immediately what is required and what are possible additional options as opposed to having to scan the method list for methods matching `WithFooBar` names
- methods become coupled to internal state of the algorithm object because let's face it - code rots, maintainers take shortcuts, state proliferates, everything that is allowed by a compiler/interpreter will eventually make it into the codebase; it's much more preferable to avoid/diminish most of these problems by writing isolated functions
- explicit arg/return declarations help with default static analysis/checks, much harder to do that with classes and internal state
also, a mention of GoF Command pattern further convinces me that all design patterns are just workaround to shortcomings of mainstream languages of the time and are simply unnecessary in proper FP languages or at least languages with higher order functions and other functional features.
cargo culting design patterns into languages that don't need them is ironic.
Design patterns often seem to rearrange complexity instead of removing it.
If you split everything up into nice manageable <40 LOC chunks, you now have tens or hundreds of nice manageable <40 LOC chunks to document, use, and remember - probably spread across a similarly large number of files.
It looks neater while you're writing it, but it's debatable if it's any easier to understand if you're trying to debug it. Especially if you're coming in cold, and there's an unnecessarily high level of abstraction, added for the sake of following the patterns.
One reason to break up a function is deep logic nesting.
If you consider the Code Coverage metric a valid metric, then it says we'd need 2^N independent tests to validate each possible branch of logic in a function.
Breaking that nesting can reduce the number of required tests by an order of magnitude. At that point we may need an integration test to validate that we can transition from one to the other neatly, but this grows linearly instead of exponentially.
Deep nesting often comes with very long functions.
How do objects removing branching from an algorithm? It may not be there as an if-statement, but (unless I'm missing something) you'll still conceptually have a branch.
It's not objects necessarily; they're simply a way to group a set of related functions.
It's functions.
Separate functions can be separately unit tested. Then you can write an integration test that uses each of those functions. The testing obligations can be overall much smaller as a result.
Because it is not. It's oop distorsion field at its finest.
You can even find talks about why not do that (pyconf iirc).
Basically it's useless réification of functions as you state. But when you love oop (and probably didnt use other things) well it's the ultimate itch to scratch.
I'm sorry, I've been burned too badly by the OO world (not helped by the fact that I went in college at peak J2EE).
And to a certain extent.. I think this crowd still has issues. I spent time on #java recently and I felt this acid confidence in OOP that stimulate their mind that I really cannot fathom. I used to be like that, until I left this field.
Now this is just personally, but of all the things I've read about, it was the less useful for my brain. SQL did teach me some, pre OO Ada did teach me some, ML taught me loads, Lisp worlds, forth etc etc yet OO falls flat to my eyes 99% of the time.
To be honest, when I read 'algorithms as objects' I upvoted, thinking it would be distributed logic as object ala smalltalk. But again, fluent lipid layers.. And I think, the paradigm is to blame.
Now IIRC, you have a lot more knowledge about PL than most, so maybe you see OO in a different light, a more rounded way.
The builder pattern is a reasonable workaround if your language does not support named and/or default parameters. It’s quite idiomatic in Rust, for example. But the builder should usually be an entity distinct from the result.
I agree with x=10.0 etc, but not all languages have named arguments :-) This is but one way to simulate them. Note that Fowler was using what appears to be a Java factory.
Breaking up the large function into smaller functions is ultimately the goal, yes. In my experience, researchers without software engineering backgrounds are very against breaking up an algorithm this way. For the most part, they think that it makes it less readable, because now their algorithm isn't in a single block like it is in the textbook. (The other argument they often give is that they fear breaking it up will impact performance).
It's certainly possible to use structured programming here and instead encapsulate the state into a little struct that is passed around; this was the way things were done in C before C++ came along and created classes, and there will always be people who think this to be the superior approach :-)
A set of isolated functions can get hairy if you're not able to hide the helpers away from the entry points; not all functions are necessarily generic and reusable elsewhere (if you end up with it that way for your specific application, great!). Encapsulation into a class or namespace can help with that.
I'm not sure I agree that isolated functions protects us against code rot more so than with classes. There is always refactoring to consider in both cases. In one camp, you have immutable data classes with functions operating on those data classes and possibly returning another data class (functional programming). In another camp, you encapsulate a reasonable amount of behavior into a class whose methods may mutate the class data (OOP). Both have their strengths.
I'm honestly impressed. After more than 20 years programming and reading about programs, this is the first time that I find such a thing: a concise and well-written description of the exact opposite of everything that I believe in. It's like the author was reading my mind and applied a NOT filter before writing it back.
Code smell #1/#2: If you have too long function/method break it apart. How does this relate code being in function VS method
Code smell #3/#4: If you don't want to put helpers to global scope then just don't. I don't see why closures are so bad. Top level function can act pretty much same as class scope so what's the difference? I don't know about python but if you really don't want to use closure another languages have other encapsulation methods too.
Code smell #5: If you use closure you can have shared state. Passing state can be noise but on the other hand if you only read/write "semi global" variables it's messy too. If you need to pass lot amount of state it's usually hint that you have some other problem in your code.
1. Like said you don't need objects
2. Function have single entry without ambiguity like @Someone shows.
3. You should test behavior, not specific implementation. You don't need mocking for pure functions.
4. Is X optional? What about Y? Does order matter when calling 'withs'? And that builder thing is directly stolen from all the Java jokes.
5. I don't know what is Pythonish way to do delayed callability but I claim that it should be independent thing rather than directly bounded to specific function/object.
If it looks like a function, swims like a function, and quacks like a function, then it probably is a function.
Similar thoughts, and I found this post very valuable for that reason. Programmers belong to different cultures, and this gives a clear idea of how (some) people from a certain culture think / what their values are. Really helps with empathy if when arguing with someone you can form a genuine mental model of another person rather than deciding they're stupid or misguided or whatever.
The start of the article is a very compelling description of why you should split your code into smaller components instead of just having a single structured procedure. Levels of abstraction, testability, etc. - Pretty basic stuff I think.
I don't know where he took the leap to consider objects those components though. You can perfectly expose a function that is internally split without making it public in the public interface. And you certainly don't want to deal with the extra problems of having shared state...
Object-oriented is not an antonym of monolithic. In fact, I'd say they tend to end up being related terms, rather than opposed.
The justification for shared state that the author presents is that threading a lot of (mutable) state as parameters through a tree of SRP-obeying internal functions gets unwieldy. Factoring that state into the fields of a class and making those functions methods is, of course, the ”obvious” solution when you’re thinking inside OOP box.
It is also completely isomorphic to the obvious imperative solution of factoring the state into a struct/record and passing that around. Both approaches have the same problem of being based on mutable state.
The obvious solution to a functional programmer is to make the state immutable and make the functions return the new state instead of modifying it in place. Furthermore, each function should only take the state it actually needs. Of course, this doesn’t really help with the unwieldiness, especially in a language without helpers such as the Clojure `->` threading macro, or a reasonable lens library (and lenses themselves are not the most obvious abstraction ever), so I guess YMMV. But getting rid of mutable state is often a win anyway.
Au contraire! Languages with no mutation at all, like Haskell, still have a well-defined concept of state. If anything, it's more well-defined than in imperative languages based on side effects. I refer you to Rich Hickey's "Values and Change: Clojure’s approach to Identity and State" [1].
The jump happens when you have parameter lists too long to fit on the screen. In C++ terms, consider a const member function that returns something - you know that it can use all the members (without having to explicitly specify each one as parameter) but not change them, so the only "mutation" it can perform is constrained to the return value (ignoring aspects like const not being transitive for pointer members).
In practice I still think this doesn't work on complex algorithms because it remains unclear what depends on what.
This is a reasonable pattern commonly used to implement recursive-descent parsers, where you have a parsing function for each language construct but they all need to access common state (such as the lexer and the error output). Passing this common state as separate parameters becomes tedious, so it makes sense to group them into a struct.
It's not all that well-motivated in the article, though, since helper functions aren't in themselves a code smell. You can just make them private. It's also not strictly necessary to convert the helper functions into methods on the object holding the state, though this can be convenient.
Also, the state object is an implementation detail and should usually be private. The public API of of the parser can be a pure function.
I sometimes wonder if people like Bob Martin aka "Uncle Bob" and Martin Fowler have done more damage than good with their arbitrary rules. Or maybe people are too blindly following their rules in every aspect of software design.
Fowler mostly just writes anthropologically about design patterns he encounters, explaining how to implement them given that you’ve decided to implement them. He says a lot of “you’ve got to do X”, but in the context of the article it means “evidence suggests that you’ve got to do X if you’re doing Y; otherwise, what you’re doing won’t be recognized as Y and people will get confused when you call it Y.” Fowler’s work is to software design-patterns as https://www.grammarphobia.com/blog is to English idioms: someone who provides analyses of what usages are accepted and why, and what usages will get your text a bemused eyebrow-raise.
Robert C. Martin, meanwhile, is certainly paternalistic, but there’s a bit of lost context to his writings: they’re targeted at the army of “Java school” devs going directly from school to working at the big Java shops of the 90s. His advice actually works quite well if your goal is to bootstrap yourself from an education of “programming” but no “software engineering”, directly to being able to leave a large pre-existing Java codebase “better than you found it.” (His advice is no substitute for reading the code, absorbing the architecture, and then extending it in natural and intuitive ways—but 90s Java shops rarely gave their devs time to do that before moving them to the next project.)
It’s fine for the algorithm to have separate logical parts, but in a function this is a violation of the single responsibility principle.
If it's contained inside the algorithm, and never done elsewhere, then it's still following the SRP to have the algorithm as a single function. This is just someone who learned a new set of principles and over applies them.
The SRP is a bit fuzzy when it comes to algorithms, though. What counts as "one thing"? Bob Martin covers this in Clean Code. "If a function does only those steps which are one level of abstraction below the stated name of the function, then the function is doing one thing." SRP falls out of that, as noted by Jeff Atwood(https://blog.codinghorror.com/curlys-law-do-one-thing/).
The author is still overly vague here; it's not that the algorithm has multiple logical parts, it's that it has those plus the kitchen sink.
45 comments
[ 3.4 ms ] story [ 96.0 ms ] threadObjects as an algorithm internal state might be a better approach: the caller doesn't have to worry about the algorithm's internal state, they only need to call the external function entry point (and you'll destroy or otherwise forget the internal state at the end of the function, rather than putting the burden on the caller).
Only one? That API opens the door for
, which, hopefully, resets exactly the right amount of state to work just fine.There’s also the ‘fun’ of sharing an ‘algorithm’ between threads. That will teach you that this ‘algorithm’ is more a set combining function arguments with function state, and a method that uses them to compute a result. Next, if you’re smart, you’ll separate the arguments from the state.
⇒ if you do something like this, use that builder pattern.
my_algorithm = MyAlgorithmClass() results = [0..100].parallelMap(lambda x: my_algorithm.run())
Same with the second example: You can write this as a filter on a function which reads the data. You get a generic reader and a generic filter which can be tested separately.
Twisting the code to fit it into the strategy pattern and preserving some hidden state just to make that possible seems silly to me. Is the class really worth writing instead of composable / reusable / testable / streamable:
i'll always prefer a set of isolated functions with explicit argument and return declarations over a class implementation with methods because:
- argument list tells me immediately what is required and what are possible additional options as opposed to having to scan the method list for methods matching `WithFooBar` names
- methods become coupled to internal state of the algorithm object because let's face it - code rots, maintainers take shortcuts, state proliferates, everything that is allowed by a compiler/interpreter will eventually make it into the codebase; it's much more preferable to avoid/diminish most of these problems by writing isolated functions
- explicit arg/return declarations help with default static analysis/checks, much harder to do that with classes and internal state
cargo culting design patterns into languages that don't need them is ironic.
If you split everything up into nice manageable <40 LOC chunks, you now have tens or hundreds of nice manageable <40 LOC chunks to document, use, and remember - probably spread across a similarly large number of files.
It looks neater while you're writing it, but it's debatable if it's any easier to understand if you're trying to debug it. Especially if you're coming in cold, and there's an unnecessarily high level of abstraction, added for the sake of following the patterns.
See also:
https://ieeexplore.ieee.org/document/4493325
Realistically, the benefits don't seem to be empirically proven - certainly not with any persuasively high level of independent replication.
Breaking that nesting can reduce the number of required tests by an order of magnitude. At that point we may need an integration test to validate that we can transition from one to the other neatly, but this grows linearly instead of exponentially.
Deep nesting often comes with very long functions.
It's functions.
Separate functions can be separately unit tested. Then you can write an integration test that uses each of those functions. The testing obligations can be overall much smaller as a result.
You can even find talks about why not do that (pyconf iirc).
Basically it's useless réification of functions as you state. But when you love oop (and probably didnt use other things) well it's the ultimate itch to scratch.
Ideological demonization is pointless for advocates of either side.
And to a certain extent.. I think this crowd still has issues. I spent time on #java recently and I felt this acid confidence in OOP that stimulate their mind that I really cannot fathom. I used to be like that, until I left this field.
Now this is just personally, but of all the things I've read about, it was the less useful for my brain. SQL did teach me some, pre OO Ada did teach me some, ML taught me loads, Lisp worlds, forth etc etc yet OO falls flat to my eyes 99% of the time.
To be honest, when I read 'algorithms as objects' I upvoted, thinking it would be distributed logic as object ala smalltalk. But again, fluent lipid layers.. And I think, the paradigm is to blame.
Now IIRC, you have a lot more knowledge about PL than most, so maybe you see OO in a different light, a more rounded way.
Breaking up the large function into smaller functions is ultimately the goal, yes. In my experience, researchers without software engineering backgrounds are very against breaking up an algorithm this way. For the most part, they think that it makes it less readable, because now their algorithm isn't in a single block like it is in the textbook. (The other argument they often give is that they fear breaking it up will impact performance).
It's certainly possible to use structured programming here and instead encapsulate the state into a little struct that is passed around; this was the way things were done in C before C++ came along and created classes, and there will always be people who think this to be the superior approach :-)
A set of isolated functions can get hairy if you're not able to hide the helpers away from the entry points; not all functions are necessarily generic and reusable elsewhere (if you end up with it that way for your specific application, great!). Encapsulation into a class or namespace can help with that.
I'm not sure I agree that isolated functions protects us against code rot more so than with classes. There is always refactoring to consider in both cases. In one camp, you have immutable data classes with functions operating on those data classes and possibly returning another data class (functional programming). In another camp, you encapsulate a reasonable amount of behavior into a class whose methods may mutate the class data (OOP). Both have their strengths.
I kept reading the post in disbelief: "OK, he must be joking... he's not joking..."
Code smell #3/#4: If you don't want to put helpers to global scope then just don't. I don't see why closures are so bad. Top level function can act pretty much same as class scope so what's the difference? I don't know about python but if you really don't want to use closure another languages have other encapsulation methods too.
Code smell #5: If you use closure you can have shared state. Passing state can be noise but on the other hand if you only read/write "semi global" variables it's messy too. If you need to pass lot amount of state it's usually hint that you have some other problem in your code.
1. Like said you don't need objects
2. Function have single entry without ambiguity like @Someone shows.
3. You should test behavior, not specific implementation. You don't need mocking for pure functions.
4. Is X optional? What about Y? Does order matter when calling 'withs'? And that builder thing is directly stolen from all the Java jokes.
5. I don't know what is Pythonish way to do delayed callability but I claim that it should be independent thing rather than directly bounded to specific function/object.
If it looks like a function, swims like a function, and quacks like a function, then it probably is a function.
I don't know where he took the leap to consider objects those components though. You can perfectly expose a function that is internally split without making it public in the public interface. And you certainly don't want to deal with the extra problems of having shared state...
Object-oriented is not an antonym of monolithic. In fact, I'd say they tend to end up being related terms, rather than opposed.
It is also completely isomorphic to the obvious imperative solution of factoring the state into a struct/record and passing that around. Both approaches have the same problem of being based on mutable state.
The obvious solution to a functional programmer is to make the state immutable and make the functions return the new state instead of modifying it in place. Furthermore, each function should only take the state it actually needs. Of course, this doesn’t really help with the unwieldiness, especially in a language without helpers such as the Clojure `->` threading macro, or a reasonable lens library (and lenses themselves are not the most obvious abstraction ever), so I guess YMMV. But getting rid of mutable state is often a win anyway.
[1] https://clojure.org/about/state
In practice I still think this doesn't work on complex algorithms because it remains unclear what depends on what.
It's not all that well-motivated in the article, though, since helper functions aren't in themselves a code smell. You can just make them private. It's also not strictly necessary to convert the helper functions into methods on the object holding the state, though this can be convenient.
Also, the state object is an implementation detail and should usually be private. The public API of of the parser can be a pure function.
Robert C. Martin, meanwhile, is certainly paternalistic, but there’s a bit of lost context to his writings: they’re targeted at the army of “Java school” devs going directly from school to working at the big Java shops of the 90s. His advice actually works quite well if your goal is to bootstrap yourself from an education of “programming” but no “software engineering”, directly to being able to leave a large pre-existing Java codebase “better than you found it.” (His advice is no substitute for reading the code, absorbing the architecture, and then extending it in natural and intuitive ways—but 90s Java shops rarely gave their devs time to do that before moving them to the next project.)
It’s fine for the algorithm to have separate logical parts, but in a function this is a violation of the single responsibility principle.
If it's contained inside the algorithm, and never done elsewhere, then it's still following the SRP to have the algorithm as a single function. This is just someone who learned a new set of principles and over applies them.
The author is still overly vague here; it's not that the algorithm has multiple logical parts, it's that it has those plus the kitchen sink.