Hacker News new | past | comments | ask | show | jobs | submit login
The use of ‘class’ for things that should be simple free functions (2020) (quuxplusone.github.io)
106 points by ddtaylor on May 29, 2022 | hide | past | favorite | 185 comments



I once, a long time ago, had a similar argument (somewhere online) with a bunch of Java coders. I asked them to create a library for solving quadratic equations. Asking to create a library itself was a trap of course.

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


>You realize that it was so trivial that there was no need for a library in the first place

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.


This is a great example of dogmatism prevailing in this industry, and the resultant cargo cult programming.

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.


>even if that means they're doing free work in the middle of the night after putting in their 40+ hours a week in

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.


I have no problem with voluntary on-call duty that is both paid handsomely with overtime rates, even for salaried employees, along with bonuses that make sacrificing free time worth it. That's how on-call duty works in many European countries, by law, and in unionized industries and companies here in the US.

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.


I really hated on-call support last time I had to do it, and we were all already putting in vastly more than 40 hours a week (seriously are there really any developer jobs that are only 40 hours in practice?).

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.


Agreed, had similar experiences, and I won't take a role with any type of on-call duty. Thankfully there are a lot of companies out there that aren't trying to cannibalize their employees' free time to cut costs.


> Minimalism can bite you, too. We have around 30 microservices written in Go…

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.


We had a large, complex, spaghetti-driven monolith before the microservices. Aside from a few microservices which support our infrastructure, a microservice usually serves a single subproduct; several subproducts are united under one UI, they only share the user model and communicate via events. I didn't mean our microservice architecture is minimalist (although when viewed alone, a microservice itself is pretty simple), I was giving an example as to why copying code has disadvantages, too, such as once you decide to copy the code instead of using a dependency you are completely on your own, you will miss any new security, performance updates and bugfixes; even supposedly trivial code can be ridden with subtle bugs.


You are right. IMHO there is zero justification for making any application that complex.


It sounds like what you described is not minimalism at all, except from the perception of individual teams. From the perspective of the product, your solution is minimalism.


I'd argue that's not minimalism. A lot of the times it relies on the engineer's intuition: is this trivial enough that it can be copied? If as you say those trivial functions were buggy and nobody saw the red flags while copying so many times by so many engineers, then you have an organizational problem.


>If as you say those trivial functions were buggy and nobody saw the red flags while copying so many times by so many engineers, then you have an organizational problem.

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.


I was talking about the kind of intuition that while looking at the code you should know whether it's safe to copy it or not, i.e. whether it should be examined closely before copying. From your story, the same class was copied 30 times by that many engineers (?) and it didn't raise any red flags, it means (and I'll be a bit blunt) you have a problem with your hiring process.


Well, not every engineer is experienced enough to immediately notice all potential problems which can arise in production. It worked in microservice A, why won't it work in microservice B? It all comes down to expertise. The average engineer is probably better off relying on a versioned, upgradable dependency maintained by more experienced folks than on copying code snippets here and there.


When things like this happen, it is in 98% because adding dependency is comething organization discourages while copying is without punishment. Discouragement can be you having to defend it in too uncomfortable process or something similar.


30 microseervices? Seriously? What on earth is the justification for having that amount of complexity? I have worked with very complex, very large monolith and client/server applications for 25+ years. And I never had trouble maintaining the code or add functionality. My guess is that using microservices was basically an excuse to rewrite the application? And that rewriting it to a new cleanly designed monolith might have been a better choice. Using “advanced” technologies like Modules and Libraries. A friend of mine has taken over a legacy (20+ years) 100+ microservices application. And it is an absolute nightmare. The complexity of building and deploying it is the worst I have ever seen. 10x worse than then worst monolith I have ever worked with. Debugging is a nightmare. Synchronisation issues makes auto testing almost impossible. The only good thing is that all the microservices are written in the same programming language. Imagine if 5+ different languages and build systems were used instead? The complexity would have been out of this world.


Perhaps this is an indication that there should be fewer than 30 microservices and/or less of a focus on "micro"


That’s an odd judgement to make when you don’t know all of the systems and use cases that those services support.


You might have missed the word "Perhaps" at the start?


From a microservices perspective, wouldn't you make a microservices to host that logic instead of copying the code for it everywhere?


And they'll end up with 300 microservices instead of 30.


> I asked them to create a library (...) I showed them a two-line solution with a single function (...) and nobody liked it. I was ridiculed right there, for not solving the problem "properly".

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.


This is just another potential problem of "overengingeered" libraries: they often try to offer a general solution to a whole set of somewhat related problem, but each individual user may just need one specific problem solved, and the correct solution for this one problem may indeed just be a very simple "two-liner".

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).


> 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").

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.


> to meet practically all conceivable usecases

...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.


> 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.


It's rather trivial to write your own specialised form of array management, that is both easier to read & debug & also produces less bloat. I've worked at several places where std:: is verboten.

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.


> Teams inter operated faster, iterated changes faster, & there were less issues with inter department libraries. Bugs were far fewer.

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.)


Good thing about standard libraries is that I don't have to debug them, because they just work.


> Can you specify what exactly your problem with that is?

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.


Actually, i wanted to ask this question to good software engineers.

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)


Depends on the interviewer I guess. Some people will be adamant to use the stdlib as much as possible because they have a war story of how something went terribly wrong by not using the stdlib, others will insist that the stdlib is rubbish because they have a war story of how something went terribly wrong by relying on the stdlib.

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)


I keep alternating between those positions

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.


Sorry for the İ/i I/ı bug. Even modern software in other languages fall to that trap time to time, so probably it's not handled well in other languages either.


I would ask the interviewer and use it as an opportunity to show your knowledge of data structures and your ability to reason about trade-offs and talk through priorities.

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.


Part of this problem is underconstrained requirements. If you do not specify that “a” can’t be zero in “ax2”, it’s reasonable to behave sensibly in that case. One thing is a free designer who states requirements and constraints. Another is an implementer who was given an isolated task and no more details. You have to be either full-defensive or renegotiate edge cases which probably could lead to unnecessary complexity.


> and only if you're very lucky the compiler will condense this down to the same few dozen lines of actually needed code

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.


I swear I've worked with people who if they were shown FizzBuzzEnterpriseEdition wouldn't be able to see the joke as that's how they naturally write all code.

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 = "/";


A friend asked me to do some web scraping for her. Since I didn’t have much time, I figured out the difficult part — some home grown encryption/obfuscation scheme used by the website in question, and handed ~30 lines of Python to another programmer friend of hers, a Fortune 500 Java programmer, to finish the remaining boring work.

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.


I wanted to some simple web scraping and wrote around 100000 lines for an HTML parser and XPath interpreter


I feel like that's code scanning tools to blame. A lot of them complain about "magical constants" even when they're cleaner than a dedicated constant


When you introduce a typo in a literal you will typically realise at runtime. Referencing a non-existing field won’t make it through compile time. I honestly don’t see the problem with this approach. I would choose better (semantic) names though.


You shouldn't introduce a typo in the int literal "1" or even in the word "https". It should be in your manual memory.


my issue is with it being named INT_1. If it were a constant with some meaning, I'd be accepting of it.

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).


I don't know if people should but people definitely do. When I review code, if a constant is inlined in several places (e.g. "https" is used instead of `PROTOCOL = "https"`), I have to check at every place that there's no typo. With a constant I just have to check once.


Some say it should be in a test.


  assert(INT_1 == 1);
Gotcha.


What? You are going to use a global state for this? No, no, no! You ought to put that in a class ...

    class MyBetterrrrInteger(MyCustomNumberClass) implements ICalculatable {
      public static MyBetterrrrInteger(OtherIntegerClass num) {
        ...
        // oh did I forget to add generics to the mix? damn.
      }
    
      ...
    }


There was the "hello world" from project GNU:

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...


Something like `SECURE_HEADER = "https://"` and `PLAINTEXT_HEADER = "http://"` can spare real headaches down the road.


How?


The s that’s so easy to miss can make a big difference in some cases.


It just goes to prove how realistic FizzBuzz Enterprise Edition is that I can't tell when people are joking about stuff like this or when they're deadly serious.

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.


Lack of constraints.. I'm sure having no memory, no cycles or not even computer access time made people aim at diamonds, not tar pits.

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&...


Is it just lack of constraints though? Minimalism also guarantees improved maintainability and fewer bugs in the code. The less you write, the fewer mistakes you make, it's that simple.


No, not really. Minimalism where I had seen it lead to spaghetti like hard to maintain and decipher the code. It was impossible to think about larger units, you had to juggle details of everything in head at the same time.


.. that sounds like the opposite of minimalism.


Exactly the opposite in my experience. The worst maintainable code is usually the minimal ”prototype” or ”MVP” solution. Couple it with time pressured continuous development and you get spaghetti.

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.


Spaghetti is not minimalism, it's quite the opposite. You should minimize dependencies, minimize every function, avoid repetitions, etc. What you mean by MVP that evolves into spaghetti is called "incompetence".


It's not the whole story, but budgets tend to get burned, regardless of whether we're talking about money, time, CPU cycles, RAM, disk storage and so on.


> I asked them to create a library for solving quadratic equations.

To me, this reads like a requirement, can you really blame them for respecting what you asked for?


Sorry, I wasn't clear: they all built classes with more than one entry point.


I’m confused, since you mentioned a two-line-solution. Was it a library then?


You've just described quite a few software problems. They don't exist because things were too simple, they exist because something that was essentially simple got made complex. One of my better tools to deal with projects that have run into trouble is to mercilessly cut stuff away.

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:

https://jacquesmattheij.com/saving-a-project-and-a-company/


> You realize that it was so trivial that there was no need for a library in the first place.

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.


> My litmus test for it is if the documentation has more lines than the code.

Not sure I am understanding this correctly. Would undocumented libraries be the best or the worst according to this metric?


Documentation substantially longer than the implementation can mean the abstraction/interface is more complex than simply reimplementing for a specific use case would have been.

Sometimes you want a library anyway in these circumstances though, since the expertise required for the short implementation might be significant.


> how someone can overengineer a solution

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.


Matrices don’t form a field so I would say they are not really “number-like” in the full sense.


> Oh, I found bugs in his code. But that was considered normal. People make mistakes, right?

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 can't help but be more impressed by a guy who quite clearly genuinly likes both the programming and math, had fun with assignment.

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.


Unless I’ve missed a radical change in recent Java versions, Java doesn’t have free functions per se. So asking Java programmers will give biased answers.


Afaik yes, java doesn't. The closest will be static class and functions.


you can't have a static outer class in java; static classes must be defined inside of a non-static class.

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++.


What so you mean by that? By static class people typically mean that every method has static in it, so you never have reason to create an instance.

Static inside inner class is something else entirely and has nothing to do with topic of this thread.


They may be trying to say that Java doesn’t have static classes that match C# static classes. In C# if a class is marked static it’s enforced as not being able to be instantiated and all of its methods (and fields) must be static.


“Simplicity is the ultimate sophistication.” – Leonardo da Vinci.


How does one solve a quadratic in just two lines? Did you omit degenerate case checks and/or number of roots?


No. Depending on the language you can find a way of returning 0 to 2 results. Could be a dynamic array, could be a tuple that allows NaN's of some sort.


What algorithm did you use? The standard formula is pretty inaccurate, whereas following most of https://people.eecs.berkeley.edu/~wkahan/Math128/Cubic.pdf, I was able to get down to near machine precision. I've seen enough cases where someone blindly copied in a formula (or numerical algorithm) from a textbook, without considering how the numerics behave, and getting bad results, to trust unsourced numerical code.


"Succinctness is power" is illustrated nicely in the array/matrix languages. Code working on time series - switching away from a matrix language seems to increase code size by a factor of x5 - x7 times. It is mentally taxing. What was a word before, is now a sentence.


Hard disagree, especially on the 90% part — essential complexity can’t be reduced. Chances are you just didn’t understand the problem domain, or you only needed a solution for a small subset of the problem which is much easier to solve.

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.


I like to use helper classes to organize things, it's just syntactic sugar for me, and a little easier to manage long term. I think of them like little toolkits. Standard libraries in many languages are essentially delivered as helper classes, I don't think it's a nefarious anti-pattern.

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.


Namespaces and modules were invented for this purpose. The only feature that classes have over functions + modules is internal state. And it's evil.

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.


Easy solution: static class.


For sure, satic classes and functions are what I would use if the language has them.


Can still have internal state.


How can a purely static class have internal state if there's no instantiation of the class or instance variables? Are you referring to variables declared on the class directly?

If so, that feels analogous to a function potentially having module-level state variables to me, in Python for instance.


I suppose, but that's if you build it that way. A 'pure' function can also have state if you pull it in through global vars or other functions. Writing stateless code has to be deliberate no matter which method you use.


We would really have to be talking about a specific language to discuss properly I think, but there is no need for internal state. There is more advantages as well, depending on language, in that one is extensible and one isn't. Some probably see that as a con, but I have done enough work backed into tight corners to know it is better to have it than to hack around something non-extensible.


Use modules and namespaces.


They are for this usecase equivalent to a static class


Then use them.


Many languages actively fight against having static free functions.

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.


Why does a language need to offer both if they’re functionally identical? Does it really matter what it’s called?


Not every language has those.


The context here is C++. Yes, if you are bound to write Java/C/php, do whatever.


Although I agree with the sentiment, one has to consider:

- 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()]


> in the JVM classes are an obligation

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.


> 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.

And you literally gained nothing. You do exact same thing with slightly different syntax.


A version of Math that had non-static methods instead of static ones would be strictly worse IMO. Extra allocations, extra line noise from instantiations, very likely many users would start keeping Math objects around as local, member, or even static (lol) variables.


> in the JVM classes are an obligation

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.


'class' is a first class concept in the JVM. You can't really argue that it isn't by saying it's not in the layer below the JVM or the the fact a layer above it can hide it from you. You can't have a JVM without classes.


I mean, the original post was about programming languages, but The JVM is a virtual machine. It does not have "classes" in the programming sense because it takes bytecode, not source code. A bytecode class isn't the same as a source code class. It would be like saying that x86 has if statements because it has branch instructions.

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".


You don't have to get all talmudic about it, you can just open the JVM spec to almost any page.

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.


Well, the JVM is very vocal about having classes as a basic abstraction. Sure, everything is just bits at the end, but the additional abstraction here is Kotlin and Clojure hiding the classes, in my opinion. But sure, it’s not a fruitful topic to argue on :D


> 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()]

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.)


> E.g. in python functions from imported modules easily litter your namespace

`import *` isn't recommended. I only use it in `__init__.py` when I want a flat package that exposes symbols implemented in multiple submodules.


> not using unneeded classes makes your code harder to read for many colleagues

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.


> JVM forces classes

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?


> It sucks that it has to be this way in these languages

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.


I dont think `class` as a module keyword, in python, sucks. It just requires learning being open to python as python. Eg.,

    class EG:
        NORTH, SOUTH, EAST, WEST = 1, 2, 3, 4

        def move(a, b):
            return a-b


    EG.move(EG.NORTH, EG.WEST)
etc.


That would be a case for an enumeration, not a plain class. This usage would make me think: "And where does it get instantiated?" It is a fake class and this usage should be avoided.


python -- syntax doesnt just correspond to one idea, the class keyword has a semantics entirely independent of an OO interpretation

the whole point of design patterns is, exactly, to reuse "class etc." syntax for essentially non-OO purposes


I'm not sure what your point about imports in Python means. Normally you shouldn't import *, you just import the functions/classes you need, so nothing pollutes your namespace.

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.


If you find yourself regularly writing functions that return more than one closure, you should consider using a class instead.

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.


I do the pair of closures fairly frequently, it's a good match for a producer/consumer pair. For something like a saturated counter an object with a getter and setter is more natural.


The most awesome way to bundle data and functions would be what I call "standalone modules".

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".


You can sorta do this with Zig, but the file you're importing has to be written with that in mind. "Modules" in Zig are just structs, so you can create instances of them (I'm handwaving a little bit here, but that's the gist of it).

    // 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();
    }


Is this a real thing? What if greet imports a module, is that one shared or instanced twice?


That would be shared unless it imports it with the "standalone" keyword.


This reminds of a principle I use. Always use the weakest type for the problem at hand. I don’t use a struct/record where a simple dictionary or tuple would do. I don’t use a class when it could be a plain record. I don’t use an interface when it could simply be a lambda.

When the time comes, I promote the types as necessary.


How do you decide that the time has come?


Can someone give an example of how (automatic) dependency injection works in functional programming?

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.


I wonder: Is automatic DI even helpful? You save some lines of boilerplate but sacrifice control over the initialization-order and get a flat, messy, implicit dependency graph.

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?


>I wonder: Is automatic DI even helpful? You save some lines of boilerplate but sacrifice control over the initialization-order and get a flat, messy, implicit dependency graph

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.

[0] https://github.com/qossmic/deptrac


In Clojure dependency injection is rare and typically explicit in some way or another. But the problem you describe does not happen as far as I understood it.

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.


You pass a partially applied function. So, example of php:

$f = fn($x) => f($x, …$deps);

Then I can pass $f to whatever function with its dependencies satisfied.

Edit: fixed example


I work with Haskell every day and this is something that I really miss from OOP. You can go with Tagless Final or Free Monads, and neither solution feels good enough.


Is something like the reader monad + data-has[1] closer to dependency injection in Haskell?

[1] https://hackage.haskell.org/package/data-has


In python:

    from functools import partial 
    new_function = partial(yourfunction, the_injected_dep=the_value)


Even in C# you can do it without interfaces and constructor injection. You just pass a Func<X, Y>.


I've had the pleasure of multiple trainings from Klaus Iglberger.

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.

[1] https://www.youtube.com/watch?v=zB_bhZCoX5M


A class also acts as a namespace. If computation is complex and requires multiple subfunctions that you don't want to leak to the global namespace, I think it's totally fine to use a class instead of a bare function. Some algorithms also require a lot of context to be passed between such subfunctions, and it's sometimes quite handy to store such context as private fields of the class to avoid passing it from function to function everywhere. Another thing is dependency injection, if the algorithm requires several dependencies, it's sometimes better to inject them in the constructor once instead of passing them from function to function, or using global state. But I agree that for small functions it's an overkill to create a whole class. It also depends on the language you use and what's idiomatic in it and what is not.


> If computation is complex and requires multiple subfunctions that you don't want to leak to the global namespace

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.


>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

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.


Sounds like a great use case for closures

https://en.wikipedia.org/wiki/Closure_(computer_programming)


Yes, this kind of approach is equivalent to closures/currying, in the style of OOP. However, using closures for this may not look idiomatic in some languages.


How? You use the closure to capture the variables, but how can you make the variables visible in another function?


Modules are already name spaces, and dependency injection can be configured using partial application.


In which language?


In Python.


>A class also acts as a namespace.

Namespaces are stateless, they don't contain variables, they contain just functions.


Objects are a poor man's closures. And closures are a poor man's objects: http://people.csail.mit.edu/gregs/ll1-discuss-archive-html/m...


It seems like an example to what I call "Object-Oriented Brain Damage."

https://emresahin.net/post/object-oriented-brain-damage/


Another way I've heard it put is that any class ending in -er or -or is an antipattern.


Unlikely to be true, even if some such classes are indeed antipatterns.

Which of these classes are antipatterns?

Observer

FileManager

HTTPServer

CircuitBreaker

ObjectBuilder

ServiceProvider

SchemaValidator

StringTranslator

Vector


Customer

Player

User

Printer


It's quite interesting that some say it's an anti-pattern while in Go it's the standard way to name interfaces.


This is one of the reasons I don't like Java.

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.)


Still needs to be in a class, too,


Besides the point, but I would hope that for the version found mid-way along the article, (compute in constructor, return value via the count function) the compiler would first optimize away the container class because the container only has one member, then optimize the call to count away.

If having a class like this makes sense, do it and leave optimizing to the compiler.


Performing the calculation in the constructor may not go well if many objects are created, but not very many are computed. The example seems more of a functor than a full blown class.

Also, classes along with static methods can be used to create a namespace to avoid polluting the global namespace, grouping similar operations together.


I may be missing something, but free functions + namespaces do namespacing better than classes with static methods, no?


Assuming there is no state shared between them, they are basically equivalent in function. Perhaps there is an advantage to namespaces regarding call overhead but that would be implementation dependent.

Some languages do not provide namespaces but do provide classes, so static methods are all that is available.


> it becomes the caller’s problem, for better or worse

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


I find this speach very relevant: https://m.youtube.com/watch?v=ZSRHeXYDLko


Reminds me of the "Hello World" module in Drupal 9+...


The solution is not "enterprisey" and using "clean code" until it uses at least 20 design patterns. Uncle Bob would not like it!


Obligatory +1 for trying to argue against OOP without mentioning "cats" or "dogs".


That makes sense when the function has two arguments

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 readable


Every IDE I've used would show you the name of parameters when you hover the function. These days there are even inlay hints that will transform countDominoTiling(4, 7) to countDominoTiling(width: 4, height: 7).

Another 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.


In Typescript I've started doing

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


There should be a firing squad for such abuse of Javascript. We were warned about what Typescript would turn Javasript into.


What's wrong with it? How do you mock functionality?


The same way you would in any other dynamic language such as Ruby or Python.


Which is?


ah, that's the reason why I don't put TS on my resume, despite having worked with it for years on personal projects.

the number of 1000 LoC solutions for 100 LoC problems I saw in open source projects fills me with dread


I'm really glad Perl was my first programming language back in 2000 and not Java or C#. The transition to Clojure was so much easier for me, not having done time in an OO prison. React succeeded in bucking the trend of OOP JS frameworks established by Microsoft and Google's promotion of Angular so they simply set their sights on converting JS itself into yet another OO monstrosity. Now we have none other than M$ as the major steward of Typesript and VS Code. As Lennon said, "Strange days indeed".


This is true


I dont think this makes much sense - a lot of them would likely share functionality and you could group them, like ISavable and just give that all save functions


In my experience I prefer grouping by domain over grouping by abstraction

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'll ignore the argument over whether a generic Save<T> (or even Repository<T>) type would be better.

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.


>Decoupling your dependency seems reasonable, but you can do it much simpler.

> 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.


Ah, I thought you were talking about _automatic_ DI. Apologies.

> 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.


Don’t cache GraphQL at request level. Cache it at schema object level, like most (all?) off-the-shelf servers do.


I cache at the schema level for each request.

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).


Typically by using the id field of objects as cache key. Even some clients do this, which is nice, except if your objects aren’t unique by their ids. It can usually be configured, however.


How though do they know the ID before the object is created (/ fetched from db)?

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?


Well, how could they cache something they haven’t fetched?


I think your intuition is getting there. Using DI always is an anti-pattern[0]. DI in general should only be used to nasty, circular-esque problems. If you can split your code between pure and impure code, and then just pass around the data, you mostly don't need it at all. Passing functions around is more complex than passive data, so you need a really good reason to do so.

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...


You're right. I don't actually DI always. Only (most of the time) when I have may have multiple implementations of a function, mock the function, or test it with different implementations of its depenencies.

I still use many static functions


Having the data in a "model" class and the controller implemented as a set of free functions operating on it works well.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: