Design Your Codebase with Low Fan-out, High Fan-in Classes

Caleb Hearth - Aug 31 '22 - - Dev Community

Smaller, simpler classes are easier to reason about. The size of a class is easy to measure with line count or method count, but complexity is more difficult to quantify. One such measure is cyclomatic complexity, a count of the different paths through code.

Another way of measuring complexity of code is with fan-out —how many classes (or modules) a class interacts with—and fan-in —how many other classes collaborate with the class.

Take for example, these two designs for DownloadAndCombineEbookChapters, a class that that scrapes a user page for purchased ebooks, downloads .zips of multiple files, and combines them into a single file with chapter markers.

class DownloadAndCombineEbookChapters
  def call
    html = GetUserPageFromLibrofm.()
    page = Capybara.string(page)
    ebooks = ExtractEbooks.(page).map { Ebook.new(...) }
    ebooks.map do |ebook|
      chapter_files = ebook.download_urls.map do |url|
        Tempfile.create { |f| f.write(LibrofmAPI.connection.get(url)) }
      end
      CombineEbookChapters.(chapter_files)
    end
  end
end

Enter fullscreen mode Exit fullscreen mode

A dependency graph showing this implementation of the class DownloadAndCombineEbookChapters connecting directly to five classes that it interacts with: GetUserPageFromLibrofm, Ebook, ExtractEbooks, LibrofmAPI, and CombineEbookChapters.
The dependency graph in this method is flat and broad.

This high fan-out design class depends on GetUserPageFromLibrofm, ExtractEbooks, Ebook, Librofm, and CombineEbookChapters. This is 5 domain classes, plus Tempfile and Capybara if you chose to count these toward the collaboration count. There is a mix of imperative calls to service objects and declarative work being done inline.

class DownloadAndCombineEbookChapters
  def call
    GetEbooksFromLibrofm.().map do |ebook|
      ebook_chapters = DownloadEbook.(ebook)
      CombineFiles.(ebook_chapters)
    end
  end
end

Enter fullscreen mode Exit fullscreen mode

Another dependency graph showing the second, lower fan-out implementation of the class DownloadAndCombineEbookChapters. This one shows DownloadAndCombineEbookChapters connected directly to only three collaborator classes: GetEbooksFromLibrofm, DownloadEbook, and CombineFiles. GetEbooksFromLibrofm also has connections to GetUserPageFromLibrofm and to Ebook. GetUserPageFromLibrofm connects to LibrofmAPI. DownloadEbook connects to LibrofmAPI and to Ebook as well, giving these two classes a higher fan-in.
The refactored class has a narrower, deeper dependency graph that indicates lower fan-out in classes doing work as well as higher fan-in for utility classes Ebook and LibrofmAPI.

Refactored to push dependencies into its collaborators, this low-to-medium fan-out class interacts only with three other classes, down from five. It’s also much easier to understand what is happening in this class than to follow the logic in the original implementation. You can imagine that references to Ebook and LibrofmAPI are now shared between GetEbooksFromLibrofm and DownloadEbook. This increases the fan-in and reuse of these two utility classes: a data object and an API adapter.

Where do these names come from?

The terms fan-out and fan-in have been used in reference to software design at least since 1994 and were also used in Code Complete, which notes that high fan-in implies “good use of utility classes” and that high fan-out indicates that a class may be “overly complex”. The terms likely originate from logic circuit design, where fan-out is the number of outputs connected to a logic gate and fan-in is the number of inputs to the gate.

Quick note: throughout this post, we’ll use “class” to refer to classes, modules, types, etc.

Fan-out

A class’ fan-out is the number of classes that it directly collaborates with. More collaborators probably means that your object is responsible for too many things. Fewer outside collaborators means less work being done by a class, making it safer to modify, easier to understand both in isolation and as a part of the system, and more likely to be reusable.

As fan-out increases in a class, consider it a code smell that is getting stronger. As with any smell, high fan-out does not necessarily mean that a class needs to be changed, but it should attract attention and the decision to ignore the smell or make a change should be intentional.

It’s difficult to suggest a specific number of collaborators to recommend, but certainly single digits. I’ve seen 7 recommended, but that feels very high and I would be suspicious of any code depending on more than 3-5 classes by name.

Fan-in

The number of classes that depend on a class is its fan-in, and having classes in your system with a high fan-in is very desirable.

Being relied upon by many parts of your system indicates that the class provides a generic, useful logic may have been extracted from one or more other classes which independently needed and implemented the functionality. It is an indication of succinctness in code.

Code that has high fan-in tends to be useful even outside of your own codebase, which makes it a prime candidate for extraction to a shared library and for open sourcing.

Some Fan-out is a good thing

This advice could be misconstrued to mean that rather than using a collaborator a class should implement work itself. This is not intended. Imagine that instead of a mix of imperative calls and declarative work, DownloadAndCombineEbookChapters above inlined all work. It would be a large class, and prone to becoming even larger and harder to maintain as complexity increased, such as if a new source of ebooks besides LibrofmAPI were introduced.

Rather, it is desirable to break this work into smaller objects and to push those utility classes further down in the dependency graph as high fan-in classes which can do this work in a generic manner useful in multiple places in your system. It is good to have a little bit of fan-out when it means that your class remains easy to understand, change, and test.

Designing Your System

Strive to create a system with very few high fan-out objects and many high fan-in objects.

One way to limit the number of collaborators is to introduce intermediate classes that encapsulate the behaviors you need in your class, remembering to consider the Law of Demeter. Doing so reduces the number of direct collaborators—and therefore, fan-out—while potentially helping to identify shared behaviors, increasing fan-in.

A directed graph showing a Blog App node connected to 9 submodules: Post, Author, Comment, Highlight, Like, Repost, User, Tag, and Category
The Blog App module has too many concerns - it fans out

Another graph with Blog App's collaborators collected into sub-modules: Social System connects to Highlight, Like, Repost, and User; Author remains directly connected from Blog App but now also depends on User; Blog App also continues to rely on Post but Post now depends on Comment, Tag, and Category.
The problem is alleviated by organizing concerns into sub-modules

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .