On the topic of verbosity of the Java code, are th...
# java
g
On the topic of verbosity of the Java code, are there any initiatives / ideas how to make that better? Other than using Kotlin or Typescript I mean 😉 I found some references to Lambda-based builders in Github issues which would go a long way by hiding the
*Args
/
builder
/
build
triad.
A quick idea I had was to make sure that for every methods that accepts an FooArgs to also have a method that accepts FooArgs.Builder instead. So instead of
Copy code
var storageAccount =
    new StorageAccount(
        "%s%s%s".formatted("sa", APPLICATION_NAME, stackName),
        StorageAccountArgs.builder()
            .resourceGroupName(resourceGroup.name())
            .sku(SkuArgs.builder().name(Standard_LRS).build())
            .kind(StorageV2)
            .build());
you'd have
Copy code
var storageAccount =
    new StorageAccount(
        "%s%s%s".formatted("sa", APPLICATION_NAME, stackName),
        StorageAccountArgs.builder()
            .resourceGroupName(resourceGroup.name())
            .sku(SkuArgs.builder().name(Standard_LRS))
            .kind(StorageV2));
Also with all the nesting it would be good to investigate the Nested Fluent Builder pattern, for which I'm not sure what the authoritative definition / description is but this article goes into some detail: https://theagilejedi.wordpress.com/2016/10/21/nested-fluent-builders-with-java-8/
Nested Fluent Builder can have the disadvantage though of hiding the nested structures, which you might want to have visible in your code
s
Fluent builders looks more like an anti pattern, to actually use it - you’ll need to manually format your code which by itself is no-go.
Only other option for java to make code less verbose is Annotation Processors or compiler plugins 🙂
but event then it would be verbose
Copy code
//Annotation Processor will throw error if var names in nested classes does not match the schema

@StorageAccount("sa-app-dev") 
@Kind(StorageV2)
class AppNameStorageAccount extends AbsStorageAccount {
  @ResourceGroup class DevResourceGroup {
    var name = "app-dev-group"; 
  }
  @Sku class SkuDev {
    var name = Standard_LRS
  }
}

usage

new AppNameStorageAccount().outputs()
👍 1
g
I agree that step-wise / nested fluent builder get confusing, either you accept the linearity, then it's very difficult to follow the nesting even or you format manually which is prone to be formatted wrong and won't help you that much. Let's look at a few modern and widely-used examples. First thing that comes to mind is the Spring WebClient class, which is itself a builder (I'm not talking about WebClient.Builder!): https://github.com/spring-projects/spring-framework/blob/main/spring-webflux/src/m[…]org/springframework/web/reactive/function/client/WebClient.java It takes you step-by-step through the request definition, headers, response and at the end you build a fully defined spec that can be executed. Typical use is something like in the screenshot (it's a screenshot so you can see the different return types of the chain).
Now of course this pattern has its fans and its haters, but it is quite commonly used and I would say it's idiomatic. I'll give another example.
RestAssured is also kind of a DSL to define http calls, this time with a focus on (integration) testing. You can find some example uses here: https://github.com/rest-assured/rest-assured/blob/master/examples/rest-assured-ite[…]t/java/io/restassured/itest/java/GivenWhenThenExtractITest.java
If we ignore the nested / chained builders for a minute, I mentioned a way to save a bit of boilerplate at the beginning of this Thread. It can be combined with another Pattern where you have aliases for the
builder()
-Methods that are named after the object you want to build. The benefit of this is that you can do a static import of the method without name clashes. My example would then look like:
Copy code
var storageAccount =
        new StorageAccount(
            "%s%s%s".formatted("sa", APPLICATION_NAME, stackName),
            storageAccountArgs()
                .resourceGroupName(resourceGroup.name())
                .sku(skuArgs().name(Standard_LRS))
                .kind(StorageV2));
It should be possible to implement these two simplifications in an optional, backwards-compatible matter.
Just found this issue, so my first suggestion is already tracked 🙂 https://github.com/pulumi/pulumi-java/issues/244
👍 1
b
thank you for this thread, as an important context I'd like add that a very important limitation for us is the physical size of the generated code, e.g. full schema azure native provider is +11M LOC, at this point we tend to find issues in build tools and compiler or the javadoc, so adding even one character that would be repeated a million times might make a provider un-buildable
g
Oh, I see!
You mean the java code, right? Not go?
b
yes, the generated java code for a provider
g
Do you have examples how the problems look, maybe in a publicly available pipeline somewhere? It sounds like an interesting problem.
b
this PR (https://github.com/pulumi/pulumi-azure-native/pull/1920) looks like a good entry point to a conversation about this with @broad-dog-22463 :)
g
I'll see if I can contribute some insight there
On the other hand, the cdktf provider jar is even a bit larger https://repo1.maven.org/maven2/com/hashicorp/cdktf-provider-azurerm/2.0.10/
b
yes, full azure schema has a lot of API surface ;)
g
The problem in that PR is exactly what's described in https://github.com/pulumi/pulumi-azure-native/pull/1920#issuecomment-1222673752 - I could get it compiling just fine locally and the Javadocs were also generated
The way the code is generated right now has a lot of benefits, the parts that users interact with directly are discoverable and simple to read. That's a very good thing. Still, if you are in serious need to reduce the size of the codebase, it looks like the builders and maybe other things could be generated at runtime with dynamic proxies (if you're willing to switch a few classes to interfaces) or using cglib. Annotation processors could also be an option, they would run at compile time, but on the user's machine. This might make their compile times unacceptably slow though. In summary, and I'm sure you have already considered a lot of this, the best bet looks to be to split the larger providers into multiple jars. I saw an issue in azure-native for go, which seems to have a similiar problem with the size.
👍 1
b
thank you for your input, I really appreciate it, and yes you are right to assume that we've considered multiple approaches, in the end we've decided to go with the simplest thing that can work and to not make the api unfamiliar or complicated at the price of verbosity
👍 1
at the moment the java support does not have many resources for invasive changes, but with enough community participation everything is possible 🙂