Arguments against ubermodules

Now we can analyze the argument against this kind of design for larger systems. As the system grows in size, the design trends will continue. Ubermodules like User will grow even more incoming references, change even more frequently, and grow even longer in lines.

Suppose that, ten years from now, rubygems.org is ten times larger. Instead of 25 methods in 159 lines of code, User now has 250 methods in 1,590 lines of code. There are now 90 modules referencing User instead of 9.

(Two quick notes about this imagined system. First, 250 methods across 1,590 lines of code is conservative; ask friends who maintain large legacy Rails apps about their User or Profile classes! Second, we're naively assuming linear growth of User's connectedness, change frequency, and length in lines. In reality, that growth is probably superlinear, but the linear assumption is sufficient for the points made here.)

Changing any method requires analyzing impact on the rest of the system, ideally done in two stages. First, we ask "which functions in this module call the changed function?" We examine each of those functions in case their behavior was affected. Then, we ask "which modules use this module?". Again, we examine each module that might be affected by the change.

This process breaks down with our typical lop-sided designs. In our case, there are no module boundaries between User's 250 methods, so changing one of them puts them all in question. We also have 90 incoming modules to consider: did our change break any of them? We don't get the full benefit of modules because each change to User puts so much of the system in question.

As the system grows, ubermodules grow more and more connected to the rest of the system, while their change frequency simultaneously grows. In practice, this causes maintenance to break down. We stop asking "what other functions do I have to change for this to work?" because there are just too many to consider.

The methods in User begin to diverge, growing inconsistencies and special cases. Some raise exceptions for errors, but others return nil, because one or the other was expedient at some point in the past. Some assume that necessary database records already exist, but others implicitly create them when they don't exist.

Inconsistency makes us uncertain, so we begin to program defensively, adding paranoid checks. Many methods try to catch exceptions that may never be thrown, because we're afraid of such an exception taking the system down. Other methods check for a database record's existence, only saving it if needed, instead of consistently saving or not saving. And so on. Over time, User becomes not only the largest and most-changed module, but also the module that we're most afraid to change.

(Again, if this isn't familiar, and you know someone who works on a legacy Rails app, ask them about their User or Profile class! Better yet, sit down with them and search User or Profile for the keyword if, asking exactly why each conditional exists.)

This is one section of The Programmer's Compendium's article on Software Structure, which contains more details and context.