📜 ⬆️ ⬇️

Understanding Dependencies

From translator


We are implementers. We have to introduce, not fantasize!
(Rina Green, film "Girl without an address")

Two reasons prompted me to translate this article: 1) the desire to better understand the Spring framework, 2) a small number of sources on the topic in Russian.

The cornerstone of OOP is “dependency injection.” If the description of the process of “introduction” as a whole is satisfactory, then the explanation of the concept “dependence” is usually left out of the brackets. In my opinion, this is a significant omission.


')
In order not to fantasize, but to introduce, you must first understand what we are introducing. And the concise article by Jakob Jenkov "Understanding Dependencies" can help us in this. It will be useful not only to those who write in Java, but also to those who write in other languages ​​and monitor the quality of application design.

UPD: I translated another Jakob Jenkov article on dependencies. Read on Habré a translation of the article Dependency Injection , which opens a series of articles of the same name and continues this article. The articles of the series deal with such concepts as Dependency, Dependency Injection (DI), DI-containers.


Understanding the dependencies



You cannot read a good OOP book that does not mention dependencies, weak connectivity, etc., and there is a good reason for this. Understanding dependencies is important in object-oriented API and application design. However, in my opinion, the subject can be explored much deeper than many books do. This is the purpose of the text. If you are an experienced OO developer, you can already know a lot of things written here. I also believe that many developers will still be able to learn something from the text.

What is addiction?


When class A uses a class or interface B, then A depends on B. A cannot do its work without B, and A cannot be reused without reuse B. In this case, class A is called “dependent”, and class or interface B is called “Addiction”.

Two classes that use each other are called related. Connectivity between classes can be either weak, or strong, or something in between. The degree of connectivity is not binary and not discrete, it is in the continuum. Strong connectivity leads to strong dependencies, and weak connectivity leads to weak dependencies or even the absence of dependencies in some situations.

Dependencies, or connections, are directional. The fact that A depends on B does not mean that B depends on A.

Why is addiction bad?


Dependencies are bad because they reduce reuse. Reducing reuse is bad for many reasons. Usually, reuse has a positive effect on development speed, code quality, code readability, etc.

How dependencies can do harm is best illustrated by an example: Imagine that you have a CalendarReader class that can read calendar events from an XML file. The implementation of CalendarReader is shown below:

public class CalendarReader { public List readCalendarEvents(File calendarEventFile){ //open InputStream from File and read calendar events. } } 

The readCalendarEvents method gets a File object as a parameter. Therefore, this method depends on the class File. The dependency on the File class means that CalendarReader is capable of reading calendar events only from local files in the file system. It cannot read calendar events from a network connection, database, or from classpath resources. We can say that CalendarReader is closely related to the File class and the local file system.

A less related implementation would be to replace a File parameter with an InputStream parameter, as in the code below:

 public class CalendarReader { public List readCalendarEvents(InputStream calendarEventFile){ //read calendar events from InputStream } } 

As you may know, an InputStream can be obtained from an object of type File, from a network socket, a URLConnection class, a Class object (Class.getResourceAsStream (String name)), columns from a database via JDBC, etc. Now CalendarReader is no longer tied to the local file system. It can read calendar event files from many sources.

With the version of the readCalendarEvents () method using the InputStream, the CalendarReader class has increased reusability. Close local file system binding has been removed. Instead, it was replaced by a dependency on the class InputStream. Dependency on InputStream is more flexible than dependency on File class, but does not mean that CalendarReader can be 100% reusable. He still cannot read data from the NIO channel, for example.

Types of dependencies


Dependencies are not just “dependencies.” There are several types of dependencies. Each of them leads to more or less flexibility in the code. Types of dependencies:


Class dependencies are class dependencies. For example, the method in the code box below receives a String as a parameter. Thus, the method depends on the class String.

 public byte[] readFileContents(String fileName){ //open the file and return the contents as a byte array. } 

Interface dependencies are dependencies on interfaces. For example, the method in the code insert below gets the CharSequence as a parameter. CharSequence is a standard Java interface (in the java.lang package). The CharBuffer, String, StringBuffer, and StringBuilder classes implement the CharSequence interface, so only instances of these classes can be used as parameters of this method.

 public byte[] readFileContents(CharSequence fileName){ //open the file and return the contents as a byte array. } 

Dependencies of methods or fields are dependencies on specific methods or fields of an object. It does not matter what the class of the object is or what interface it implements, as long as it has a method or field of the required type. The following example illustrates method dependencies. The readFileContents method depends on the method named “getFileName” in the class of the object passed as a parameter (fileNameContainer). Please note that the dependency is not visible from the method declaration!

 public byte[] readFileContents(Object fileNameContainer){ Method method = fileNameContainer .getClass() .getMethod("getFileName", null); String fileName = method.invoke(fileNameContainer, null); //open the file and return the contents as a byte array. } 

Dependencies of methods or variables are characteristic of APIs that use reflection. For example, Butterfly Persistence uses reflection to detect class getters and setters. Without getters and setters, Butterfly Persistence cannot read and write class objects from / to the database. Thus, Butterfly Persistence depends on getters and setters. Hibernate (similar ORM API) can use both getters and setters, and fields directly, as well as through reflection. Thus, Hibernate also has a dependency either on methods or on fields.

Dependencies of methods or (“functions”) can also be seen in languages ​​that support pointers to functions or pointers to methods that must be passed as arguments. For example, delegates in C #.

Additional dependency characteristics


Dependencies have other important characteristics besides the type. Dependencies can be dependencies of compile time, runtime, visible, hidden, direct, indirect, contextual, etc. These additional features will be covered in the following sections.

Interface implementation dependencies


If class A depends on interface I, then A does not depend on the concrete implementation of I. But A depends on some implementation of I. A cannot perform its work without some implementation of I. Thus, when the class depends on the interface, this class also depends on the implementation of the interface.

The more methods the interface has, the less likely it is that developers will provide their own implementations if they are not asked to. Consequently, the more methods there are in the interface, the greater the possibility that developers will be “stuck” on the standard implementation of this interface. In other words, the more complex and cumbersome the interface becomes, the more closely it is associated with its default implementation.

Due to the dependencies of the interface implementation, you do not have to add functionality to the interface blindly. If the functionality can be encapsulated in its component, in its own separate interface, you need to do so.

Below is an example of what this means. The example code shows a tree node for a hierarchical tree structure.

 public interface ITreeNode { public void addChild(ITreeNode node); public List<ITreeNode> getChildren(); public ITreeNode getParent(); } 

Imagine that you want to be able to count the number of descendants of a particular node. First you may be tempted to add the countDescendents () method to the ITreeNode interface. However, if you do this, everyone who wants to implement the ITreeNode interface will also have to implement the countDescendents () method.

Instead, you can implement the DescendentCounter class, which can view an ITreeNode instance and count all the descendants of this instance. DescendentCounter can be reused with other implementations of the ITreeNode interface. You have just saved your users from the problem of implementing the countDescendents () method, even if they need to implement the ITreeNode interface!

Dependencies of compile time and runtime


A dependency that can be resolved at compile time is called a compile time dependency. Dependency, which can not be resolved before the start of execution - the dependence of the execution time Compile-time dependencies can be seen more easily than run-time dependencies, however, run-time dependencies can be more flexible. For example, Butterfly Persistence, finds class getters and setters at runtime and automatically maps them to the database tables. This is a very easy way to compare classes with database tables. However, to do this, Butterfly Persistence depends on correctly named getters and setters.

Visible and hidden dependencies


Visible dependencies are dependencies that developers can see from the class interface. If dependencies cannot be detected in the class interface, these are hidden dependencies.

In the example above, the String and CharSequence dependencies of the readFileContents () method are visible dependencies. They are visible in the method declaration, which is part of the class interface. The dependencies of the readFileContents () method, which takes an Object as a parameter, are invisible. You cannot see from the interface that the readFileContents () method calls fileNameContainer.toString () to get the name of the file, or as it actually happens, calls the getFileName () method.

Another example of hidden dependency is dependence on static singleton or static methods inside a method. You cannot see from the interface that the class depends on a static method or a static singleton.

As you can imagine, hidden dependencies can be evil. They are difficult to detect by the developer. They can be identified only by studying the code.

This is not the same as saying that you should never use hidden dependencies. Hidden dependencies are often the result of providing reasonable defaults (providing sensible defaults). In this example, this may not be a problem.

 public class MyComponent{ protected MyDependency dependency = null; public MyComponent(){ this.dependency = new MyDefaultImpl(); } public MyComponent(MyDependency dependency){ this.dependency = dependency; } } 

MyComponent has a hidden dependency on MyDefaultImpl as can be seen in the constructor. But MyDefaultImpl has no dangerous side effects, so in this case the hidden dependency is not dangerous.

Direct and indirect dependencies


Dependence can be either direct or indirect. If class A uses class B, then class A is directly dependent on class B. If A depends on B, B depends on C, then A has an indirect dependence on C. If you cannot use A without B, and you cannot use B without C, then you can not also use A without C.

Indirect dependencies are also called concatenated (chain), or transitive (in "Better, Faster, Lighter Java" by Bruce A. Tate and Justin Gehtland).

Unreasonably extensive dependencies


Sometimes components depend on more information than they need to work. For example, submit a login component in a web application. This component needs only a login and password, and it will return the user object if it finds one. The interface might look like this:

 public class LoginManager{ public User login(HttpServletRequest request){ String user = request.getParameter("user"); String password = request.getParameter("password"); //read user and return it. } } 

A component call might look like this:

 LoginManager loginManager = new LoginManager(); User user = loginManager.login(request); 

Looks easy, right? And even if the login method needs more parameters, you will not need to change the calling code.

But now the login method has what I call “unnecessarily extensive dependencies” on the HttpServletRequest interface. The method depends on more than he needs to work. LoginManager only requires a username and password to find the user, but receives the HttpServletRequest as a parameter in the login method. HttpServletRequest contains much more information than the LoginManager needs.

The dependency on the HttpServletRequest interface causes two problems:

  1. LoginManager cannot be reused without an HttpServletRequest object. This can make LoginManager unit testing more difficult. You will need to lock the HttpServletRequest object, which requires a lot of work.
  2. LoginManager requires that the username and password parameter names be “login” and “password”. This is also an optional dependency.

A much better interface for the LoginManager login method would be:

 public User login(String user, String password){ //read user and return it. } 

But look what happens with the calling code now:

 LoginManager loginManager = new LoginManager(); User user = loginManager.login( request.getParameter("user"), request.getParameter("password")); 

He became more difficult. This is the reason why developers create unnecessarily wide dependencies. To simplify the calling code.

Local and contextual dependencies


When developing applications, it is normal to split applications into components. Some of these components are general-purpose components that can also be used in other applications. Other components are application specific and will not be used outside the application.

For general purpose components, any classes belonging to a component (or API) are “local.” The rest of the application is “context.” If the general purpose component depends on application-specific classes, this is called context dependency. Contextual dependencies are bad because they make it impossible to use a general-purpose component outside the application. It is tempting to think that only a bad OO developer will create contextual dependencies, but this is not the case. Contextual dependencies usually occur when developers try to simplify the creation of their application. A good example here is applications that handle requests, such as applications connected to message queues or web applications.

Imagine that an application that receives a request in the form of XML, processes requests and receives XML in response. Several separate components are involved in processing an XML request. Each of these components needs different information, some information has already been processed by previous components. It is very tempting to assemble the XML file and all associated processing inside a request object of some kind, which is sent to all components, in a processing sequence. The processing component can read information from this request object and add information from itself for components that are further in the processing sequence. Taking this request object as a parameter, each of the components processing the request depends on this request. The request object is specific to the application, it causes dependence on the context of each request processing component.

Standard vs custom class / interface dependencies


In many situations, it is better for a component to depend on a class or interface from standard Java (or C #) packages. These classes or interfaces are always available to everyone, which simplifies satisfying these dependencies. Also, these classes are less likely to change and cause a drop in the compilation of your application.

However, in some situations, depending on standard libraries is not the best thing. For example, the method needs 4 lines to configure it. Therefore, your method takes 4 lines as parameters. For example, this is the driver name, database url, username and password to connect to the database. If all these strings are always used together, it may be clearer for the user of this method if you group these 4 strings into a class and pass an instance of it, instead of 4 strings.

Summary


We looked at several different types and characteristics of dependencies. In general, interface dependencies are preferable to class dependencies. In some situations, you may find that class dependencies are preferable to interface dependencies. Dependencies of methods and fields can be very useful, but remember that they are usually hidden dependencies, and hidden dependencies make it difficult for the users of your components to find them and satisfy their requirements.

Interface implementation dependencies are more common than you might think. I have seen them in many applications and APIs. Try to limit them as much as possible, keeping the interfaces small. At least those interfaces that the component user implements. Move additional functions (for example, counting, etc.) to external components that accept an instance of the interface in question as a parameter.

Personally, I prefer the dependencies of the compile time to the dependencies of the execution time, but in some cases the dependencies of the execution time are more elegant. For example, Mr. Persister uses runtime dependencies on getters and setters, which frees your pojo from implementing a persistent interface. Dependencies of execution time in this way may be less invasive than
compile-time dependencies.

Hidden dependencies can be dangerous, but since runtime dependencies are sometimes also hidden dependencies, you may not always have a choice.

Remember that even if a component does not have direct dependencies on another component, it may still have an indirect relationship to it. Less restrictive, but nonetheless, mediated dependencies are also dependencies.

Try to avoid unnecessarily broad dependencies. Keep in mind that unnecessarily broad dependencies arise when you group multiple parameters into a class. This is a general refactoring, which is done to make the code simpler, but as you can see, it can lead to unnecessarily broad dependencies.

The component to be used in different contexts should not have any contextual dependencies. That is, a component should not depend on other components in the context in which it was originally developed and in the one in which it is integrated.

This text only described dependencies. He does not tell you what to do with them. Other texts on this training site will immerse you in this topic ( approx. Transl .: I mean the personal site of the author ).

To top
To a series of articles Dependency Injection

Source: https://habr.com/ru/post/349836/


All Articles