The use of ‘class’ for things that should be simple free functions (2020)
quuxplusone.github.io
quuxplusone.github.io
Everybody came up with some solver classes in Java, some stateful, some stateless. One guy presented the most monstrous solution with 100+ lines of code where you could individually assign the coefficients, individually modify them, trigger recalculation, get the number of roots, get the roots. That guy was very proud of what he did.
Oh, I found bugs in his code. But that was considered normal. People make mistakes, right?
I showed them a two-line solution with a single function (where you'd try hard to make a mistake) and nobody liked it. I was ridiculed right there, for not solving the problem "properly".
It struck me how someone can overengineer a solution to the simplest problem and be proud of it. I thought at the time this industry is screwed if this is the norm.
In fact probably some 90% of so called library code on GitHub is overenginnered crap. 90% is arbitrary but from my experience every time I look for some solution let's say for some GUI effect on iOS, find a library or a framework, take the source, analyze it, start simplifying it and I end up with a version that is orders of magnitude shorter and can be just copied into my project it's so trivial.
You realize that it was so trivial that there was no need for a library in the first place.
I liked the post about minimaism the other day here on HN [1]. It still amazes me how minimalism is not the norm, is not taught as the only way you should solve problems in software engineering. There's no "proper" way other than the most minimalist one, period.
However, minimalism requires some extra effort to achieve, and that's the whole point of engineering.
[1] The post was about minimalism in programming languages, but the author had another, more general post: https://pointersgonewild.com/2018/02/18/minimalism-in-progra...
Edit: mandatory favorite essay: http://www.paulgraham.com/power.html
One thing I'd add over the 'screwed industry', at least in the java,model era is that the more they added, the more "tools" they needed and it was seen as a quality. Basically quadratic complexity at the cultural level..
ps: as usual, https://duckduckgo.com/?q=stop+writing+classes+pycon&t=ffab&...
Maintainable code has sane abstractions, it wraps external dependencies, and it has minimal interfaces between them and other code. Minimal here doesn’t mean quantitatively minimal but qualitatively minimal.
https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpris...
Things like this:
static final int INT_1 = 1;
I've actually seen production code with: static final string HTTPS = "https";
static final string COLON = ":";
static final string SLASH = "/"; assert(INT_1 == 1);
Gotcha. class MyBetterrrrInteger(MyCustomNumberClass) implements ICalculatable {
public static MyBetterrrrInteger(OtherIntegerClass num) {
...
// oh did I forget to add generics to the mix? damn.
}
...
}As is, the only way I'd approve of it is if it was required to have the value be addressable. I'm not sure how often of a concern that is in java, but in Go for example, from ~1.3 to somewhat recently sticking an integer in a interface would cause a small allocation to hold the integer. By having it stored as a variable (you can't take the address of a constant in go) one could elide the allocation by storing the address of the variable in the interface instead of it's value. Of course now go will automatically do that for small values (N < 256).
That's a pretty niche use, but it does exist. I'll assume it's not novel, and there are other, perhaps similar use cases for having a symbol instead of using the literal.
wrt https: Yeah, it should be in muscle memory, but humans make mistakes, fat fingering is a thing. Using a symbol can give you a compilation time check. Of course, you may typo the symbol and have the exact same problem. The ability to compare by address actually applies more to strings though, since you can check the address & length for equality rather than having to do a full comparison of the string. This may or may not be an issue in any given language/implementation based on a variety of reasons (string interning, multiple definitions of a literal due to runtime/dynamic loading).
A day later the Java guy got back to me with a full blown Java project three levels deep which was just a translation of my ~30 lines of Python, and it “didn’t work”. I had to read what was now hundreds of lines of Java to find 3(!) bugs in it.
That day I realized FizzBuzzEnterpriseEdtion is much closer to reality than I thought.
In fact, I came across a line of code that was basically `two = 2;` in my own company's codebase just a few days ago.
https://www.gnu.org/software/hello/
For a while I did not clue in to the fact that they were serious, I thought they were making fun of their own build tools...
Minimalism can bite you, too. We have around 30 microservices written in Go, each one one of them living in their own repository (separate codebases owned by different teams). Initially we followed Go's idiom "a little copying is better than a little dependency". So every team simply was borrowing code snippets from other microservices' codebases without creating a common shared library, because those snippets were "trivial". Then it hit production and we found that under load/diverse user input many of those trivial functions were quite buggy or inefficient with edge cases. Since those functions/classes were copypasted over multiple codebases, now you had to contact owners of 30 repositories and coordinate bugfixing in all of them, which is slow and painful. Now we're back to having a shared library of common functions, because it's well tested and it's easy to fix bugs by just upgrading. Often what seems trivial is not trivial at all.
If all potential problems with code could be noticed on spot, we wouldn't have the concept of "bugs". Yes, I mentioned minimalism in my post, but I was specifically referring to the idea of copying "trivial" code as opposed to using a dependency.
There's a long-standing trend of inflexible dogmatism that's pervasive in software development. If some thought leader, blog or Google/FB/Microsoft/etc said to do something in one way, then things must be done that way.
I once had someone get red in the face and call me stupid because I suggested that hiring more employees to run things after-hours would be preferable to forcing developers to spend their free time working on-call, because apparently Google's SRE bible says that developers must support operations themselves, even if that means they're doing free work in the middle of the night after putting in their 40+ hours a week in. That was unacceptable because Google said so, who cares if employees are leaving in droves and those that remain are spread so thin that their contributions during actual work hours were suffering. Google knows best, we must do what Google says to do, even if that tanks productivity and retention. The company eventually hired teams in different timezones to keep things running 24/7.
We follow this too, but additional work is paid. It kind of makes sense, because developers who own a service can solve a problem faster and with a better solution than someone from a different team, because they know the full context. In practice, I had to work at night only a few times when production was down. If it's not supercritical, it can wait till the morning.
The company didn't push for on-call duty at all for our EU developers, because that would cost the company more money for higher rates and bonuses. Instead, on-call duty was the responsibility of US and Canadian workers, because the company didn't have to pay them for the extra burden and loss of free time. EU developers worked on the same products, and could have done on-call duty with us, but that was never on the table for reasons that had nothing to do with developers being familiar with the projects they're supporting. It was all about reducing costs and getting free work out of developers dressed up as "best practices" from Google.
The ridiculous thing was that it was only about a 1 in 10 chance one would get called, but I still couldn't get any sleep on those nights and would rock up even more tired than usual for work the next morning.
The callers would be end-customers from USA so I was always stressed I'd be too blur to answer well right after wake up and give them a bad impression. Didn't help I only understood some parts of the system so would have to muddle through the rest. My spouse wasn't overly impressed by having the phone randomly ringing at 3am either.
I guess it depends on your idea of minimalism.
To me, having 30 microservices sounds tremendously complex. The minimalist in me would want to see those 30 microservices converge into a single project.
And did you solved the problem "properly"?
Odds are you did not.
I mean, it's terribly easy to come up with all sorts of two-liners if you just ignore all requirements and constraints, and don't pay attention to usecases.
This is a major problem plaguing software development. Plenty of people act like they have to carry the burden of being the only competent and smart individual in a sea of fools, and proceed to criticize everything that everyone around them does, for the sin of not doing something that exactly matches your personal opinions and tastes.
Best example are C++ stdlib classes like std::vector. Sometimes you just need a trivial growable array implemented in a few dozen lines of code, but std::vector pulls in roughly 20kloc of code into each compilation unit, and only if you're very lucky the compiler will condense this down to the same few dozen lines of actually needed code (the fabled "zero cost abstraction").
IMHO OOP languages are more prone to this sort of code bloat problem because they require (or at least "expect") to write a lot of "ceremony code" which is entirely unrelated to the actual problem (such as in C++: constructors, destructors, copy-, move-operators, etc etc... and the actually important "two-liner" problem-solving method is completely buried).
This is actually a very poor and ill-thought example. C++'s standard template library is expected to provide generic components that are flexible enough to meet practically all conceivable usecases, so that they meet anyone and everyone's needs.
This, obviously, means it supports way more usecases than the naive implementation you can whip out with "a few dozen lines of code".
There are very good reasons why everyone just picks up std::vector over any other implementation, and only extremely rare edge cases (like stuff that sits in the hot path of some games) ever justify using something exotic.
...and that is exactly the fallacy with the C++ stdlib design philosophy, one-size-fits-all classes like std::vector should either be split into several much more specialized classes, or its runtime behaviour should be much more configurable (but ideally both) - in any case there's no justification for pulling in such an enormous amount of code into each compilation unit.
Can you specify what exactly your problem with that is? So far you've handwaved at "I don't trust the optimizer to produce good code", but loc is not inherently tied to how many user-facing abstraction layers there are. In fact, you'll find that there's maybe one additional layer of data abstraction in the actual std::vector itself. There's not much to cut through for the optimizer, and in my experience it has zero trouble doing so.
In terms of compilation speed, you pay the frontend cost for those 20 kloc exactly once if you use precompiled headers.
> should either be split into several much more specialized classes
Could you elaborate what "specialized" (or runtime-configurable) versions of std::vector you are thinking of? Just the fact that some people care about exception guarantees, and some people care about move construction, and some people care about custom allocators, and some people care about emplace semantics, and that not all of these are the same people, doesn't mean that it's inherently good to have separate classes for these aspects.
Compile times mainly. This quickly adds up in a C++ project using the stdlib and gets worse with each new C++ version, it's almost at a point now where each stdlib header includes everything else from the stdlib.
> Could you elaborate what "specialized" (or runtime-configurable) versions of std::vector you are thinking of?
First and foremost more control over growth (e.g. when growing is triggered, by how much the memory is grown). More control over what erasing an element means (e.g. whether the remaining elements are required to stay in order, or if the gap can be filled by swapping in the last element). A POD version which is allowed to replace piece-wise memory operations with bulk operations (here I'm actually not sure if optimizers are clever enough to replace many unique moves with a single memmove). An more straightforward way to define an allocator (most importantly, the allocator shouldn't be part of the vector's type signature - not sure if that's what the new polymorphic_allocator in C++17/20 is about though).
...those are just some obvious requirements I had in the past, but I'm sure other people will have different requirements.
Only some of the people coming onboard never got over the shock of such a rule, but for the most part, all developers embraced the different paradigm. Teams inter operated faster, iterated changes faster, & there were less issues with inter department libraries. Bugs were far fewer.
It really gives you freedom to have complete ownership of your code, rather than 100% relying on boilerplate libraries. I now see std:: as a disempowering limitation to modern development.
Of course, YMMV.
It also makes it easier to work on other platforms, like embedded, where resources are a premium. Writing your own vector & hashmaps then become part of your muscle memory & it's no longer daunting to write a custom allocator.
It's hard for me to come up with a scenario where even a single one of these follows from not using the STL. How do you "iterate changes faster" when you re-invent vectors and hashmaps so much that it becomes muscle memory? How does each team writing their own containers mean "teams interoperated faster"? How does the STL cause "issues with inter department libraries", and what bugs were caused by using standard library containers? At which point did you ever have to debug a std::vector?
I'm genuinely interested if you can retell what problems you ran into there. Maybe my creativity is limited, but I'm drawing blanks.
(To be clear, at the scale of Facebook or Google, writing folly or abseil can easily pay off because you can integrate, say, your profiling or debugging tooling more tightly. But that doesn't appear to be what you're alluding to. I'll also concede resource management on embedded devices.)
I once had a coding problem interview where a half of the logic could be handled by an autobalncing tree. I\ve never really used autobalancing trees in real software before, but i knew how to make them from scratch, quickly as RB tree is a very common school problem. I spend twenty minutes choosing between coding one from scratch and picking an already existing solution. I ended up choosing gnl's RB trees, with all the makefile/autoinstall issue that i would have to fix instead. I did not gain any time, really, but i wanted to show i did not suffer the NIH syndrome. Was that a mistake? Should i stay within the stdlib during coding interviews (i don't know if they could run the code, i think the interviewer was running windows)
The problem is that both are right.
(but jokes aside, I guess that the interviewer is more interested in your ability to solve a problem from scratch instead of you ability to google for an existing solution - even if googling makes a lot of sense in the real world)
And I use FreePascal. Its stdlib is vastly larger than the C++ stdlib, but also completely untested/unusable, because no one is using it.
I wrote my own unicode handling functions for everything.
Yesterday I found a new test case, and noticed that my convert utf8 to lowercase function did not handle the Turkish İ correctly (it should turn into two codepoints rather than one for reasons. although I had a check for that symbol, but it was in the wrong branch). And my function had quadratic runtime, so it was also nearly unusable.
So I fixed my function. Then I thought, why am I even writing a convert to lowercase function? FreePascal already has a convert to lowercase function. There is a lesson here, do not write your own functions, you will miss cases.
So I loaded the stdlib Unicode functions to compare my function to their function. And, segmentation fault. Not in the convert to lowercase function, but just loading that Unicode part of the stdlib broke something.
Although while writing this post, I thought, perhaps I test it again, just the convert to lowercase function, without the crashing part. It also fails to handle the İ. And worse, it returns a string that is one byte too large, like a garbage null terminator. There is a lesson here, do not use the stdlib, it is just broken.
Then I compared it to a compare to lowercase function from another library I had included. Twice as fast as even my new fixed function. But it also does not handle the İ symbol. There is a lesson here, do not use other libraries, they do not do what you need them to do.
I would say something like: "I think a balanced tree, such as an rb-tree, would be useful here for <reasons that make sense given the problem and the properties of rb trees>. I've written rb trees before and think I could write a basic one in 10-15 minutes or I could use <class from the std library, which uses a balanced tree>. Which would you prefer?"
Assuming what you said made sense I would take an interaction like that as a positive signal.
The code consists almost entirely of templates and inline functions and every modern C++ compiler will optimize that away. In a release build there is basically no overhead compared to using raw pointers.
If his solution doesn't work with arbitrary number-like objects (matrices at the very least), doesn't support voice input, can't send results via email and can't seamlessy resume the calculations after a hardware failure, it's far from overengineered.
Should have used TDD/BDD, developed some Gherkin test cases, and used those to develop some JUnit automated tests before starting on the implementation. Oh, and each class -- for the solver, coefficients, variables, etc. -- needs to leverage dependency injection so they can be properly mocked in the tests...
I'm reminded of a story about Kent Beck or somebody, one of the big TDD gurus, who wanted to get a microcontroller to draw something on the screen or something. So he first wrote the program in Java, being sure to employ proper TDD principles, and then hand-translated the Java code to microcontroller assembly or something. But this was feted as a win for TDD and evidence that TDD can be used for anything and therefore should be used for everything. The details of the story are vague, and I can't source it right now, but I know it exists. Some Hackernews is bound to dig up a link, they usually do when I make an obscure reference.
I don't know how your two liner worked nor why it was deemed improper. I don't know whether it solved all special cases. But it in fact failed your political goal.
But the 80-20 principle still holds true for many things — the rare edge cases may indeed bring a whole lot of complexity with themselves. They can easily be a difference between an O(n) algorithm and an NP one.
Minimalism is okay (in that we should strive to decrease the accidental complexity), but I found that some people overdo it to such a naive and frankly, dumb levels that it is actively harmful.
AFAIK, the closest you can get to free functions is a non-static class that only has static functions. infuriatingly, many coverage tools will count the "constructor" of this class as uncovered code. stuff like this makes me really miss C++.
Static inside inner class is something else entirely and has nothing to do with topic of this thread.
To me, this reads like a requirement, can you really blame them for respecting what you asked for?
I struggle with that all the time. Libraries always tend to accumulate such trivia. My litmus test for it is if the documentation has more lines than the code.
I hate libraries that are a mile wide and an inch deep.
Not sure I am understanding this correctly. Would undocumented libraries be the best or the worst according to this metric?
Sometimes you want a library anyway in these circumstances though, since the expertise required for the short implementation might be significant.
One such case I was permitted to write about (most of my work is under NDA), I can't believe it's already been 8 years since then:
The difference between `setup_environment_variables` and `Environment.setupVariables` is just ease of use, auto-complete and organization. Not some scary over-engineering creeping in. Tracking down related helper functions is far easier like this too, if I want to see what environment related helper functions I have, I just type Environment. and it shows me everything available to autocomplete, or I click/press into Environment to go see all the related code.
Anyone who's ever opened the "functions" file of a long running codebase to find 50+ free-form functions knows it's not a great way to be organized.
PHP, easy.
Java, sacrifice testability and even the ability to access the functions under multiple contexts without adding MORE code, in a self-defeating exercise.
I am especially frustrated by Uncle Bob suggested refactoring of long pure function into a class with private fields. You had a function you could not call wrongly and now you have internal state, race conditions, lifecycles. Good job.
If so, that feels analogous to a function potentially having module-level state variables to me, in Python for instance.
- in the JVM classes are an obligation
- using functions instead of classes puts stronger requirements on sensible naming of modules and functions. E.g. in python functions from imported modules easily litter your namespace
- not using unneeded classes makes your code harder to read for many colleagues. E.g. if I have a python module universe my colleagues hate it if I import the module and call a function like universe.create() [instead of Universe().create()]
Fair enough but it does suck that it has to be that way. But yeah, fair enough.
> Classes allow namespacing
This is the same mode of issue as the above: when your language doesn’t support arbitrary namespacing or (hello Python, hello Java) then yes classes can get used as namespaces in disguise. It sucks that it has to be this way in these languages, but we play the hand we’re dealt. Great point
> My colleagues have stylistic preferences
I’m struggling to understand this one. Are you saying that your colleagues prefer instantiating classes over functions, just because, or is there a technical reason for that convention?
class EG:
NORTH, SOUTH, EAST, WEST = 1, 2, 3, 4
def move(a, b):
return a-b
EG.move(EG.NORTH, EG.WEST)
etc.the whole point of design patterns is, exactly, to reuse "class etc." syntax for essentially non-OO purposes
Not really? I don't see why it matters whether the namspacing keyword is `namespace` or `module` or `class`. If all you want is modularity, they work pretty much the same.
This feels like a bikeshed issue. There's nothing fundamentally wrong with `Math.min()` or just `min()` with a static import.
You're always free to use a final class with a private constructor as a namespace for static methods. Many classes in the standard library are of this kind, including `Math`. It's not ideal (see my next point), but it's a far cry from adding extra object instantiations everywhere.
> E.g. in python functions from imported modules easily litter your namespace
I think this is more a problem with the modularity features in a language than free functions themselves. In Python, Haskell, and Agda, I make sure that every symbol I use is explicitly imported (or defined in the same file). This makes it way easier to trace where each symbol I use is defined.
You're basically forced to do this in Java, too, with exception of wildcard static imports, which I very rarely use.
> if I have a python module universe my colleagues hate it if I import the module and call a function like universe.create() [instead of Universe().create()]
They hate it? I can understand if your colleagues aren't developers by trade, but I expect anyone who maintains code for a living to understand the language they're using. There will always be dark corners, but this ain't one.
And you literally gained nothing. You do exact same thing with slightly different syntax.
In Java, not the JVM. Some JVM languages like Kotlin and Clojure allow for classless functions.
You could argue that it still gets compiled to bytecode with classes, but then that bytecode gets JIT'd to not have classes again anyway so it's kind of a meaningless distinction.
I interpreted the comment to mean "JVM languages" but if you wanna discuss the JVM itself (which is nonsensical comparison here imo) then yeah sure, it needs "classes".
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.ht...
The semantics of Java classes 'in the programming sense' are largely specified and implemented in the JVM.
This sounds like a bit of a weird argument to me. Of course convoluted or strange solutions should be avoided, but `import universe; universe.create()` doesn't sound particularly strange in Python, and it's hard to see how there could be a problem reading it unless it's simply due to being used to a different style. People being used to a particular style could be seen as an argument for that style if that style is indeed a common convention in the language, but if `Universe().create()` is the style they're used to, that sounds more idiosyncratic than idiomatic in Python to me. Do your colleagues perhaps have a background in a different language?
Local conventions at a particular organization may of course also be valuable to follow even if they're not global conventions for the language. But that doesn't really change what is and isn't a good style more generally.
(`from universe import Universe; Universe.create()` would sound like a typical style to me if a class for universes were warranted in the first place, but in that case the class isn't unneeded.)
`import *` isn't recommended. I only use it in `__init__.py` when I want a flat package that exposes symbols implemented in multiple submodules.
This would be really depressing for me but ultimately team limitations sometimes win out. If your team can only "understand" code that looks like Java, then maybe all code has to look like Java.
In white collar environments, "don't understand" can be a sneaky, faux-polite way of saying "don't like and refuse to be open to". But it might add up to the same thing in the end.
And for your last example did you mean `Universe.create()`? It should be a static or class method. Those are perfectly fine when they're closely related to the class itself, as with factory methods.
That’s an ironic joke of course — who does this? — but it’s a helpful way to then think about the inverse. If you have a class with one function then it could probably just be a closure.
Even better, if it closes one thing, one thing only, and uses that thing while never modifying it then your class is really just a group of functions all of which share the same first argument. Quite a lot of “Database” abstractions are like this, with methods built around a single internal reference to a database connection.
The downside of returning a function instead of an object is your caller has to give it a name:
lol = build_handle()
lol()
…versus the class-as-sensible-name-enforcement version: lol = Handle()
lol.sensible_name()
As always, quite a lot of these problems are stylistic (or cultural, if you work in a team) rather than technical.Coding is, amongst other things, an exercise in design.
So in Python you could do:
import greeter standalone greet1
import greeter standalone greet2
greet1.name = 'Joe'
greet2.name = 'Sue'
greet1.greet()
greet2.greet()
And greeter.py would look like this: name = 'nobody'
def greet():
print ('Hello '+name)
Output: Hello Joe
Hello Sue
So just like it is now, but with a "standalone" keyword that turns an imported module into an "instance". // Greeter.zig
const std = @import("std");
const Self = @This();
name: []const u8,
pub fn greet(self: *Self) void {
std.debug.print("hello {s}!\n", .{self.name});
}
The above file is equivalent to the struct: pub const Greeter = struct {
name: []const u8,
pub fn greeter(self: *Greeter) void {
std.debug.print("hello {s}!\n", .{self.name});
}
};
--- // main.zig
const Greeter = @import("Greeter.zig");
const greeter1 = Greeter{
.name = "Joe",
};
const greeter2 = Greeter{
.name = "Sue",
};
pub fn main() !void {
greeter1.greet();
greeter2.greet();
}
A lot of data structures in the stdlib are in written in this style.EDIT for comparison, a non-standalone version would be written like this:
// greeter-nonstandalone.zig:
const std = @import("std");
pub var name: []const u8 = "nobody";
pub fn greet() void {
std.debug.print("hello {s}!\n", .{name});
}
// main-nonstandalone.zig
const greeter = @import("greeter-nonstandalone.zig");
pub fn main() !void {
greeter.name = "Joe";
greeter.greet();
}When the time comes, I promote the types as necessary.
This to me is the only real benefit of OOP, that I can just add another service into the constructor and not have to update call sites of other signatures which would happen if I passed everything as parameter arguments.
I work in Asp Net land, and most of my work is linking services together and not so much solving mathematical problems.
$f = fn($x) => f($x, …$deps);
Then I can pass $f to whatever function with its dependencies satisfied.
Edit: fixed example
The decision of what goes where must be made somehow. So you either do it through the type (eg. `@inject(MySuperSpecificServiceThatOnlyExistsOnceAnyways.self)`), or write provider functions.
Isn't the second solution basically the same thing as doing it manually?
Initialization order doesn't matter if your services are stateless. At least in our codebase, all of them are stateless, as it greatly simplifies reasoning about concurrent code (both in-process and between servers). Yes, it's easy to end up with a very convoluted dependency graph under the hood, but I don't think it's a problem you really should care about. I mean, your code most likely already compiles to a very convoluted mess of machine code under the hood (with all the optimizations, ABI quirks etc.) and I doubt it matters to you much, as long as it does its job well and doesn't hinder your productivity.
If you are talking about messy dependency graphs from the architectural standpoint (someone can easily add a dependency in the constructor without thinking about the consequences), we use deptrac for our PHP monolith which can validate your architecture is clean at build time [0]
However, for our microservices written in Go, we decided to use manual DI to stimulate developers to prefer simpler design, otherwise our microservices could quickly turn into monoliths again.
Dependency injection in that sense is only used in the "impure" parts, at the edge of your program, to build up application state.
A popular approach is using something like integrant, which describes dependencies as a plain data structure, which gets resolved as a dependency graph and calls into multimethods (polymorphic functions) that you provide to start/stop individual services or what have you.
Another popular approach is to use something like mount, which is basically just a macro which you use to describe how a service is started or stopped. All the dependency stuff is simply resolved via Clojure namespaces.
Both of these (and others) are done in a way so you can start/stop your whole app or parts of it, or just individual services in a REPL. Most of your code doesn't interact with these services, but gets called from them (functional core), so you typically don't need mocks or other such things in testing as your domain logic doesn't know anything about application state.
from functools import partial
new_function = partial(yourfunction, the_injected_dep=the_value)Here's a talk he gave concerning free functions at CPPCON'17 [1].
It's high-level enough to be understood by non-C++ devs and I believe it can be applied to any OO language.
https://en.wikipedia.org/wiki/Closure_(computer_programming)
In cases like this, I still use a top-level free function, but it just instantiates a (private) class and invokes those (private) subfunctions itself. There's usually no need to leak the implementation's strategy for managing complexity to callers.
Of course, if your computation required interactivity with the caller, then it's not a one-and-done batch deal anyway, so free functions would not have been as appropriate in the first place.
That's a good approach I follow myself, too. The fact that I decided to use a class to manage complexity is an implementation detail; callers should not be aware of it. I don't think there's an opposition "free functions vs. classes", they complement each other.
Namespaces are stateless, they don't contain variables, they contain just functions.
Which of these classes are antipatterns?
Observer
FileManager
HTTPServer
CircuitBreaker
ObjectBuilder
ServiceProvider
SchemaValidator
StringTranslator
Vector
Player
User
Printer
I feel like the language gets annoyed with me when I just want a pure function, and goes "fine, i guess i support that. Still needs to be in a class though... Because... I like classes"
var f = x -> x + 2;
Yes, this is a syntactic sugar for a class. (And the class is a syntactic sugar for machine code.)If having a class like this makes sense, do it and leave optimizing to the compiler.
Also, classes along with static methods can be used to create a namespace to avoid polluting the global namespace, grouping similar operations together.
Some languages do not provide namespaces but do provide classes, so static methods are all that is available.
This is the real crux of the article and the tradeoff you are weighing; it's too bad that the consequences of this change aren't explored
With more arguments the function call is hard to read
countDominoTilings(4, 7), what is 4, what is 7?
Some languages have named parameters. Then you can write
countDominoTilings(width = 4, height = 7), which is very readable.
But without named parameters,
DominoTilingCounter tc;
tc.width = 4;
tc.height = 7;
tc.count()
would be more readableAnother option would also be to just pass a struct:
struct dimensions dim = { .width = 4, .height = 7}; countDominoTiling(dim);
My C++ is rusty so this might not compile but you get the idea.
There isn't a big difference with just one simple thing, but when you're having 10 of these things, with some classes having classes in themselves, it quickly becomes a mess.
interface IHasSaveProject {
saveProject(...): ...
}class HasSaveProject implements IHasSaveProject {
saveProject(...): {...}
}class Controller {
constructor(private readonly IHasSaveProject) {}
post(...) {
this
.hasSaveProject
.saveProject(...)
}
}And basically have a different class for every function
In cases where it really makes sense to have multiple methods on a single class, I do this
interface IProjectService extends IProjectService.IHasGetProject ,IProjectService.IHasGetProject {}
namespace IProjectService {
export interface IHasGetProject {
getProject(...): ...
}
export interface IHasSaveProject {
saveProject(...): ...
}
}class ProjectService implements IProjectService {
getProject(...): {...}
saveProject(...): {...}
}class Controller {
constructor(private readonly projectService: IProjectService) {}
// or
// constructor(private readonly projectService:
// IProjectService.IHasGetProject
// & IProjectService.IHasaveProject) {}
// or
// constructor(
// private readonly hasSaveProject: IProjectSercice.IHasSaveProject
// private readonly hasGetProject: IProjectSercice.IHasGetProject
// ) {}
...
}Seems to be working well for me. Lots of flexibility. Not sure how others like it though.
Please forgive my formatting
An ISaveable interface would need to be generic but "save" functions might take different numbers of arguments, and of different types.
I've found the advantage of interfaces to be dependency injection, where i can inject a different implementation of an interface without breakijg anything (eg save to s3, to google cloud storage, to filesystem, to memory), or a test version of the function/class.
Abstractions like ISaveable on the service interface just make the code more fragile in my experience in an attempt to save some extra but simple lines of code. Granted it may make sense on active record model classes though.
I'm unsure about DI in general [1], but using the Objectifier-Pattern just to appease the DI-System is bad. Likely, you can use `Symbol()` to indicate how your code should be wired.
Decoupling your dependency seems reasonable, but you can do it much simpler.
type GetProject = (...) => ...
type ProjectCRUD = { get: GetProject, set: ... }
constructor(private projectCrud: ProjectCrud)
Depending on the situation, I would go even further and inline the type definition: constructor(args: {
getProject: (...) => ...,
setProject: (...) => ...,
})
[1]: https://news.ycombinator.com/item?id=31547975 - I'd love to hear opinions on that.> type GetProject = (...) => ... > type ProjectCRUD = { get: GetProject, set: ... }
> constructor(private projectCrud: ProjectCrud)
Yep that is a more concise way of doing the same thing. I've considered that approach and it seems perfectly reasonable, even cleaner. The only reason I haven't is because constructors offer a more standardised approach to object creation for service type objects that other developers are familiar with, rather than higher order functions or function constructors. Although my method is kind of bespoke anyway so I could go either way. Another advantage of your approach is composing finer grained functions or objects with methods into more expansive services becomes delightfully easy.
>[1]: https://news.ycombinator.com/item?id=31547975 - I'd love to hear opinions on that.
I've tried to use TypeScriot DI containers... Oh I've tried.. But eventually they all just feel gross.
These days I'm perfectly happy having a bootstrap / createServices method for the application, or with multiple entry points each requiring a subset of services with different configurations, different create<command>Services functions. Works well for CLI apps. Downside is when there are too many entry points with different depenencies, like an HTTP API, you don't want to create a bootstrap method for each entrypoint. In this case I create all services once on bootup. I pass the request context as method argument. Don't have a perfect way yet of doing request-level services like GraphQL caching. Currently I lazy load request level services on request context.
How do GraphQL servers automatically cache objects? I was not aware servers do this. I generally avoid them because of their inflexibility (eg. Unable to support the two websocket subscription sub protocols simultaneously).
Sounds like it's inferior to manually using a DataLoader within every resolver that fetches an object.
eg project has a client. In the resolver for a projects client, you fetch the client from DB. With DataLoader you manually cache the request for the client. With in-built caching you must resolve the client for the server to know the ID, and then on next resolve of the client, after fetching, it matches the ID and doesn't have to resolve it's fields again, but it still had to fetch both times?
> Yep that is a more concise way of doing the same thing
Your interfaces still dictate a method name and signature that must be known by both parties. I would argue that this connection is the concern of the integrating layer. If either party changes, here's where the error should occur:
const service = new ProjectService()
const controller = new ProjectController({
getProject: service.get.bind(service) // this is why I don't like js classes
setProject: service.set.bind(service)
})
Naturally, this needs some caution. If the boundaries aren't chosen well, the integration layer grows.I think you're right that our fundamental difference is the desire to stick to a more standardized class-based architecture. I like js-classes to communicate that something is stateful. But conceptually, they're more hindering than helpful.
The GP asked how to do mocking, and my answer is: Don't mock. Unit test pure code. Integration test impure code.
[0]: https://blog.ploeh.dk/2017/01/27/from-dependency-injection-t...
I still use many static functions
the number of 1000 LoC solutions for 100 LoC problems I saw in open source projects fills me with dread