1
\$\begingroup\$

My gut tells me this is poor practice, so, I thought I would ask for some feedback.

The goal here is to fake a fluent API design for any class without using reflection while keeping it as pretty as possible. If I were to use reflection, I would just use jOOR.

No need to comment on null-checks. I have omitted them in this post for the sake of simplicity.

Class:

public final class Fluent<T> {
  private final T object;

  public Fluent(final T object) {
    this.object = object;
  }

  public Fluent<T> with(final Consumer<T> consumer) {
    consumer.accept(object);

    return this;
  }

  public T get() {
    return object;
  }
}

Example:

final MenuItem item = new Fluent<>(new MenuItem("Foo"))
                                   .with(o -> o.setAccelerator(accelerator))
                                   .with(o -> o.setOnAction(this::callback)).get();
\$\endgroup\$
4
  • 1
    \$\begingroup\$ Haven't worked with Java in a while so might be completely wrong, but shouldn't the Supplier be Consumer in with? \$\endgroup\$ Commented May 17, 2020 at 3:39
  • \$\begingroup\$ Yes, I copied my code over wrong, thank you. Editing... \$\endgroup\$ Commented May 17, 2020 at 3:40
  • 6
    \$\begingroup\$ Dont take me wrong, but what is the true goal here? One usualy does not code just for the code itself, one usualy has a problem to solve. This does not seem to be any problem at all. You can just call those methods directly on the menu item... \$\endgroup\$ Commented May 17, 2020 at 4:59
  • \$\begingroup\$ Aside: Kotlin has this pattern baked in: journaldev.com/19467/kotlin-let-run-also-apply-with \$\endgroup\$ Commented May 17, 2020 at 14:21

4 Answers 4

3
\$\begingroup\$

Yes, your implementation is sound and possible...however, it doesn't make any sense to have it in the first place, at least not for your example.

final MenuItem item = new Fluent<>(new MenuItem("Foo"))
                                   .with(o -> o.setAccelerator(accelerator))
                                   .with(o -> o.setOnAction(this::callback)).get();

Compared with:

final MenuItem item = new MenuItem("foot");
item.setAccelerator(accelerator);
item.setOnAction(this::callback);

That's less code to type, easier to type and a little bit easier to read.

And if you wanted to be a little bit more...uh...fancy, you could simply use the double-brace initialization for non-final classes at least:

final MenuItem item = new MenuItem("foot") {{
    setAccelerator(accelerator);
    setOnAction(this::callback);
}};

No need to comment on null-checks. I have omitted them in this post for the sake of simplicity.

Please don't the next time.

\$\endgroup\$
4
  • \$\begingroup\$ Thank you for your constructive criticism. I do agree that the example is insufficient. However, the double-braced initialization will not work for final classes, so, it will not fulfill my goal. \$\endgroup\$ Commented May 17, 2020 at 13:57
  • \$\begingroup\$ Ah, I see, never used that before so I didn't know, will edit the answer. \$\endgroup\$ Commented May 17, 2020 at 16:35
  • \$\begingroup\$ The so-called "double brace initialization" is an anonymous subclass with an object initializer. I have never seen this "solution" on par with the solved problem, i.e. creating a complete CLASS for a single object with concrete parameters. \$\endgroup\$ Commented May 18, 2020 at 8:38
  • \$\begingroup\$ @mtj I don't like the syntax either...but, that's just me. Whether that matters in the use-case is a completely different question. \$\endgroup\$ Commented May 18, 2020 at 16:15
2
\$\begingroup\$

I get the feeling you just want to call the setters in a chain to spare you from writing item. and ; before and after every call, instead of actually having a fluent interface.

Does this actually make your code more fluent and more maintainable or are you introducing foreign concepts that confuse the people who read it the future? Will you even remember what that code does in 6 months?

I would say that your gut feeling is right here. You're jumping through hoops to not make much of an improvement.

\$\endgroup\$
3
  • \$\begingroup\$ Thank you for your feedback. You just described fluent programming in your first paragraph. You are right, I am going with my gut. \$\endgroup\$ Commented May 20, 2020 at 4:18
  • 1
    \$\begingroup\$ No, he didn't. Fluent programming isn't that. It's a lot more nuanced and detailed and simply having a wrapper add a 'with' command isn't the same thing at all. \$\endgroup\$ Commented May 20, 2020 at 9:12
  • \$\begingroup\$ Method chaining is an integral part of fluent interfaces but an interface isn't fluent if the methods do not form a domain specific language. Also, "fluent interfaces are evil" is a good search query for studying the topic. :) \$\endgroup\$ Commented May 20, 2020 at 9:22
0
\$\begingroup\$

I wrote code like the question once for a Fluent Builder which also accepted Consumer as parameter to allow callers to provide a set of opaque modifications determined earlier. In a menu example this might be:

new Fluent<>(new Menu("File")))
  .with(o -> o.setAccelerator(accelerator))
  .with(contextSensitiveMenuItem)

This rapidly required a varargs call as the opaque-items are variable in number

public Fluent<T> with(Consumer<T>... consumers) {
  for (consumer in consumers) {
    consumer.accept(object);
  }
  return this;
}

Generically, I'm not sure how useful final parameters are in this context. THe compiler will figure that out itself.

\$\endgroup\$
0
\$\begingroup\$

You seem to have grasped some of the mechanics of a fluent interface without grasping the fundamentals around purpose or fit (maybe your gut has, but your mind has not yet reached this understanding). When Evens and Fowler first document this pattern, their intent was to make code clearer and more readable by allowing methods to be chained together.

Your code, while technically correct, is not clearer or more readable. It adds extra verbiage and doesn't remove the object.setter() syntax at all. It simply obfuscates it by hiding these calls in the heart of a functional interface. Carefully consider the point in bold and italics because it's really important here -- you haven't removed the thing you were trying to remove. This code fails in its most basic purpose, as well as making things considerably worse.

This is a very bad idea. As @Bobby said:

final MenuItem item = new MenuItem("foot");
item.setAccelerator(accelerator);
item.setOnAction(this::callback);

Is much simpler, clearer, and easy to read. If you wanted to make this fluent you would change the setters to return this and then you could get something like:

final MenuItem item = new MenuItem("foot").setAccelerator(accelerator).setOnAction(this::callback);

However, in this specific case, you would be better off with one or more constructors to cover the most common use-cases.

The purpose of code is 2-fold. First, it has to execute correctly, and second, it has to communicate its purpose clearly to future engineers. Code is read many more times than it is written, and it is this fact that is constantly prominent in a senior developer's mind while coding.

\$\endgroup\$

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.