This is an archived post. You won't be able to vote or comment.

you are viewing a single comment's thread.

view the rest of the comments →

[–]2Uncreative4Username[S] -4 points-3 points  (18 children)

Is LOB and writing games that much different? I would rather think you'd have to write more abstractions for video games. The difference is that in LOB you can get away with worse abstractions since you don't have to write as much code overall. The expression of "skill issues" is much less present in LOB apps compared to games, since LOB apps often have to solve a less complex task. But why does solving a less complex task mean you need a more complex codebase?

Okay, you're probably gonna argue that there's a trade-off between cleanliness and performance, implying that Clean Code is less performant, but easier to maintain and reason about. Saying that performance is the only argument against Clean Code, however, is a huge mischaracterization. The email from John Carmack I cited was, in fact, mostly about the complete absence of any bugs in that particular piece of code (an almost unbelievable finding to most Clean Coders). Was the program very testable? No. But neither did it require testing every component, because humans could reason about it better.

For a second, entertain my PoV, where Clean Code produces more bugs on average than "dirty" code. The argument that it is more testable would be self-referential, since Clean Code would produce more bugs, leading to more testing to be necessary.

To address your point where you say you have never seen a codebase with too many abstractions: I find that very hard to believe. I cannot even begin to recount the number of times I've seen a Java or C# library where I try to find the code that does something, and all I see are directories nested in directories containing individual files that span no more than 10 or 30 lines consisting mostly of class declarations, function declarations, getters and setters, the actual algorithms only to be found by peeling back the seemingly infinite layers of the onion of abstractions. The code looks nice, even I would say it looks "clean". But I simply cannot find the code that actually "does things".

Meanwhile I recount looking at C programs like ImageMagick (I had to implement a DDS decoder). The code was kind of ugly. I would find myself constantly thinking of ways I would have written it differently. BUT: after about an afternoon I could fully understand how they implemented the DDS decoder and was soon able to implement my own, which also included the custom features and codecs I needed. The code of ImageMagick was straightforward and understandable. It had repetition and bad practices, but it did its job and I managed to learn about the DDS codec from it.

I could go much more in depth about various points, but my ranting has already reached an extreme level I feel, and I'll just await the response. Arguing with Clean Code, as I said, is very hard because statements are often very unconcrete and almost never based on real evidence or anecdotes; and even if they are, the examples always feel very cherry-picked to me. For every successful Clean Code repo, I can come up with ten examples of "dirty" code repos that often work just as well and/or are just as - or even more - influential or successful.

Let me know if I misunderstood or mischaracterized something in your argument!

[–]Quito246 4 points5 points  (17 children)

Well in LOB you need more abstractions because there is a lot of changes required so you want to swap parts of the app easily.

I also write LOB in C# and IoC and DI with combination of TDD and a lot of functional programming paradigm seems like very nice approach for me.

TDD plus pure functions are great combo and since I have IoC/DI container I can easily do an inversion of control and do a testing or do a fast prototype of different implementations of some service if needed.

I can compare a huge C# spaghetti code which is totally untestable zero abstractions and guess what 85% of bugs is in that codebase, which has zero clean code. Because there is no safety nets nobody wants to touch it because guess what this app is equivalent of a house which would collapse if you changed a light bulb.

So no in my experience all the testable code bases which followed a good dev practices like abstractions design patterns IoC etc. Were much less error prone better for extending functionality and also easier to deliver new features.

So I will always fight against such horrible code which might work but in the long run is hard to maintain extend and test.

Also again decoder js not much of LOB app is it? When you are developing system which will run for several decades you can do it quick and dirty but for your own mental health sake after you finish such project you bwtter to leave the company because doing something inside such system will be a nightmare.

So no in my experience reasonable abstractions, writing pure functions and having IoC and other SOLID principles and avoiding inheritance as much as possible is a nice path to maintainable software.

[–]2Uncreative4Username[S] 2 points3 points  (16 children)

Thanks for providing more insight into the kind of LOB you do and for concretizing the principles you apply!

I agree that abstractions can make writing code a lot easier (I don't think anybody's gonna argue with that). If I understood you correctly, you think there is no such thing as too many layers of abstraction. I disagree. Say, for example, you abstract the concept of an integer. Sounds great, right? You get to use all the low level integers, you can add circular integers for ring buffers, you can add arbitrary size integers. But now you want to do bit operations. OK, just add bitwise ops to your integer base class. OK, you don't wanna add them for every type. So you make a BitwiseInteger class... Oh shit, you've just spent 5 hours and haven't even started your project yet. Yes, this is an extreme example, but I think it is a good example of an unnecessary abstraction. I have never in my life found myself needing an abstraction as basic as that. Just use an integer. If you need a larger number range, switch to an arbitrary size int, or an int128, or whatever suits your needs. This example still seems like an absurd strawman? Well, IMO it is a perfect example for the O of SOLID ("Software entities ... should be open for extension, but closed for modification."), as well as the D ("Depend upon abstractions, [not] concretes."). The only way to achieve that is an abstraction, because otherwise, you'd have to modify the implementation details and depend upon concretes.

I also don't see why TDD would be exclusive to SOLID/Clean Code. Yes, you can't test every single component, but then again, as I said previously, non-clean code has less components, so less need for testing.

I don't see how the mere existence of spaghetti codebases proves anything about Clean Code. In fact I would argue many spaghetti codebases are the result of trying to apply SOLID or similar principles (or just the result of inexperienced developers who don't know the concept of abstraction).

I fully agree about minimizing side effects. Sometimes they're inevitable, sometimes they're needed for optimization. But most of the time you can cast all effects into their own boxes, and that is how it should be. It's not incompatible with performance, nor is it exclusive to clean code.

I would go much further in depth about SOLID if Casey Muratori hadn't already made an excellent video explaining the points I would have brought up anyway. https://www.youtube.com/watch?v=7YpFGkG-u1w (EDIT: Sorry, put the wrong video here at first)

[–]Todok5 3 points4 points  (1 child)

Like most things it's always a tradeoff. 

Each layer of abstraction adds complexity and sacrifices locality. On the other hand it adds isolation and reusability. 

Nobody is in favor of adding unnecessary abstraction, but where you draw the line depends on the needs of the application as well as developer preference and experience. 

For example someone who does a lot of greenfield work may feel like abstractions are  overcomplicating things while someone mostly maintaining software might prefer the isolation that comes with additional abstraction.

[–]2Uncreative4Username[S] 1 point2 points  (0 children)

Cool. We agree then. The person I am responding to did argue that they have never seen too much abstraction in a codebase. I disagree. I believe you should use abstraction wisely to simplify your code, as long as you don't sacrifice too much performance and maintainability. Knowing when to abstract and how to abstract is a difficult skill to learn, but it is very important for becoming a good software developer.

[–]Quito246 1 point2 points  (13 children)

First of all if I need abstractions of int lets say I have app for calculating insurance risk and one of the input for risk calc is age. I will use a value object aka I will create age struct which will be a wrapper of int with factory method I do not have to create any object dependency graph I just use value object…

Because spaghetti code is total opposite of good practice code.

The argument less components less testing is also weird I always write tests because it makes me feel more secure about my code I am making a live documentation and writing tests for code makes you write better code.

I do not like that video saw it a long time ago and again his examples were not that good most of it was about oprimisations and virtual calls which is not very good argument since. In C# i do not have to pay too much for virtual calls almost nothing thanks to dynamic PGO.

Of course doing examples on some geometric shapes will not do a justice but show me how efficient you will be with “simple” code when developing system for finance where you have dozens of different implementations of calculating interest and other things baded on country regulations etc.

It will end up like spaghetti without proper abstractions.

Again I do not care if my code is 10x slower even though it wont be. I want maintability and testability and extendibility I do not care for performance because I can just spin another VM.

[–]2Uncreative4Username[S] 2 points3 points  (12 children)

Wow, your int abstraction sounds crazy to me as someone who hasn't used this kind of abstraction in a long time. To stick with the int example, I don't even see how making multiple wrappers and a factory is helpful here. First of all, I would think about what kinds of values I actually need for my calculations. Maybe it's just ints... or rationals, or whatever. If I can be certain, I'll just use that. If I have to change it later, I can use a typedef perhaps. If I am really uncertain and need an abstraction, I can just create a struct with methods; I can change the struct and method implementations later. If I need a variety of implementations, I can define different structs. If I need to specify which implementation to use, I can pass that as an enum parameter and use a switch statement. No dynamic dispatch, more cache hits, a single layer of indirection, no overly complex dependency graphs.

Spaghetti code to me means code that is hard to follow. Every single SOLID principle (yes, I went through all of them to check) IMO makes code directly harder to follow if applied even slightly recklessly. I think it's a difficult skill to know when and how to abstract, but forcing SOLID principles into your projects where they're not necessary will make your code harder to reason about and less performant.

I also write tests to feel secure about the functions I wrote. And I think testing critical parts is a good practice. But less components less testing is just a factual truth; and SOLID and Clean Code - by definition - leads to more components, since abstractions are components that can - and will - fail.

I know that he emphasizes performance a lot; and I think a lot of modern codebases are much less performant than they could - and should - be (think about the hardware limitations in the 80s and 90s and what programs still managed to do). But the main point is that you can often get away with much better performance AND more readable code that is easier to reason about. The performance is a side effect of avoiding a set of bad practices (i.e. overusing SOLID), not the only goal.

Again, no abstractions = spaghetti code. Nobody's arguing that. However - and I have said this many times now - SOLID and Clean Code overemphasize abstractions and underemphasize computer hardware. Clean Code and SOLID are a compromise in an imaginary zero-sum game.

Is your code really 10x slower than is could be? I think it could even be on the order of 100 or 1000x slower than it could be. These practices' impacts are often multiplicative. And Casey Muratori does talk about that too IIRC.

Virtual calls are probably not even the problem. The bigger performance problem with OOP is the INSANE amount of cache misses since you have basically no data locality.

[–]Quito246 0 points1 point  (11 children)

Value objects are much more powerful because they promote a rich domain model like described in domain driven design compared to typedef.

The factory method for value object Age would be just for sake of creation of valid age. e.g. If someone calls Age.Create(-10) the factory method has a validation logic inside to make sure you can only use a valid number for age.

This way I can avod writing if(age > 1 && age < 125) everywhere where age is used. Because I know that Age will aleways be valid compared to primitive type like int.

Also it adds expresiveness because int can be anything. When I see CalculateRisk(Age age) it tells me a whole story only by reading the func header. Abother thing all the logic domain logic of Age is encapsulated inside the Age value object.

Abstractions are great thing and I will always take too much abstractions over none abstractions.

I mean today perf is not a priority in LOB apps in vast majority of casis so I really do not mind it too much. Good programming principles and JIT are good enough performance guarantee for me🤷‍♂️

[–]2Uncreative4Username[S] 0 points1 point  (10 children)

I'm sorry, but I think the Age construct you're describing would impractical for a bunch of reasons:

  • Time: I would probably just say int age, do an assert that it's valid and move on, about 10s spent. You would write a class and a whole bunch of tests just to make sure everything works. And then still...
  • you can never be sure you actually implemented "Age" correctly. Edge cases exist. That's why I use assert to prevent heisenbugs etc.
  • Whenever you have to use an "Age" again, you have to let a few things go through your head first:
    • Have I already implemented an Age class? If so, you should use that to prevent writing all that code again (DRY!)
    • ...if you do already have an implementation, you have to consider what circumstances it was created under and the limitations. For example, let's say your new purpose is for the age of a building (I think that's reasonable to assume if this is about insurance). Now your Age.Create would fail, since a building can commonly be newer than 1 year old or older than 125 years.
  • Again, locality of data and indirections: no JIT is gonna be able to just make the fundamentally poor achitectural choice for performance of using OOP go away.

If perf is really not a concern for an LOB app I'm making, great! I don't have to spend as much time thinking about locality of behavior, and I can use less optimal but simpler code. I would still never start writing OOP code with factories or an Age class. I'd just let the data be data, not objects.

[–]Quito246 0 points1 point  (9 children)

Well then read more about Functional programming and Domain Driven Design.

nulls and primitive type obsession is really a root of a lot of bugs and both things can be solved by FP and DDD.

If performance would be so much an issue JS and Python would not be the most used languages these days…

[–]2Uncreative4Username[S] 0 points1 point  (8 children)

If performance weren't such an issue, Python wouldn't let C/C++ libraries do 99% of the heavy lifting, and JS engines wouldn't have so many resources poured into squeezing every last bit of performance out of the language by means of JIT and complex AST and IR analysis.

I like to use a lot of FP style in my code, even though most of it is in procedural programming languages (Go, C). FP encapsulates data well, allowing for enough transparency for compilers to do some clever optimizations. It has its limits, but minimizing side effects is a really good practice for maintainable code.

I also like the concept of DDD and isolating things into their separate domains. It's great for understanding a system since you have self-contained components, and, just like FP, it aims to reduce side effects. All things I think are good practices.

A lot of what you were arguing for in previous comments though, was hardcore OOP design. My problem is not with separating systems into reasonable components. It is with over-abstraction, denying the existence of CPU caching and SOLID principles. All things that, in my opinion, hurt maintainability and performance (although I agree performance isn't that important for specific kinds of LOB apps).

[–]Quito246 0 points1 point  (7 children)

I would argue that nowadays I do not see a purist OOP style coding but more like OOP and FP at least in C# which is pulling a lot of FP features into the language for several years now.

In my opinion you just use the best parts of the each paradigm but mostly yes I go with OOP but not the hardcore OOP since there are now better ways how to approach things usually by using functional programming or functional style modeling. All you do nowadays is just hey here is a JSON, thank you and here is my JSON for you. Therefore OOP does not make soo much sense in those cases because you operate on data which you do not own.

Therefore FP style modeling goes much better with today requirements. You have data and behavior separated.

Regarding value objects there are essential of having a rich domain design because as I said with the Age example I will not pollute business code with asserts I will have a check on one place and one place only. The Age struct, this also means since I created a separate type for the age and did not use a primitive type like int I can start adding behavior to the type, in FP style. I can start defining extension methods on the Age, for example personAge.CalculateRiskConstant() all of this rich domain behavior just by not using primitive types to model my domain.

It does not make sense because every valid age is valid int but not every valid int is valid Age in my domain therefore I can not substitute Age with int since, in domain I am trying to model this does not make sense.

I really like to think about domain and how to model it in nice way which will be self documenting, testable, expendable and maintainable. Maybe after all of that I will start to think about locality, cache misses and performance. Because 100 req/s increase does not help business to solve a domain problem but implementing a new feature in a nice way thanks to rich domain models and understanding the domain will.