Collaborators and Libraries: Java Design Patterns for Success


My fellow Zone Manager Sam Atkinson wrote an excellent article on Beautiful Constructors. While I would totally agree that the builders in his article are beautiful, I was not so sure that his prescriptions could be universally applied. He graciously allowed me to use his piece as a springboard for a counterpoint, in the hope of a good discussion. Of course, the opinions expressed in this article are my own.

Collaborators

The design style described by Sam seems to work best for a collaborater to classify. I am not so convinced that this is the right approach for a library to classify. Here is what I mean.

In any well-designed modular program, we have more structure than just a “group of objects that call each other”. Classes are part of a larger group and work together within that group to perform a distinct function for the entire program. Ideally, there are very few points where these large groups reach outside the group and interact with other groups. This concept is called “weak coupling, strong cohesion” or “interface width” control.

At best, this style of design means that even in a large application, we only have a few classes that are the entry point for each of these modules that we have defined. They are our collaborators: with regard to the other modules, they represent the whole behavior of the module, hiding the implementation details. It all looks like the interaction of the great powers in the Concert of Europe: each employee undertakes to remain outside the internal affairs of others and to respect their territory.

Employees must not expose their internal condition to other employees; it’s their job to keep it hidden. In addition, employees should not manage any “null” values ​​for other employees, because knowing what to do in the event that another employee is missing usually requires knowing a lot about what that employee is doing. which breaks the encapsulation. Finally, employees should generally not throw exceptions from their manufacturer (except in the case of fatal programming errors) because they are supposed to manage exceptional conditions themselves and not propagate them to other employees. The great powers suppress their own rebellions.

So I’m definitely a fan of simple constructors for a collaborator (with a simple non-zero assertion so we fail quickly):

public class Austria {
private final Prussia prussia;
private final France france;
public Austria(Prussia prussia, France france) {
    Assert.notNull(prussia, france);
    this.prussia = prussia;
    this.france = france;
}

The above non-zero assertion is from Spring; if you’re not already using Spring, don’t bring it just for this claim. (By the way, I think introducing a chain of dependencies to get a simple function should always be known as “padding on the left“.)

Of course, if all the collaborators depend on each other, we cannot strictly use constructors, because we end up with a circular dependency problem. But in many cases, dependencies between well-designed collaborators end up being mostly acyclic because there is a natural order in the application.

Library courses

On the other hand, I don’t think this style of constructor is always the right one for library classes. By library class I mean a class that can be an entry point to multiple classes with related functionality, but a class that is not at a collaborator level because it serves too specific a purpose. Often this goal is specific to the technology rather than the application. A few examples will illustrate the difference.

When I saw Sam’s suggestion to avoid complex constructors and instead have a separate initialization method, a counter example that came to my mind is this well-known library class from Java network programming, java.net.Socket.

client = new Socket("localhost", 12345);

Socket definitely has some complex logic in the constructor, which can throw either UnknownHostException or the most generic IOException. The constructor actually establishes the socket connection!

To me that makes perfect semantic meaning, and thinking about the “why” has helped me understand what I think is the important difference between collaborators and library classes. On throwing a constructor exception, this class says:

  • I only exist to wrap a socket connection
  • If I don’t connect successfully, you don’t have a “Socket”

This appears to be exactly the right set of semantics for a socket class. It’s totally different from a collaborator that you have to create early and keep for a long time. I just wish this class would go one step further and eliminate “unconnected” constructors and the public “connect” method. A better example might be FileOutputStream:

outStream = new FileOutputStream("temp.txt");

This class does not have a public “open” method, so the only way to get an instance of this class is to successfully open a file, and the only way to reopen a closed file is to create a new instance. This seems quite correct because it naturally leads the user to the correct behavior, which is to keep this object only as long as it takes to write to the file. So this class says implicitly:

  • I only exist to wrap an open file
  • If I can’t open the file, you don’t have a file output stream
  • Once you close me my instance cannot be used and must be deleted

This is an impressive semantic richness that we get just by throwing constructor exceptions and not having a public “init” or “open” method. Note that this is all in the Javadoc, but we didn’t need the Javadoc, as we could infer it from the available methods and their signatures. This is the best type of documentation.

Zero values

So what about zero values? Can I justify their use sometimes? May be. I really, really like the design pattern that Sam suggested, which is to have an operation-less implementation of the interface that can be used as “default” by users of the class who don’t need the behavior.

But I don’t think it can be applied universally. I’m not sure what the “no-op” implementation of a transaction is; it seems dangerous to trick users into thinking they have a transaction when they don’t. And often what we need is not a transaction but a TransactionManager; not a link but a ConnectionFactory. I don’t know what a no-operation implementation of either of these would look like.

It comes down to the distinction I made between collaborators and library classes. A collaborator has to combine the behavior of several things, but he does so in the context of a single application. A library class does one thing, but it does it in the context of many different applications. With library classes, it’s pretty much inherent that you want your functionality to be usable in a wide variety of contexts, which means you end up writing code that says, “I’ll use a transaction manager if you give me one, but I won don’t fail if you don’t “.

Conclusion

Hope I added to the conversation here. I think there is a lot of value in thinking about topics like this so I am very grateful to Sam for his article and look forward to reading his, whether in response to this one or on an other subject. Above all, I think that as developers we need to cultivate a aesthetic sense of what a good design is, because it’s easier to “see” a good design when we don’t have to stop and reason about it every step of the way. But the only way to cultivate this aesthetic sense of design is to argue about “what makes design good” and then try to back it up with examples.


Source link

Previous Design the machines that will design the strategy
Next Design models are not blueprints

No Comment

Leave a reply

Your email address will not be published. Required fields are marked *