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
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
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.
The Blog App module has too many concerns - it fans out
The problem is alleviated by organizing concerns into sub-modules
Top comments (0)