I dont think this is true any more, especially with the changes in the last few years.
My beef is with Spring. Spring seems to make simple problems very complicated. Its like you have to learn a second more complicated API on top of the original. Plus when something goes wrong you get some 100 call deep stack trace. I just wish there were more pure Java applications without the Spring magic.
Have you tried Spring Boot? It reduces lots of the "complicated" stuff to declarative annotations and will provide you with very reasonable defaults for most use cases.
I really enjoy using spring boot. My problem with Spring is that it feels like I’m mostly metaprogramming with annotations.
Also I hate that I need to literally use a program to create the boilerplate for a new spring app. And I can’t run my program without special IDE settings.
Weird, I've never really needed to use special IDE settings for my Spring boot projects at all; I can just gradle run my project, maybe with a different profile set in the environment.
This works for me most of the time. I use a program to generate the build.gradle, and then that file builds my project through my IDE. It’s meta all the way down.
I admittedly didn’t spend a whole lot of time with Spring Boot; I mostly did Ops with projects other teams build with the occasional Dev assistance when they painted themselves into a corner. But I’m hoping you might be able to help with a question I haven’t had much of an answer to: I agree there’s a lot of “metaprogramming with annotations”, but I didn’t ever find a great way to dig into an annotation and figure out what the hell it was actually doing, or how. Do you have any pointers for pulling back the curtain?
Edit: for example, Python “annotations” are generally just functions that return wrapped functions/attributes/classes/whatever. Java annotations seem significantly more “magical” than that.
Java annotations are meta-data, they don't do anything. Actual code which uses those annotations is located elsewhere. Usually you can use "Find usages" for a given annotation to find the actual code.
That's what I figured... it seemed like a lot of things didn't really take effect until something else happened at runtime. It seemed to me (as a seasoned developer with almost no Spring Boot experience) that there was a lot of mysterious magic there that was great until it didn't work, and then it turned into a debugging nightmare (e.g. stack traces that don't, at all, mention the code where the error exists).
I, personally, despite modern trends, don't like Spring Boot. It was never a big issue for me to configure required beans manually. Spring Core reference has all the necessary examples to cover it.
I guess, Spring Boot makes sense, when you're producing a lot of little web applications with microservice architecture, so copying over that manual configurations can become burden. My applications are monolith and last for many years, so it's not an issue for me to spend some time configuring everything as I need it and it's always visible and obvious what's happening.
For example for a rest service, https://spring.io/guides/gs/rest-service/ has everything you need to get started. A simple pom with a few dependencies, and a main class that invokes SpringApplication.run. You can just invoke this main class from your IDE like any other java application.
If, like 99% of Spring Boot use cases, you're implementing "microservices" you should look into Javalin[1]. Javalin will forever sour you to Spring Boot the first time you try it. No annotations in sight, no code generation, no special IDE support involved, no multi-page stack traces, no POM gymnastics, no magic. With Java 11 type inference the boilerplate is reduced to near parity with Kotlin.
I recall a veteran software developer commenting back in 2000 that a class written in Java was less than one third the length of the same one written in C++, so it's compactness definitely fueled it's popularity.
Since then, J2EE and EJB led to a lot of boilerplate and now everyone thinks class and member names in Java have to be at least three or four words long.
The boilerplate isn't just in writing an individual class, though. It's also in the extra classes and interfaces that you might not create in C++.
And I wonder how much of the "compactness" you describe is simply because of Java's library. Library calls are very compact; having to write the code yourself (because your ecosystem doesn't have that library) is much more verbose.
I can identify myself as a strictly Java developer (nowadays) and my experience with Spring can be summarized as either:
> I imported that autoconfiguration library and everything works!
> Damn it, the imported autoconfiguration library does not work (or I need to change something), now I have to spend a day around Spring docs and stackoverflow to find how to get it working. Than I have to create a new class, override some random methods and inject my ugly code which feels like a hack now...
Just read the source. You can jump to the implementation of the autoconfiguration in your IDE, and they're usually only a dozen lines or so of bean creation.
This hits home. A lot of my work seems to be mopping up the mess that developers created by doing the former and applying the latter. Autoconfiguration is great for quick demo-days and a literal cancer for everything else.
Frankly, after some recent development on ES6 + node + express, I'm starting to question whether staying on Spring + Java is worth it.
I'm strongly against creating interfaces for non-library (not shared) code, that will only ever have one concrete implementation. I see it all the time in internal service code, code that will never have a second implementation. It makes code hard to navigate, harder to understand, and tedious to manage as instead of updating one file, now you'll need to update two.
I find myself doing this often and it’s annoying. Usually it’s because I want to use some other implementation for testing that never materialized, and then the interface sits there.
It is - but my point is about complexity not ease.
The fact that dependency is versioned separately from your code base is one source of complexity that it adds. The fact that it has many lines of code is another complexity.
It is however often easier, even although i'm advocating very small self-written test doubles, they are still less easy than using mockito.
Adding interfaces whose only purpose is unit tests is not keeping the code simpler. Using a test library that automatically generates mocks for unit testing is what keeps the code simpler.
>> automatically generates mocks for unit testing is what keeps the code simpler
You're possibly confusing easy with simple but adding a versioned library of some thousands of lines of code to a project is in no objective way simpler than adding a small test double which you wrote.
Simple - composed of a single element; not compound
Complex - consisting of many different and connected parts
Easy - achieved without great effort; presenting few difficulties.
This is not as easy but is simple. It is explicit, fundamental Java.
public void allocationOnlyPossibleWhenOpen() {
LocalDateTime expected = LocalDateTime.now();
TimeAllocator sut = new TimeAllocator(new Clock() {
public LocalDateTime getTime() {
return expected;
}
});
sut.setOpen(false);
assertThat(sut.allocate()).isNull();
sut.setOpen(true);
assertThat(sut.allocate()).isEqualTo(expected);
}
This is easier but more complex. It depends on a multi-thousand line library with its own API.
public void allocationOnlyPossibleWhenOpen() {
LocalDateTime expected = LocalDateTime.now();
Clock mockClock = mock(Clock.class);
when(clock.getTime()).thenReturn(expected);
TimeAllocator sut = new TimeAllocator(mockClock);
sut.setOpen(false);
assertThat(sut.allocate()).isNull();
sut.setOpen(true);
assertThat(sut.allocate()).isEqualTo(expected);
}
The 2nd example has:
A versioned external dependency, externally versioned == opportunity for future conflict. Remember the java logging library wars? Today it's trivial to encounter conflicting spring dependencies on your classpath
A complex (but convenient) reflection based implementation, the object has non-obvious behaviour with a clever implementation
An entire api surface distinct from the components of your software system
Tens of thousands of lines of opportunity for bugs, security risks or just plain old $$ cost to hide in
Once you're able to see the world in complexity, it opens up a whole new dimension of cost & quality for your code.
All this said, in practice complexity is just a consideration. Often the complexity is worth paying. I don't want to risk the impression that i'm saying never use mockito. If my argument is anything then it's consider the impact of complexity.
Using a test framework that bends the rules of the language is far from simple. One of the things I appreciate about Go is that any mortal can understand how a unit test works: you "just" plug in something that satisfies the interface. However, we are spoiled to have implicitly satisfied interfaces, which can be declared on the consumer side without the implementation even knowing.
> Adding interfaces whose only purpose is unit tests is not keeping the code simpler. Using a test library that automatically generates mocks for unit testing is what keeps the code simpler.
I've yet to find a library for C++ that generates mocks without requiring interfaces, but I'll admit I'm not an expert on it. Can you recommend any?
As an example, Google Mock/Test will require interfaces in a C++ code base.
Of course, we may be disagreeing on what is meant by the term "interface".
Not on all platforms. In .NET you need an interface or virtual method in order to allow the mocking framework to dynamically generate a concrete subclass that mocks out that method's functionality.
I do that for this reason but only if I am able to inject the implementation into the classes under test. Very often I just resort to constrictor injection for internal code.
I did this, and then switched back to just running the code on a test database. I found I was writing two sets of tests: one that tested the code to see if it injected the right parameters into the SQL, and another to see if the SQL worked. It just got easier to test them both together.
Once you know there’s a bug, you will definitely want to write more tests, in order to find the cause. But always writing a lot of tests, which are only usable in case you find a bug, seems like wasted effort to me.
If you need to triangulate the origin, do that when it’s necessary (i.e. when your broad test fails) and not all the time.
I prefer to just runs tests using a real database too. They might not complete as fast as mocked out ones, but they give me much more confidence of correctness, and still complete in milliseconds.
This can become a challenge when you want to scale a dev team. You either have people conflicting in the DB, or each dev needs to set up a local DB and deal with DB migrations. Containerized DBs help with this to some extent, but it's still a maintenance headache.
There's also a tradeoff around testing error states, but that can be a wash. Some errors are easier to create with a mock, some are easier to create with the actual driver.
No. This is what many considered test induced damage. If the only reason you create an interface is because a test requires it, you are damaging the integrity of your design.
Interfaces are one of the more powerful abstractions available and should be well thought out points of flexibility you specifically designed into your solution. If you use interfaces for other reasons your codebase is full of false flexibility and not-actually-abstracted abstractions which makes it difficult for other people to figure out what they can actually do with it.
> No. This is what many considered test induced damage. If the only reason why you create an interface is because a test requires it, you are damaging the integrity of your design.
Writing testable code is not "damage". An interface is a small concession in exchange for the well known benefits of automated testing.
I see where you're coming from and many programmers use too many interfaces as a reflex. But I absolutely think interfaces should be used to create seams for automated tests in front of components that do things like interact with databases, file systems, networks etc.
A common pattern to get around this situation is to use mocking frameworks to fake those calls without using interfaces. It’s in the air whether this is better or worse in the general lifespan of a piece of code; but, there are alternatives.
How does that work in a static language? In most (all?) static languages, you'd need an interface or virtual method in order to on-the-fly generate a class that implements the mocked functionality.
There are plenty of mocking frameworks for C# (Moq, FakeItEasy, etc), but for all but the most trivial classes, you generally need to use an interface or mark methods as virtual.
It's one of the reasons I tend to minimise my use of mocks as much as possible.
It's the same with Java where it's very hard/not possible to mock final methods. It's just that methods in Java are virtual by default so it's less "in the way" when you need to mock something.
A "unit test purist" would likely shun this, but I take more of a realist approach; in many real-world cases, mock-heavy tests make for brittle tests that don't give confidence of correctness.
I tend to focus on integration tests, as I find these provide the most value while still executing very quickly (depending on the stack, obv).
That doesn't mean I don't use unit tests, or that I never use mocks - I just use mocks very sparingly.
Correct, but you can go a long way in writing testable code without requiring mock implementations. While I don’t deny the usefulness of mock testing, it comes at a steep cost (= vastly more complex code base), and I increasingly question whether that’s worth it. In practice I find it more useful to unit test what can be tested in isolation, and to perform integration testing for the test, and to limit mock testing (and thus unit tests which would require mocking) to a few well-defined interfaces. This cuts down drastically on boilerplate, with little (if any) loss of testability. And these few remaining mock object dependencies can be modelled via simple parameters and higher-order functions, further cutting down on extraneous interface classes.
> Correct, but you can go a long way in writing testable code without requiring mock implementations.
For some languages (e.g. C++), if you are using classes, it can actually be fairly challenging to do unit tests without mocks. And almost every solution to this problem out there is to use interfaces/dependency injection/inversion.
> [in C++] it can actually be fairly challenging to do unit tests without mocks
Yes, but you can write integration tests instead. There’s no rule that says that unit testing must cover every aspect of the software. Unit test those units that can be used in isolation, integration test the rest. Case in point, our main software at work, written in C++, has close to 100% test coverage, and uses almost no mock tests. Instead, we have extensive integration tests where unit testing without mocking doesn’t work.
And, again: I’m not saying that there are no tradeoffs involved in that. But the benefit of having a drastically simpler code architecture outweigh the cost of having to move those tests into integration.
Incidentally this is vastly easier when the architecture cleanly separates business logic from general logic and moves as much of it as possible into side-effect free general-purpose functions.
> Yes, but you can write integration tests instead. There’s no rule that says that unit testing must cover every aspect of the software. Unit test those units that can be used in isolation, integration test the rest. Case in point, our main software at work, written in C++, has close to 100% test coverage, and uses almost no mock tests. Instead, we have extensive integration tests where unit testing without mocking doesn’t work.
In my last C++ job, perhaps less than 5% of our code was unit testable. We had very good test coverage using integrated tests, but those were very expensive tests. Running a full test suite took 10 hours. Our SW heavily relied on certain equipment. Fortunately, the vendor had provided a very good simulator for it - so almost all our integration tests needed that simulator. And the simulator was such that there was a fair amount of overhead in loading and setting it up and unloading it. We didn't do this for every test - that would have taken days/weeks to run - but for logically grouped tests. And unfortunately many of the integration tests we wrote were much more suited to unit tests, but because we didn't have interfaces, we were forced to test them via an integration test, which was very expensive.
Oh, and the simulator would not allow parallelism - you could have only one simulator running on a given machine at a time. So all our tests had to be run in sequence.
And of course, the simulator had bugs, so whenever we had a failure, we had to determine if the simulator was at fault. Especially for intermittent failures.
So yes, we had very thorough tests and were proud of it, but unit testing for perhaps 30% of our tests would have been better. But to unit test those we really needed interfaces. I know because I convinced management to give me some weeks to convert one of our simplest portions of code into something we could unit test. I thought it would be trivial, but I ended up needing interfaces.
There is a whole other approach which our sister team (very similar SW, but different HW) used. They did not have a reliable simulator. So they wrote a "fake" simulator. They simply reproduced the APIs and wrote code to return certain values under certain conditions - a poor man's mock. On the plus side their test suite ran very quickly. The down side was they needed to write many mocks for the same function (for when they needed to return different values). Writing tests was very expensive for them.
Why did they write fakes instead of using a mocking framework? Because every mocking framework they found in C++ required interfaces, and they didn't want to change their SW architecture.
In many cases integration tests are OK. But in our case, they were extremely expensive.
> Incidentally this is vastly easier when the architecture cleanly separates business logic from general logic and moves as much of it as possible into side-effect free general-purpose functions.
This is precisely where interfaces come from! It's not trivial to separate business logic from general logic without them. When I took the time to show how we could write unit tests for our code base, I ended up with interfaces. And no, I didn't read any books/sites telling me I should do it. I didn't even know they were called interfaces. I just took some of the simplest code in our codebase, and asked "How can I write unit tests for this?" and essentially reinvented interfaces. I separated the business logic from the logic requiring the simulator, and then wrote unit tests for the business logic, and had an interface class to interface with the simulator.
To give you an idea, the code I converted to unit tests was trivial: Given this input string with these flags set, return this converted string, etc. It should be one of the easiest things to unit test! However, the input string and flags were entities understood by the HW and so we had simulator dependencies. I wrote a class that dealt only with strings and booleans to implement the...
I appreciate your comment and your experience. And I agree: integration tests tend to be time consuming to run. At work we actually had similar issues (and many of our integration tests need to exercise cloud APIs) before a rewrite that made the time manageable.
I just wanted to pick up one of your points:
> I've gone into the details I end up with interface classes
My reply to this is that functional programming languages tend to manage entirely without, just by doing “dependency injection” via higher-order functions (in fact, outside Java I’ve never [needed to] use a DI framework). This (often? always? I honestly don’t know!) makes mocking possible without impacting the overall architecture. And like you, I practice strict TDD for all my current own hobby projects, and I manage without interfaces. But I don’t want to make a claim of generality here: it’s entirely possible that this approach doesn’t work everywhere, or even in the majority of cases.
I'm so glad to see someone write this. I keep seeing all these horrible code bases that are supposed to be easily testable but all I see is a horrible design that is impossible to understand - all because Mocking is supposed to be a good idea.
Testing with mocks is generally a bad idea; instead, one should just test using the same dependencies that are used in production (after testing those dependencies, if also part of the project).
This avoids having to introduce the extra interface, having to code the mock, sometimes having to change the mock or tests when changing the code and also lets you catch bugs in the dependencies that the tests on the dependency itself missed and that would otherwise show up in production.
One might have to do some work to partially replicate the production setup in testing and making it fast to start and run, but that's usually less work and gives a much better result.
Not true, and where the Spring Framework <strong>allows</strong> to use interfaces only (i.e. without having to write an implementing class) it's for neat reasons, e.g. autogenerating Spring Repositories.
I think the GP is referring to the use of interfaces with only one implementing class, rather than interfaces with no implementing class. i.e. you end up creating a UserProvider and UserProviderImpl for every service class you write.
OP is probably referring to the JDK Proxy support which is used by Spring and requires classes to implement at least one interface.
So much over-engineering comes from the fear of changing already written code. Some programmers, I believe, have the impression that changing code any reason other than new features or requirements is wasted effort. So they try and build out all possibilities they can imagine first so they won't have to change it later.
If you eventually need that interface, you can change your existing code to use it. Making that change is relatively easy compared to maintaining a bunch of code that isn't needed. But still I find it hard for some programmers to get past that mentality.
In my own progression as an engineer who writes Java, I read Clean Architecture by Robert Martin which lead to AbstractFactory(s) and interfaces up the wazoo. The reasoning would always be "this can change!"
It definitely took me some time to understand that not everything will change, and if it does change, having some extra work for simplicity is a reasonable tradeoff sometimes. In general, knowing when to utilize OO design patterns takes time and experience.
This is taken to a pathological degree in the VIPER architecture. Every thing is decomposed and wrapped in and interface. Unfortunately this has gained some popularity in the iOS world. It makes code so much harder to follow and maintain.
AppKit is pretty deeply MVC. I generally prefer to just work with the underlying APIs than try to invent a new paradigm on top of them. With VC containment I feel like I have enough abstraction power already.
In my case the "Flows" are usually UINavigationControllers hidden behind a protocol. I tend to chop up my UI's really deeply into VC's but most of them get a VM because I like my VC's really, really small.
It took a long time before I realized that a lot of the changeability that come with writing aggressively SOLID code is actually a self-fulfilling prophecy: Some tightly intertwined chain of steps that might have been implemented as five consecutive (albeit tightly coupled) statements gets blown out into 10 or 15 modules and abstractions spread across as many files. All this added complexity was invariably designed to make it easy to change the code in a way that is not actually how you ended up needing to change it, so the end result is that it increased the difficulty of changing things: Instead of editing 5 lines, you now need to edit 10 files. And, since the logic is spread across 10 files whose members are, thanks to the magic of Java's access control facilities, all public, and whose connections to other parts of the codebase are all, thanks to the magic of dependency injection, loose (read: not explicit), it's hard to even understand the scope of impact of those changes without relying extensively on an IDE.
I've since taken a big turn back toward procedural programming. Object-oriented and functional techniques have their place (and the best procedural languages will let you use both), and I've learned a lot from working in both paradigms. But most of what I learned is that no amount of abstraction can replace the KISS principle.
> The reasoning would always be "this can change!"
As I understood Uncle Bob's argument for using interfaces though, the main benefit is not supporting alternative implementations, but rather aiding in the enforcement of the Dependency Rule[1] and of the Single Responsibility Principle[2].
Interfaces make the relation between components explicit in two ways:
- they make it clear what depends on what
- they make it clear what are the responsibilities of the components on either side of the interface
So aside from the fact that it might make it easier in the future to swap implementations, it first makes it easier when writing the code to decide how to organize its components (i.e. laying out its architecture).
Of course I agree with you that interfaces can definitely be abused, and can make code more indirect and difficult to read. I just wanted to point out what I think is Uncle Bob's point. :)
Too many Java programmers take the SRE too far, IMO.
If you have a class that only has a single function, then it probably should be a function, not a whole class.
To give an example that I've seen, I've seen code with classes named UserGetter, UserCreator, UserDeleter, etc. All of these should have just been bundled up into a single User class.
I've gone through the same learning process, and I've seen it happen with numerous devs - they go through a cargo-culting stage, where they see abstractions and patterns espoused by people they respect, only to later come to the conclusion that it makes for code that is anything but clear.
Abstractions are not evil, but it takes experience to use just enough abstraction.
I catch myself doing this all the time. I'm never going to move this project off Postgres, so why the hell have I created an interface to abstract the database?
Interfaces are not the only option - you can also mark methods as virtual.
But overuse of mocking is a very big problem in a lot of codebases. Often I look at a test, and all it seems to do is setup mocks, such that I'm not sure if anything is actually being tested!
My hope is that C#'s new default interface implementations feature will put an end to this mess.
As mentioned elsewhere, refactoring a concrete class into an interface is actually pretty trivial using an IDE. At least within an codebase. The only time to really be diligent about interface design is if you're designing an API.
I've been in the workforce for nearly 10 years and I still have this issue when starting a new project.
It takes a conscious effort to just move forward, and it still leaves a lingering thought in my mind about what I'm doing wrong while building exactly what I need.
This is my main complaint with OOP. It's not a feature of OOP explicitly but I've only seen it in OOP codebases. And the most complicated, awful, snaggletoothed codebases I've had to unravel were all like this - heavily laden with abstractions that weren't really necessary.
I actually said the opposite. Any tool of sufficient power is going to more abused than a tool with less power. You can do more damage with a backhoe than a shovel.
But the fact that you can do more damage with a backhoe than a shovel doesn't mean there is anything wrong with a backhoe or anything good about shovel. Same is true of OOP.
Couldn't agree more, and I feel like I've spent half my career tilting at windmills trying to get other engineers to use less abstraction, instead of more.
I can't count the number of C#/Java projects I've walked into where it felt like I had to drill down through a dozen layers of abstraction just to find code that actually does something. Hundreds (thousands?) of files that contain nothing but autogenerated boilerplate crap.
Layers upon layers of boilerplate, "just in case we need to swap something out later!" No. I promise you, you won't. And if you do, I promise you things aren't as loosely coupled as you think they are. You're going to have to do a bunch of rewriting anyway.
Also, 9 times out of 10, by the time you realize some seemingly interchangeable piece of your architecture needs to be replaced, your team will be chomping at the bit to blow the whole thing up and rewrite from the ground up anyway.
In the meantime, having a simple codebase that people actually enjoy working in will make everyone's job so much easier. And fun.
You can do that with classes -- as long the dependents don't instantiate their dependencies directly.
However, I personally like interfaces (well, traits, I'm doing scala), because it makes it much easier for readers to understand what bits are supposed to be part of the interface and which parts are supposed to be an internal implementation detail. Without interfaces it's really easy to accidentally change e.g. a return type into a more concrete one which accidentally leaks impl. details and suddenly other bits of code will come to rely on those impl. details. I work on rather large/complicated systems and I've seen this gradual spaghettification time and time again.
Disclosure: I'm usually the person who gets called in to fix the mess -- so that might bias my perceptions here.
> I'm strongly against creating interfaces for non-library (not shared) code, that will only ever have one concrete implementation.
Even if it does, for a mild behavior change or a test implementation, the simple fact that Java methods are virtual-by-default is a huge benefit here.
C#, on the other hand, is final-by-default. This makes code much more annoying to deal with unless it implements an interface and/or is thoughtfully written with these cases in mind.
You're actually reinforcing the OP's point. Coding to an interface is ideal but we are reluctant to do so because it involves more boilerplate that is a pain to manage.
If you know there'll only be one concrete implementation, then yeah don't bother.
What people sometimes forget though is that any implementations of this in your tests are also concrete.
Some things like a DB accessor, will use a mocked/lite implementation in testing for code that doesn't need access to the db (but still needs to be provided data) to test it's functionality.
I strongly agree with this, but if are using an inversion of control pattern for your business logic vs. I/O, then interfaces are often needed to prevent circular references. If using multiple compilation modules, this becomes evident because the code wouldn't even compile without the interface! When using this design, interfaces can sometimes be required more often than not.
That's the litmus test I use to determine if I need an interface: Will the code compile without it? If not, the interface is declared alongside the code that depends on it, and the implementation is in a second compilation module which depends on the first. Inversion of control.
> I'm strongly against creating interfaces for non-library (not shared) code, that will only ever have one concrete implementation. I see it all the time in internal service code, code that will never have a second implementation. It makes code hard to navigate, harder to understand, and tedious to manage as instead of updating one file, now you'll need to update two.
I was just telling someone this yesterday. Not every damn thing needs to be an interface you implement if you're only implementing it once. Especially if you have to put "I" in front of it (as is standard in C#) and name the class the same thing, you should think if it's even necessary...
> I'm strongly against creating interfaces for non-library (not shared) code, that will only ever have one concrete implementation.
I agree with this, but the problem I see is that people have bad judgement when thinking about if something will only be implemented once or not.
Personally, I don't think the boiler plate / interface bloat is as cumbersome as others do. I'm currently working in a code base where things were decided to "never change", and now we need to change those things.
In a perfect world, I'd rather have less abstract code that never changes. But if it's a choice between poorly written highly coupled code or poorly written code that relies on interfaces, I'll take the interfaces and their bloats.
I see a lot of this with C# too, with devs creating separate projects for "contracts" and "implementations" - yet almost every interface has 1 implementation, and will never have more.
The rationale I'm given is generally one of two:
1. "It's best practice"/"it's clean architecture" - which is basically dogmatic cargo-culting
2. To support mocking in unit tests
The later is also found with unit test projects rammed with mocks, making for brittle, unreadable tests that often don't really seem to actually test anything!
Yep. People fundamentally misunderstand how to use mocks and stubs, and often end up writing tests that test little more than “the function was written the way it was currently written”.
This is the worst of all possible worlds, because a test suite should accomplish two goals: identify bugs and enable refactoring. Tests like these fundamentally can’t identify bugs because they don’t test any behavior. And worse, they prohibit refactoring because any meaningful code change will alter what functions are called and in what order they’re called. So any refactor breaks almost every test, and it’s impossible to quickly tell if something “real” was broken or if it’s just an artifact of poor testing.
Ironically, I see this in golang all the time. Interfaces are defined with a single implementation in the code, and then the interface is used to generate mocks.
Needless to say the implementations have never been replaced with something else. The middle layer is completely useless. UserBo, UserBoImpl and UserDao should be tossed into the garbage. Every single time you wanted to add a new SQL query you had to edit 5 files... Code navigation features of your IDE become useless and you are better off with string based search.
I use the term “monomorphism”, as an antonym to polymorphism.
Yeah, not good.
And you meant update 3 files, not 2. You forgot the 1 file that uses the 1 implementation, which should have simply been nested the only place it was used.
Of course you can write more performant code with lower-level languages, though there is a reason that almost no web framework/application is written in them - the additional burden of managing memory outweighs the (actually surprisingly little) performance benefit. If you look at examples on the site, in basically every other benchmark Java and sometimes Go rank the best.
As a genomicist I'm half biologist, half data scientist. I think for the majority of my problems, it's the verbosity of Java that I just cannot stand. Maybe the instruction I've had in Java is flawed, but the amount of codewords to do anything has really kept me away from it.
I don't really ever run into the same problem twice. I've never been able to write anything as concise for a one-off problem as I can using Python or Perl (or even Awk) for data wrangling.
Scripts are superior in term of usability for what you do. Period.
Java is for enterprise scale applications, where dozens or hundreds of developers contribute to the code base, for a decade or more.
The boilerplate, the verbosity, strong types, the dependencies on complementary tools is what makes a large code base maintainable in the long run. If cared for.
If you need to scrap a website a couple of times, convert text from one format to another. Yea just use some script. Throw away code that nobody but you will ever need to touch or even look at.
It is possible to write a python, Perl, JavaScript, or even Php app at scale. It may require a bit more due diligence though. Frameworks have been introduced to help with that.
The article is written by someone who doesn't fully understand Java. No need for interface to support injection. Interfaces are commonly written by CS graduates or uninformed long time OOP developers, but composition and DI have been around for over a decade. I stopped reading when the interface was pointed out as the only way to go.
> You can write concise pure Java if you don't rely on libraries
Hum... No, you can not. Java is missing basic tools for abstracting idioms, break your code semantically, and reusing your code. Without those, you'll just write stuff again and again, hope you have a very good IDE.
> It's simple
It's simple the same way Go is simple. The language itself is small, in ways that make your code complicated. On this competition, Brainfuck is unbeatable.
> it's super fast
C, C++, Rust and Fortran are fast. Java is not. It's a second tier, at around the same speed as C# (obviously), Go, and Haskell.
Any kind of flexibility on the syntax, metaprograming, reflection is so useless that it could as well not be there, high order constructions where the compiler can infer the idiom, well, really anything.
It has relatively recently gained usable high order functions, and seems to have stopped there.
Well, I'm saying that Java doesn't have any of those. Other languages have some concept they run away with, like macros for Lisp, monads on Haskell, named parameter enumeration and monkey patching on Python. Even PHP relies heavily on object enumeration.
All of those techniques are kind of equivalent in power, spread through a curve trading flexibility (expresivity) with organization (possibility of analysis). Java has nothing near that curve.
I'm not sure what you mean by named parameter enumeration or object enumeration to be honest.
> spread through a curve trading flexibility (expresivity) with organization (possibility of analysis). Java has nothing near that curve.
Java definitely leans towards the organization part of the curve. It doesn't need a single concept to run away with, because it knows that there is no magic bullet that will solve everything. Monkey patching is dangerous, especially when you're programming large systems that end up being used in finance or ecommerce.
On a side note, if I'm not mistaken, Brian Goetz mentioned that something similar to higher kinded types was not off the table for Java sometime down the line in a recent panel.
Do I see a case of the Blurb? I'm not sure, but it does resemble it. (By the way, you'll have a hard time finding any site written on Python that doesn't use monkey patching, e-comerce or not).
Anyway, parameter or objects enumeration are when a library uses both the field names and their values as input, people use them to define DSLs on a function call or object construction. Parameter enumeration does afford applicative semantics, but (mutable) object enumeration affords full monadic semantic, so it's easy to construct Turing complete DSLs (and I guess, why Python people avoid it).
It is really hard to explain how those concepts are useful, mostly because they only work when everything else on the language help. If you try them on Java, they will be mostly useless. C# for example has a fully generic implementation of monads, with specialized syntax that isn't very far from Haskell's, do notation. Yet, with its bad type inference and the inflexible interface hierarchy it got from copying Java, it is nowhere near as useful.
Compared to the currently fashionable Go, Java has quite little boilerplate. Java 8+ has a number of quite powerful APIs in the standard library, some limited type inference, reasonable interfaces that allow default methods. The compiler has a well-defined custom processing interface (@annotations) which allows for powerful boilerplate-reducing things, from Lombok to Spring to JUnit.
I write lots of Java and Go. I much prefer writing Java where I have things like "sets" and lambdas. I also find the type system to be a lot better in Java. In Go I'm often left wondering what I'm looking at and don't know unless I dive down into the source.
> I also find the type system to be a lot better in Java. In Go I'm often left wondering what I'm looking at and don't know unless I dive down into the source.
Could you please provide an example in both Go and Java? I am not quite sure that it has to do with the type system itself but your familiarity with the language and its libraries, and/or lack of basic functionality that should be in your text editor when you write Go.
Alternatively, use your text editor. For me, even Emacs displays the type when my cursor is on or around value, and if you put it on or around getValue(), you get its function prototype that includes its return type.
So it's almost exactly the same in both languages.
It's not a problem for most IDEs, just hover your mouse over the value or use keyboard shortcut to get the type. This was never an issue for me in Java or Go.
Exception handling and closing resources keep to be a major pain in the ass. Worst, the lambdas do not fit well with checked exceptions and the standard library is full of them. Another major pain are closing methods which throw checked exceptions themselves. This makes wrapping this into a decent lambda painful.
I'd have preferred much lambdas just exposing checked exceptions they experience inside. I can't imagine why the compiler shouldn't be able to do this. Maybe one would have to come up with a fancy definition of functional interfaces (although I think this could be tackled with an annotation if the language doesn't provide this for lack of another keyword a la "throwsall").
Checked exceptions should just be removed entirely from Java. It's a completely failed experiment.
If you really want something like checked exceptions then (anonymous) intersection types or row polymorphism would be much better approaches than the adhoc thing Java does... but I suspect that that wouldn't well with the variance system in Java.
(Thankfully it's only the Java compiler itself which imposes checked-ness, so other languages on the JVM thankfully don't have to deal with the insanity.)
I am not against checked exceptions per se. Their excessive use of them in the standard library and especially the inability of lambdas in handling them are a problem imho. Take the stream close methods: There is absolutely no use in having those throwing a checked exception because there is nothing you can do. If closing is not possible you have problems with the underlying OS. That would rather warrant an error instead of an exception.
Yes there is. I have enough material enough on this for several blog posts, but I won't belabor the point.
One, why do you think almost all non-standard java libraries almost exclusively use exceptions derived from RuntimeException?
Two, I encourage you to look up guava's Throwables utility class and consider why it is even a thing. Just for a single example: Methods with no throws clause cannot throw exceptions, so you must wrap in a RuntimeException... but that means that outer catch clauses which match on the checked exception WILL NOT MATCH on the RTExc-wrapped-exception. This is broken as all hell.
I like checked exceptions. It's nice getting a reminder about issues I need to handle and its trivial to just get everything to pass on exceptions if that's the disposition I choose for my code base
It's not trivial if you're forced to conform to an interface which has a narrower "throws" clause than what you need. It's an issue of contravariance vs. covariance. Implementations of an interface/superclass are typically more concrete, and so they need to be able to throw more more types of exceptions... but throws clauses work in the opposite way: You're only allowed to narrow them, not expand them in subclasses/implementations-of-interfaces.
It's simply broken.
EDIT: Just to add: What happens in practice is that either
- you just give up and declare "throws Exception" on the interface (in which case: why bother? You might as well just remove checked exceptions at that point), or
- you wrap checked exceptions in RuntimeException, but now you have a new problem: Consider a method that calls methods f and g where f declares a "throws Foo", but g doesn't declare any exceptions[0], but may throw Foo's at runtime by wrapping. Now you're totally screwed, because a "catch Foo" will not match exceptions from g. The Guava people have written a whole Throwables utility class to help with this, but using that consistently requires discipline that no team can handle, and it only handles a small subset of the problem.
This is just a small sample of the problems with checked exceptions in Java.
[0] g may be used in contexts where it's not "allowed" to declare checked exceptions in a throws clause, e.g. lambdas or just higher-order functions in general.
I suspect people see a lot of enterprise-y boilerplate (IAbstractFactoryBeans, for example), absurd inheritance hierarchies, and other things like getters and setters--they think these things are "part of writing Java" in a way that isn't "part of writing Go". In some sense, they're right--Java has more of a boilerplate culture even though it technically requires less boilerplate than Go. You can break from that culture (and as I understand it, Java is moving away from that boilerplate culture) to a certain extent, but you're still likely to have to use libraries that encourage it (by extending a ridiculous inheritance hierarchy, for example) and you'll probably end up working on teams that require a certain amount of boilerplate (e.g., mandates like "getters and setters for everything!").
Since Go doesn't even have inheritance or getters and setters, they may have a point (though getters and setters are pretty easy to do I guess if you want them, it's not part of the culture).
Culture is really important in programming languages and I'm not sure it is something you can ever separate from the language itself, as for example the standard library dictates in large part the structure of your own code.
I do agree though that there is no great reason Java has to be more verbose or more confusing than say Go.
I'm generally of the opinion that Go is less verbose than Java as well, but I'm of the impression that a lot has changed in Java since I last used it. It now has lambdas and other niceties, for example. Go has error boilerplate and lacks generics. Personally, I think Go's boilerplate is less of a big deal in general (Java's generics, lambdas, and exception handling come with their own issues which are often more significant than Go's boilerplate). Mostly I just don't think a debate about which language has less boilerplate will be very enlightening.
With Lmobok, you can use `var` for local variables, and it will infer the type for you. It also can produce default constructors of various kinds.
Can't have nice syntax for object literals, maps, slices, etc, though :( I mean, I have no hope for it to be introduced to the language any time soon, even if the desire is there among the language stewards.
With a default constructor or a builder (both provided by Lombok, maybe with other libraries, too) this level of succinctness is quite achievable in Java, too.
> Do not need to explicitly declare interfaces a struct conforms to.
Which comes with its own (sometimes dangerous) downsides.
> 2. Do not need to declare types of variables inferred on initialization.
Java has `var` for quite some time now:
var foo = new Foo<Bar>();
is valid.
> Syntax for slice, map and struct literals
I assume you mean overloading `[]` for slices and maps. This is a nice feature (C# and Kotlin already do this). But it doesn't reduce boilerplate by much in practice.
> Which comes with its own (sometimes dangerous) downsides.
I don't buy this. I've heard the theoretical arguments to the contrary, but I've never (in 7 years of consistent Go use) heard of anyone being bit by this in practice--certainly not to any significant consequence. On the other hand, implicit interfaces materially benefit nearly every project in the Go ecosystem. I would make this tradeoff all day every day.
There was an actual bug in the golang standard library precisely due to this feature, so it's far from theoretical.
Secondly, I found that in practice, it doesn't buy you much, and on the contrary, makes working with code more difficult (even with an IDE).
It's valuable information to know what interface(s) a type implements, not to mention finding all types down the hierarchy that implement a given interface (much more tedious and resource hungry with golang).
One incident doesn't make it "far from theoretical". I concede that it's "vanishingly small", however.
> Secondly, I found that in practice, it doesn't buy you much, and on the contrary, makes working with code more difficult (even with an IDE).
I don't know about you, but I don't like having to write boilerplate to make a third party class implement a first party interface.
> It's valuable information to know what interface(s) a type implements, not to mention finding all types down the hierarchy that implement a given interface (much more tedious and resource hungry with golang).
How is that information valuable? The only thing I can think of is "I want to change the interface and consequently I need to update the implementations". In which case, just change the interface and the compiler will tell you what broke. As for "resource hungry", who cares? I'll trade 50ms of computing time per search (note that this is a generous figure) if it means developers don't need to maintain "implements" annotations and the corresponding boilerplate discussed above.
It's useful when you want to know where a certain concrete type is being passed as an interface. If you have a type Foo that has functions bar() and baz(), then it becomes very tedious to see where Foo is being used as a `Barrer` (in golang style), `Bazzer` or even `BarrerBazzer` because it now automatically implement all three interfaces.
Another use case is that it becomes awkward to define "tag" interfaces (look at Rust's `Send` trait for instance). Now you're forced to define an interface with a function `isSend` or something and hope that no one else declares another interface with the same signature and passes in types that implement that. Again, more bug-prone behavior.
> I'll trade 50ms of computing time per search (note that this is a generous figure)
On any non-trivially sized project, it takes way longer than that to look up implementations of interfaces in the IDEs I've used so far. You're looking at 30s+ sometimes.
> It's useful when you want to know where a certain concrete type is being passed as an interface. If you have a type Foo that has functions bar() and baz(), then it becomes very tedious to see where Foo is being used as a `Barrer` (in golang style), `Bazzer` or even `BarrerBazzer` because it now automatically implement all three interfaces.
That's just a generalization of the example I gave, and the same solution applies (although there are probably automated solutions). Unless there are other special cases that you have to do so frequently that the solution previously mentioned is too tedious, then I don't see what the fuss is about.
> Another use case is that it becomes awkward to define "tag" interfaces (look at Rust's `Send` trait for instance). Now you're forced to define an interface with a function `isSend` or something and hope that no one else declares another interface with the same signature and passes in types that implement that. Again, more bug-prone behavior.
I don't know how to take this comment seriously. The odds of someone implementing isSend and passing it to the wrong interface have to be astronomically low. The frequency of such bugs must be approaching zero. It's patently unreasonable to consider this to be "bug prone", moreover, if you're really, really concerned about it, randomly generate your private method name to whatever degree of entropy you prefer.
> On any non-trivially sized project, it takes way longer than that to look up implementations of interfaces in the IDEs I've used so far. You're looking at 30s+ sometimes.
File those bugs with the IDEs. Any IDE worth its salt holds the set of types in memory. Even if the set is just a list/array (as opposed to some data structure that is indexed by the interfaces they implement), it should take no time at all to filter that to the set that implements the interface, even if there are tens of thousands of types.
Well, it was only added in the latest LTS release, which was released a little over a year ago. (Or a few months before that if you used the non-LTS Java 10.)
Personally I think Java is more verbose because of culture and one big feature go does not have - inheritance. Indeed some regret has been expressed by the Java authors on inheritance.
The things you mention don’t bother me as much as pointless ceremony like getters, factories, and insane object hierarchies, all of which are really optional. I quite like Go and use it a lot, but it is not IMO radically different from Java in syntax, except perhaps in what it leaves out (inheritance, exceptions, explicit declarations), and its culture of radical simplicity.
> Since Go doesn't even have inheritance or getters and setters
Does Go have objects with functions? If so, it has getters and setters, Go programmers just choose not to use them. I could absolutly write Python code like this:
The thing is...Java doesn't have getters and setters. There's nothing about the language that requires them. This is perfectly legal Java code:
class MyObject {
public int foo;
}
Look at it! It's a public member! You can read/write to it without a getter/setter! You know, something every language has, but for some reason, it's considered verboten in Java.
Java is a perfectly fine language. It's Java programmers that are awful for sticking to these unnecessary paradigms that necessitate ridiculous amounts of boilerplate.
I agree with your point, but the snark here is unwarranted (pretty sure the parent isn't even contesting your point) and very likely to derail the otherwise interesting conversation (IMHO).
Re-reading the other comment and my response, yeah, you're probably right.
Java programmers are a source of frustration for me. I don't write Java professionally, but I do application security which involves reading a lot of Java code. And because the developers are sticking so strictly to the typical Java paradigms, the code is very difficult to read. Testing things locally is a nightmare, since stack traces are a ridiculous stacks of .invoke, as if Java programmers are afraid of directly calling functions.
I let my frustration get the better of me and I apologize.
> for some reason, it's considered verboten in Java.
It's not just "for some reason". There are plenty of reasons why getters are a superior approach to naked fields, and these have been documented to death for the past twenty years.
If your field is not final, you should hide it behind a getter/setter.
I've read a lot of documentation, and I've never been convinced. Maybe it's because I've just never been involved in writing a large Java project, and so never seen the advantages?
Many other languages do just fine without them. What makes Java so different?
Other languages (e.g. Python, C#, Kotlin, even old-school Visual Basic) have actual language support for getters/setters, usually called properties.
What makes Java different is that they have properties as a concept (in the Java beans specification) but not as an actual language feature, so you have to implement them manually yourself for every field and every class. (Or let the IDE generate the code, but all that boilerplate is code that wouldn't need to exist in other languages.)
1. Many other languages let you redefine field assignment syntax to be a function call, which is basically what a 'property' is in Kotlin. Java doesn't.
2. The Java ecosystem distributes software in binary bytecode form and cares about binary/API compatibility.
The combination of these two mean that even if you don't want to run any code on field assignment today, if there's any chance you might want it tomorrow you have to plan for it now by using functions instead of naked fields. Changing the latter to the former means updating all the use sites.
The big win of language integrated properties is you can go from (in Kotlin):
class Thing(val veryLongText: String)
to
class Thing(text: String) {
val veryLongText by lazy { text.toLowerCase() }
}
without any of the places that use Thing.veryLongText noticing the difference. The first code just stores the string as an ordinary field. In the second, we've decided the text should actually be lower case and that lower-casing should be done on demand then memoized for efficiency.
In Java, if you want to do that, you'd have needed to write a getVeryLongText() method from the start, as otherwise you'd have nowhere to add the extra code.
The funny thing is that if you want to create an immutable type in Java - no problem, just create a constructor and mark fields as final[1].
In Go - you can't do it like that. You need those getters, preferably an interface (because exported struct allows anybody to create a zero-value) and a factory function.
Immutability is neat, but you can get a long way by passing by copy and/or documenting whether a value shouldn't be mutated. I would like to see immutability added to Go, but it would break my heart if it was inspired by Java (i.e., final) instead of Rust's notion of immutability.
In my opinion, if Go does one thing exactly right, it's the "OOP". That is, interfaces and explicit receiver arguments in functions. Interfaces are extendable, classes are not, because there are no classes per se, let alone abstract classes.
Maybe conforming to an interface without explicitly declaring it (like you declare an instance of a typeclass in Haskell) may look like a controversial decision. I still think it does more good by removing boilerplate than bad by accepting an object as conforming to an interface in a rare case when you did not mean it.
I agree with all of this, but Go also has a pretty great standard library, tooling, and deployment story that you're overlooking. Notably, Go's tools are fast (no need to run a daemon because CLIs take too long to start up), there's no imperative DSL for its build system, there's a builtin/standard test runner, there's a standard formatter, documentation is simple and comes out of the box (no special javadoc syntax, just comments above functions; no need to write a CI job to build and publish docs nor a webserver to host them), profiling and benchmarking are builtin.
That said, the thing I like about Java is that its JIT compiler and bytecode system allow for efficient metaprogramming; Go's runtime package works, but it's not fast at all. These use cases are few and far between, but it would be nice if Go had a good web framework story (e.g. rails, django, spring, etc).
> Maybe conforming to an interface without explicitly declaring it (like you declare an instance of a typeclass in Haskell) may look like a controversial decision. I still think it does more good by removing boilerplate than bad by accepting an object as conforming to an interface in a rare case when you did not mean it.
Yeah, I hear this objection pretty frequently, but I've literally never heard of a single instance of this resulting in a real production bug. If it's a rare case, it's vanishingly rare, while it's no exaggeration to say that virtually every project benefits from the advantages.
Yes, I personally find inheritance an anti-pattern, and with immutable objects you don't need both getters or setters.
Unfortunately, inheritance is deeply ingrained into many libraries and standard classes, though it has been replaced by interfaces in most standard APIs.
Yes, the boilerplate culture is a problem, not of the language but of the ecosystem :-\
You can stay reasonably away from it in many cases with modern libraries, though.
If you restrict yourself fully to the standard library it's still not that largly exaggerated, even with Java 8.
> The compiler has a well-defined custom processing interface (@annotations) which allows for powerful boilerplate-reducing things, from Lombok to Spring to JUnit.
If you add Lombok + sth. like guava or apache-commons and in general choose modern libraries/frameworks it's really OK now. The only downside of Lombok is that it really maxes out what compiler(s) allow you to do which is not always without issues and it requires an IDE plugin to be installed.
If you're writing a lot of boilerplate in Go, you're either doing it wrong or chose it for something wildly out of its wheelhouse (i.e., numeric computing).
If you're writing a lot of boilerplate in Java, you're either doing it wrong, chose it for something wildly out of its wheelhouse, or you choose frameworks or libraries that have either forced or afforded high boiler-plate code.
The latter is probably the major difference that is what people are feeling. On paper Go and Java are virtually the same language, but just a few differences in the spec and a bit of focus by the developer community means that Go just generally has less annotations, external XML files for this, weird build systems, and a whole bunch of other frippery.
Although I do think that it's easy to underestimate how much cleaner it turns out to be to do interfaces the way Go does rather than Java. It seems like such a small change, but it has a big effect on how much OO boilerplate you need. You don't have to guess what interfaces someone may someday need and write them all out in advance.
And, I mean, in both languages, let's not underestimate the amount of "doing it wrong". There's a lot of us whose exposure to Java is being on the receiving end of a pile of Java code that is not written with maximal skill and precision. That's not really Java's fault. (To the extent it is, it is not uniquely so; I can tell that story about a lot of languages.)
> If you're writing a lot of boilerplate in Go, you're either doing it wrong or chose it for something wildly out of its wheelhouse
Like shoehorning a set use case into a map? Or trying to get the list of keys or values in a map without writing out 5 lines of code?
I find golang code to be much more verbose than Java. You find code littered with one off functions that amount to nothing more than map/filter/etc. operations which are 1 liners in Java.
I find my code occasionally has those things, and that it is a minor inconvenience, not that it is littered with it. If it is littered with it, you are probably in the "doing it wrong" category. Do not try to force Go to do "functional programming". Or you're way out of scope. (I have lost count of the number of people I've counseled to not use Go if their first interest is numeric programming, for instance.)
(I am underwhelmed by the crowd that insists map/reduce/filter is so important anyhow. I've used a substantial amount of Haskell. In Haskell, map/reduce/filter is the beginning of the functional style, not the completion of it. It goes beyond just "an alternate way to write for loops", which is most of what I see non-Haskell usage doing with it. In Go... just write the for loops. It's the same thing, just spelled differently. In Haskell, it's not the same thing.)
> If it is littered with it, you are probably in the "doing it wrong" category.
How else do you suggest transforming slices from one type to another, removing entries in slices based on certain conditions, etc. without looping? This is one of the most common operations in any basic application.
TL;DR - "Here is a lot of boilerplate code that doesn't seem necessary for what you are doing in this case..."
...
"Hey look another tool that can generate a lot of boilerplate code you then have to deal with and makes the easy stuff easier and the hard stuff almost impossible!"
When I used to use Java, the thing that I found remarkable was how much stuff you had to write in other languages. You had Spring, Ant, Hibernate, Ibatis, text-based "properties" files etc. It seemed that Java programmers were willing to go to great lengths to avoid writing anything in Java. But if Java was so unsuitable for such a wide range of tasks, what made us think it was a good general purpose language?
In contrast, when I started using Python, one of the things I liked was how suitable it was for all the things that Java developers typically used other languages for. Your typically Python project back then contained very little that wasn't Python, because there weren't many things that it wasn't useful for.
Things may have changed with Java in the 9-10 years since then but I think that was certainly one of the things that led to Python becoming more popular.
Spot on. Thankfully it is becoming more common to Just Write Java. Java without the frameworks and other bloated crap is actually decent. The language itself has a lot of warts, but the Java ecosystem is one of the best (Python too).
Not in maintainability. It is pretty hard to follow Scala when people use the flexibility too much. You have so many options that everyone develops their own flavor of overrides/overloads and style. This makes transferring skills between jobs or even just projects much harder as the same looking code line can mean several things.
You don't actually need to do any of that. If you're writing Ant, I assume this was at least 10 years ago. Modern Spring Boot is all annotations and using an ORM is out of fashion. Maven can be a little complicated, but it's easy to work off an archetype and it's far more sophisticated than npm. Python doesn't have this problem, but it also just doesn't have these features as an option. You can certainly build a Java project with javac but we generally don't because Maven is better.
This is not really true any longer. Spring, thankfully, seems to be going away. Java itself is now far more feature-rich in terms of lambdas, anonymous classes, stream processing etc.
I came back to java about 4 years ago after a long time away and have found it quite pleasant, and quite different to how you describe it.
I think I skipped the 'enterprise' years, thankfully!
Spring is by far the easiest way to write modern web applications with REST APIs, a frontend, database layer, messaging integration, whatever. It basically does all the boilerplate for you and leaves it up to you to just write your business logic. I don't know how you can miss that.
>But if Java was so unsuitable for such a wide range of tasks, what made us think it was a good general purpose language?
Other competing languages you would use today didn't exist, and C++98 was a pain in the ass. The selling point back then was Java was C++98 with all of the difficult parts stripped out, and with good tooling and IDE support. Back then people flocked to Java like some sort of oasis. Today, even modern C++ is better in most ways.
Java seems to rely a lot on IDE tools to autocomplete, autorefactor, generally shovel around all this boilerplate.
It always seemed weird to me. Like if my house was really cluttered but instead of building shelves or closets into the house itself, I buy an elaborate conveyor belt and excavator system to rearrange all my things all the time so I can walk through all the mess.
I am sure many people feel the same way about Python and Javascript. What you described seems to be a result of building larger systems in which multiple people are contributing code.
I confirm this. I rename classes and move them around, and without a refactoring tool that renames files and changes imports automatically, life would be very hard. Before refactoring tools got to this level, I was running pytest to see what import statements got affected during my refactors.
Uh. Question. Why are you executing tests to get the compiler to tell you what was effected instead of doing a top level recursive grep to identify any classes or configs with an explicit symbolic dependency?
I mean, you get the same result, sure, but I find I can hardly ever rely on test coverage alone as an accurate safety net to clearly indicate whether I've reached an actual stopping point for a full refactor.
This is one of the ways in which Java excels, in that that loop is fully accommodated for in most major IDE's.
You can still get caught by surprise at runtime, but I've found that for a strongly typed language like Java, the syntax, and necessity of boilerplate makes most bugs obvious until you start getting overly dependent on frameworks you don't understand.
I am not executing tests to perform import error detection. That was before refactoring tools got better. I was executing tests to check for import errors because it doesn't rely on full coverage on every method. It relies on all modules being imported by tested modules. That is easy to do even if you are too lazy to test every module. Just put an empty test function in all of them and pytest will detect them recursively. Given all that, I would avoid executing manual grep commands. However, you have a valid point for configuration files. Especially log configs need to be checked.
The criticism doesn't really apply to Python and JavaScript though. Not to say that Python and JavaScript don't have their own issues, but dependence on an IDE tightly coupled to the language semantics is not one of them.
I can't speak for Python but the JavaScript community has been investing heavily in TypeScript, and the desire for a more integrated IDE experience is a huge driver for that growth.
Definitely true, though I think the problem is less severe with regard to TS because open source language-servers afford TypeScript IDE features universal availability which is not the case in JVM land. Also, consider the trend towards Kotlin in the Java world where a part of Jetbrain's business model relies on the fact that they created and maintain the language while also selling the IDE that makes the language practical to use (Kotlin being an amazing language notwithstanding).
I don't know about JS, but my python work is done in a repl like ipython in conjunction with a text editor. It gets me half of what I wanted from an IDE.
This kind of automation depends on a language being easy to parse, and Java achieves this better than C/C++ or most scripting languages. The LSP (language server protocol) is a language-independent standard that can be used to enable this more generally, by connecting parsers/compilers/etc. with editors/IDE's.
This is just one example of a general tension between compiling to produce an asset vs compiling as a service to provide data for tooling. The environments and things you optimize for are different, so you get differences like this. Compilers like what C# and F# use unify these into the same codebase (no presentation compiler) but the tradeoffs still exist.
I agree that Java is almost unwritable without an IDE, but that strictness has led to Java having better IDEs than most languages. For large code bases, most languages get unmaintainable without an IDE, putting Java at an advantage. I'd still use Python for something simple that won't grow past a few thousand lines, though.
I think part of what's happening right now is that the IDE for most languages other than Java seem to be consolidating on VS Code as the lowest common denominator. This makes sense given how many projects these days are a hodgepodge of 3 or 4 different languages, but the fact that working with the JVM more or less requires a dedicated IDE makes Java a lot less attractive to most teams.
(Yes, I know you can do Java in VS Code but I wouldn't want to manage a large Java codebase with it.)
while I agree VS Code is starting to get a lot of traction, it's far from the only solution for polyglot development (IntelliJ...) and it certainly has the facilities to support large java codebases.
This. Far from being a fanboy of Java (and I write JS for a living), but you can generally quickly understand what's going on with the boring code, and can refactor mercilessly.
Whereas in huge JS codebases relying on framework magic you can really have hard time to understand what's going on without plugging a debugger (and even then it's still not easy). Trying to refactor legacy code that relies on `this` and prototypes is a nightmare.
I sometimes wonder if JS programmers know that with C#/VS you can just change the name of a class, and without doing anything else (not even replacing), all involved names just change, and everything works 100%, without writing any test.
And the same goes for moving a method to another class, or renaming member variables, modules, files... everything just works and is instantaneous.
To me, not being able to aggresively refactor withour worrying something will break somewhere is like backwards and old school (in the bad way)
When a project evolves, a class, a struct... grows and suddenly a name, perfectly fine at the beggining, does not make sense any longer. With C#/VS you just change and 2 seconds later you're still working.
Even reflection is going to vary based on your IDE. If you use ReSharper or Rider, a safe rename refactoring will locate and optionally update string references as well. I don't know if VS itself has adopted this feature.
IDEA does the same thing. It's amazingly useful, even if you don't use reflection -- variable names end up in comments and documentation as well, all of which it will offer to change.
I’ve never touched a “real life” code base in a static language (including C#) where this wouldn’t be an issue. C# is not a sufficiently high level language that I can avoid duplication without the use of reflection.
People try to avoid reflection like the plague, most use cases don't depend on a hard-coded method/class name, and the ones that do can sometimes be detected by tools. It's pretty rare to run into this, and the one of two times I did something horrible like this, I wrote a test for it and made if fail loudly because what I was doing was brittle (by Java standards) and unexpected.
Some might be surprised to know that in modern PHP this is also the case. The whole point of all the typing and structure is so you and the IDE can sanely reason about and understand the structure of the code.
If I get code and all of a sudden not even my IDE knows what's going on, it clues me in that there are layers of indirection in the codebase I'm going to have to waste time reading into, like magic methods or loose typing.
Is it really a memory hog? I'm using IDEA, PHPStorm's more fully featured Java IDE (That also loads PHP code!) ; I have 6 projects open right now, including a PHP project, some java projects, some ruby, groovy, etc; I have 20 files open, numerous plugins enabled, and all sorts of connections to databases and tests; it's been running for days it's using 500MB.
Both PHPStorm and IDEA are written in Java (they share a common code base). Don't confuse [Java eagerly allocating memory from the operating system but then not using it for anything (yet)] with java using a lot of memory. For speed, java allocates memory it anticipates using in the future. This lets it manage it's own memory without involving the operating system and thus context switches. If you start using a lot of other applications and available system memory starts running low; java will detect this and relinquish the pre-allocated memory it's not using.
I sometimes wonder if IDE users realize you can do 99.9% of the same things with simple tools. Personally I don't want my editor magically updating the codebase because there will be edge cases and I don't want magic happening in the background when those edge cases arise. In practice I'm sure the class name change has likely not given you issues in a long time or maybe ever, but that doesn't mean edge cases don't exist and when they happen to someone it can be at the worst times and add significant troubleshooting to an unrelated problem.
edit: further learning those simple tools has a more generic application to other problems while the IDE magic will likely go until there's an issue to be understood and you're depriving yourself of learning generally applicable tooling to trade a slight inconvenience for magic that has the potential to cause great inconvenience and once that magic from the IDE bites you and you learn how it works - you've learned something with no general application to other problems.
edit2: since I can't reply in-line
jcelerier - I _could_ do that with CLI tooling and depending on the language it would be trivial. However, I would _never_ do that because changing the method/function name to suit my context would be very unlikely to avoid making other code less readable. That's a huge anti-pattern that your IDE is making easy.
Further, consider the implications for language design when making language design decisions that require a specific type of IDE magic to be considered a reasonable language design decision. I have a hard time believing relying on the IDE lock-in is good for the community of that language.
philwelch - I genuinely don't know the answer, but what happens when you change a class name -> forget you do it before saving the file -> go to another file and try to change the class name there but have a typo -> go back to the original file and save the change?
I would bet the typo'd change goes unchanged and now you're left scratching your head since that magic has always worked before.
With a statically typed language and an IDE this isn’t a real issue. There’s no “edge case” when you rename a class or method symbol in a Java project because the IDE deterministically knows what are the invocations of the same symbol.
To quote myself, "fix a single file manually, instead of a dozen or a hundred or a thousand".
And what happens when you need to rename, say, a method name that's also used as a variable name in many places? xWhat happens if multiple classes define the same method name, but you only want to rename that method for a single class?
Yes, you can probably do it in sed. If you're used to it enough, you can probably do it pretty quick, with sufficient regexps that you only have to debug a few times.
Or you can right click or hit CMD-. or hit F2 or whatever with your cursor on a method name, type in the new name, wait 2 seconds, and you're done.
So your argument is that something might, in some extremely rare cases, go wrong, and that is why you'd rather do it the manual way? That's basically like a carpenter refusing to use power tools, because they might, theoretically, explode.
Unless you're using something like .Net remoting (or the Java equivalent), and now suddenly everything breaks.
Or you have a larger codebase split up into multiple .Net solutions and DLLs where code is referenced through the generated DLLs.
The refactor looks great, until it starts breaking things for the many consumers of your code.
In fact, the latter is a situation where simple text tools will make life a lot safer and easier than whatever the IDE does (which pretty much will be limited to the bounds of the .sln file, whereas a simple text tool operating over files in the entire codebase will alert you to the fact that this is being used elsewhere).
My argument isn't to claim that simple text tools are better at refactoring. They aren't. However, I've run across several situations where we've had issues in a massive codebase because developers believed that refactoring was as easy as hitting the Refactor button in Visual Studio, and everything will work magically.
The belief in programming 'magic' (whether through IDEs or frameworks that abstract a ton) without understanding what those tools actually do is what I'm worried about. Use IDEs to do what you need to by all means, but it's much better when you actually understand what it's doing before doing something with it.
> Or you have a larger codebase split up into multiple .Net solutions and DLLs where code is referenced through the generated DLLs.
Sure--and to be fair, I've never worked in .Net much. But if you're changing the package interface, you should arguably do so in a backwards-compatible fashion anyway, depending on how your stuff actually gets built and deployed. If everything's contained in the same repo and you can't refactor across multiple components in the same repo, that sounds like a problem.
> The belief in programming 'magic' (whether through IDEs or frameworks that abstract a ton) without understanding what those tools actually do is what I'm worried about. Use IDEs to do what you need to by all means, but it's much better when you actually understand what it's doing before doing something with it.
Except when you use reflection. Or code generation tooling that tends to crop up around DI frameworks.
Yes, in general, the ability of IDE to make these kind of large-scale changes quickly, reliably and mostly bug-free is nice. But I rarely miss it in languages where it's impossible.
It may be that one of the ways your language shapes your code is the way you structure it. For instance, in my Lisp codebases, I can hardly think of any place where Java-style automated refactoring could be useful; in the current largish codebase I work on, it rarely is the case that constructs introduced in one file are used directly in more than one or two other files - which makes refactoring entirely doable with a dumb text editor, and Java-like automated refactoring wouldn't save all that much time over doing a regex search and manually visiting every line listed in results.
> I sometimes wonder if IDE users realize you can do 99.9% of the same things with simple tools
I'd really like you to show me how you can refactor e.g. a method called "write" in a 500+kloc codebase in less than 5 seconds with "simple tools". With any C++ ide from the last decade it's basically a keyboard shortcut + typing the new name + fixing the few remaining compilation errors that may have cropped up in generic code.
With sed & al ? good luck, see you in a few weeks.
> I'd really like you to show me how you can refactor e.g. a method called "write"
I'm not taking sides on the issue discussed here, but regarding this quote... This is one of the biggest reasons I don't do OOP. Using plain functions with slightly longer but globally unique names so many issues go away.
And the biggest issue is that I can't read a code base if so many function names are not unique and possibly not conveying enough information for local understanding. That style always requires the reader to carry so much context in their head, to be able to resolve the "polymorphism" to concrete implementations. (Yes, in a proper IDE we can jump to the implementation interactively with a few keypresses, but it's still much harder to read).
Oh, don't get me wrong, I'm not against abstract interfaces where they make sense. What I'm against is using underqualified names for stuff that has a statically resolveable implementation.
In other words, don't make it look complicated before it actually is.
Even on that case, with good modularity on C code (TU == module), there will be plenty of hidden symbols and some indirection (function pointer) going on, and not a pile of global symbols.
Provided that both approaches do the same, which is more clear? It is the second one, because it clearly suggests that there is no polymorphic switching based on foo/bar.
Wading through non-trivial projects, pervasiveness of the first approach makes me nervous, because I'm told at every occasion, "don't even try to understand what's going on in the system globally. We .log(), shouldn't that be enough for you to know?".
While the second approach is calming to me, "You see, we have this simple logging system, where everything ends up being written to. It's not that complex after all, and if you want to know what's going on you can just jump directly to the log_event()" function.
Yes, by asserting that foo and bar are of the same type, and asserting that the .log() method is not virtual, I can reach the same conclusion. But that's the point, it requires work that you can't practically do if each line of code contains a method call. There is no mincing words, you're using the wrong syntax...
Or maybe log_event is a macro whose behavior depends on the environment. And I have used some crazylogging ones back in the 2000's when coding C, that would even dump the current state of the process alongside its parameters.
Or then again, log_event() plugs into a configurable logging system and one cannot predict its behavior just from the call site, even when using the same types.
Finally if we move away from C semantics, maybe log_event is overloaded or is doing multiple dispatch, thus the two calls, depending on the argument types, are not going to land on the same implementation.
> First foo or bar can be function pointers. Or maybe log_event is a macro whose behavior depends on the environment.
Then different uses of the same identifier still resolve to the same implementation, and make the IDE jump to the same location, etc.
(I acknowledge that everything can be something else. And in theory you can redefine macros to have uses of the same identifier resolve to different implementations. Etc, etc. I'm sure you know about the IOCCC. But here we talk about good programming practice. It's a good practice not to suggest something complicated while the reality is simple. There's no argument to win here.)
> Finally if we move away from C semantics, maybe log_event is overloaded or is doing multiple dispatch, thus the two calls, depending on the argument types, are not going to land on the same implementation.
Which is why I do not use C++, or at least avoid most of its features. For me, it's all about clarity and reducing number of moving parts. It's not a contest in making complicated things that pretend to be simple. And it's not a contest in making simple things that look complicated. And it's not a contest in reducing the number of characters in individual identifiers at all costs.
> It is the second one, because it clearly suggests that there is no polymorphic switching based on foo/bar.
so, question : suppose you have your log_event, and now your bosses comes and tells you that the client needs to have two additional different logging mechanisms - say, to journald and to some websocket log server. The system can log to either of your three logging mechanisms at any point through changing a configuration somewhere in a GUI but you also need some core classes to always be logged through journald no matter the state of the rest of the system.
I might change log_event() to log to a facility depending on the configuration, and make another function log_core_event() that has the special logging implementation, and change the core modules to call log_core_event() instead. (gee, it's a simple text replace!). This way you can actually see what happens without any indirection.
but now you also have to change every log_event(x) to log_event(configuration, x), and pass the log configuration as an argument in literally every function of your code (you would not want to use a global prone to thread initialization issues if you have multiple threads doing logging on startup, right ? :-) )
Also you've made your logging library project-specific by making a log_core_event function. Thanks but I'll keep using spdlog in my projects, which do not need newcomers to learn about your log_core_event, what is a core module and what is not, just to be able to write the three functions they are called for in that project !
That's kinda moving goalposts since the configuration seemed to be a given. In any case, I don't see a problem with global variables. Otherwise, the threading of the configuration (and other context) I assume to have existed before that little change.
That all has nothing to do with virtual methods.
> thread initialization if you have multiple threads doing logging on startup, right ? :-) )
There is no technical difference between "global" and "local" variables. It's merely a syntactical difference.
If you run into concurrency problems with global variables, you'll run into problems just as well with objects. Unless you make multiple objects. In which case, you could also have done thread-local storage, no?
In the end, it's much clearer to me to use global variables (TLS or not) since this way I can actually see the data relations. If there is just one instance of a thing, why would I pass it around as objects? It's not going to help understandability.
> Also you've made your logging library project-specific by making a log_core_event function.
That doesn't make sense. Can't I just add another function without breaking everything? Note that I probably wouldn't even add this function to "the logging library" but to the core modules...
> what is a core module and what is not
If you prefer having everything switched behind your back, resulting in unreadable and unintelligible source code... you do you.
More technical notes...
If you don't like the presence of two or more functions for logging, then why not have an explicit context argument? You call log_event(MODULE_FOO, foo, xyz) instead, and the log_event function implements all the plumbing logic in a central place, easily understood (Most logging approaches do that, I think. They have "facility" and "severity" arguments). This approach seems much more preferable to me compared to an implementation that you can't see locally, and that can't be influenced locally. What would you do if we go for the OOP approach with a virtual method, and now call into a different module that would need a different logger?
If having a dynamically switched implementation was really what we want to do, there is no problem with doing that. My post was just about using method syntax for static things, which I think harms readability.
> In any case, I don't see a problem with global variables.
well, that cuts short to the discussion. There are plenty of places & coding standards where those are outright forbidden.
> If you prefer having everything switched behind your back, resulting in unreadable and unintelligible source code... you do you.
we have very different notions of readable. For me the less stuff I have to read and the more readable it is - I just want my code to be the pure domain problem and could not give a rat's ass about how logging or networking or whatever is implemented if those aren't my domain.
> Can't I just add another function without breaking everything?
that does not break the code but that breaks Joe intern's mental model and expectations of anyone who will think that log_* functions are from the same library
> then why not have an explicit context argument?
and here we are, reinventing OOP by hand in C. There is literally no difference between log_event(context, ...) and context.log_event(...) - but you get better tooling :
* completion -for the first I'd most certainly end up typing the full name if there are a lot of l-started functions while for the second I'd type c<shortcut>.l<shortcut><enter> and only see the few relevant functions in the autocompletion list) etc... in the second.
* namespacing - if I want to see all the logging functions available I can just click on "context" and my IDE will happily show me all the available methods in a panel and nothing else. With C functions, well, I have to scroll in a list of 150000 functions. And also wonder (& have to explain) why log_event() is in <util/logger.h> and log_event_core is in <core/core_logger.h>.
* split implementations - if at some point you want to port your software to, say, an arduino without network capabilities (talking literally forom past experience here), the monolithic log_event function now becomes an #ifdef HAS_NETWORK / #ifdef HAS_JOURNALD / ... mumbo-jumbo. While with multiple logger subtypes it's just a matter of changing your build system to not compile the unavailable implementations, without even needing to change the code (or change a list of types in a header if you use static polymorphism instead).
> What would you do if we go for the OOP approach with a virtual method, and now call into a different module that would need a different logger?
dependency injection ? modules can request either for a default logger which will be sourced from the gui configuration, or whatever specific logger they actually need due to functional requirements.
Besides, I still don't see the readability problem with virtuals. Again in my IDE if I want to see "what happens" I press F2 on context.log_event and it displays me the list of all the possible implementations if it isn't able to determine that one is used statically at that point. And I much prefer to read three five-lines email_logger::log, journald_logger::log, websocket_logger::log functions than a pile of if/else's.
> HAS_NETWORK / #ifdef HAS_JOURNALD / ... mumbo-jumbo. While
But when I had a glance at the project you linked on github, that was basically the first thing I found there...
A suggestion that I have there would be to use the linker instead (make different source files based on implemntation / existance of an implementation). This way you can eliminate most of the #ifdefs.
Regarding dependency injection, I don't see a practical difference to just using global variables / global functions, apart from the fact that by convention DI gives you back objects with virtual implementations, which I don't like.
I won't go into the rest of the things you said because we pretty much discussed it all.
Java interface methods do have globally unique* names when fully qualified, but nobody types the full thing out because namespacing is good. It is neither expected nor even desirable for readers or authors to keep resolutions of interface implementations in their heads - that would defeat the purpose of interfaces.
There is a difference between interfaces and abstract interfaces. It is very helpful to know if all calls to the same function/method resolve to the same implementation. Method syntax always suggests that their could be arbitrarily many implementations.
> Using plain functions with slightly longer but globally unique names so many issues go away.
That makes me assume two things: you work alone and your projects don't exceed 5.000 (fivethousand) lines of code. Without some namespacing and an expected method length less than 100, more likely 10 lines than 90 or more, you end up with roughly 160 names. An average human knows 150 people, maybe by their name. And while you have friends or at least parents and grandparents occupying some of the »address sace« few of your methods will not be »at hand« for your brain.
And in case you already prefix your methods with something like io_write(...), db_query(...) and you hand in context-specific references like io_write with a sort of file handle and db_query with a sort of database connection I transform all this 1:1 in an OO-style language and back.
Given the truly excellent quality of refactoring toolsets like Resharper for C# I'd wager that you're more likely to miss an edge case with your manual approach than with the tools. The tools can reliably differentiate between methods of the same name in different classes at the call sites (or between different classes with the same name in different namespaces). They will search comments and strings for occurrences of the name in question and prompt you with a nicely formatted overview dialog box which ones to adjust.
The tools are at a point where I'm usually breaking my code when I'm doing manual refactoring, but never when I let the tools do the job.
Obviously you don't have much experience with IDEs and statically typed languages, which is a shame because they make our life so much easier. There are no edge cases. And even if there were the IDE can show you the refactoring and you can decide on a case-by-case basis.
> what happens when you change a class name -> forget you do it before saving the file -> go to another file and try to change the class name there but have a typo -> go back to the original file and save the change?
A good IDE does not care if you save the file or not. It is always up-to-date. Renaming a class in just one file is also impossible, since the IDE will do refactorings atomically over the whole project. But lets just assume you somehow managed to fuck it up that badly (things can always go wrong): You will still realize your error almost instantly because a statically typed language does not defer such checks to runtime. The program wouldn't even run anymore.
By far the biggest issue with automatic refactoring is accidentally renaming stuff in comments (or forgetting to do so) because I'm too lazy to look at every single place. But find&replace has the same problems.
IDEs are not magical. They understand your code because they use compilers to parse it, therefore they can also reason about it at a higher level than a simple text editor or CLI tool which treat your code as nothing but characters.
Is this really true? The hard part of renaming a class isn't usually changing the name Foo -> Bar, it's finding all the variables with type Foo named stuff like result_foo and renaming them result_bar. Can it help with that?
> I sometimes wonder if JS programmers know that with C#/VS you can just change the name of a class
How exactly does this knowledge help? I know this is possible and I do miss it. But it's extremely diffcult to support it in JS because of how the languages works. VSCode tries, but it's still limited.
So should we all switch to writing C#?
You can't avoid writing JS if you want to create a web app. There are tools to avoid it, but one has to know what's under the hood or they'll have a bad time.
TypeScript is doing great progress on this part, and it supports easy refactors, but it's still lacking in some parts, for example the type inference is still poor when it comes to complex cases and the type declarations depend on will of maintainers.
Uh right, sure. But then there's the Spring lifecycle to take into consideration and it may happen that your annotations may or may not work. It depends (of course it's not totally aleatory, there's some logic behind it, usually obvious after several hours debugging ;)
Another honorable mention is Spring Boot @Conditional for which adding a minor little diversion from defaults will turn a functioning happy project in a dysfunctional nightmare.
I agree with you completely on that. I'll clarify that, to me, there's a difference between straight-forward and obvious. And Spring is terrible at being obvious. Some annotation on a class 5 transitive dependencies deep can affect your code. It's definitely really deep into "spooky action at a distance", and that's always a nightmare for debugging. But once you see all the pieces, it's (usually) relatively straight-forward to describe what is happening.
The worst experience I had refactoring JS was the need to change functions that were used elsewhere from plain to async. I really hoped to have had a strong IDE that could assure me the affected code was as intended and did not have the flow completely break because a promise was returned where none was expected before. I distinctively thought "I wish this was Java". Some of the affected code was already in Typescript and that did help a bit, but I was still not too confident because I still didn't trust the IDE completely (Typescript support was comparatively new).
I have even come to question if we should make all our javascript apis async from the start to avoid this scenario though it seems a rather extreme guideline. We didn't really do that, but I still dread this type of refactoring.
Boilerplate is the enemy. IDEs can easily write piles of it, but offer very little help reading any of it, and editing it is a trap. Anything that can be generated behind the scenes, compiled, and thrown away should be.
Indeed, IDEs are still oriented towards writing code, not reading it. I'm yet to find a good tool that would help with exploring and familiarizing yourself with a large codebase. Soucetrail comes the closest, but it has like 5% of the features I'd like to see in it.
(My dream code exploring tool would be a cross between an IDE and a tablet&pen friendly PDF reader. I should be able to navigate through files using all the semantics-aware IDE goodies, the tool should also understand version control, but it should also be able to generate and manipulate structural graphs (as in, boxes and arrows), as well as let me highlight, draw and freehand over the code.)
I disagree. The cognitive overhead of sifting through so much boilerplate hides a lot of bad code. The identifiers and logic seem reasonable in an island surrounded by getters, setters, builders, injection annotations, overloads, and trivial constructors. But in context of the actual code invoking it, also surrounded in an ocean of boilerplate, the code does the wrong thing.
The overhead has significant cost for trivial gains. The costs result in overly complicated systems to solve simple problems. Most of the complexity in these systems is for dealing with complexity brought in by the ecosystem, not the problem domain.
I disagree with the idea that you can't maintain a robust system/codebase in python, mainly because nearly our entire distributed computing stack is written in python.
Granted it isn't 10s of thousands of lines, but it isn't small either. It does a lot of things ranging from webapis to data pipelining with ingestion and egestion of large datasets. One batch job can result in 10s of GBs in output files. It has to be performant too, 10s of millions of "operations" in one batch job is common.
Admittedly the process/threading model can be painful to work around at times, but totally doable, and is honestly somewhat friendly to distributed systems to an extent because it somewhat forces you into an actor model paradigm.
> Granted it isn't 10s of thousands of lines, but it isn't small either.
I'm not trying to belittle what you do, but if your good base is in the 4 figures in size then it's not large (in fact yes, it is small). When people talk about large code bases they are generally talking 100-1000x larger than yours, or more.
It sits on top of our own IP database which is 6 figures in lines and is written in C++. Java has fancy bells and whistles when it comes to IDE's, but that's all they are.
I just think it's foolish to think Java is the only language that can handle large codebases. I mean just look at the linux kernel.
EDIT: Removed comment about processing capabilities because it's irrelevant to the discussion. I left my original comments about lines and the IDE though.
I don’t think anyone is saying Java is the only one they’re saying because it has good IDE support it is better than many other languages, e.g. python. I tend to agree. Once a python program gets over 20k lines (I touched a program like that before) it’s an absolute nightmare to maintain and without type annotations can be near impossible to figure out what is going on.
This is a little off topic but is the performance mainly coming from the computation being distributed? I wrote this text parser in python a few years ago, rewrote the same thing in Java and got like a 10x speedup. It’s possible I’m a bad python programmer.
Strictness of syntax and language. It means that ctrl+space gives you exactly right methods into popup, that if you rename the class all places that use that class and no other will be changed, you can rename method and trust that all correct calls will be renamed too and no other.
You can find all the places that call a method and know they are really all. That sort of basic thing.
E.g. not for writing, but for analyzing what unknown code does and for refactoring that code. Comparing to javascript, in js you have to be much more careful when refactoring and changing things.
"Code should be primarily written for humans to read. Only in rare cases should the code be primarily written for the machine in mind"
In other words, don't doo premature optimization. Write the code so that it's readable, except on the chance occurrence where you need to eke out more performance.
> “Code should be written for humans to read and only incidentally for machines to run”
So this is a cute statement but isn't it actually false? None of us would be sitting around code reviewing each other's code for fun if there wasn't a mountain of money / value / knowledge to be extracted from a computer running the code at the end of the day.
Actually this is easily falsifiable. As soon as quantum computing becomes possible the first languages will probably be quite horrible, with painful tooling and torturous debugging and difficult to read control flow compared to current modern languages. Nonetheless, a huge army of programmers will be trying to write code for these quantum computers, for the sake of the computer, and not for any human to read.
Another "Actually this is false in the corner case I'm going to call out".
Of course its not a universal rule, its not a law of physics. Doesn't make it less useful as a guiding principle (similarly, design patterns in software architecture).
The core argument is that we're besides the point in conventional (2019) computing where we need to worry too much about optimizing code to run as efficiently as possible. The complexity and bottlenecks to delivering features is now in writing software that is meant to be worked on and maintained by a large number of humans. And for that purpose, it is better to optimize for clarity.
But it's not really a corner case though. For a lot of professional applications it's perfectly fine to make a tradeoff of complexity or readability in order to get performance. And that tradeoff is done for business reasons - because sometimes the computer is more important than the programmer.
> For large code bases, most languages get unmaintainable without an IDE
I disagree. Most people working on the Linux kernel (including Linus) or the major GNU projects (including RMS) are using vim or emacs with maybe some supplemental find/grep/sed/awk.
This is my development stack, vim is my text editor with some minor modern features using plugins and tmux lets me tile into any other command line tool that is best for the job. I prefer my tools to be Turing complete, and there is nothing an "IDE" can do that I cannot (most likely far faster too).
I tried writing Kotlin in Vim, I really did. Java was somewhat manageable, but Kotlin was absolutely impossible. The biggest thing those bloated Java-based IDEs offer is deep introspection into the language. You use a reference, the IDE can auto-import the package for you. When learning a new language, knowing what package to import is invaluable. Even the Kotlin in Action book from Manning Publications flat out says, "the IDE will automatically import that for you". It was quite a turn-off but.... it's a nice language, so when I write it, I have to use IntelliJ.
For large code bases, most languages get unmaintainable without an IDE, putting Java at an advantage
That’s an assumption which advantages Java by default. Other languages may be able avoid having a large code base entirely. I recall reading some stories of people rewriting giant Java code bases into very small Clojure programs that were more flexible, maintainable, and scalable.
Yes, the secret is to write software that is composed of several modules that operate independently. Java's culture is to write a big ball of classes that are combined to everything you need. In other languages you can create different programs that compose to provide the desired functionality. This was the original "aha" moment of C and UNIX designers. Even though C is a brittle language, it allows the decomposition of systems into small programs thanks to files and filters.
There's nothing stopping you from writing uncoupled classes that work like small modules and maintaining them in one repository. Tightly coupled classes are a code smell even in Java, I wouldn't say it's the languages fault, more that the language is used more often in enterprise software and so all the frameworks/libraries/tooling is geared toward monoliths.
From a sample size of one - My Python and Go code tend to be fewer lines per file compared to Java, for projects that are similar in complexity. This is not about the terseness/dense nature of the language, but the sheer simplicity in other languages to decouple file structure from class structure (at least within a given directory)
> Yes, the secret is to write software that is composed of several modules that operate independently.
This isn't a secret, and in certain corners not even considered generally true -- backlash against microkernels and microservice architectures have intensified in the last half-decade, to give two examples.
90% of code bases that attempt modulability get it wrong. The idea is to be able to lift out functionality to be reused elsewhere. But what 90% of code bases do wrong is that they move code into modules, but you can not lift out those modes as they depend on all other modules. It might help if you use the words library instead of module. And public API instead of micro service.
An eCommerce framework is never going to have a small footprint, for one example. That's why Java or other strongly typed/structured languages get used in enterprise software.
Java is used in enterprise software primarily because it's what gets used in enterprise software. Language popularity is a positive feedback loop, and once a language establishes itself in a sector to the point universities start to teach it with that sector in mind, it's hard to unseat it. Businesses, especially large ones, don't choose tech based on technical merits; they tend to focus on aspects like "can we get support with SLA for parts of our tech stack", and "how easy to find/cheap the developers specialized in that stack are", and "let's do what the IBM/Oracle salesman says we should, that must be the right thing to do".
I don't see a reason an eCommerce framework couldn't have a small footprint. The lower bound is the amount of actual, necessary complexity in the problem space. But that's usually orders of magnitude less than the size of the codebase, because of all the boilerplate and greenspunning that happens in Java, Enterprise Java in particular.
(Note that in commercial work, I moved from Java to Common Lisp, so I have a different view than typical about just how much boilerplate and repetition can be simplified and removed when your language lets you do it.)
You're not going to cut your lines of code by an order of magnitude by switching from Java or C# to JavaScript or Python, but I would say you do get an order of magnitude increase in ease of managing large code bases with the popular statically typed languages.
I'd rather have 1M lines of Java than 100k lines of JavaScript, and if essential complexity took 1M lines of Java to flesh out, you aren't reaching parity with 100k lines of JS.
> You're not going to cut your lines of code by an order of magnitude by switching from Java or C# to JavaScript or Python
Why not? I can easily see reducing SLOC 10-50x by switching from Java to JavaScript.
> if essential complexity took 1M lines of Java to flesh out
I very much doubt that in a 1M SLOC Java codebase - at least in pre-Java 8 codebase - the essential complexity is any more than 1% of that. My work experience with Java suggests that the language and the surrounding ecosystem and practices promote extreme proliferation of accidental complexity.
I am sure you could reduce complexity quite quickly but then you are building a different product. The selling point of an eCommerce framework is the structure and existing feature toolkit. Like modules, events, mechanisms to safely override core functionality and all of that. I am also sure you could build a more compact eComm framework, but I think you would start to trade off structure for reduced complexity. For me though, more structure equals less need to worry about the complexity as that's been abstracted away.
I wouldn't mind seeing the other side of the fence though, I will admit I have worked in enterprise codebases for most of my career so I am definitely biased.
disclaimer: elixir fanboi here just trying my best to temper my bias for this comment.
I recently rewrote a c# project that had not been completed in elixir. Hugely, anecdotal (as all of these types of stories are) but the project went from ~5kLoC to <800LoC when rewritten in elixir. That's after the elixir app was feature complete too. Now you might believe that's a developer experience gap, but the c# dev had way more experience with c# and in my experience this other dev is much better than me generally. Within a week of learning elixir he was catching my bugs all over the place.
Neither 800LoC nor ~5kLoc qualifies as a large codebase.
In my experience sh*t starts to get real when you go beyond 30-50kLoC, at least that's when I start losing the needle in the haystack. I've done that with Erlang and it's not really fun.
In comparison, my current project in TypeScript is at this stage now and by using an IDE things are quite under control. Refactoring is easy and even major architectural changes are manageable (we did ~5 so far, depending on how you count).
Wasn't the case with Erlang. We only managed to do one such major change and it almost killed the project. It probably even actually killed the project as we were late to the party and failed afterwards.
> Other languages may be able avoid having a large code base entirely. I recall reading some stories of people rewriting giant Java code bases into very small Clojure programs that were more flexible, maintainable, and scalable.
I can rewrite any bad written code in less lines, same language same framework as initial code.
I find it really amazing when a language's weakness drives an unintended strength to evolve like this.
Ruby Example:
Ruby projects tend to always have really well maintained test suites. The language makes it easy for you to introduce non-obvious bugs or write unreadable (yet efficient) meta code.
This has lead to 1) very mature testing frameworks (meh) and 2) high adoption and usage of these frameworks (actually impressive).
Funny enough, I think adoption of IDEs is extremely low in the Ruby world. 90% of the developers I interact with in my city are on Sublime/VSC/Vim. I've seen some pretty impressive usage of Rubymine that has made me curious, but never bothered to really spend time with it myself. I'd be really curious what the teams at GitHub/Stripe/[Insert big Ruby company] typically use editor-wise.
I do most of my ruby work in rubymine. I use it not so much for the IDE tools like refactoring etc, but because I can use it to condense multiple bash sessions into a single UI. I also use pycharm and IDEA, and certainly in IDEA's case I use it primarily because it's the best-in-class Java IDE.
Haven't used tmux much, and last time I used screen was over 10 years go. Rubymine gives me my iterative test results, multiple independent launch commands, the ability to run a single rspec test, refactoring support, autocompletion, a coherent gem environment, debugging etc all in one place. Yes, I could do all this on the cli, but I'm lazy, and Rubymine means I don't need to.
Well, having tried both Eclipse and IntelliJ Ultimate, I can say that
* Eclipse has a crappy UI and can't handle Java9+ code very well, but can handle large code bases
* IntelliJ Ultimate has nice UI and assistance, but it totally fails with large code bases and still tricks you if you run code inside the IDE, especially when you want to use jigsaw modules and not classpath, as it uses by default
Unfortunately, using Java with other editors (vim, VS Codium, etc) is just difficult
I have a bit of opposing viewpoint. One of my biggest hurdles ever in understanding large codebases comes from the sheer amount of code involved per any architectural unit. IDE may let me move around a codebase quicker, but if a single functionality is smeared into 20 classes in three separate inheritance hierarchies, this will still cause me headaches until I draw the entire structure on a large sheet of paper and keep it on my desk. There is only so much thing I can keep in my head or in front of my eyes at the same time, so the denser the language (in terms of ability to abstract and DRY things), the easier I find a large codebase to work with.
I dare anyone of you who thinks like this to study as much modern idiomatic Java as I've studied Python or Javascript.
I'll give you this though: Old Spring and JSF code could be really bad, I remember updating two different XML files every time I changed something in a controller.
Edit: even back then with all that boilerplate it was really nice compared to legacy Delphi where it took three full days with an expert to find and install all required dependencies before one could even start compiling the old source.
By comparison my Java dev environment was up and running in less than half a day with Ant, and when we introduced Maven a few months later setip time for a new developer was mostly the time it took to check out code, install dev databases (yep, back in the days) and set necessary environment settings. (Protip: many people hate NetBeans but one thing it got more right than all other Java IDEs AFAIK was accepting the pom file as the truth instead of creating its own.
I am a big fan of rust, but I am starting to feel like this is a thing with it too. Not to the same extent at all, but I often find myself writing what feels like scaffolding boilerplate code.
That said, the code is usually there for a good reason and I'm extremely thankful for it in the instances where things are refactored and the compiler catches it, or some deviation from the norm comes up and the scaffold allows for doing things the way you need.
ditto for Go. Huge fan, but it does involve a lot of boilerplate.
Not that I think that's a bad thing. I prefer it to "magic" (I never got on with Rails because of this... what just happened? why did it do that? where does it say it should do that?).
And you never know when you have a class/use case that needs the boilerplate to change. Having it the same 99% of the time so that you have the option to change it for that 1% edge case is a good thing, imho.
Go seems to have much less boilerplate than Java. I’m really glad that OOP was explicitly unsupported and the language designers opted for using interfaces instead.
Interfaces are as much a part of OOP as inheritance. Go is as OOP heavy as Java. They differ mostly in how/when an "class" is bound to an interface. Java declares up front, Go does this implicitly at usage sites aka static duck-typing (which is great!).
That said, I think OOP education focused a lot on inheritance during Java's rise and that was mostly a mistake in hindsight.
This is strange, I've been programming mostly Rust for the last six months and I'm finding it very terse actually. It is much shorter than the original Javascript code it's replacing, mostly because of the error handling.
Are you using an MVC web framework, or something like that? Asking because I'm curious about other people's experience with Rust.
Boilerplate may not be the best term. It's more the presence of fitting into types and structures that certain libraries/frameworks provide where you just need to be much more explicit than in some languages. It's definitely a good thing long term though.
Writing code without dependencies/frameworks that have a required structure is a joy, however.
This is exactly why I've always had a strong aversion to Java. I like 'easy to remember' languages, with a strong preference towards Go and Python, and -somewhat- C though I don't use it often. In any of these languages, I can use an IDE with nice integrations, or if I want to write a quick one off, I can drop into vim and pound out a 50 liner and run it. This is an absolute nightmare to attempt in Java.
On the other hand consider this: suppose you’ve built retail business out of your house, then it grew well. And now all those conveyor belts come in very handy, right?
Sometimes project is big enough to have a lot of code, boilerplate or not. And then all that IDE automation suddenly becomes essential.
I’ve been to some huge Scala codebases, it was much harder to navigate or make changes there than shove around Java cruft with IDE.
I imagine you have an old keyboard somewhere where the "." key is worn smooth with no "." visible. Maybe the tab key on your current keyboard is now in similar condition.
This is a trend I see reemerging with the hyper focus on VSCode. At some point people won’t be able to work on their projects without it. That might not be a good thing... as we have seen with Java.
I like being able to consider a shell to be the lowest common denominator for a codebase. If basic shell commands, environment variables, a vanilla vim/nano editor etc... can’t be used to hack on your system then I think it’s far too coupled to tooling.
I don't think it's necessarily (just) for a love of 80s editors. I think it's more of a litmus test that your developer experience is too tightly coupled with specific tooling.
i.e. if you can't (realistically) choose your own tools, then your experience with the ecosystem is that of your experience with the tool. No tool can be a good experience for everybody.
I think you may be projecting others' arguments onto the parent.
I did not read it as "everything should support the UNIX IDE" as much as expressing a preference for it and that it's a red flag if you can't even use standard vanilla tools (e.g. grep) to do basic things in an ecosystem.
I think it's fair to say that you should be able to edit code in your editor of choice and not have a terrible time.
Which brings back to having a language designed for poor developer experience, given that tooling support cannot be part of the overall experience.
So if for language X, if I as language designer want to provide the same experience as Smalltalk, either I consider the interaction with IDE workflows, or I design command line tooling to go along the language (e.g. Go).
And the command line experience comes back again to the universe where it is more prevalent, POSIXy systems.
So I don't think I am making false assumption here, as one cannot have it both ways.
>If basic shell commands, environment variables, a vanilla vim/nano editor etc... can’t be used to hack on your system then I think it’s far too coupled to tooling.
The above does not evince a fear of modern text editors. Nor does it equate to clinging to 1980s technology. Why do you think it does?
Why are people still flying 737's? Why cling to 1970's airliners?
Just because something was created long ago does not mean it hasn't improved. Vim and Emacs are both extremely featureful and powerful editors and many people prefer to be able to pick and choose specific tools that do their jobs extremely well.
It's not about using "old" technology - it's about using a tool that excels at its targeted task, and letting you choose the best tool for ancillary tasks like searching, formatting, testing, debugging etc.
I can write code in Notepad, but why would I do that when I can see type annotations/errors on the fly?
It greatly increases my productivity, probably by a factor of least 1.5x.
You don't need an IDE in any language I'm aware of beyond exotica like language workbenches. People are using the word "need" here to mean "is really really useful".
youre not wrong and i may be biased as a long time java guy but if i had to do java without the tooling to make it less annoying , id probably try to switch languages... and this is an independent problem from the other benefits i get from JVM... namely platform (JVM) and ecosystem (open source libraries).
In some ways the Java philosophy seems like the antithesis of the Unix philosophy. Instead of a lot of small, fast, composable tools that do one thing and do it well, Java culture mostly promotes giant self-contained systems that capture everything in a sprawling OO type system.
This distinction is reflected in even small design details. For example Java programs are comparatively slow to boot up. If you're mostly used to the Unix way of repeatedly piping the STDIN/STDOUT of small utilities in an interactive command line, this is painfully inconvenient.
But if your workflow involves a monolithic server that manages every component of the business logic in a single process, then 5 second boot times aren't really a big deal. Because maybe you bounce the server once a week at most.
Yes! Things like Spring Boot are actually a reaction to the days of Java EE and the web of Java-centric technologies.
This always confused me as a newer developer. Vendors competed with each other with different JavaEE implementations (WebSphere, Glassfish etc) that all conformed to the same interface and provided everything including the kitchen sink, but what really differentiated the products were vendor-specific features that require you to go outside the interface, but if you tried to use that stuff, another senior developer would tell you that we can't marry our implementation to a specific vendor... meanwhile, literally every company you go to uniformly uses Spring and Hibernate, and both will inevitably require you to not pretend that they're dumb implementations of javax.inject and javax.persistence.
back in the day when i started java, which was its early days(90's), we didnt even have a package manager type solution for java libraries (i.e. maven) - UNIX had a massive head start on java in some respects
I agree, it’s verbose. That being said, The boilerplate often helps when reading unfamiliar code. More importantly, the IDE support is fantastic, especially on large codebases.
An example: if I want to refactor a simple thing like Rename a method called “doFoo()” I can do this trivially.
Now, this seems simple but it’s not - I only want to refactor uses of this particular method, NOT any other methods that happen to be called “doFoo()” on unrelated classes or interfaces. Moreover, I want the refactor to intelligently work on everything, including inherited classes, abstract classes and interfaces, while correctly ignoring the stuff i mentioned previously.
None of this matters much if you have a small codebase and a fee developers, but on a huge codebase with many developers, all that boilerplate lets people quickly understand new code and easily refactor that code while being sure they did not break anything.
>The boilerplate often helps when reading unfamiliar code
I don't believe this to be true at all, especially for large code bases. Often the people who support this position have not worked on 100k, 500k, 1m LOC applications I've found.
Boilerplate makes it much more difficult to read and scan large swatehs of code. I think when people say boilerplate 'helps', they often noticing a correlation between the fact that many strongly typed systems happen to have excess boilerplate. I believe it's the typing that helps, not the boilerplate.
That being said, IntelliJ's IDEA is fantastic at hiding a lot of the boilerplate in Java (like getters and setters and all those import statements at the top of the file) and modern Java is FAR FAR better about being less verbose than it was in 1.0
Java also has Lombok (https://projectlombok.org/) that hides almost all of the repetitive stuff completely.
Java is not a cluttered house, it a mega warehouse with over billion products lying around. No amount of organizing will make it simple for you top find what you want. Hence the conveyer belt instead of better organization.
A code base is not like a house. There is no "done" state. You have to rearrange and extend code bases every few months due to changing business needs.
That's a design feature. Java is the modern COBOL.
The idea is that you hire an army of marginally qualified developers through an agency that are making $14-18/hr in the US and have them churn out code.
To use your house example, it's like addressing clutter by having 5 children and making them clean.
My comment for years has been that most programming languages are for humans first, and computers second. But Java is for tools first, humans second and computers third.
The result is an excellent toolchain. But also you are completely unproductive unless you master said toolchain.
As proven by Xerox PARC, EHTZ, Apple, Borland and Microsoft tooling as well.
The best programming languages for developer productivity are those that are designed with tooling in mind, instead of grammar + semantics and then we see later how everything evolves.
my question is how do you make sense of a seemingly endless stacktrace? (really curious, it's some time since i did something in java)
Another problem i had with java: most projects are impossible to understand by looking at the source code, as they tend to do things like Spring or EJB: with delayed loading where class names are specified in some configuration files - and it is often hard to figure out how the system is initialized.
That leads to another question: Is it possible to learn about an unfamiliar java project from source code in this day and age?
i mean they used to say that perl is a write only language; but enterprise java isn't very readable either.
> Is it possible to learn about an unfamiliar java project from source code in this day and age?
Yes it is, but it helps a lot if you're familiar with the framework and the original developer used sensible conventions. I guess that's the same with any language though.
Lots of the boilerplate they create is totally unnecessary; other boilerplate can be avoided through use of common open source libraries, like AutoValue [1] for equals/hashcode/toString. No need for custom compilers, new ASTs, or a whole new language. Don't create interfaces for classes that only have one implementation, and don't create gratuitous indirection.
So tired of stupid articles like this hating on java. Java is an extremely flexible language with an insane amount of libraries, frameworks, and design patterns. Furthermore, using groovy is amazing, and can reduce your lines of code quite a bit. The author of this article just sucks at implementing java.
You just cherry picked one part of my rebuttal. Again, java is an extremely flexible language, and the verboseness is up to the user. You can implement very verbose code in plain java, or you can write extremely concise code in plain java. It’s up to the user.
Well, the punchline is they wrote a tool to implement Java for you, with either a GUI tool or a DSL.
So you still have those libraries and frameworks, and its a similar strategy to using Groovy, but presumably doesn't have the same performance penalty as it generates idiomatic Java code.
"...they've developed a way of writing maintainable code, but the language knows nothing about it, and is not expressive enough to describe it succintly."
Good quote. I used to struggle with this feeling when I got to roughly the ten year mark professionally and finally started to feel like I knew what I was doing. But, with the help of a little Stockholm Syndrome perhaps, I've learned to embrace the boilerplate as part of my workflow and use it to an advantage. I came up with a saying "write twice, debug once", from the old carpenter's saying "measure twice, cut once". The idea is that if you're writing sort of the same thing twice, the compiler and/or PR reviewers can at least check if those two things are consistent with each other, and debugging is more straightforward instead of a long slog. You may still write the same thing wrong twice, but that's much harder to do than writing it wrong once. I think this is a hidden value of automated testing as well. I could talk for hours about whether automated testing is "worth it" depending on the project/language/team/etc., but one thing most people don't consider is the value in the mere act of writing out some logic that must be consistent with other logic, and the inconsistencies you can find without even having to run the test. Essentially another "write twice, debug once" scenario.
I know that's all kind of vague, and I don't have time to get into specific examples, but this encapsulates the feeling I have now working with Java. The feeling that I'm sort of fact-checking myself, even though it's tedious. Sort of a "show your work" thing.
This may have a kernel of truth to it, but it's difficult to treat as objective when the article is essentially an advertisement for the language the author is developing.
Absolutely important to bring up. I don't care how creative you allow your developers to be while they leverage Ruby/Python/etc.; At the end of the day, they're accessing data from a DB and serving it to a client...
Looking at the youtube video of actually using Unitily, it seems way more complicated to me of having to learn how to use this UI interface. Personally I think with tools like Project Lombok, auto/value, and IDE tools, you can streamline boilerplate code fairly well.
I personally like the explicitness of the language even if it is boiler plate (with some exceptions like checked exceptions in lambdas). It makes average code very readable with only little caveats. More than one time I struggled with my own code at a later point in time because I wanted to avoid duplication at any cost.
Using StringBuffer may be premature optimization. Java compiler will replace simple String concatenation with StringBuffer automatically [1] and you do not need to polute your code with StringBuffer unless you really find that to be performance bottleneck.
ie A lot of the 'boiler plate' is stuff that allows the compiler to check for errors, and humans understand the code but at the same time allows you to vary behavior if you want.
Java also has language simplicity and consistency [1] ( few syntax short cuts ) - which helps humans understand code - the coding equivalent of 'plain english'.
Whether you see that as a good thing or not depends on whether you are writing self contained scripts or large long lived complex systems.
Just figured this article (or copy of an article originally posted by Joel on Software Engineering) fit in this discussion.
https://pastebin.com/VJUNqsU3
Java OOP is embarrassing. Design patterns are inconsequential in FP / Go / Erlang. It should not be taught in schools. Startups should stay as far as possible away from it. Java Enterprise software and programmers need to die a slow death. Ruby style OOP has its place but nothing closed-minded needs to be respected, let alone coddled.
I find it interesting how you think I need to die slowly ;-)
I have a much more constructive approach: people in this thread should spend as much time and effort learning correct Java as I've spent learning correct Python and Javascript.
In fact a lot less should do: I came to Java with a dislike (school did a very poor job lf teaching it and I already knew enough from other languages to see how hopeless what school taught me was).
I kept programming my favourite language but after seeing the immense benefit of having a working type system and a good ide I mostly left lther languages behind until modern .Net became cross platform and also I had to start doing frontend work.
> Java Enterprise software and programmers need to die a slow death.
The Java 'enterprise' era is over. Java 8 onwards is much more expressive, functional and concise. Java is no longer what it once was, nor is it the domain of the enterprise nerds any longer.
My biggest beef with idiomatic Java code is that it is so annotation-based. For example, I recently had to do a small CLI app in Java, and the command line parser library that came up in searches was picocli. Now, I mean no offence to the creators of picocli, it's pretty typical of most java code in my experience, but why does it have to do everything via annotations? Take their example for instance: https://github.com/remkop/picocli#example
Not sure why a "just plain data" approach couldn't have been taken so that instead of:
But they are in my opinion really difficult to understand, test or customize. In the example above, the reified Option<boolean[]> can be passed around, transformed, inspected, invoked in tests, etc. The annotation approach weaves together the aggregate class (the overarching config class), the specific field of the class (`verbose`) and how that field should be parsed from command line arguments.
Ok. I can try to help: In this particular case, when you test you just set the field to what you want.
In other cases where you actually want and need to test that the annotations works at runtime you use a test container (Arquillian was the big thing for integration testing JavaEE last time it was relevant to me, and since we are talking enterprise Java there's a fair chance that Arquillian will still work and maybe even still be the most popular solution.)
Annotations are frequently abused. They are useful in some cases but in other cases it would be easier to just call a function. It is more transparent and easy to understand, you don't have to figure out how the magic works when something fails.
550 comments
[ 6.3 ms ] story [ 335 ms ] threadMy beef is with Spring. Spring seems to make simple problems very complicated. Its like you have to learn a second more complicated API on top of the original. Plus when something goes wrong you get some 100 call deep stack trace. I just wish there were more pure Java applications without the Spring magic.
Also I hate that I need to literally use a program to create the boilerplate for a new spring app. And I can’t run my program without special IDE settings.
Edit: for example, Python “annotations” are generally just functions that return wrapped functions/attributes/classes/whatever. Java annotations seem significantly more “magical” than that.
I guess, Spring Boot makes sense, when you're producing a lot of little web applications with microservice architecture, so copying over that manual configurations can become burden. My applications are monolith and last for many years, so it's not an issue for me to spend some time configuring everything as I need it and it's always visible and obvious what's happening.
For example for a rest service, https://spring.io/guides/gs/rest-service/ has everything you need to get started. A simple pom with a few dependencies, and a main class that invokes SpringApplication.run. You can just invoke this main class from your IDE like any other java application.
[1] https://javalin.io/
... that also doesn't actually benefit you in any tangible way.
Since then, J2EE and EJB led to a lot of boilerplate and now everyone thinks class and member names in Java have to be at least three or four words long.
And I wonder how much of the "compactness" you describe is simply because of Java's library. Library calls are very compact; having to write the code yourself (because your ecosystem doesn't have that library) is much more verbose.
> I imported that autoconfiguration library and everything works!
> Damn it, the imported autoconfiguration library does not work (or I need to change something), now I have to spend a day around Spring docs and stackoverflow to find how to get it working. Than I have to create a new class, override some random methods and inject my ugly code which feels like a hack now...
Frankly, after some recent development on ES6 + node + express, I'm starting to question whether staying on Spring + Java is worth it.
This. My god, this.
I'm strongly against creating interfaces for non-library (not shared) code, that will only ever have one concrete implementation. I see it all the time in internal service code, code that will never have a second implementation. It makes code hard to navigate, harder to understand, and tedious to manage as instead of updating one file, now you'll need to update two.
But yes - Java/python, etc. you shouldn't need to do this.
I’m generally hesitant to add test libraries if there’s an easy design choice alternative that keeps the code simpler.
edit: Not "Moquitto".
https://site.mockito.org/
The fact that dependency is versioned separately from your code base is one source of complexity that it adds. The fact that it has many lines of code is another complexity.
It is however often easier, even although i'm advocating very small self-written test doubles, they are still less easy than using mockito.
You're possibly confusing easy with simple but adding a versioned library of some thousands of lines of code to a project is in no objective way simpler than adding a small test double which you wrote.
(What people don't care about is the size of libraries that are scoped for the test context only, and don't even impact artifact size at all.)
Also, a Mockito mock is simpler in usage than a hand-written and normally instantiated test double.
A versioned external dependency, externally versioned == opportunity for future conflict. Remember the java logging library wars? Today it's trivial to encounter conflicting spring dependencies on your classpath
A complex (but convenient) reflection based implementation, the object has non-obvious behaviour with a clever implementation
An entire api surface distinct from the components of your software system
Tens of thousands of lines of opportunity for bugs, security risks or just plain old $$ cost to hide in
Once you're able to see the world in complexity, it opens up a whole new dimension of cost & quality for your code.
All this said, in practice complexity is just a consideration. Often the complexity is worth paying. I don't want to risk the impression that i'm saying never use mockito. If my argument is anything then it's consider the impact of complexity.
I've yet to find a library for C++ that generates mocks without requiring interfaces, but I'll admit I'm not an expert on it. Can you recommend any?
As an example, Google Mock/Test will require interfaces in a C++ code base.
Of course, we may be disagreeing on what is meant by the term "interface".
Once you know there’s a bug, you will definitely want to write more tests, in order to find the cause. But always writing a lot of tests, which are only usable in case you find a bug, seems like wasted effort to me.
If you need to triangulate the origin, do that when it’s necessary (i.e. when your broad test fails) and not all the time.
There's also a tradeoff around testing error states, but that can be a wash. Some errors are easier to create with a mock, some are easier to create with the actual driver.
Interfaces are one of the more powerful abstractions available and should be well thought out points of flexibility you specifically designed into your solution. If you use interfaces for other reasons your codebase is full of false flexibility and not-actually-abstracted abstractions which makes it difficult for other people to figure out what they can actually do with it.
Writing testable code is not "damage". An interface is a small concession in exchange for the well known benefits of automated testing.
I see where you're coming from and many programmers use too many interfaces as a reflex. But I absolutely think interfaces should be used to create seams for automated tests in front of components that do things like interact with databases, file systems, networks etc.
It's one of the reasons I tend to minimise my use of mocks as much as possible.
A "unit test purist" would likely shun this, but I take more of a realist approach; in many real-world cases, mock-heavy tests make for brittle tests that don't give confidence of correctness.
I tend to focus on integration tests, as I find these provide the most value while still executing very quickly (depending on the stack, obv).
That doesn't mean I don't use unit tests, or that I never use mocks - I just use mocks very sparingly.
Correct, but you can go a long way in writing testable code without requiring mock implementations. While I don’t deny the usefulness of mock testing, it comes at a steep cost (= vastly more complex code base), and I increasingly question whether that’s worth it. In practice I find it more useful to unit test what can be tested in isolation, and to perform integration testing for the test, and to limit mock testing (and thus unit tests which would require mocking) to a few well-defined interfaces. This cuts down drastically on boilerplate, with little (if any) loss of testability. And these few remaining mock object dependencies can be modelled via simple parameters and higher-order functions, further cutting down on extraneous interface classes.
For some languages (e.g. C++), if you are using classes, it can actually be fairly challenging to do unit tests without mocks. And almost every solution to this problem out there is to use interfaces/dependency injection/inversion.
Yes, but you can write integration tests instead. There’s no rule that says that unit testing must cover every aspect of the software. Unit test those units that can be used in isolation, integration test the rest. Case in point, our main software at work, written in C++, has close to 100% test coverage, and uses almost no mock tests. Instead, we have extensive integration tests where unit testing without mocking doesn’t work.
And, again: I’m not saying that there are no tradeoffs involved in that. But the benefit of having a drastically simpler code architecture outweigh the cost of having to move those tests into integration.
Incidentally this is vastly easier when the architecture cleanly separates business logic from general logic and moves as much of it as possible into side-effect free general-purpose functions.
In my last C++ job, perhaps less than 5% of our code was unit testable. We had very good test coverage using integrated tests, but those were very expensive tests. Running a full test suite took 10 hours. Our SW heavily relied on certain equipment. Fortunately, the vendor had provided a very good simulator for it - so almost all our integration tests needed that simulator. And the simulator was such that there was a fair amount of overhead in loading and setting it up and unloading it. We didn't do this for every test - that would have taken days/weeks to run - but for logically grouped tests. And unfortunately many of the integration tests we wrote were much more suited to unit tests, but because we didn't have interfaces, we were forced to test them via an integration test, which was very expensive.
Oh, and the simulator would not allow parallelism - you could have only one simulator running on a given machine at a time. So all our tests had to be run in sequence.
And of course, the simulator had bugs, so whenever we had a failure, we had to determine if the simulator was at fault. Especially for intermittent failures.
So yes, we had very thorough tests and were proud of it, but unit testing for perhaps 30% of our tests would have been better. But to unit test those we really needed interfaces. I know because I convinced management to give me some weeks to convert one of our simplest portions of code into something we could unit test. I thought it would be trivial, but I ended up needing interfaces.
There is a whole other approach which our sister team (very similar SW, but different HW) used. They did not have a reliable simulator. So they wrote a "fake" simulator. They simply reproduced the APIs and wrote code to return certain values under certain conditions - a poor man's mock. On the plus side their test suite ran very quickly. The down side was they needed to write many mocks for the same function (for when they needed to return different values). Writing tests was very expensive for them.
Why did they write fakes instead of using a mocking framework? Because every mocking framework they found in C++ required interfaces, and they didn't want to change their SW architecture.
In many cases integration tests are OK. But in our case, they were extremely expensive.
> Incidentally this is vastly easier when the architecture cleanly separates business logic from general logic and moves as much of it as possible into side-effect free general-purpose functions.
This is precisely where interfaces come from! It's not trivial to separate business logic from general logic without them. When I took the time to show how we could write unit tests for our code base, I ended up with interfaces. And no, I didn't read any books/sites telling me I should do it. I didn't even know they were called interfaces. I just took some of the simplest code in our codebase, and asked "How can I write unit tests for this?" and essentially reinvented interfaces. I separated the business logic from the logic requiring the simulator, and then wrote unit tests for the business logic, and had an interface class to interface with the simulator.
To give you an idea, the code I converted to unit tests was trivial: Given this input string with these flags set, return this converted string, etc. It should be one of the easiest things to unit test! However, the input string and flags were entities understood by the HW and so we had simulator dependencies. I wrote a class that dealt only with strings and booleans to implement the...
I just wanted to pick up one of your points:
> I've gone into the details I end up with interface classes
My reply to this is that functional programming languages tend to manage entirely without, just by doing “dependency injection” via higher-order functions (in fact, outside Java I’ve never [needed to] use a DI framework). This (often? always? I honestly don’t know!) makes mocking possible without impacting the overall architecture. And like you, I practice strict TDD for all my current own hobby projects, and I manage without interfaces. But I don’t want to make a claim of generality here: it’s entirely possible that this approach doesn’t work everywhere, or even in the majority of cases.
You should be writing pure functions which are easily testable, and using integration code to wrap the pure and testable parts.
Interfaces are typically a hack to get around the challenge of writing well designed, pure, and testable code.
This often ends up with interfaces...
> and using integration code to wrap the pure and testable parts.
This avoids having to introduce the extra interface, having to code the mock, sometimes having to change the mock or tests when changing the code and also lets you catch bugs in the dependencies that the tests on the dependency itself missed and that would otherwise show up in production.
One might have to do some work to partially replicate the production setup in testing and making it fast to start and run, but that's usually less work and gives a much better result.
OP is probably referring to the JDK Proxy support which is used by Spring and requires classes to implement at least one interface.
If you eventually need that interface, you can change your existing code to use it. Making that change is relatively easy compared to maintaining a bunch of code that isn't needed. But still I find it hard for some programmers to get past that mentality.
In my own progression as an engineer who writes Java, I read Clean Architecture by Robert Martin which lead to AbstractFactory(s) and interfaces up the wazoo. The reasoning would always be "this can change!"
It definitely took me some time to understand that not everything will change, and if it does change, having some extra work for simplicity is a reasonable tradeoff sometimes. In general, knowing when to utilize OO design patterns takes time and experience.
It took a long time before I realized that a lot of the changeability that come with writing aggressively SOLID code is actually a self-fulfilling prophecy: Some tightly intertwined chain of steps that might have been implemented as five consecutive (albeit tightly coupled) statements gets blown out into 10 or 15 modules and abstractions spread across as many files. All this added complexity was invariably designed to make it easy to change the code in a way that is not actually how you ended up needing to change it, so the end result is that it increased the difficulty of changing things: Instead of editing 5 lines, you now need to edit 10 files. And, since the logic is spread across 10 files whose members are, thanks to the magic of Java's access control facilities, all public, and whose connections to other parts of the codebase are all, thanks to the magic of dependency injection, loose (read: not explicit), it's hard to even understand the scope of impact of those changes without relying extensively on an IDE.
I've since taken a big turn back toward procedural programming. Object-oriented and functional techniques have their place (and the best procedural languages will let you use both), and I've learned a lot from working in both paradigms. But most of what I learned is that no amount of abstraction can replace the KISS principle.
As I understood Uncle Bob's argument for using interfaces though, the main benefit is not supporting alternative implementations, but rather aiding in the enforcement of the Dependency Rule[1] and of the Single Responsibility Principle[2].
Interfaces make the relation between components explicit in two ways:
- they make it clear what depends on what - they make it clear what are the responsibilities of the components on either side of the interface
So aside from the fact that it might make it easier in the future to swap implementations, it first makes it easier when writing the code to decide how to organize its components (i.e. laying out its architecture).
Of course I agree with you that interfaces can definitely be abused, and can make code more indirect and difficult to read. I just wanted to point out what I think is Uncle Bob's point. :)
[1] https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-a... [2] https://en.wikipedia.org/wiki/Single_responsibility_principl...
Too many Java programmers take the SRE too far, IMO.
If you have a class that only has a single function, then it probably should be a function, not a whole class.
To give an example that I've seen, I've seen code with classes named UserGetter, UserCreator, UserDeleter, etc. All of these should have just been bundled up into a single User class.
Abstractions are not evil, but it takes experience to use just enough abstraction.
But overuse of mocking is a very big problem in a lot of codebases. Often I look at a test, and all it seems to do is setup mocks, such that I'm not sure if anything is actually being tested!
My hope is that C#'s new default interface implementations feature will put an end to this mess.
It takes a conscious effort to just move forward, and it still leaves a lingering thought in my mind about what I'm doing wrong while building exactly what I need.
It's kind of like how cars are implicated in traffic problems. Everywhere you have heavy traffic, you'll find cars.
But the fact that you can do more damage with a backhoe than a shovel doesn't mean there is anything wrong with a backhoe or anything good about shovel. Same is true of OOP.
I can't count the number of C#/Java projects I've walked into where it felt like I had to drill down through a dozen layers of abstraction just to find code that actually does something. Hundreds (thousands?) of files that contain nothing but autogenerated boilerplate crap.
Layers upon layers of boilerplate, "just in case we need to swap something out later!" No. I promise you, you won't. And if you do, I promise you things aren't as loosely coupled as you think they are. You're going to have to do a bunch of rewriting anyway.
Also, 9 times out of 10, by the time you realize some seemingly interchangeable piece of your architecture needs to be replaced, your team will be chomping at the bit to blow the whole thing up and rewrite from the ground up anyway.
In the meantime, having a simple codebase that people actually enjoy working in will make everyone's job so much easier. And fun.
However, I personally like interfaces (well, traits, I'm doing scala), because it makes it much easier for readers to understand what bits are supposed to be part of the interface and which parts are supposed to be an internal implementation detail. Without interfaces it's really easy to accidentally change e.g. a return type into a more concrete one which accidentally leaks impl. details and suddenly other bits of code will come to rely on those impl. details. I work on rather large/complicated systems and I've seen this gradual spaghettification time and time again.
Disclosure: I'm usually the person who gets called in to fix the mess -- so that might bias my perceptions here.
Even if it does, for a mild behavior change or a test implementation, the simple fact that Java methods are virtual-by-default is a huge benefit here.
C#, on the other hand, is final-by-default. This makes code much more annoying to deal with unless it implements an interface and/or is thoughtfully written with these cases in mind.
What people sometimes forget though is that any implementations of this in your tests are also concrete.
Some things like a DB accessor, will use a mocked/lite implementation in testing for code that doesn't need access to the db (but still needs to be provided data) to test it's functionality.
That's the litmus test I use to determine if I need an interface: Will the code compile without it? If not, the interface is declared alongside the code that depends on it, and the implementation is in a second compilation module which depends on the first. Inversion of control.
I was just telling someone this yesterday. Not every damn thing needs to be an interface you implement if you're only implementing it once. Especially if you have to put "I" in front of it (as is standard in C#) and name the class the same thing, you should think if it's even necessary...
I agree with this, but the problem I see is that people have bad judgement when thinking about if something will only be implemented once or not.
Personally, I don't think the boiler plate / interface bloat is as cumbersome as others do. I'm currently working in a code base where things were decided to "never change", and now we need to change those things.
In a perfect world, I'd rather have less abstract code that never changes. But if it's a choice between poorly written highly coupled code or poorly written code that relies on interfaces, I'll take the interfaces and their bloats.
The rationale I'm given is generally one of two:
The later is also found with unit test projects rammed with mocks, making for brittle, unreadable tests that often don't really seem to actually test anything!This is the worst of all possible worlds, because a test suite should accomplish two goals: identify bugs and enable refactoring. Tests like these fundamentally can’t identify bugs because they don’t test any behavior. And worse, they prohibit refactoring because any meaningful code change will alter what functions are called and in what order they’re called. So any refactor breaks almost every test, and it’s impossible to quickly tell if something “real” was broken or if it’s just an artifact of poor testing.
UserController -> UserBo -> UserBoImpl -> UserDao -> UserDaoImpl.
Needless to say the implementations have never been replaced with something else. The middle layer is completely useless. UserBo, UserBoImpl and UserDao should be tossed into the garbage. Every single time you wanted to add a new SQL query you had to edit 5 files... Code navigation features of your IDE become useless and you are better off with string based search.
Yeah, not good.
And you meant update 3 files, not 2. You forgot the 1 file that uses the 1 implementation, which should have simply been nested the only place it was used.
You can write concise pure Java if you don't rely on libraries, have unit tests, or do things the Java way.
It's too bad, too, because there are some things to love about Java, really. It's simple, it's super fast thanks to the jvm, and it's well supported.
to what baseline are you comparing it to?
Too late to edit now though.
Rust, C, and Go appear to be on the top.
I don't really ever run into the same problem twice. I've never been able to write anything as concise for a one-off problem as I can using Python or Perl (or even Awk) for data wrangling.
Java is for enterprise scale applications, where dozens or hundreds of developers contribute to the code base, for a decade or more.
The boilerplate, the verbosity, strong types, the dependencies on complementary tools is what makes a large code base maintainable in the long run. If cared for.
If you need to scrap a website a couple of times, convert text from one format to another. Yea just use some script. Throw away code that nobody but you will ever need to touch or even look at. It is possible to write a python, Perl, JavaScript, or even Php app at scale. It may require a bit more due diligence though. Frameworks have been introduced to help with that.
The article is written by someone who doesn't fully understand Java. No need for interface to support injection. Interfaces are commonly written by CS graduates or uninformed long time OOP developers, but composition and DI have been around for over a decade. I stopped reading when the interface was pointed out as the only way to go.
Uhhh... you should have unit tests no matter what language (or culture) you're dealing with.
Hum... No, you can not. Java is missing basic tools for abstracting idioms, break your code semantically, and reusing your code. Without those, you'll just write stuff again and again, hope you have a very good IDE.
> It's simple
It's simple the same way Go is simple. The language itself is small, in ways that make your code complicated. On this competition, Brainfuck is unbeatable.
> it's super fast
C, C++, Rust and Fortran are fast. Java is not. It's a second tier, at around the same speed as C# (obviously), Go, and Haskell.
Such as?
It has relatively recently gained usable high order functions, and seems to have stopped there.
I'm just trying to understand where you're coming from.
All of those techniques are kind of equivalent in power, spread through a curve trading flexibility (expresivity) with organization (possibility of analysis). Java has nothing near that curve.
> spread through a curve trading flexibility (expresivity) with organization (possibility of analysis). Java has nothing near that curve.
Java definitely leans towards the organization part of the curve. It doesn't need a single concept to run away with, because it knows that there is no magic bullet that will solve everything. Monkey patching is dangerous, especially when you're programming large systems that end up being used in finance or ecommerce.
On a side note, if I'm not mistaken, Brian Goetz mentioned that something similar to higher kinded types was not off the table for Java sometime down the line in a recent panel.
Anyway, parameter or objects enumeration are when a library uses both the field names and their values as input, people use them to define DSLs on a function call or object construction. Parameter enumeration does afford applicative semantics, but (mutable) object enumeration affords full monadic semantic, so it's easy to construct Turing complete DSLs (and I guess, why Python people avoid it).
It is really hard to explain how those concepts are useful, mostly because they only work when everything else on the language help. If you try them on Java, they will be mostly useless. C# for example has a fully generic implementation of monads, with specialized syntax that isn't very far from Haskell's, do notation. Yet, with its bad type inference and the inflexible interface hierarchy it got from copying Java, it is nowhere near as useful.
80% boilerplate is a large exaggeration.
Could you please provide an example in both Go and Java? I am not quite sure that it has to do with the type system itself but your familiarity with the language and its libraries, and/or lack of basic functionality that should be in your text editor when you write Go.
value := getValue()
Java:
int value = getValue()
Once you get into more complicated types things get even harder to understand.
var value = getValue()
So it's almost exactly the same in both languages. It's not a problem for most IDEs, just hover your mouse over the value or use keyboard shortcut to get the type. This was never an issue for me in Java or Go.
I'd have preferred much lambdas just exposing checked exceptions they experience inside. I can't imagine why the compiler shouldn't be able to do this. Maybe one would have to come up with a fancy definition of functional interfaces (although I think this could be tackled with an annotation if the language doesn't provide this for lack of another keyword a la "throwsall").
If you really want something like checked exceptions then (anonymous) intersection types or row polymorphism would be much better approaches than the adhoc thing Java does... but I suspect that that wouldn't well with the variance system in Java.
(Thankfully it's only the Java compiler itself which imposes checked-ness, so other languages on the JVM thankfully don't have to deal with the insanity.)
The lack of sum types and pattern matching notwithstanding, such a signature can be implemented reasonably in Java.
One, why do you think almost all non-standard java libraries almost exclusively use exceptions derived from RuntimeException?
Two, I encourage you to look up guava's Throwables utility class and consider why it is even a thing. Just for a single example: Methods with no throws clause cannot throw exceptions, so you must wrap in a RuntimeException... but that means that outer catch clauses which match on the checked exception WILL NOT MATCH on the RTExc-wrapped-exception. This is broken as all hell.
It's simply broken.
EDIT: Just to add: What happens in practice is that either
- you just give up and declare "throws Exception" on the interface (in which case: why bother? You might as well just remove checked exceptions at that point), or
- you wrap checked exceptions in RuntimeException, but now you have a new problem: Consider a method that calls methods f and g where f declares a "throws Foo", but g doesn't declare any exceptions[0], but may throw Foo's at runtime by wrapping. Now you're totally screwed, because a "catch Foo" will not match exceptions from g. The Guava people have written a whole Throwables utility class to help with this, but using that consistently requires discipline that no team can handle, and it only handles a small subset of the problem.
This is just a small sample of the problems with checked exceptions in Java.
[0] g may be used in contexts where it's not "allowed" to declare checked exceptions in a throws clause, e.g. lambdas or just higher-order functions in general.
Culture is really important in programming languages and I'm not sure it is something you can ever separate from the language itself, as for example the standard library dictates in large part the structure of your own code.
I do agree though that there is no great reason Java has to be more verbose or more confusing than say Go.
Not a Go developer, but two things off the top of my head:
1. Do not need to explicitly declare interfaces a struct conforms to.
2. Do not need to declare types of variables inferred on initialization.
3. Syntax for slice, map and struct literals.
I'm sure there's more...
Can't have nice syntax for object literals, maps, slices, etc, though :( I mean, I have no hope for it to be introduced to the language any time soon, even if the desire is there among the language stewards.
`var` is part of standard Java, you don't have to use Lombok.
> Can't have nice syntax for object literals
Could you elaborate on what you mean by object literals in this context?
Which comes with its own (sometimes dangerous) downsides.
> 2. Do not need to declare types of variables inferred on initialization.
Java has `var` for quite some time now:
is valid.> Syntax for slice, map and struct literals
I assume you mean overloading `[]` for slices and maps. This is a nice feature (C# and Kotlin already do this). But it doesn't reduce boilerplate by much in practice.
I don't buy this. I've heard the theoretical arguments to the contrary, but I've never (in 7 years of consistent Go use) heard of anyone being bit by this in practice--certainly not to any significant consequence. On the other hand, implicit interfaces materially benefit nearly every project in the Go ecosystem. I would make this tradeoff all day every day.
Secondly, I found that in practice, it doesn't buy you much, and on the contrary, makes working with code more difficult (even with an IDE).
It's valuable information to know what interface(s) a type implements, not to mention finding all types down the hierarchy that implement a given interface (much more tedious and resource hungry with golang).
> Secondly, I found that in practice, it doesn't buy you much, and on the contrary, makes working with code more difficult (even with an IDE).
I don't know about you, but I don't like having to write boilerplate to make a third party class implement a first party interface.
> It's valuable information to know what interface(s) a type implements, not to mention finding all types down the hierarchy that implement a given interface (much more tedious and resource hungry with golang).
How is that information valuable? The only thing I can think of is "I want to change the interface and consequently I need to update the implementations". In which case, just change the interface and the compiler will tell you what broke. As for "resource hungry", who cares? I'll trade 50ms of computing time per search (note that this is a generous figure) if it means developers don't need to maintain "implements" annotations and the corresponding boilerplate discussed above.
It's useful when you want to know where a certain concrete type is being passed as an interface. If you have a type Foo that has functions bar() and baz(), then it becomes very tedious to see where Foo is being used as a `Barrer` (in golang style), `Bazzer` or even `BarrerBazzer` because it now automatically implement all three interfaces.
Another use case is that it becomes awkward to define "tag" interfaces (look at Rust's `Send` trait for instance). Now you're forced to define an interface with a function `isSend` or something and hope that no one else declares another interface with the same signature and passes in types that implement that. Again, more bug-prone behavior.
> I'll trade 50ms of computing time per search (note that this is a generous figure)
On any non-trivially sized project, it takes way longer than that to look up implementations of interfaces in the IDEs I've used so far. You're looking at 30s+ sometimes.
That's just a generalization of the example I gave, and the same solution applies (although there are probably automated solutions). Unless there are other special cases that you have to do so frequently that the solution previously mentioned is too tedious, then I don't see what the fuss is about.
> Another use case is that it becomes awkward to define "tag" interfaces (look at Rust's `Send` trait for instance). Now you're forced to define an interface with a function `isSend` or something and hope that no one else declares another interface with the same signature and passes in types that implement that. Again, more bug-prone behavior.
I don't know how to take this comment seriously. The odds of someone implementing isSend and passing it to the wrong interface have to be astronomically low. The frequency of such bugs must be approaching zero. It's patently unreasonable to consider this to be "bug prone", moreover, if you're really, really concerned about it, randomly generate your private method name to whatever degree of entropy you prefer.
> On any non-trivially sized project, it takes way longer than that to look up implementations of interfaces in the IDEs I've used so far. You're looking at 30s+ sometimes.
File those bugs with the IDEs. Any IDE worth its salt holds the set of types in memory. Even if the set is just a list/array (as opposed to some data structure that is indexed by the interfaces they implement), it should take no time at all to filter that to the set that implements the interface, even if there are tens of thousands of types.
Well, it was only added in the latest LTS release, which was released a little over a year ago. (Or a few months before that if you used the non-LTS Java 10.)
It's about time – C# had it since 2007.
The things you mention don’t bother me as much as pointless ceremony like getters, factories, and insane object hierarchies, all of which are really optional. I quite like Go and use it a lot, but it is not IMO radically different from Java in syntax, except perhaps in what it leaves out (inheritance, exceptions, explicit declarations), and its culture of radical simplicity.
Does Go have objects with functions? If so, it has getters and setters, Go programmers just choose not to use them. I could absolutly write Python code like this:
The thing is...Java doesn't have getters and setters. There's nothing about the language that requires them. This is perfectly legal Java code: Look at it! It's a public member! You can read/write to it without a getter/setter! You know, something every language has, but for some reason, it's considered verboten in Java.Java is a perfectly fine language. It's Java programmers that are awful for sticking to these unnecessary paradigms that necessitate ridiculous amounts of boilerplate.
Java programmers are a source of frustration for me. I don't write Java professionally, but I do application security which involves reading a lot of Java code. And because the developers are sticking so strictly to the typical Java paradigms, the code is very difficult to read. Testing things locally is a nightmare, since stack traces are a ridiculous stacks of .invoke, as if Java programmers are afraid of directly calling functions.
I let my frustration get the better of me and I apologize.
It's not just "for some reason". There are plenty of reasons why getters are a superior approach to naked fields, and these have been documented to death for the past twenty years.
If your field is not final, you should hide it behind a getter/setter.
Many other languages do just fine without them. What makes Java so different?
What makes Java different is that they have properties as a concept (in the Java beans specification) but not as an actual language feature, so you have to implement them manually yourself for every field and every class. (Or let the IDE generate the code, but all that boilerplate is code that wouldn't need to exist in other languages.)
1. Many other languages let you redefine field assignment syntax to be a function call, which is basically what a 'property' is in Kotlin. Java doesn't.
2. The Java ecosystem distributes software in binary bytecode form and cares about binary/API compatibility.
The combination of these two mean that even if you don't want to run any code on field assignment today, if there's any chance you might want it tomorrow you have to plan for it now by using functions instead of naked fields. Changing the latter to the former means updating all the use sites.
The big win of language integrated properties is you can go from (in Kotlin):
to without any of the places that use Thing.veryLongText noticing the difference. The first code just stores the string as an ordinary field. In the second, we've decided the text should actually be lower case and that lower-casing should be done on demand then memoized for efficiency.In Java, if you want to do that, you'd have needed to write a getVeryLongText() method from the start, as otherwise you'd have nowhere to add the extra code.
In a lot of cases, immutable objects are superior: simpler to reason about, have less code, and cost as much as mutable objects.
In Go - you can't do it like that. You need those getters, preferably an interface (because exported struct allows anybody to create a zero-value) and a factory function.
[1] - assuming their types are also immutable
Maybe conforming to an interface without explicitly declaring it (like you declare an instance of a typeclass in Haskell) may look like a controversial decision. I still think it does more good by removing boilerplate than bad by accepting an object as conforming to an interface in a rare case when you did not mean it.
That said, the thing I like about Java is that its JIT compiler and bytecode system allow for efficient metaprogramming; Go's runtime package works, but it's not fast at all. These use cases are few and far between, but it would be nice if Go had a good web framework story (e.g. rails, django, spring, etc).
> Maybe conforming to an interface without explicitly declaring it (like you declare an instance of a typeclass in Haskell) may look like a controversial decision. I still think it does more good by removing boilerplate than bad by accepting an object as conforming to an interface in a rare case when you did not mean it.
Yeah, I hear this objection pretty frequently, but I've literally never heard of a single instance of this resulting in a real production bug. If it's a rare case, it's vanishingly rare, while it's no exaggeration to say that virtually every project benefits from the advantages.
The language may be lacking here and there, but the tooling around it is good, and the runtime of it is great (sub-ms garbage collection, etc).
Unfortunately, inheritance is deeply ingrained into many libraries and standard classes, though it has been replaced by interfaces in most standard APIs.
Yes, the boilerplate culture is a problem, not of the language but of the ecosystem :-\
You can stay reasonably away from it in many cases with modern libraries, though.
If you restrict yourself fully to the standard library it's still not that largly exaggerated, even with Java 8.
> The compiler has a well-defined custom processing interface (@annotations) which allows for powerful boilerplate-reducing things, from Lombok to Spring to JUnit.
If you add Lombok + sth. like guava or apache-commons and in general choose modern libraries/frameworks it's really OK now. The only downside of Lombok is that it really maxes out what compiler(s) allow you to do which is not always without issues and it requires an IDE plugin to be installed.
If you're writing a lot of boilerplate in Java, you're either doing it wrong, chose it for something wildly out of its wheelhouse, or you choose frameworks or libraries that have either forced or afforded high boiler-plate code.
The latter is probably the major difference that is what people are feeling. On paper Go and Java are virtually the same language, but just a few differences in the spec and a bit of focus by the developer community means that Go just generally has less annotations, external XML files for this, weird build systems, and a whole bunch of other frippery.
Although I do think that it's easy to underestimate how much cleaner it turns out to be to do interfaces the way Go does rather than Java. It seems like such a small change, but it has a big effect on how much OO boilerplate you need. You don't have to guess what interfaces someone may someday need and write them all out in advance.
And, I mean, in both languages, let's not underestimate the amount of "doing it wrong". There's a lot of us whose exposure to Java is being on the receiving end of a pile of Java code that is not written with maximal skill and precision. That's not really Java's fault. (To the extent it is, it is not uniquely so; I can tell that story about a lot of languages.)
Like shoehorning a set use case into a map? Or trying to get the list of keys or values in a map without writing out 5 lines of code?
I find golang code to be much more verbose than Java. You find code littered with one off functions that amount to nothing more than map/filter/etc. operations which are 1 liners in Java.
(I am underwhelmed by the crowd that insists map/reduce/filter is so important anyhow. I've used a substantial amount of Haskell. In Haskell, map/reduce/filter is the beginning of the functional style, not the completion of it. It goes beyond just "an alternate way to write for loops", which is most of what I see non-Haskell usage doing with it. In Go... just write the for loops. It's the same thing, just spelled differently. In Haskell, it's not the same thing.)
How else do you suggest transforming slices from one type to another, removing entries in slices based on certain conditions, etc. without looping? This is one of the most common operations in any basic application.
...
"Hey look another tool that can generate a lot of boilerplate code you then have to deal with and makes the easy stuff easier and the hard stuff almost impossible!"
In contrast, when I started using Python, one of the things I liked was how suitable it was for all the things that Java developers typically used other languages for. Your typically Python project back then contained very little that wasn't Python, because there weren't many things that it wasn't useful for.
Things may have changed with Java in the 9-10 years since then but I think that was certainly one of the things that led to Python becoming more popular.
I came back to java about 4 years ago after a long time away and have found it quite pleasant, and quite different to how you describe it.
I think I skipped the 'enterprise' years, thankfully!
Other competing languages you would use today didn't exist, and C++98 was a pain in the ass. The selling point back then was Java was C++98 with all of the difficult parts stripped out, and with good tooling and IDE support. Back then people flocked to Java like some sort of oasis. Today, even modern C++ is better in most ways.
It always seemed weird to me. Like if my house was really cluttered but instead of building shelves or closets into the house itself, I buy an elaborate conveyor belt and excavator system to rearrange all my things all the time so I can walk through all the mess.
Python is not static typed, it won’t refactor as easily.
I mean, you get the same result, sure, but I find I can hardly ever rely on test coverage alone as an accurate safety net to clearly indicate whether I've reached an actual stopping point for a full refactor.
This is one of the ways in which Java excels, in that that loop is fully accommodated for in most major IDE's.
You can still get caught by surprise at runtime, but I've found that for a strongly typed language like Java, the syntax, and necessity of boilerplate makes most bugs obvious until you start getting overly dependent on frameworks you don't understand.
For me it seems weird that the Java IDE is "smarter" than the compiler. Then why can't the compiler abstract stuff...
Like the 'auto' keyword that took how long to exist again?
(Yes, I know you can do Java in VS Code but I wouldn't want to manage a large Java codebase with it.)
Why not?
Whereas in huge JS codebases relying on framework magic you can really have hard time to understand what's going on without plugging a debugger (and even then it's still not easy). Trying to refactor legacy code that relies on `this` and prototypes is a nightmare.
I sometimes wonder if JS programmers know that with C#/VS you can just change the name of a class, and without doing anything else (not even replacing), all involved names just change, and everything works 100%, without writing any test.
And the same goes for moving a method to another class, or renaming member variables, modules, files... everything just works and is instantaneous.
To me, not being able to aggresively refactor withour worrying something will break somewhere is like backwards and old school (in the bad way)
When a project evolves, a class, a struct... grows and suddenly a name, perfectly fine at the beggining, does not make sense any longer. With C#/VS you just change and 2 seconds later you're still working.
The same can't be said for languages like JS.
If I get code and all of a sudden not even my IDE knows what's going on, it clues me in that there are layers of indirection in the codebase I'm going to have to waste time reading into, like magic methods or loose typing.
Both PHPStorm and IDEA are written in Java (they share a common code base). Don't confuse [Java eagerly allocating memory from the operating system but then not using it for anything (yet)] with java using a lot of memory. For speed, java allocates memory it anticipates using in the future. This lets it manage it's own memory without involving the operating system and thus context switches. If you start using a lot of other applications and available system memory starts running low; java will detect this and relinquish the pre-allocated memory it's not using.
edit: further learning those simple tools has a more generic application to other problems while the IDE magic will likely go until there's an issue to be understood and you're depriving yourself of learning generally applicable tooling to trade a slight inconvenience for magic that has the potential to cause great inconvenience and once that magic from the IDE bites you and you learn how it works - you've learned something with no general application to other problems.
edit2: since I can't reply in-line
jcelerier - I _could_ do that with CLI tooling and depending on the language it would be trivial. However, I would _never_ do that because changing the method/function name to suit my context would be very unlikely to avoid making other code less readable. That's a huge anti-pattern that your IDE is making easy. Further, consider the implications for language design when making language design decisions that require a specific type of IDE magic to be considered a reasonable language design decision. I have a hard time believing relying on the IDE lock-in is good for the community of that language.
philwelch - I genuinely don't know the answer, but what happens when you change a class name -> forget you do it before saving the file -> go to another file and try to change the class name there but have a typo -> go back to the original file and save the change? I would bet the typo'd change goes unchanged and now you're left scratching your head since that magic has always worked before.
Are you relying on a correct and comprehensive project/workspace setup within the IDE?
Of course. Not much point in using an IDE otherwise.
To quote myself, "fix a single file manually, instead of a dozen or a hundred or a thousand".
And what happens when you need to rename, say, a method name that's also used as a variable name in many places? xWhat happens if multiple classes define the same method name, but you only want to rename that method for a single class?
Yes, you can probably do it in sed. If you're used to it enough, you can probably do it pretty quick, with sufficient regexps that you only have to debug a few times.
Or you can right click or hit CMD-. or hit F2 or whatever with your cursor on a method name, type in the new name, wait 2 seconds, and you're done.
You generally get compilation/build errors highlighting the bits that were missed (for whatever reason)
That's not something I "rely" on, it's the first thing I make happen whenever I use an IDE.
Or you have a larger codebase split up into multiple .Net solutions and DLLs where code is referenced through the generated DLLs.
The refactor looks great, until it starts breaking things for the many consumers of your code.
In fact, the latter is a situation where simple text tools will make life a lot safer and easier than whatever the IDE does (which pretty much will be limited to the bounds of the .sln file, whereas a simple text tool operating over files in the entire codebase will alert you to the fact that this is being used elsewhere).
My argument isn't to claim that simple text tools are better at refactoring. They aren't. However, I've run across several situations where we've had issues in a massive codebase because developers believed that refactoring was as easy as hitting the Refactor button in Visual Studio, and everything will work magically.
The belief in programming 'magic' (whether through IDEs or frameworks that abstract a ton) without understanding what those tools actually do is what I'm worried about. Use IDEs to do what you need to by all means, but it's much better when you actually understand what it's doing before doing something with it.
Sure--and to be fair, I've never worked in .Net much. But if you're changing the package interface, you should arguably do so in a backwards-compatible fashion anyway, depending on how your stuff actually gets built and deployed. If everything's contained in the same repo and you can't refactor across multiple components in the same repo, that sounds like a problem.
> The belief in programming 'magic' (whether through IDEs or frameworks that abstract a ton) without understanding what those tools actually do is what I'm worried about. Use IDEs to do what you need to by all means, but it's much better when you actually understand what it's doing before doing something with it.
Agreed!
Yes, in general, the ability of IDE to make these kind of large-scale changes quickly, reliably and mostly bug-free is nice. But I rarely miss it in languages where it's impossible.
It may be that one of the ways your language shapes your code is the way you structure it. For instance, in my Lisp codebases, I can hardly think of any place where Java-style automated refactoring could be useful; in the current largish codebase I work on, it rarely is the case that constructs introduced in one file are used directly in more than one or two other files - which makes refactoring entirely doable with a dumb text editor, and Java-like automated refactoring wouldn't save all that much time over doing a regex search and manually visiting every line listed in results.
I'd really like you to show me how you can refactor e.g. a method called "write" in a 500+kloc codebase in less than 5 seconds with "simple tools". With any C++ ide from the last decade it's basically a keyboard shortcut + typing the new name + fixing the few remaining compilation errors that may have cropped up in generic code. With sed & al ? good luck, see you in a few weeks.
I'm not taking sides on the issue discussed here, but regarding this quote... This is one of the biggest reasons I don't do OOP. Using plain functions with slightly longer but globally unique names so many issues go away.
And the biggest issue is that I can't read a code base if so many function names are not unique and possibly not conveying enough information for local understanding. That style always requires the reader to carry so much context in their head, to be able to resolve the "polymorphism" to concrete implementations. (Yes, in a proper IDE we can jump to the implementation interactively with a few keypresses, but it's still much harder to read).
Only on monolith code bases without any signs of modularity.
In other words, don't make it look complicated before it actually is.
That was kind of the gist of my comment.
Wading through non-trivial projects, pervasiveness of the first approach makes me nervous, because I'm told at every occasion, "don't even try to understand what's going on in the system globally. We .log(), shouldn't that be enough for you to know?".
While the second approach is calming to me, "You see, we have this simple logging system, where everything ends up being written to. It's not that complex after all, and if you want to know what's going on you can just jump directly to the log_event()" function.
Yes, by asserting that foo and bar are of the same type, and asserting that the .log() method is not virtual, I can reach the same conclusion. But that's the point, it requires work that you can't practically do if each line of code contains a method call. There is no mincing words, you're using the wrong syntax...
First foo or bar can be function pointers.
Or maybe log_event is a macro whose behavior depends on the environment. And I have used some crazylogging ones back in the 2000's when coding C, that would even dump the current state of the process alongside its parameters.
Or then again, log_event() plugs into a configurable logging system and one cannot predict its behavior just from the call site, even when using the same types.
Finally if we move away from C semantics, maybe log_event is overloaded or is doing multiple dispatch, thus the two calls, depending on the argument types, are not going to land on the same implementation.
Then different uses of the same identifier still resolve to the same implementation, and make the IDE jump to the same location, etc.
(I acknowledge that everything can be something else. And in theory you can redefine macros to have uses of the same identifier resolve to different implementations. Etc, etc. I'm sure you know about the IOCCC. But here we talk about good programming practice. It's a good practice not to suggest something complicated while the reality is simple. There's no argument to win here.)
> Finally if we move away from C semantics, maybe log_event is overloaded or is doing multiple dispatch, thus the two calls, depending on the argument types, are not going to land on the same implementation.
Which is why I do not use C++, or at least avoid most of its features. For me, it's all about clarity and reducing number of moving parts. It's not a contest in making complicated things that pretend to be simple. And it's not a contest in making simple things that look complicated. And it's not a contest in reducing the number of characters in individual identifiers at all costs.
so, question : suppose you have your log_event, and now your bosses comes and tells you that the client needs to have two additional different logging mechanisms - say, to journald and to some websocket log server. The system can log to either of your three logging mechanisms at any point through changing a configuration somewhere in a GUI but you also need some core classes to always be logged through journald no matter the state of the rest of the system.
How do you update your design to reflect that ?
What is needed in the face of changing requirements is actually this: lean structure with few dependencies that can be adapted.
That wasn't hard.
Also you've made your logging library project-specific by making a log_core_event function. Thanks but I'll keep using spdlog in my projects, which do not need newcomers to learn about your log_core_event, what is a core module and what is not, just to be able to write the three functions they are called for in that project !
That all has nothing to do with virtual methods.
> thread initialization if you have multiple threads doing logging on startup, right ? :-) )
There is no technical difference between "global" and "local" variables. It's merely a syntactical difference.
If you run into concurrency problems with global variables, you'll run into problems just as well with objects. Unless you make multiple objects. In which case, you could also have done thread-local storage, no?
In the end, it's much clearer to me to use global variables (TLS or not) since this way I can actually see the data relations. If there is just one instance of a thing, why would I pass it around as objects? It's not going to help understandability.
> Also you've made your logging library project-specific by making a log_core_event function.
That doesn't make sense. Can't I just add another function without breaking everything? Note that I probably wouldn't even add this function to "the logging library" but to the core modules...
> what is a core module and what is not
If you prefer having everything switched behind your back, resulting in unreadable and unintelligible source code... you do you.
More technical notes...
If you don't like the presence of two or more functions for logging, then why not have an explicit context argument? You call log_event(MODULE_FOO, foo, xyz) instead, and the log_event function implements all the plumbing logic in a central place, easily understood (Most logging approaches do that, I think. They have "facility" and "severity" arguments). This approach seems much more preferable to me compared to an implementation that you can't see locally, and that can't be influenced locally. What would you do if we go for the OOP approach with a virtual method, and now call into a different module that would need a different logger?
If having a dynamically switched implementation was really what we want to do, there is no problem with doing that. My post was just about using method syntax for static things, which I think harms readability.
well, that cuts short to the discussion. There are plenty of places & coding standards where those are outright forbidden.
> If you prefer having everything switched behind your back, resulting in unreadable and unintelligible source code... you do you.
we have very different notions of readable. For me the less stuff I have to read and the more readable it is - I just want my code to be the pure domain problem and could not give a rat's ass about how logging or networking or whatever is implemented if those aren't my domain.
> Can't I just add another function without breaking everything?
that does not break the code but that breaks Joe intern's mental model and expectations of anyone who will think that log_* functions are from the same library
> then why not have an explicit context argument?
and here we are, reinventing OOP by hand in C. There is literally no difference between log_event(context, ...) and context.log_event(...) - but you get better tooling :
* completion -for the first I'd most certainly end up typing the full name if there are a lot of l-started functions while for the second I'd type c<shortcut>.l<shortcut><enter> and only see the few relevant functions in the autocompletion list) etc... in the second.
* namespacing - if I want to see all the logging functions available I can just click on "context" and my IDE will happily show me all the available methods in a panel and nothing else. With C functions, well, I have to scroll in a list of 150000 functions. And also wonder (& have to explain) why log_event() is in <util/logger.h> and log_event_core is in <core/core_logger.h>.
* split implementations - if at some point you want to port your software to, say, an arduino without network capabilities (talking literally forom past experience here), the monolithic log_event function now becomes an #ifdef HAS_NETWORK / #ifdef HAS_JOURNALD / ... mumbo-jumbo. While with multiple logger subtypes it's just a matter of changing your build system to not compile the unavailable implementations, without even needing to change the code (or change a list of types in a header if you use static polymorphism instead).
> What would you do if we go for the OOP approach with a virtual method, and now call into a different module that would need a different logger?
dependency injection ? modules can request either for a default logger which will be sourced from the gui configuration, or whatever specific logger they actually need due to functional requirements.
Besides, I still don't see the readability problem with virtuals. Again in my IDE if I want to see "what happens" I press F2 on context.log_event and it displays me the list of all the possible implementations if it isn't able to determine that one is used statically at that point. And I much prefer to read three five-lines email_logger::log, journald_logger::log, websocket_logger::log functions than a pile of if/else's.
But when I had a glance at the project you linked on github, that was basically the first thing I found there...
A suggestion that I have there would be to use the linker instead (make different source files based on implemntation / existance of an implementation). This way you can eliminate most of the #ifdefs.
Regarding dependency injection, I don't see a practical difference to just using global variables / global functions, apart from the fact that by convention DI gives you back objects with virtual implementations, which I don't like.
I won't go into the rest of the things you said because we pretty much discussed it all.
* jar-hell (and dll-hell) aside
That makes me assume two things: you work alone and your projects don't exceed 5.000 (fivethousand) lines of code. Without some namespacing and an expected method length less than 100, more likely 10 lines than 90 or more, you end up with roughly 160 names. An average human knows 150 people, maybe by their name. And while you have friends or at least parents and grandparents occupying some of the »address sace« few of your methods will not be »at hand« for your brain.
And in case you already prefix your methods with something like io_write(...), db_query(...) and you hand in context-specific references like io_write with a sort of file handle and db_query with a sort of database connection I transform all this 1:1 in an OO-style language and back.
For a clearer description of what I mean, see my other comment below where I write two lines of code in both ways.
The tools are at a point where I'm usually breaking my code when I'm doing manual refactoring, but never when I let the tools do the job.
> what happens when you change a class name -> forget you do it before saving the file -> go to another file and try to change the class name there but have a typo -> go back to the original file and save the change?
A good IDE does not care if you save the file or not. It is always up-to-date. Renaming a class in just one file is also impossible, since the IDE will do refactorings atomically over the whole project. But lets just assume you somehow managed to fuck it up that badly (things can always go wrong): You will still realize your error almost instantly because a statically typed language does not defer such checks to runtime. The program wouldn't even run anymore.
By far the biggest issue with automatic refactoring is accidentally renaming stuff in comments (or forgetting to do so) because I'm too lazy to look at every single place. But find&replace has the same problems.
https://tryidris.herokuapp.com/compile#YWRkIDogSW50IC0+IElud...
The above link refers to the following Idris code:
How exactly does this knowledge help? I know this is possible and I do miss it. But it's extremely diffcult to support it in JS because of how the languages works. VSCode tries, but it's still limited. So should we all switch to writing C#? You can't avoid writing JS if you want to create a web app. There are tools to avoid it, but one has to know what's under the hood or they'll have a bad time.
TypeScript is doing great progress on this part, and it supports easy refactors, but it's still lacking in some parts, for example the type inference is still poor when it comes to complex cases and the type declarations depend on will of maintainers.
Another honorable mention is Spring Boot @Conditional for which adding a minor little diversion from defaults will turn a functioning happy project in a dysfunctional nightmare.
:'(
I have even come to question if we should make all our javascript apis async from the start to avoid this scenario though it seems a rather extreme guideline. We didn't really do that, but I still dread this type of refactoring.
(My dream code exploring tool would be a cross between an IDE and a tablet&pen friendly PDF reader. I should be able to navigate through files using all the semantics-aware IDE goodies, the tool should also understand version control, but it should also be able to generate and manipulate structural graphs (as in, boxes and arrows), as well as let me highlight, draw and freehand over the code.)
The overhead has significant cost for trivial gains. The costs result in overly complicated systems to solve simple problems. Most of the complexity in these systems is for dealing with complexity brought in by the ecosystem, not the problem domain.
Granted it isn't 10s of thousands of lines, but it isn't small either. It does a lot of things ranging from webapis to data pipelining with ingestion and egestion of large datasets. One batch job can result in 10s of GBs in output files. It has to be performant too, 10s of millions of "operations" in one batch job is common.
Admittedly the process/threading model can be painful to work around at times, but totally doable, and is honestly somewhat friendly to distributed systems to an extent because it somewhat forces you into an actor model paradigm.
I'm not trying to belittle what you do, but if your good base is in the 4 figures in size then it's not large (in fact yes, it is small). When people talk about large code bases they are generally talking 100-1000x larger than yours, or more.
I just think it's foolish to think Java is the only language that can handle large codebases. I mean just look at the linux kernel.
EDIT: Removed comment about processing capabilities because it's irrelevant to the discussion. I left my original comments about lines and the IDE though.
Current job I've seen single files that are over 30k lines.
“Code should be written for humans to read and only incidentally for machines to run” (paraphrasing a famous quote by Harold abelson)
You can find all the places that call a method and know they are really all. That sort of basic thing.
E.g. not for writing, but for analyzing what unknown code does and for refactoring that code. Comparing to javascript, in js you have to be much more careful when refactoring and changing things.
1. used when a person has something more to say, or is about to add a remark unconnected to the current subject; by the way.
2. in an incidental manner; as a chance occurrence.
That quote seems pretty misinformed, code has to be a lot more connected to the machine to be runnable that that quote would have us believe.
Otherwise this would be valid code:
Ask the user their name. After they enter the name, load a game. Use some cool graphics.
That being said, perhaps that will be possible in 100 years. (Yes the first line may be possible in 10 years)
In other words, don't doo premature optimization. Write the code so that it's readable, except on the chance occurrence where you need to eke out more performance.
So this is a cute statement but isn't it actually false? None of us would be sitting around code reviewing each other's code for fun if there wasn't a mountain of money / value / knowledge to be extracted from a computer running the code at the end of the day.
Actually this is easily falsifiable. As soon as quantum computing becomes possible the first languages will probably be quite horrible, with painful tooling and torturous debugging and difficult to read control flow compared to current modern languages. Nonetheless, a huge army of programmers will be trying to write code for these quantum computers, for the sake of the computer, and not for any human to read.
Of course its not a universal rule, its not a law of physics. Doesn't make it less useful as a guiding principle (similarly, design patterns in software architecture).
The core argument is that we're besides the point in conventional (2019) computing where we need to worry too much about optimizing code to run as efficiently as possible. The complexity and bottlenecks to delivering features is now in writing software that is meant to be worked on and maintained by a large number of humans. And for that purpose, it is better to optimize for clarity.
I disagree. Most people working on the Linux kernel (including Linus) or the major GNU projects (including RMS) are using vim or emacs with maybe some supplemental find/grep/sed/awk.
That’s an assumption which advantages Java by default. Other languages may be able avoid having a large code base entirely. I recall reading some stories of people rewriting giant Java code bases into very small Clojure programs that were more flexible, maintainable, and scalable.
I must also admit I woild rather ten lines of code and be clear and certain, so being verbose doesn't bother me much.
This isn't a secret, and in certain corners not even considered generally true -- backlash against microkernels and microservice architectures have intensified in the last half-decade, to give two examples.
I don't see a reason an eCommerce framework couldn't have a small footprint. The lower bound is the amount of actual, necessary complexity in the problem space. But that's usually orders of magnitude less than the size of the codebase, because of all the boilerplate and greenspunning that happens in Java, Enterprise Java in particular.
(Note that in commercial work, I moved from Java to Common Lisp, so I have a different view than typical about just how much boilerplate and repetition can be simplified and removed when your language lets you do it.)
I'd rather have 1M lines of Java than 100k lines of JavaScript, and if essential complexity took 1M lines of Java to flesh out, you aren't reaching parity with 100k lines of JS.
Why not? I can easily see reducing SLOC 10-50x by switching from Java to JavaScript.
> if essential complexity took 1M lines of Java to flesh out
I very much doubt that in a 1M SLOC Java codebase - at least in pre-Java 8 codebase - the essential complexity is any more than 1% of that. My work experience with Java suggests that the language and the surrounding ecosystem and practices promote extreme proliferation of accidental complexity.
I wouldn't mind seeing the other side of the fence though, I will admit I have worked in enterprise codebases for most of my career so I am definitely biased.
I recently rewrote a c# project that had not been completed in elixir. Hugely, anecdotal (as all of these types of stories are) but the project went from ~5kLoC to <800LoC when rewritten in elixir. That's after the elixir app was feature complete too. Now you might believe that's a developer experience gap, but the c# dev had way more experience with c# and in my experience this other dev is much better than me generally. Within a week of learning elixir he was catching my bugs all over the place.
In comparison, my current project in TypeScript is at this stage now and by using an IDE things are quite under control. Refactoring is easy and even major architectural changes are manageable (we did ~5 so far, depending on how you count).
Wasn't the case with Erlang. We only managed to do one such major change and it almost killed the project. It probably even actually killed the project as we were late to the party and failed afterwards.
I can rewrite any bad written code in less lines, same language same framework as initial code.
Ruby Example:
Ruby projects tend to always have really well maintained test suites. The language makes it easy for you to introduce non-obvious bugs or write unreadable (yet efficient) meta code.
This has lead to 1) very mature testing frameworks (meh) and 2) high adoption and usage of these frameworks (actually impressive).
Funny enough, I think adoption of IDEs is extremely low in the Ruby world. 90% of the developers I interact with in my city are on Sublime/VSC/Vim. I've seen some pretty impressive usage of Rubymine that has made me curious, but never bothered to really spend time with it myself. I'd be really curious what the teams at GitHub/Stripe/[Insert big Ruby company] typically use editor-wise.
How does it compare to tmux/screen?
* Eclipse has a crappy UI and can't handle Java9+ code very well, but can handle large code bases
* IntelliJ Ultimate has nice UI and assistance, but it totally fails with large code bases and still tricks you if you run code inside the IDE, especially when you want to use jigsaw modules and not classpath, as it uses by default
Unfortunately, using Java with other editors (vim, VS Codium, etc) is just difficult
I think the conundrum of java is not quite the same thing as strictness.
I'll give you this though: Old Spring and JSF code could be really bad, I remember updating two different XML files every time I changed something in a controller.
Edit: even back then with all that boilerplate it was really nice compared to legacy Delphi where it took three full days with an expert to find and install all required dependencies before one could even start compiling the old source.
By comparison my Java dev environment was up and running in less than half a day with Ant, and when we introduced Maven a few months later setip time for a new developer was mostly the time it took to check out code, install dev databases (yep, back in the days) and set necessary environment settings. (Protip: many people hate NetBeans but one thing it got more right than all other Java IDEs AFAIK was accepting the pom file as the truth instead of creating its own.
That said, the code is usually there for a good reason and I'm extremely thankful for it in the instances where things are refactored and the compiler catches it, or some deviation from the norm comes up and the scaffold allows for doing things the way you need.
Not that I think that's a bad thing. I prefer it to "magic" (I never got on with Rails because of this... what just happened? why did it do that? where does it say it should do that?).
And you never know when you have a class/use case that needs the boilerplate to change. Having it the same 99% of the time so that you have the option to change it for that 1% edge case is a good thing, imho.
That said, I think OOP education focused a lot on inheritance during Java's rise and that was mostly a mistake in hindsight.
Maybe if you compare to Java from 10y ago. Have you ever tried to print a map in an alphabetical order of its keys?
The only thing more verbose in Java are getters/setters which hopefully will disappear once records are available.
With Spring you get to ask all these questions...with all the boilerplate of Java as an added bonus!
Are you using an MVC web framework, or something like that? Asking because I'm curious about other people's experience with Rust.
Writing code without dependencies/frameworks that have a required structure is a joy, however.
Sometimes project is big enough to have a lot of code, boilerplate or not. And then all that IDE automation suddenly becomes essential.
I’ve been to some huge Scala codebases, it was much harder to navigate or make changes there than shove around Java cruft with IDE.
I like being able to consider a shell to be the lowest common denominator for a codebase. If basic shell commands, environment variables, a vanilla vim/nano editor etc... can’t be used to hack on your system then I think it’s far too coupled to tooling.
i.e. if you can't (realistically) choose your own tools, then your experience with the ecosystem is that of your experience with the tool. No tool can be a good experience for everybody.
I did not read it as "everything should support the UNIX IDE" as much as expressing a preference for it and that it's a red flag if you can't even use standard vanilla tools (e.g. grep) to do basic things in an ecosystem.
I think it's fair to say that you should be able to edit code in your editor of choice and not have a terrible time.
So if for language X, if I as language designer want to provide the same experience as Smalltalk, either I consider the interaction with IDE workflows, or I design command line tooling to go along the language (e.g. Go).
And the command line experience comes back again to the universe where it is more prevalent, POSIXy systems.
So I don't think I am making false assumption here, as one cannot have it both ways.
The above does not evince a fear of modern text editors. Nor does it equate to clinging to 1980s technology. Why do you think it does?
Just because something was created long ago does not mean it hasn't improved. Vim and Emacs are both extremely featureful and powerful editors and many people prefer to be able to pick and choose specific tools that do their jobs extremely well.
It's not about using "old" technology - it's about using a tool that excels at its targeted task, and letting you choose the best tool for ancillary tasks like searching, formatting, testing, debugging etc.
I use Sublime with a handful of plugins, linters, etc... enabled. But if someone wanted to hack on my projects with Notepad they totally could.
This distinction is reflected in even small design details. For example Java programs are comparatively slow to boot up. If you're mostly used to the Unix way of repeatedly piping the STDIN/STDOUT of small utilities in an interactive command line, this is painfully inconvenient.
But if your workflow involves a monolithic server that manages every component of the business logic in a single process, then 5 second boot times aren't really a big deal. Because maybe you bounce the server once a week at most.
This always confused me as a newer developer. Vendors competed with each other with different JavaEE implementations (WebSphere, Glassfish etc) that all conformed to the same interface and provided everything including the kitchen sink, but what really differentiated the products were vendor-specific features that require you to go outside the interface, but if you tried to use that stuff, another senior developer would tell you that we can't marry our implementation to a specific vendor... meanwhile, literally every company you go to uniformly uses Spring and Hibernate, and both will inevitably require you to not pretend that they're dumb implementations of javax.inject and javax.persistence.
Small programs can be written in java. It's a general purpose programming language. You could write grep in java.
But I thought we were talking about Java:
"For example Java programs are comparatively slow to boot up."
An example: if I want to refactor a simple thing like Rename a method called “doFoo()” I can do this trivially.
Now, this seems simple but it’s not - I only want to refactor uses of this particular method, NOT any other methods that happen to be called “doFoo()” on unrelated classes or interfaces. Moreover, I want the refactor to intelligently work on everything, including inherited classes, abstract classes and interfaces, while correctly ignoring the stuff i mentioned previously.
None of this matters much if you have a small codebase and a fee developers, but on a huge codebase with many developers, all that boilerplate lets people quickly understand new code and easily refactor that code while being sure they did not break anything.
The real reason to move away from Java is to move towards different software architectures (microservices!)
This is a SpringBoot microservice:
@RestController public class BasicController {
}I'm not sure it's any more verbose than the equivalent Ruby on Rails microservice (for example).
I don't believe this to be true at all, especially for large code bases. Often the people who support this position have not worked on 100k, 500k, 1m LOC applications I've found.
Boilerplate makes it much more difficult to read and scan large swatehs of code. I think when people say boilerplate 'helps', they often noticing a correlation between the fact that many strongly typed systems happen to have excess boilerplate. I believe it's the typing that helps, not the boilerplate.
That being said, IntelliJ's IDEA is fantastic at hiding a lot of the boilerplate in Java (like getters and setters and all those import statements at the top of the file) and modern Java is FAR FAR better about being less verbose than it was in 1.0
Java also has Lombok (https://projectlombok.org/) that hides almost all of the repetitive stuff completely.
The idea is that you hire an army of marginally qualified developers through an agency that are making $14-18/hr in the US and have them churn out code.
To use your house example, it's like addressing clutter by having 5 children and making them clean.
https://en.wikipedia.org/wiki/COBOL#COBOL_2014
The result is an excellent toolchain. But also you are completely unproductive unless you master said toolchain.
The best programming languages for developer productivity are those that are designed with tooling in mind, instead of grammar + semantics and then we see later how everything evolves.
Another problem i had with java: most projects are impossible to understand by looking at the source code, as they tend to do things like Spring or EJB: with delayed loading where class names are specified in some configuration files - and it is often hard to figure out how the system is initialized.
That leads to another question: Is it possible to learn about an unfamiliar java project from source code in this day and age?
i mean they used to say that perl is a write only language; but enterprise java isn't very readable either.
Yes it is, but it helps a lot if you're familiar with the framework and the original developer used sensible conventions. I guess that's the same with any language though.
[1] https://github.com/google/auto/tree/master/value
> using groovy is amazing, and can reduce your lines of code quite a bit
?
So you still have those libraries and frameworks, and its a similar strategy to using Groovy, but presumably doesn't have the same performance penalty as it generates idiomatic Java code.
Have you considered that you might be doing it wrong?
Good quote. I used to struggle with this feeling when I got to roughly the ten year mark professionally and finally started to feel like I knew what I was doing. But, with the help of a little Stockholm Syndrome perhaps, I've learned to embrace the boilerplate as part of my workflow and use it to an advantage. I came up with a saying "write twice, debug once", from the old carpenter's saying "measure twice, cut once". The idea is that if you're writing sort of the same thing twice, the compiler and/or PR reviewers can at least check if those two things are consistent with each other, and debugging is more straightforward instead of a long slog. You may still write the same thing wrong twice, but that's much harder to do than writing it wrong once. I think this is a hidden value of automated testing as well. I could talk for hours about whether automated testing is "worth it" depending on the project/language/team/etc., but one thing most people don't consider is the value in the mere act of writing out some logic that must be consistent with other logic, and the inconsistencies you can find without even having to run the test. Essentially another "write twice, debug once" scenario.
I know that's all kind of vague, and I don't have time to get into specific examples, but this encapsulates the feeling I have now working with Java. The feeling that I'm sort of fact-checking myself, even though it's tedious. Sort of a "show your work" thing.
Each concatenation will create a new string. That is not good.
If you find Java verbose, just use Scala or Kotlin.
[1] https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.htm...
ie A lot of the 'boiler plate' is stuff that allows the compiler to check for errors, and humans understand the code but at the same time allows you to vary behavior if you want.
Java also has language simplicity and consistency [1] ( few syntax short cuts ) - which helps humans understand code - the coding equivalent of 'plain english'.
Whether you see that as a good thing or not depends on whether you are writing self contained scripts or large long lived complex systems.
[1] the later bolted on generics, not so much.
I have a much more constructive approach: people in this thread should spend as much time and effort learning correct Java as I've spent learning correct Python and Javascript.
In fact a lot less should do: I came to Java with a dislike (school did a very poor job lf teaching it and I already knew enough from other languages to see how hopeless what school taught me was).
I kept programming my favourite language but after seeing the immense benefit of having a working type system and a good ide I mostly left lther languages behind until modern .Net became cross platform and also I had to start doing frontend work.
The Java 'enterprise' era is over. Java 8 onwards is much more expressive, functional and concise. Java is no longer what it once was, nor is it the domain of the enterprise nerds any longer.
https://news.ycombinator.com/newsguidelines.html
Not sure why a "just plain data" approach couldn't have been taken so that instead of:
You could've had something more...Annotations work really well: they are easy to write, easy to write and typesafe enough.
In other cases where you actually want and need to test that the annotations works at runtime you use a test container (Arquillian was the big thing for integration testing JavaEE last time it was relevant to me, and since we are talking enterprise Java there's a fair chance that Arquillian will still work and maybe even still be the most popular solution.)
In each case (and presumably many more) you can just go and write a unit test for each of these disproving them.
argparse4j has existed forever and its a port of python's argparse [0].
[0] https://argparse4j.github.io/