Hacker News new | past | comments | ask | show | jobs | submit login
Growing object-oriented software vs. what I would do (dpc.pw)
91 points by vivty on Aug 9, 2021 | hide | past | favorite | 95 comments



This is a hot take but: I have a growing sense that one defining feature of some software engineers is that they’re embarrassed by dense logic. I think you see this in the Java world where people seem to hide the core logic of their program amidst a dizzying array of interfaces and deep function call chains. Maybe with enough DI and whatnot, the business logic itself can melt into the structure of the program.

In comparison, C programs tend not to hide this stuff. C functions are often long and complex. If you were implementing quicksort in C, you would write (more or less) one function with all the logic packed in there you can just read top to bottom. In Java it would be a nest of SortComparator interfaces and SortAlgorithm implementors, which would act to hide the algorithm itself.

There’s something more honest about the C style. It’s like, yeah, the algorithm is complicated. So we put it all together in one dense function. Here it is - go nuts! You don’t have to go hunting for the right implementing class. Or divine how FooFactory has configured your Foo object instance.

All that Java style class abstraction seems to (intentionally or otherwise) make the actual logic of your program hard to find and hard to trace. It’s coy. When I’m trying to read someone’s code, that’s simply never what I want.


Personally, I don't think it has anything to do with being embarrassed about dense logic. I think it's about a love of abstractions.

Many developers get trapped trying to recognize patterns and come up with perfect mental models for whatever problem they're trying to solve when the straight forward dense function is probably the simpler and more maintainable solution. I often fall for this trap myself and am constantly trying to be wary of it.


> In Java it would be a nest of SortComparator interfaces and SortAlgorithm implementors, which would act to hide the algorithm itself.

That is odd because when I look at the Java style, DualPivotQuicksort [1], it seems that the language authors did not do that. This is so very strange! Their methods are long, complex, and highly documented. Maybe they must be really incompetent Java programmers? I mean, they did put this obtuse abstraction by hiding it in Arrays. That's super crazy, just look at this monstrosity!!!

    public static void sort(int[] a) {
      DualPivotQuicksort.sort(a, 0, 0, a.length);
    }
Maybe you need to have an intervention with them? Somehow, in some crazy world, even Java programmers are capable of writing good and efficient code. Its like bad developers might do an awful in job in whatever language they used? Can't be true...

[1] https://github.com/openjdk/jdk/blob/master/src/java.base/sha...


The "enterprise" programming culture which has emerged around Java doesn't impinge on all java code. But it is still a real thing. The fact that openjdk authors can rise above it and write clean, reusable Java code is obviously good. The fact that there is something that needs to be risen above is unfortunate.

I think the same about Javascript, though the specifics are different. I've been writing JS for years, but I've been moving away from it lately because increasingly I feel like an odd duck in the JS world. Most javascript programmers have much less experience (in any language) than I do. When I mention I write a lot of javascript professionally, people assume I'm an fresh faced bootcamp grad. Its sometimes hard to find high quality libraries on npm because the average quality there is reasonably low. Eg good luck finding a password generator which doesn't use Math.random(). Or finding an email parsing library which preserves the order of email headers. (This has semantic content!)

Does there exist high quality java + javascript code? Sure. But even macdonalds makes good food sometimes. That isn't enough to make me a regular customer.

Sometimes the right call is to fight for the ecosystem you're a part of and help it improve. I've done a lot of that. But you don't have to fight for high quality software if you just go where the high quality software is being made. Its easier to switch languages than change a culture.

... I'm being a bit sloppy and judgemental here. Maybe it would be better to say, each ecosystem has a set of values. Javascript values programming velocity, accessibility to new programmers and simplicity. Java has a different list. Insomuch as you're living inside an ecosystem, you don't get to simply ignore and dismiss those influences when you don't like them. It sucks writing Go if you hate gofmt. It sucks writing rust if you don't even want the borrow checker. And it sucks writing Java if you hate dealing with AbstractIteratorFactoryImpl. Even if there's some redeeming code in openjdk.


The "enterprise" programming culture did not emerge around Java.

It was already been there regardless of the language.

I am old enough to have done Enterprise C, Enterprise C++, Enterprise VB, Enterprise Delphi, Enterprise xBase, ...


I've seen and worked with enterprise golang code. Ugh.


A language's core values is not determined by a small, vocal, subculture of its ecosystem. In Java's case, it was set early on to be a "blue collar" language [1]. If programmers fail to follow the spirit of the language then it will be awkward.

"So, how does Java feel? Java feels playful and flexible. You can build things with it that are themselves flexible. Java feels deterministic. You feel like it’s going to do what you ask it to do. It feels fairly nonthreatening in that you can just try something and you’ll quickly get an error message if it’s crazy. It feels pretty rich. We tried hard to have a fairly large class library straight out of the box. By and large, it feels like you can just sit down and write code."

Please do not push the programming community towards a tribal, us vs them, hostile environment where one belittles their neighbor. We can have interesting, fun, insightful technical debates! There's no reason to devolve into bigotry to "win" an argument, it's a lot more fun to learn from each other and do cool, new things.

[1] https://www.win.tue.nl/~evink/education/avp/pdf/feel-of-java...


I don’t see it as winning and losing. I see it as deciding where to stand and where to contribute in my technical life. How do you personally trade off velocity (code fast) vs correctness, completeness or performance? Javascript generally values velocity over generalisability. (“It’s better to implement something quickly and worry about adapting it to other problems later”) compared to most Java code. It’s not necessarily better and worse. It’s a question of fittedness with your own values and the values of your project.

If you consistently write code which fights the ecosystem your code is written inside of, it’s quite painful. Writing Lua without ever blocking is hard. Writing javascript with large, deep class hierarchies is awkward. I’ve seen people write pure functional Java, but if that’s what you’re into you should probably consider just using a different language.

And to name it, Java does not feel playful and flexible to me. Not compared to ruby, Haskell, python and javascript.


> Java feels playful and flexible.

Insane that anyone could say this with a straight face


>Javascript values programming velocity, accessibility to new programmers and simplicity.

You are joking aren't you?


No, I'm not. Or at least, not as I see it.

How would you describe javascript's values, compared to other languages? This is a great chart / explanation of this kind of thinking from Bryan Cantrill:

https://www.youtube.com/watch?v=2wZ1pCpJUIM


No integers. What's the difference between == and ===. Why do I need to learn two ways to declare a class. What module system should I use and why are they all different. Why isn't there much of a standard library. WTF is Grunt, NPM, etc. Just explain 'this' to me again. What is the distinction between null and undefined. What is the difference between for/of and for/in.

JavaScript is far from accessible to new users neither is it simple.


You're arguing that javascript doesn't succeed at being accessible to new programmers. I'm not arguing that. I'm saying that javascript nominally values about being accessible to new programmers. There are way more novice programmers using javascript than (almost?) any other language. This isn't an accident.


>This isn't an accident.

No, it's because it's effectively the only option in the browser. If you are going to write front end code then you will end up writing JavaScript in some form or another.

And being popular doesn't necessarily mean it is accessible nor simple. If it had been I doubt we would have seen Python becoming the dominant language in education.


Yep. Python is a dominant language because it also cares a great deal about being easy to learn and accessible to newcomers.

In comparison, C++ does not optimize to be accessible to newcomers. C++ cares a lot more about compiled code running efficiently - as is evident from its insanely complex / powerful templating system. Its hard to learn, but its very powerful. Javascript and Python have nothing like that because that wouldn't align with the values of those languages.

Python values simple code that runs fast enough over complex code that runs super fast. C++ values code running fast enough, even if the code needs to be more difficult to write to achieve that speed.


I agree with most of your points. But the distinction between null and undefined is an important one. Its absence is a legitimate complaint about SQL.


In A Philosophy of Software Design the author talks at length about this and proposes that "deeper" modules (classes/methods/functions etc.) provide the most cost/benefit ratio, where the interface of a module is the cost and the functionality is the benefit.

Code doesn't magically become less complex by hacking it into pieces.


No hacking code to pieces makes it more complex. The trade off here is that the code becomes more modular.

Whether you want your code to be more modular is an opinionated decision but most people don't realize the benefits of high modularity. Almost all major design mistakes that necessitate code rewrites come from lack of modularity.


There are also a lot of cases where people "modularize" code without actually modularizing it --- they extract certain functions into separate modules or files just to break up the current file, but that new module they've created can't function or do anything on its own outside of the context it was extracted from. So in these cases they've really obfuscated the code in the name of "modularization", but the code is no more modular than it was before -- it's just more obfuscated.


Right; breaking it up doesn't necessarily make it more modular, it just necessarily spreads it around. This is -a bad thing-, with no other context. The hope is that it modularizes the code enough to enable better understanding/reuse/extension (thus being a necessary evil).


Right. The problem I've seen way too much is modularizing code too early in its lifecycle. This mistake seems to happen a lot by smart programmers who are inexperienced.

The instinct seems to be that they want hinges in their code, so their code can adapt and be reusable between projects. But they don't actually know where the hinges should go, because they don't need them yet. So they just put hinges all over the place - even where hinges aren't useful. If the metaphor is confusing, I'm talking about things like making an interface around a class, when there's only one implementor of that interface anyway. Or breaking a complex function into small, "reusable" pieces spread over multiple files - except where those small pieces are only ever used by that one call stack anyway. (And where they aren't that semantically self contained.) The result is harder to follow code with no actual benefit. And the resulting code is almost always bigger and more complex, and thus harder to work with.

Usually code thats the easiest to refactor is code thats the easiest to understand. That means, small, dense, correct code, with a simple test suite. If you write code knowing that you can (and will refactor) later anyway, the result is almost always better software. You will come up with a better design after you know more about your problem domain. Plan for that, and set yourself up to take advantage of that knowledge later.


> Right. The problem I've seen way too much is modularizing code too early in its lifecycle. This mistake seems to happen a lot by smart programmers who are inexperienced.

I don't see this as bad. Modularization protects against an uncertain future. Most code that's modularized is only used once and this Looks bad only because you haven't seen the alternative of what could have happened if code wasn't modularized.

Non modularized code is often designed wrong because of an uncertain future. Once the project is too far down the line, people just keep piling technical debt on top of the design flaw. Code is rarely rewritten until it's at a point where it's horrible than it takes a massive engineering effort to rewrite and even this rewrite could be wrong.

The alternative is code littered with modules that are used once. Which is better? Obviously the more modular code.


> Code is rarely rewritten until it's at a point where it's horrible than it takes a massive engineering effort to rewrite and even this rewrite could be wrong.

This is your problem, right here. Your instincts know when refractors and rewrites are appropriate. But if you live in a corporate culture where those things are never allowed to happen, of course you’re going to run into problems. Premature modularity is a poor man’s substitute for simple-then-refactor. It’s not a good process.


>This is your problem, right here.

Nah. There's no problem here. This is the most common behavior in any place corporate or not. Humans resist change. However specifically for corporations, it is very very very rare for a company to allow a rewrite because of two reasons: First the company is often way to busy with creating features and solving problems then to do a code rewrite. Second it is in direct conflict with the bottom line. Business people don't see the necessity so there is huge resistance.

When a company allows a rewrite it's 99% of the time only to serve a new feature or fix a flaw that no longer can be ignored.

>Premature modularity is a poor man’s substitute for simple-then-refactor.

Do you have any evidence to back up your claim or is this just your opinion? Using words like "poor mans substitute" doesn't lend any credibility to your claim. For example I can say the opposite and we can go in circles forever: Writing unmodular code is a garbage technique only done by junior developers who can't abstract things and by a good number of senior developers who've never learned how to code properly in a modular way. These guys don't even understand the true meaning of a module.

See what I did there?


> There's no problem here. This is the most common behavior in any place corporate or not.

Its hard to say statements like that universally because it really depends on where you work (and have worked). There are plenty of places which make time to refactor along the way, in small and large ways. Its good engineering, and a healthy code base nets better long term outcomes for the business too. Business people care about the team's velocity (since programmers are expensive). And I'm disheartened you think its the norm but not all companies have MBAs telling software engineers how to do our jobs. (Eg, Google / Facebook / Netflix / Github / etc.)

> Do you have any evidence to back up your claim or is this just your opinion?

Its just my professional opinion, backed up by almost 30 years writing software at all sorts of companies and in all sorts of environments. But no, I wish we had quantitative evidence for this stuff. But there's a weird dearth of quantitative research around software engineering. We as an industry don't really know what we're doing yet. I'd love to read some studies on this stuff if you can find any.

But I'm very confident about what works best for me, from trying lots of things on solo projects. I'm way more productive when I throw something together quickly, iterate like mad on how it works internally, then tidy it up and modularize as I get more confident around the best way to structure the code. Whenever I modularize up front I basically always end up regretting my abstraction boundaries - and either have to rework them (which is expensive) or I leave them alone, and just ship vaguely mediocre code.

But if prematurely modularizing your code is the only way you can modularize anything at your workplace then - well, you do you. But I find that pretty sad.


>universally because it really depends on where you work (and have worked). There are plenty of places which make time to refactor along the way, in small and large ways. Its good engineering, and a healthy code base nets better long term outcomes for the business too.

And I have worked in places where people know the exact formal definition of modularity and they modularize things correctly so such refactors never need to happen. I've worked and have experience in enough places to know that both a place where people understand true modularity and a place where people constantly refactor their designs on the regular are incredibly rare. My sample size is large enough for me to confidently say that your view point is highly highly inaccurate. Your experiences are in the minority of the minority along with the experience of working at any company that actually properly understands modularity. Understanding modularity is more of an individual thing.

The only time where I have been at a company that allows you to write shitty code and constantly refactor it are Big companies that have tons of low impact useless projects. Get on the project that is on the critical money path and all your luxuries of refactoring shitty code disappear because actual business realities influence the process.

>Whenever I modularize up front I basically always end up regretting my abstraction boundaries - and either have to rework them

This is because you lack knowledge about modularization and what should be a proper abstraction boundary. Almost know one knows what this properly is. You know the design pattern book by the GOF and every derivative book on that topic? Read it and then know that it's completely and utterly wrong. If you abstract things using traditional techniques and "art" and "experience" like this you ARE not making your code more modular.

>But if prematurely modularizing your code is the only way you can modularize anything at your workplace then - well, you do you. But I find that pretty sad.

You have a lot of confidence, and it's a subtle derogatory jab by saying your sad as if you're superior to me. Literally you're implying your sad that I can't rise to your level of "superiority." Get off your high horse. Have you ever thought of the possibility that you actually lack knowledge on something? That you've never actually properly abstracted things?


If you can move your code and spread it around it is Modular by definition. This is never a bad thing from a design perspective. It is only a bad thing from a complexity and readability perspective.

More likely you think it's a bad thing because your code isn't actually modular. Likely you need one piece of logic but that logic isn't modular so to move it to another location you need to drag a bunch of extra baggage around with it. You wanted a banana but instead you got the gorilla holding the banana and the entire jungle. Sound familiar?

The smallest primitive that is modular is a pure immutable function. If the modules you are moving around are not pure functions then likely your code isn't actually modular.


> If you can move your code and spread it around it is Modular by definition.

For it to be truly modular, you also need to be able to use it in multiple contexts. I could take any random 5 lines of a complex function and pull it out into another function in another file, but that doesn't guarantee that this was a smart thing to do in that particular scenario. What I'm saying is there are tons of times when people do this merely to get the linter to pass, instead of for the actual purpose of modularization.


Right.

If A and B are coupled so tightly that one cannot exist without the other, you don't really have two modules. You have one, AB. This is a common problem with improper application of the idea of modularity as well as OO concepts (in particular, breaking things into classes and thinking it creates a module). The latter is particularly pernicious in languages which don't have a clear separation between classes and modules (Java, for instance, or historically I think this has changed).

The reality is that there are two (at least) kinds of modularity. "Syntactic" (I need a better name for this) modularity like Java classes, or C's translation units (why I need a better name). These act as modules, but don't necessarily define a real, proper module. And then there are your real modules which are comprised of the things that must exist together (like in the earlier example), regardless of the project structure or language's notion of a module.

I had a team try to convince me their code was "modular" but their GUI portion was directly tied to the DB portion and other logic. Nothing could be instantiated separately, so it wasn't really modular, it was just using the language and build system's notions of modules to create the illusion of modularity (C++ and VS in their case). In contrast, another team had developed a collection of libraries (C# and VS again) that were used in multiple applications. That was real modularity.


Yes this. With I often think about questions like:

- Is this code useful in other contexts? Are there other contexts where I might want this? (Or someone else might want this?)

- Modules have a clear API boundary

- Modules can be described independently of the code which uses them

- Modules can (and should) have their own testing suite, independent of the test suite of the containing module

There's lots of examples; but you can kind of spot code like this in your projects. It has a property of being disentangled. "This just solves this one specific problem I have with strings / event emitters / random numbers / my database, separate from anything else I'm trying to do". "I kinda want to just document this code separate from the documentation of everything else". "I don't want to pull that whole library in, but can I just steal these 3 functions for this other project?"


> This is a common problem with improper application of the idea of modularity as well as OO concepts (in particular, breaking things into classes and thinking it creates a module).

Exactly! The class is not the smallest unit of modularity. You unionize several pieces of mutable state with several functions (aka methods) in class based programming and if your unionization was a design flaw you're pretty much screwed. To make your code truly modular you need to break it down further. Separate state and function. To go even deeper make the function pure and make state immutable. Abstract mutability to a small unsafe portion of your code.

>The reality is that there are two (at least) kinds of modularity. "Syntactic" (I need a better name for this) modularity like Java classes, or C's translation units (why I need a better name). These act as modules, but don't necessarily define a real, proper module. And then there are your real modules which are comprised of the things that must exist together (like in the earlier example), regardless of the project structure or language's notion of a module.

The only true logic module is a stateless function that is independent of all context. Think on that, that is literally the smallest unit of logic that can be moved around anywhere.

The problem here is how do you program in a way such that even for these functions are easily de-composable and rearranged without necessitating a rewrite in the case where you find the logic needs massive changes?

Use point free programming with pure non-procedural functions solves this. The problem is mutability must happen somewhere and usually this is abstracted to a very small section of your code.

>I had a team try to convince me their code was "modular" but their GUI portion was directly tied to the DB portion and other logic. Nothing could be instantiated separately, so it wasn't really modular, it was just using the language and build system's notions of modules to create the illusion of modularity (C++ and VS in their case). In contrast, another team had developed a collection of libraries (C# and VS again) that were used in multiple applications. That was real modularity.

GUI programming is inherently hard as the state of the GUI is by nature muteable and it's hard to write modular code as a result. However modern programming frameworks generally get around this using the technique I outlined above, see React redux and MVU. https://guide.elm-lang.org/architecture/


Read the 2nd paragraph of my post you replied to. You talk about true modularity and it is addressed in my second paragraph. If you find these problems with your code then the code was Never modular in the first place.

Modular code involves writing code independent of context. One way of doing this is to wrap every expression in a pure functional context. Another way is to make every variable immutable.

> I could take any random 5 lines of a complex function and pull it out into another function in another file, but that doesn't guarantee that this was a smart thing to do in that particular scenario.

Doing this doesn't hurt. There's nothing stupid about it unless you coded everything in a way where it's NOT modular. Procedural programming is usually not modular because the results of a computation depend on the Order of the procedures. Immutable state is isomorphic to an expression so procedural code with immutable state solves the issue.

Let's say I need a function to add 3 numbers. I impliment it like this:

  addThree = func(x, y, z){ return addTwo(x, y) + z}
  addTwo = func(x, y) {return x + y}
You're saying addTwo is unecessary and pointless if it's not reused. I'm saying you can't predict the future, there may be a time where you need addTwo, but if you never need it, it doesn't really matter, you don't lose anything here. Modularity doesn't hurt the structure and organization of the code. It only effects qualitative aspects like how easy is it to interpret understand or read.


"it doesn't really matter, you don't lose anything here"

Except you do. It's harder to understand and less readable; and in a real life rather than made up example, addTwo is in some other module entirely and has side effects that are completely invisible (in most languages) when just looking at addThree, making debugging and understanding WTF is going on far harder.

You mention "pure functional context", and yet this whole thread exists under a post about OOP, which is all about state management; it's no good trying to create toy examples that are side effect free to try and dismiss the points being made. Yes, of course pure code is fairly trivial to slice and dice, and the cost of doing so tends to be low (not zero, but low), but that's not really what is being talked about.

Otherwise you're kind of "no true Scotsman"ing this; "if you find these problems with your code than the code was never modular in the first place" - yeah, that's the point. Splitting code doesn't by definition make it more modular.


>Except you do. It's harder to understand and less readable; and in a real life rather than made up example, addTwo is in some other module entirely and has side effects that are completely invisible (in most languages) when just looking at addThree, making debugging and understanding WTF is going on far harder.

You don't lose anything from a hard structural standpoint. Readability and being harder to understand is an opinionated based metric. It can be influenced by how you even name a function. It is a soft metric therefore weaker than the hard one. Additionally I DO mention the qualitative opinionated cost of modularity several times in this thread. So I address it but it is definitively weaker because anyone can have an opposite opinion and say that "in my opinion modular code is more readable"

>You mention "pure functional context", and yet this whole thread exists under a post about OOP,

Did you read the article? The author of the article was not convinced by OOP, he never supported OOP and generally the entire article is critical of OOP. I would say my reply is highly relevant and that you didn't read the article.

>it's no good trying to create toy examples that are side effect free to try and dismiss the points being made.

The examples serve a purpose to illustrate my point not to illustrate a real world example. If you want a real world example look at the react + redux architecture. The gold standard model for all web UIs exactly do what I mention with state management. They completely separate State away from pure functions. The entire web generally follows this model abstracting state management away from even code. Web apps are stateless with the database handling the entire job of state management. Examples of this pattern are EVERYWHERE, the example I gave is a toy example to help you understand not to say "hey this is a real world application." I thought that was obvious, my fault for not being clear on that.

>yeah, that's the point. Splitting code doesn't by definition make it more modular.

Completely and utterly wrong. Splitting code by definition makes it more modular. See: https://en.wikipedia.org/wiki/Modularity

The definition from above says: "Broadly speaking, modularity is the degree to which a system's components may be separated and recombined, often with the benefit of flexibility and variety in use"

If you are "splitting" code then by definition you are separating code which was previously combined. By Definition it is more modular. You really can't argue this point. More likely what's going on here is you have your own personal fuzzy definition of modularity which you are unconciously combining with your opinionated view on what constitutes good code. I am using the english definition not some fuzzy notion of good code so I am definitively right.

Be more exact and formal with your terminology it will lead to much less misunderstandings.


From the Wikipedia link you provide - "modularity is the degree to which a system's components may be separated and recombined"

Lines of code != system's components.

It's why we don't split a 20 line function into twenty 2 line functions (where the first line is one of the original, and the second is the call to the next function), and no one would say that doing so has made the code more modular.

This is itself backed by the wikipedia link, 'In software design, modularity refers to a logical partitioning of the "software design" that allows complex software to be manageable for the purpose of implementation and maintenance. The logic of partitioning may be based on related functions, implementation considerations, data links, or other criteria'; what we have done by splitting up the code this way has not helped it be more manageable.

So no, by the definitions you yourself cited, splitting up the code does not translate to more modular code.


>Lines of code != system's components.

Did I say this? No. Does this have anything to do with the conversation? No.

>It's why we don't split a 20 line function into twenty 2 line functions (where the first line is one of the original, and the second is the call to the next function), and no one would say that doing so has made the code more modular.

You think of logic as procedural lines of instructions. Procedural lines of instructions are not modular. If you think like this, none of your code is ever truly really modular. All code should be writable in one line. You split into several lines ONLY for readability, but your core logic should easily be isomorphic to one line code.

If your code doesn't share this isomorphism then none of your code is modular.

Inevitably you have to eventually address the procedural nature of the world in your code. When this case arises you have to abstract this all away to the smallest section of your code as possible.

>what we have done by splitting up the code this way has not helped it be more manageable.

Modularity and Manageability ARE TWO different words with two different definitions.

Wikipedia in this case is just being dumb and fuzzy with it's definitions. Right above your cited link it has an alternative definition that is in direct conflict with the one you stated. Modularity in modular programming is different from modularity in software? Come on. This is stupid.

Let's not play pedantic games here. Modularity and Managability are different. We know this. No need to play games. Combining the two into one thing is a lack of coherence. Be coherent.

When you take one thing and split it into two things. The two things are modules and thus more modular than the one thing. That is simple intuitive and clear.


> Doing this doesn't hurt. There's nothing stupid about it unless you coded everything in a way where it's NOT modular.

It does. If I open up an arbitrary Ruby on Rails app for example, it is going to be easier to navigate for example a super fat model file than it is to navigate one that has been divided up into 10s of helpers and companion modules. There are similar scenarios in every other language. Extracting code and moving it somewhere else can truly be a death by a thousand cuts. My rule of thumb is if there aren't at least two places in the code that need to call My Extracted Thing (tm), then it shouldn't be extracted in the first place. If you're being DRY, it's OK to have a long class/file/method/function/module, and it's probably preferable to obfuscating your codebase and making it more difficult to navigate.


>Extracting code and moving it somewhere else can truly be a death by a thousand cuts.

This is not what I mean by modularity. Let me be more specific. The code CAN be seperated and it can be recombined without a structural code rewrite. That is what I mean by modularity. Actually dividing code up and organizing it among different files is an opinionated organization scheme and NOT what I'm talking about.

When you do extract code and move it anywhere it's not exactly "death" it's just harder to read. Structurally the code is still sound. You can meet the requirements without rewriting the code, it's just the code is more complex to understand.

The bad scenario I'm talking about here is when you can't extract code and you can't move it around. When your requirements necessitates logic to be moved around and you can't because code is too coupled with other code. That is more closer to literal death by one cut. You are technically unable to meet requirements with existing code.

The modular primitive that prevents this from happening is the immutable pure function. But it's not a one size fits all solution.


Exactly, and what I'm saying is probably over 50% of the time people modularize code, they aren't actually modularizing it in the good sense you are talking about, but in the bad sense where they are just placating the linter without putting any thought into it and calling it "modularizing".

I'm all for modular code and interfaces, but linting rules like "functions must be a maximum of 15 lines long" are stupid and lead to stupid code. That's what my OP was about.


I would say the file thing is unnecessary. That's just an OCD thing. You can break stuff up and keep it within the same file, it literally has the same effect.

I understand why you think that type of modularity is bad, but it is actually good. It only appears bad because most of the "modularized" code is only used once.

No one can predict the future so the way to minimize rewrites is to make composable units of code that are small and highly modular. It's not about breaking up your code. It's about writing small logical component then building up the larger component by composing the smaller components.

This results in a large number of modules that are only used one time, but it prepares your code for the inevitable point of the future where you find out the design was wrong and you have to rearrange the logic.

When such a time comes you most likely just trivially rearrange some of your logic and add additional pure functions into your pipe line for any actual new logic. The majority of your modules remain untouched and this only seems bad, but it prevented a rewrite of the entire framework.

The timeline where the definitively worse outcome occured didn't happen because your code was too modular. So you have no point of comparison and you assume the modular code that is hard to read is bad only because it's hard to read. You failed to see how it prevented a massive refactor.

Usually if code is so unmodular, people just live with it and keep accumulating massive tech debt on top of everything. If all your seeing is tiny functions everywhere then this is definitively better than the alternative.

From a design perspective highly modular code is always better. From readability perspective though, you are right, modular code is harder to read. But there are ways to mitigate this.


Hacking code into pieces does give one the opportunity to name the pieces, though

Mixed blessing, that


> name the pieces

Solved without splitting into functions.

https://ilya-sher.org/2019/10/21/section-syntax-next-generat...


I completely agree.

Another similar issue that I see a lot in both Java and C++ codebases is "premature wrapping" of foreign APIs. Basically when building a program that has to consume a certain API that is somewhat incompatible, every single concept of this API is wrapped in a separate class before any planning, to the point each one-line procedure call turns into a 20 line class.

Of course, after the wrapper is written, the program still needs higher lever abstractions that use those wrappers. But since zero planning went into the design, now you need exactly the same call order as before, however instead of an ugly (but simple) procedure call, you have a class wrapping it, and to understand a simple workflow you have to go trough at least two layers of classes.


Ugh same. Very common that I get slightly irritated by folks who blindly wrap something with no real reason other than "somebody else wrapped something similar so I'll wrap this for consistancy". Too many juniors thinking that they need more files/PR due to impostor syndrome...


Having worked in games, my pet peeve is the cottage-industry of amateur game engines and Youtube game-engine series that are pretty much just that: wrappers around OpenGL, SDL, Entt, Imgui and a multitude of other libraries.

Most of those never really produce a game, since the authors know how to wrap the libraries, but the engines don't have enough substance to help making a real game.

A notable exception however is Casey Muratori (of Handmade Hero), who actually skipped the wrapping and went for a more direct code. Interestingly he has a nice inversion of control architecture.


I think it's a combination of effort and culture that leads to such differences in style; in C, creating objects and functions and overriding them etc. takes far more effort than it does in Java (where IDEs can also generate tons of code automatically), so programmers naturally think more about whether the additional effort expended is worth it. Asm is an even more extreme case in the C direction --- every additional machine instruction is explicitly written, and so is dispensed with if not absolutely necessary.

That said, I've also seen "object-oriented obfuscation" in C, so some people seem to just love complexity and writing tons of code to do a simple task, or were taught "abstraction is great, use as much of it as you can" and never thought about when to stop.

Overabstraction usually increases macro-complexity while decreasing micro-complexity; a function with a single line of code is "simpler" in that its immediate purpose may become obvious, but having to mentally stack from its callers means that the big picture is harder to comprehend.

At the extreme high end of density are languages like the APL family, where the density is so high that the "big picture" becomes a slightly smaller one, and Arthur Whitney has been famously quoted as hating scrolling; but at that density, you can no longer "skim" large portions of code --- instead, each individual character needs to be read and pondered carefully, because each one says a lot.


>>In Java it would be a nest of SortComparator interfaces and SortAlgorithm implementors, which would act to hide the algorithm itself.

Java developer here, no such thing. I guess it varies based on who is doing the coding. Although, I did start out as a C developer.

I would only write code like that if the use-case called for it. Otherwise, no.

Writing code is more of an art form, some times you may need to do crazy stuff like that, but a lot of times not.

KISS


You’re essentially arguing about Abstraction which has its pros and cons. Leaning on one of those sides while ignoring the other is a trap for inexperienced players.


Things I've learned from 25+ years of programming in C++ (and 5+ years of C before that):

1. not all software is about pushing and pulling to/from a database; if yours isn't, be sure you understand why that's the case.

2. "backends" (not "web backends", but the more general "where the mechanisms are") should know nothing about "frontends" (again, not web, but the more general "user interface of some kind"). This is really just MVC in its most basic sense. One good way I've found to think about this is to assume that there's always at least two UIs running simultaneously. Make sure this can work.

3. if your program has a user interface, everything the user can do without further interaction should be represented by a closure that can be invoked from anywhere (but always in the correct thread).

4. single-threaded GUI code seems like a limitation but in most projects, it's the right choice. By all means use helper threads when needed, but never allow them to use any API that's part of your GUI toolkit. Knowing that your GUI code is ALWAYS serialized is a huge conceptual assist when reasoning about behavior.

5. access to an excellent cross-thread message queueing system is likely to be a must if your software uses threads. This should include a way for one thread to cause arbitrary code execution in another thread.

6. direct memory access for the UI is nice from a programming perspective (that is: just directly call methods of backend objects), but can erode the wall of separation between the UIs and the backend.

7. lack of direct memory access for the UI(s) can significantly impede performance, but enforces a conceptual clarity that can be valuable.

8. when notifying the View(s) about changes in the Model(s), there's a tradeoff between fine-grained notifications ("frob.bar.baz.foo just changed") and high-level notifications ("something about frob just changed"). Finding the sweet spot between these two can be a challenge across the life of a long-lived piece of software.

9. lifetime management will never be trivial. Accept it, and move on to thinking about how it is going to work even if it is not trivial.

10. try to refer to as many things as possible indirectly. if something has a color, don't make it's state refer to the color, but the name or ID of the color. do not over-use this pattern when performance matters, but also do not over-estimate your ability to understand when performance matters.


> "backends" (not "web backends", but the more general "where the mechanisms are") should know nothing about "frontends" (again, not web, but the more general "user interface of some kind"). This is really just MVC in its most basic sense. One good way I've found to think about this is to assume that there's always at least two UIs running simultaneously. Make sure this can work.

This is one I constantly struggle convincing my colleagues about. It becomes much more "obvious" if you are trying to write unit tests in C++ code[1], but unit tests are a mere side benefit. It's more about reducing coupling.

Currently working on a code base that outputs to an Excel file. We recently started dealing with more data than the Excel file can handle easily, and the system came to a crawl. So we had to allow for the option to output to CSV (easily 100x faster in our use cases). At least now some of my colleagues have a bit of appreciation on what I've been harping on.

The Excel library is still intrinsically tied to much of our code. We've been getting over 15GB RAM usage for data that I'm sure would not take more than 2GB if we manage to bypass the Excel library.

[1] Why does my class that computes X need to know that something called email exists? So to write a test for this class I need to instantiate a whole other set of classes just for output? Just have it "ReportMessage" on the Reporter interface and let whatever class that inherits from it figure decide if the message will go out via email or SMS.


40+ years of programming experience here. The next step is to realise that MVC is just an example of events driven programming. An application is a state that “instantly” changes to the next state when an event happens. Multiple separate states (logging/UI/DB/remote/…) coordinate by reacting to events. The business logic is generating “this is now true” events. That’s it. There is nothing else to it.


That might be true for something where the phrase "business logic" is applicable.

But in my niche (realtime audio software), there is underlying data in the system that changes over time independently of events. So there is "something else to it".


I use the term “Business Logic” when I talk about the code that decides what is now true when an event has happened. I real-time audio software it would be the logic that decides which sample to play next and what the (say) audio volume should be now. The non-logic code is the code that read events from the environment (user input) and changes the environment (the audio hardware).


Not meant to be pedantic, just playing devil's advocate, but isn't the real time audio bitstream just a continuous source of events that gets blended with the rest of the application state like active filters and what not?


Individual samples do not in any significant sense constitute events. The only thing that really pays them any attention is metering, and the result of that process is only displayed to the user periodically (i.e. something roughly equivalent to the screen refresh rate).

Even higher level objects, such as what are various called "clips" or "regions" or "events" frequently pay no role in any type of event notification system. In some designs, the boundaries of such objects may play a somewhat event-like fole.


A lot of wisdom to unpack here. Thank you for sharing!


number three comes from Javascript specifically?


Not at all. I've likely written no more than 100 lines of JS in my entire life. This is an observation based on C++ development, exclusively, but I believe likely relevant to any language that can implement something semantically equivalent to a closure.


Here’s another review / critique of this book, but with a full implementation based on the author’s grievances: https://enterprisecraftsmanship.com/posts/growing-object-ori....

The main point of it is to avoid testing via mocks and to also avoid interfaces that are only there for testing purposes. The end design is extremely simple, and because it has command / query separation, the actual logic is trivially testable.

Give it a read. I love Growing Object oriented software guided by tests as well, these are all just different approaches with different mindsets.


I really enjoyed reading this one and loved the example. Unwittingly I found that my approach feels similar to what the author did as far as isolating the domain and writing unit tests. I learned some new things as well. Thanks for the link.


> I've quickly realized that the authors have a concrete idea of what an object means to them. I was confused why their code was always so... “callback-y” and after studying it a little more, I've discovered the reason. I might have missed it, but I don't think they ever explicitly state it. All calls between objects are unidirectional: no public method of an actual object (not a plain data class) returns any value. They are always void methods. Objects don't “call” each other. They send a message and don't wait for a response. (Well, actually since they actually “send” it via method call, they do wait, but they pretend they don't.

https://en.wikipedia.org/wiki/Command–query_separation

https://martinfowler.com/bliki/CQRS.html

That sounds very much like Command-query separation or Command Query Responsibility Segregation to me. I haven't read this particular book (and almost certainly won't be getting to it within the next decade based on my ever increasing pile of unread books), but I wonder if they call this out. It's a critical decision in the architecture/design of a piece of software and it's worth stating that it's how they intend to design the system.

Coincidentally, to my mind, that model (CQS/CQRS) fits well with the blog author's idea of using an agent-based event system. Moving the objects into distinct threads of execution or processes, which also coincides with one of the intended ideas of OO by Alan Kay. OO-as-message-passing very much fits within the agent-based execution model.


To the author: if you haven't read POODR, I highly recommend that. It's the book that felt the most "real OOP" to me. No `class Dog extends Animal` in sight. It's written by one of those Smalltalkers, and I think it's got a lot of very valuable ideas about software design, not just OOP specifically.

The "callback-y" approach in Growing Object-Oriented Software sounds fascinating.

> Immediately I remind myself that the implementation from the book ignores the problem of persistence completely. If you close that application it loses all the state. I think this is not an accident. This is where things go wrong for OOP really fast.

This is a really good point.

EDIT: I see you really did not like 99 Bottles of OOP, which is by the author of POODR. In that case maybe her way of explaining things doesn't agree with you and you should skip it!


POODR = Practical Object-Oriented Design in Ruby by Sandi Metz.


Thanks. I tend to leave it abbreviated because it's always the top Google result!


> does anyone really care?

I liked this piece. Enough to log in and comment at least. It's the rare article where the author is open about his biases but gives an opposing approach an honest go. Reading three books about OOP is way more generous than I'd ever be. And when that approach still doesn't make sense, he offers a better one. Always enjoy reading the informed hater's perspective.


> Immediately I remind myself that the implementation from the book ignores the problem of persistence completely. If you close that application it loses all the state. I think this is not an accident. This is where things go wrong for OOP really fast.

This point hits hard.

Managing "live" scattered state that is gonna go away when the program dies is hard in itself. But as soon as you have to persist it or do anything fancy with it, you pretty much have to change the whole approach of your app. This is why it's always a good idea to start with established frameworks that handle persistence if you're ever gonna need it. Bolting-on is just too hard.

It also reminds me of my first job, in a Desktop app. State became so complex that to apply a "global change", like currency or language, it was required to close the app and open again. It was something very common, seeing how many apps required such things.

In the middle of my career I also worked in a very large video game. The higher ups wanted to change how "game saving" worked and instead of having to serialise just the basic stuff (health, lives, level) we needed to change it to serialise the whole game state including enemy positions and actions. It was the biggest change we did and we ended up having to add lots of boilerplate because of the scattered state. IMO, ECS was a very interesting development purely because now state is not encapsulated anymore, making serialisation completely separate from everything else.

Curiously, as much as modern frontend programming is maligned, such issues are easier to solve with central state management libraries like Redux.


Ha

I do like how ECS and Redux have that common thread of rediscovering global state


True. There's lots of parallels between those two things and relational databases, too.

In the end it seems that the "web app with a database" is becoming the template for other types of apps.


> does anyone care?

I do care and I read this with interest just now. The idea that it took three hours to write a review of a month+ of work as part of "professional development" and that this person must bear that costs of that education, is a tip of the iceberg indicator to me as to the difficult work world we live in today. How is it that capital owners make money while sleeping, while craftsmen intellects spend a month+ without compensation to "get up to speed?" I am on the tail end of this after decades and it jumps out for me now, past the OOP part.

Second - I learned OOP approaches long ago, have written a lot of software and have used OOP in my own ways, much like this author. I appreciate the effort here! It is an interesting, technically somewhat shallow (no code in this essay) yet as noted, good balance of critical and open mindedness.

I do not understand OOP-hatred past "I hate the music my parents liked" and "Java is so tedious that it makes me hate all of the whole structure of it".

I used OOP code myself to separate parts in loosely coupled systems of several flavors; to make a systematic ordering of commands, to enable scripted or menu-driven command sets; and to wrap an interface around data for the convenience of other code. I feel that a strong point of OOP is to REDUCE the cognitive load for the human. Yet many snipes in articles about OOP specifically complain about the lengthy, spread-out, tedious nature of OOP code. Your mileage may vary ! Use it badly or use it well .. its not my doing.

The specific kind of software system described in the third book here, with messages passed without state between objects, is interesting, and reminds me to say now: I think there is vastly insufficient distinction made between software solutions, their design and implementation, in OOP criticism. What are you trying to solve? How much persisted data is there? or state, or interface to XYZ external system. This matters in design choices and I feel like OOP-critics often race to their favorite annoying thing rather than do the intellectual work of distinguishing for a reader, what the assumptions are and what the finished product requires..

Overall, this essay is worth reading, feels short to me despite obvious effort on the part of the author, and personally, I get a nagging feeling that people doing this kind of work should be less scammed by low-morals middlemen and more valued socially for the architects of software that they are.


The part about data is interesting. I remember than in Clean Code, Robert Martin made the distinction between "data structures" (objects that have lots of parameters, few functions) and "objects" (objects that don't have many parameters, lots of functions). The author seems to have rediscovered this distinction here. If people keep rediscovering it, maybe an object is a too abstract building block? Maybe languages should offer a struct/dataclass/record as a basic building block too?


Java offers Records now, which are pretty much that.

Pretty cool feature IMO: https://docs.oracle.com/en/java/javase/14/language/records.h...



What are the tradeoffs between records and structs?


From MS:

  You can use structure types to design data-centric types that provide value equality and little or no behavior. But for relatively large data models, structure types have some disadvantages:

  * They don't support inheritance.
  * They're less efficient at determining value equality. For value types, the `ValueType.Equals` method uses reflection to find all fields. For records, the compiler generates the `Equals` method. In practice, the implementation of value equality in records is measurably faster.
  * They use more memory in some scenarios, since every instance has a complete copy of all of the data. Record types are reference types, so a record instance contains only a reference to the data.


The biggest difference is that Records are Immutable.


structs can be made `readonly` as well. The biggest difference IMO is a record is still a class, so it's still copy-by-reference rather than copy-by-value, potentially giving performance gains when passing data around, and leaving a shortcut for value-comparisons (if two variables reference the same object, you obviously know it is the same).


Alternatively you could just make a final class with public member variables and no getters/setters and just treat it like a struct.


That's a great point, there's absolutely nothing wrong with public members if you only want a simple struct.


Records seems very nice! What I wonder is if people are going to use them, considering Java is a bit old at this point and lots of code already exists. Refactoring to use object would be a heavy cost. Maybe some new framework and ecosystems will appear based on new features of Java?


Refactoring to use them is not really hard, as they are easily interchangeable with regular classes. They are pretty much syntax sugar for this class pattern here: https://stackoverflow.com/a/63615514


That's true, but that's still a large refactoring to do, especially with going from mutability to immutability. The community might also just not like them.


On the other hand in some cases this type of refactoring might actually reveal design issues with the code, maybe even bugs.


> That discovery blew my mind initially. I panicked. “OMG, is this the secret sauce? Is the joke on me?

Not only is that not the secret sauce, an OOP program with almost nothing but methods that return nothing (i.e. have some effect without reporting a result), is a giant red idiot flag.


What an excellent read. His approach is very similar to how I have developed successful software for years. It scales from the smallest throw away code to the largest Enterprise system and is flexible/adaptable without 27 levels of empty abstractions.


https://twitter.com/martinfowler/status/1423700358233305096?... This made sense for me right before I gave up on good OOP.


> I've purchased three OOP books (in the order I've read them): ...

If your gonna pick three books on OOP it should include Design Patterns at the top of the list. At least if you want to understand why OO is a thing people still use and talk about.


Isn't "Design Patterns" about how to solve problems through an OOP approach, rather than about the strict benefits of OOP?


More a bag of tricks to get around issues you tend to run into when doing OOP.

Bridge comes to mind, I ran into that specific example myself trying to OO-design a GUI framework with different back ends.

With a pinch of common sense stuff like Facade etc.

Much of it only makes sense for static languages such as C++ and Java.

I'd say it gets way more credit than it deserves.

Stepanov on OOP is interesting (just search for object oriented):

http://www.stlport.org/resources/StepanovUSA.html


Article's stated motivation:

> I'm looking to gain more confidence in my criticism and understanding of OOP. In the past, I have published multiple posts criticizing Object Oriented Programming ... I always feel this anxiety that... maybe there is such a thing as “good OOP”, maybe all the OOP code I wrote, and the OOP code I keep seeing here and there is just “incorrect OOP”.

To that I'm saying read DP and critique that and if you still feel that way, your on to something. Cherry picking 3 crap OOP books to critique then concluding OOP is crap feels like a bit of a strawman argument.


I don't know anything about the other two books, but the one discussed in depth is well-respected.

http://www.growing-object-oriented-software.com/praise.html


Again, I'm saying that I think DP isn't a good idea because DP is about how to solve problems using OOP and not why OOP is a good idea in the first place.


I don't think you can separate the two. To understand whether OOP is a good idea you need to understand how it's used to solve software problems.


I don't think those are the same "problems". "Design patterns" is about code, not solving business problems. To stay in the well known books in OOP, DDD would be about solving business problems.


"business problems"? I feel like the goal posts are moving here. EOD all I'm saying is I didn't really understand or appreciate OO until I'd read and digested DP. Until you've at least covered those basics it's hard to have an opinion on whether OOP is crap or not in my view. But maybe I've just been indoctrinated by it.


Many of the Design Patterns emerge from the fact that a given language does not have first class functions AKA closures AKA objects. So in a way it is working around the limitation of a language not being object oriented enough.




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

Search: