Different Strategies for Acquiring Dependency inside a Method

When we create or refactor a method, we find some input data is needed for the method to do its job. What are the various options to acquire these dependency? I just went through a major code refactoring and module restructuring, and would like to share some thoughts:

  1. Pass it in as method parameters. This seems to be most common approach, and some considerations are:
    • First forget about implementation details and needs, does it make sense to require the additional data in order to do the work? Think in terms of raw materials for completing the task, as opposed to method parameters.

    • The client code will need to acquire the new data, if not already available, or the target method declares a parameter injection.

    • All subtypes (e.g., subclasses overriding this method) and callers need to be updated to the new method signature. That may not be a problem if we are changing internal interfaces or implementation classes. But for public interfaces, it presents a backward compatibility problem.

    • To what granularity do we want to add the new parameter? Is it a coarse-grained or fine-grained parameter? For example, do we pass in (String name), or (UserInfo userInfo)? Some guidelines:

      • Conceptually, what input is needed for the method to perform its task? Try to reduce the granularity to what's really necessary, to make it usable in more contexts. Some calling code may only have the fine-grained data (e.g., zipCode), but not the coarse-grained data (e.g., userInfo)

      • Be consistent with other methods in the same interface or class. If many peer methods take UserInfo, it makes sense to have UserInfo in the new method even if only part of UserInfo is needed.

      • Unless it's remote invocation, the overhead of parameter passing is the same between a fine-grained and a coarse-grained parameter.

      • Avoid exporting information that is specific to a design tier or implementation layer. If the coarse-grained data fall into this category, then choose to export the fine-grained data that are not tied to the current tier or layer. For example,

        • HttpServletRequest or HttpServletResponse are tied to web tier, and may be passed around during the current request processing inside web tier, but should never be exported to business tier.

        • Security realm instances should not be passed outside security layer.

      • The above point is more evident and enforced by a module system like OSGi. A public class that is not exported by its host module will not be visible to other modules, and so may not be passed outwards. In this case, a fine-grained data type or even a string literal is more appropriate.

  2. Derive from existing method parameters, when the required data is indirectly reachable from existing parameters. For example,

    • String zip = person.getAddress().getZip();

  3. Is it already available as static or instance fields, or inherited from super classes?

    If some data are intrinsic attributes and relationship fields of a class, they should be initialized in constructors or injected via IoC framework and available to be shared by all methods. They consistute the class and instance state. It's possible that a method takes a type of parameter that is already available in class or instance state. They are intended to be distinct objects. For example,
    * @param userInfo a different UserInfo instance than represented by this person.
    * @return true if userInfo is a potential friend of this person; false otherwise.
    public boolean maybeFriends(UserInfo userInfo) {
    return this.userInfo.similarTo(userInfo);

  4. Derive it from existing static or instance fields, for example,

    • String zipCode = this.userInfo.getZipCode();

  5. Is it available from a global registry, or naming service? Typically, the global namespace is initialized and populated upon program start. If subsequent concurrent and write operations are supported, the global namespace needs to be thread-safe. For example,

    • User user = GlobalPlace.getCurrent(User.class);

    • String m = InitialContext.doLookup("config/mode");

  6. Create my own instances, using direct instantiation or some sort of factory method. Use this option if the current method has adequate information and knows how to create. For example,

    • Config config = new Config(mapping);

    • Handler handler = HandlerFactory.createHandler();

    • Logger logger = Logger.getLogger("abc"); //find or create the named logger


Anton Shaykin said...

One question, that I found asking myself a lot lately, is this:
Sometimes a method needs additional data just for logging. Say it operates on UserInfo#id field. But when something goes wrong I also want to get access to UserInfo#name to properly log the exception. Should I prefer to pass UserInfo object in this case? Or maybe passing separate id and name fields would suffice?

javahowto said...

In simple cases, I would choose to pass UserInfo (sort of container object), which encapsulates id, name, etc. But chances are your case is more complex and may not be a clear-cut answer. I updated the guideline section in the post and hope they are useful.

Anna said...

Great and Useful Article.

Online Java Training

Java Online Training India

Java Online Course

Java EE course

Java EE training

Best Recommended books for Spring framework

Java Interview Questions

Java Course in Chennai

Java Online Training India