Blog Infos
Author
Published
Topics
, ,
Published

In a large team, developers often find themselves in a situation where previously very similar entities, which might even pass the “duck-test”, begin to diverge in meaning and implementation over time, but still retain similar properties. That’s unpleasant in a single-module application, but in a multi-module one… In short, if you thought that multi-modularity only makes life easier, by the end of this article I will try to convince you that multi-modularity makes it harder in at least one aspect.

Convergence
What is convergence?

First of all, let’s figure out what convergence is and why I used this term. Mainly because the word sounds smart. The second reason is just as important. It’s a perfect word to describe the essence of the problem.

So what is convergence? It’s a term from biology that means two biology species have independently acquired similar properties. They could do this for various reasons and ways. For a better understanding, let’s take a look at a few maximally punchy examples.

Examples of convergence

Check out this beautiful but now extinct animal — Tetrapodophis.

Long as a rope, with four short paws and a snake-like face. Who could it be? The first thought that comes to mind is some kind of snake ancestor. Alas, no! In fact, it is phylogenetically closer to mosasaurs than snakes. BTW, mosasaurs look like this:

At some point in the past, the ancestors of Tetrapodophis, like the ancestors of snakes, opted to inhabit the less crowded niche of the forest floor, where a long body and narrow head offered distinct advantages. And if Tetrapodophis and snakes look like two peas in a pod (well, almost) at this stage of evolution, their paths later diverged. The snakes began to look familiar, but Tetrapodophis generally extinct as unnecessary.

Here you are another example — Prionosuchus.

Well, it’s definitely some kind of crocodile — a long mouth, scales, and four stubby legs. But that’s not true. Prionosuchus is more just like an overgrowth and underdeveloped frog. Never before or since have the frog and the crocodile been so similar.

I’ve given you these examples to keep in mind that “yes, at some stage of evolution they may have been very similar creatures, but they were not anymore before or after, only at a certain point in time it happened that two different groups of animals had similar properties”.

But let’s move on to the code. After all, we are programmers here.

Code convergence

In programming, convergence is usually used to refer to a situation where several programmers independently reach similar code solutions. Let’s look at a rather striking example — a programmer reinvents the wheel without knowing that there is already a solution to this problem in the SDK. And that wheel ends up being very similar to what is in the SDK, but internally the implementations may differ and behave differently in some situations. I think everyone has experienced it in one way or another.

Today I’d like to take a look at a slightly different aspect of convergence — when we have a multi-module application and different developers are responsible for different modules. Convergence can then become a problem.

Problem

Let’s take the standard example of animals which is pretty appropriate here. We take a bird and a dragonfly. They are absolutely different creatures, but they share one important property — they both fly (yes, I know it’s not a property but rather a skill, however it’s a good illustration). With a flick of the wrist we create two classes: Bird and Dragonfly.

class Bird {
}

class Dragonfly {
}

Now we must somehow give them the ability to fly, which is shared between classes.

Single-module application

What would we do in a typical single-module application?

In a kindergarten they teach us that the best solution in such a case is the creation of an interface that each of the classes will implement. But if you think about it… Is this really the right solution? Of course it is! Don’t argue with the kindergartener!

That’s why we also create a separate interface that will be responsible for the flight — Flight.

class Bird : Flight {
}

class Dragonfly : Flight {
}

interface Flight {
}

From the perspective of a single-module application, everything is fine — we can package the classes where each one will slowly flesh out.

Multi-module application

But let’s try to break our solution into modules. For simplicity, we won’t mention that we have different types of modules — api, feature modules, libraries, etc. I’ll probably talk about them later. We just have the modules. That’s enough for now. The fact that the modules are of a certain type won’t solve the problem.

For example, the logic of the dragonfly has become so complex that it requires a separate module. Same with the bird. Now we have to place the Flight interface somewhere so that both modules will be able to access it.

We can create a separate module for Flight.

But a separate module for the interface is a little bit too much, don’t you think? After all, this is only one example with flight. Generally, there can be an infinite number of such common properties. Creating a separate module for each of these interfaces is a pretty questionable solution. Too many modules complicate the project structure and its build, and navigating through a thousand or so half-empty modules is not a pleasant activity.

So how to get out of this situation? We can create a separate module for all property interfaces. This will give us one huge module with all the properties. And in many ways it is even worse than having a lot of separate modules, because we got what can be called a God Module (it’s like there’s a God Object that’s an anti-pattern and… well, you know).

The problem is that the bird-module, for example, knows about properties it doesn’t need at all and others that it should never have known about, such as the ability to grow six legs. Have you ever seen six-legged birds? Neither have I. But that’s normal for dragonflies. So we let it access the module for the sake of the Flight interface, but in fact, the bird-module can access all the available properties. But as we know, one of the key features of multi-modularity is the ability to access classes only on “request”. For access, the communication between the modules must be explicitly specified.

As an analogy — it’s the same as if we wanted to give someone access to, say, a photo from a company event, but the only option was to give access to all of our photos. That doesn’t sound very safe.

So what can be done about it? For example, it is possible to group all properties related to “sky” creatures, including flight, in one module, and all properties related to insects in another module.

And it’s actually kind of a nice solution. On the one hand, we don’t have hundreds of half-empty modules, but on the other hand, you can’t just access random things. Going back to the photo analogy, instead of giving access to all photos, we now give access to a specific album, for example, “New year”.

But we have a problem with this solution as well. In fact, there is no hard and fast rule for how to divide it into modules. Everything depends on the decisions of a particular developer, and everyone has a different way of thinking. He may not want to create a sky-module and an insect-module, but rather group the properties together in a different way, for example, a fly-module and a walk-module. A reasonable number of such inconsistencies in vision can accumulate when there are many developers.

Let’s look at a real-life example.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

Real-life example

Let’s say that we have an abstract app for the submission of real estate listings. We have the object of the offer itself — Offer, and the object of the user who has posted this offer — User.

The offer has an image. This image has three versions: the full one, so you can zoom in and take a closer look, the medium one, and the small one for use in previews. As people sophisticated in application architecture, we will create a separate data class for this image because we are looking towards the future, and in the future an offer may have more than one image. Let’s name the data class Image, and there will be a separate property with a link for each version of it.

data class Image(
  val bigUrl: String,
  val mediumUrl: String,
  val previewUrl: String,
)

Now, we simply begin using Image in our offer object.

data class Offer(
  val title: String,
  val image: Image,
)

Everything seems to be cool and logical, but somewhere in a completely different part of the application, a completely different developer starts creating a section with a list of users, where each user also has an image that can be three sizes: full, medium and small…

Single-module application

It would be pretty straightforward if we had a classic single-module app — we reuse the data class Image for both the user and the offer.​​

data class User(
  val name: String,
  val image: Image,
)

Perhaps it would be beneficial to put Image in a separate package. In general, the scheme is as follows:

Multi-module application

In a multi-module application, we have separate modules for offers and for users. So should the Image be put in a separate module as well? The situation is very similar to the Flight interface.

So we put Image in some third module. It is up to the user-module developer to decide which one to put in.

And everything seems to be fine: there are feature modules that look at some generic module and use its data class Image. But… Three months later, it turns out that we need one more image size for the offer object — the original one. It will be required to use the photo download feature. The backend developer of the offer feature adds another property, originalUrl, to the image object in the response of the method that returns the offers.

The mobile developer of the offer feature is of the opinion that it is basically a half-hour task — to create a download button and add a new property to the data class Image. But it must be made nullable, since no one has changed the image for the user. Well, it’s not a big deal, there’s no need to change the entire logic with modules just for one field.

data class Image(
  val originalUrl: String?,
  val fullUrl: String,
  val mediumUrl: String,
  val previewUrl: String,
)

Another month goes by, and it is decided to add another image format to the user feature — a circle. And it is chosen to make the image round on the server to avoid loading the client. The developers of the user feature follow the same logic as the developers of the offer feature, and we end up with a new nullable property in the data class Image.

data class Image(
  val originalUrl: String?,
  val circleUrl: String?,
  val fullUrl: String,
  val mediumUrl: String,
  val previewUrl: String,
)

As a result, we have two nullable properties in Image. To a third-party developer not previously familiar with either the offer or the user features, it will be completely unclear which case is null and which is not. He will have to figure it out or add a bunch of comments to the code.

The image example doesn’t look very scary. And I think it’s not hard to imagine what will happen to these objects in the future when there are not two, but five or ten features that use them. Things will only get worse and we’ll end up with a monstrous object where it’s not even possible to understand in which case and which fields will be null.

In a single-module application, however, it’s not that bad: all we need to do is duplicate the data class Image, which doesn’t take much time. But in a multi-module application it can become a problem, because everything isn’t limited to a regular copy — we have to deal with the hierarchy of modules. In reality, it can be a lot more complicated than just a direct connection. For example, the offer-module and the user-module can transitively connect to the module that contains Image. Additionally, it is possible to open the gallery screen from the offer screen, where Image must be passed. And that’s when things will get really tough.

Essence

I don’t want to say that these situations only happen with multi-modularity. This also happens in a single-module application. But the cost of such a mistake is high in a multi-module app. Instead of half an hour, it may take several hours or even a whole day to solve it. And all of this is done to deal with the connections of just one object, which is used everywhere. So we should be vigilant to avoid such situations in a multi-module application.

How does this happen?

The thing is, at some point in their “evolution”, image objects for the user and for the offer looked the same. Further, their paths diverged because they are essentially different objects.

We had a similar situation in our application with the offer object — Offer. Both the offer display screen and the offer creation screen used the same class. Then that class was used in several places again, and again, and so on. It turns out that even its id is nullable, not to mention that it takes almost 3000 lines (in Java). The number of uses of the class in the project is more than 1500, even though we’ve been actively moving away from using it lately.

This makes it difficult to work with: to add a new field — a problem, to rename something — a problem, to understand wherever a field will be null or not — a problem, and even to stop using it — a problem. All this breaks the module hierarchy and structure. And this is because, at some point, the offer objects for the offer display screen and for the offer creation screen were very similar, and the developers decided to use only one object. But neither before nor after were they so similar. On second thought, they weren’t meant to be similar, they just happened to be. Just like the animals I mentioned earlier.

It seems to me that this is all due to the natural desire of every programmer to reuse as much as possible, coupled with the pressure of Don’t repeat yourself (DRY) principle, which makes a “young developer” reluctant to create two identical objects.

What should we do?

Think. And then think again. It doesn’t sound like a solution, does it? However, no matter how much I think about it, I always come to the conclusion that before trying to reuse a class, it’s worth asking yourself a few questions — “Are they essentially the same object? Or are they just similar at the moment?”

There is a nice exception with the objects that we use to communicate with the server. Just ask the backend-developer. If you have different tables on the server, you should definitely create different objects.

In this article, I wanted to demonstrate a fairly common problem for which there is no perfect solution. Which, moreover, is greatly complicated when multi-modularity is used. I’m sure all developers face it in one way or another. This problem isn’t limited to the use of data models. Something similar can be found with the reuse of utilities, the enrichment of classes, methods and so on. By framing the problem for ourselves and coming up with a name for it, we were able to get rid of the verbosity in discussions and CodeReview by simply asking the question: “Aren’t you getting convergence here?” What do you think? Have you ever faced such a problem? How did you solve it?

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE
Menu