12.
Dependency Maps
Written by Joshua Greene & Michael Katz
Before you start making changes in a large project, you first need to understand how the system works and how its classes are related. This chapter will help you visualize this using a tool called dependency maps. You’ll learn:
- What is a dependency map?
- How can you use it to understand complex systems?
- How can you use it to identify problematic relationships?
- How can you use it to break up a complex system into modules?
Feel free to continue using your project from the last chapter, or start fresh from this chapter’s starter project. For the best hands-on experience, you’ll need a pencil, red marker, green marker and paper. This is going to get… analog!
Alternatively, a drawing program — or even Keynote — will work too.
Getting started
You may be wondering, “What exactly is a dependency map?” Great question!
Dependency maps are a way to illustrate dependencies between types. Its primary purpose is to help you understand how a change will affect an entire system. You can use dependency maps to identify change points, test points and places where you can pull out types to make your app more modular.
Before making a code change, your first step is to identify what the new behavior should be. In this case, your job is to move MyBiz’s login functionality into a separate module. Long-term, the plan is to use the login module in multiple apps.
Moving login into a separate module also has side benefits: Faster incremental compile times, separation of unit tests and more.
Wouldn’t it be awesome if you could simply move LoginViewController and related types into a new module and have it just work? Unfortunately, real-world apps aren’t usually so well architected…!
Consequently, you’ll need to break up dependencies to make this possible. This is the perfect problem a dependency map can help you solve.
Choosing where to begin
Choosing the “right” place to begin can be a daunting task in a large app. Fortunately, creating a dependency map is a journey of discovery and you can iteratively refine it. An educated guess for a starting point is good enough.
Since this is about login, the LoginViewController is a good starting point. Open MyBiz.xcodeproj and select LoginViewController.swift from the File Hierarchy.
Do you have that pencil and paper handy? (Or a drawing program?) Write LoginViewController inside a box in the middle of the paper like this:
And there you go, you have a starting point!
Finding direct dependencies
The next step is to identify the type’s direct dependencies.
This was very easy to do when iOS apps were written in Objective-C: You’d simply look at which files were imported. Swift is trickier because it automatically imports types within the same module and an iOS app itself is a module. Consequently, you need to actually scroll through a type and see which types are used to determine its direct dependencies.
Open LoginViewController.swift and you’ll see the first line is import UIKit. This isn’t surprising because it’s a UIViewController, after all. Because system libraries are readily imported anywhere, you can skip adding this to your dependency map.
The next dependency you’ll find comes from the api property, which is of type API and is accessed directly on the AppDelegate. This is interesting and should be added to your diagram. Do the following:
-
Draw a box directly above LoginViewController and write AppDelegate within it.
-
Draw an arrow from the LoginViewController box pointing at the AppDelegate box. This indicates
LoginViewControllerhas a dependency onAppDelegate. -
Draw another box to the right of AppDelegate and write API within it.
-
Draw an arrow from the AppDelegate box pointing at the API box. This indicates
AppDelegatedepends onAPI. Even ifAppDelegatedidn’t actually call any methods or properties onAPI, it depends on it simply by having a reference to it. -
Lastly, draw an arrow from LoginViewController pointing at API to indicate it also depends on it.
The next dependency is Skin, a helper object for styling the view. Add another box for Skin to the left of LoginViewController and draw an arrow from LoginViewController pointing at Skin.
Your diagram should now look like this:
There aren’t any dependencies within viewDidLoad. So, scroll past it and down to signIn(_:). This method is tricky because its dependencies aren’t explicitly shown. Rather, the computed properties isEmail and isValidPassword are defined in Validators.swift and showAlert(title:subtitle:type:skin:) is defined in UIViewController+Alert.swift.
Draw a new box for Validators to the bottom left of LoginViewController and draw another box for UIViewController+Alert directly below LoginViewController. Then, draw one arrow from LoginViewController pointing at Validators and another arrow pointing at UIViewController+Alert.
Your diagram should now look like this:
Dependency maps show how your code interrelates — the types don’t need to be classes. Rather, they can be protocols, extensions, files, libraries or anything else that makes sense for your use case.
It’s also important to note that dependency maps do not use UML, Archimate or any other formal specification. Rather, the arrows only indicate that one type depends on another.
Continue scrolling down and you’ll see LoginViewController conforms to APIDelegate via an extension. Draw a box for this to the right of API and write APIDelegate within it. Then, draw an arrow from LoginViewController pointing at APIDelegate.
There are several models used in this extension: Event, Employee, Announcement, Product, PurchaseOrder and UserInfo.
You have a few choices for how you represent this on your dependency map:
-
Draw a separate box for each type. This has the advantage of clearly representing each type, but it takes up more space. This is a good option if the models have a complex relationship. For example, dependencies on other types, circular dependencies on the view controller, etc.
-
Draw a single box for Models. This has the advantage of taking up the least amount of space, but it doesn’t clearly define which models are used. This is a good option if the models are simple, don’t have complex relationships and it’s not important for your use case to show exactly which models are used.
-
Draw a single box for Models and list each within it. This is a tradeoff between the two options above. It minimizes space but also still clearly defines which models are used. This is a good option if the models don’t have complex relationships but you still want to clearly show which models are used.
In this app, the models don’t have complex relationships. However, it’s a code smell that LoginViewController depends on so many models and you should clearly show this on the diagram. Hence, let’s go with the last option.
Draw another box for Models to the bottom left of LoginViewController and list each type within it. Then, draw an arrow from LoginViewController pointing at Models.
Your dependency map should now look like this:
Fantastic! You’ve reached the end of LoginViewController and you’ve identified all of its direct dependencies. However, its dependencies also have dependencies themselves. These are so-called “secondary dependencies” of LoginViewController.
Finding secondary dependencies
Your dependency map looks nice right now with all of the arrows pointing away from LoginViewController. However, this is because you’ve only inspected LoginViewController and not any other classes yet.
The next step is to identify secondary dependencies of LoginViewController. This will give you a better idea of how making a change to LoginViewController might have ripple effects on other classes.
In particular, the semi-circle between LoginViewController, AppDelegate and API is very suspicious and warrants further investigation.
Open AppDelegate.swift and repeat the investigation you did for LoginViewController.
The first interesting dependency you’ll find is Configuration. Draw a new box for Configuration above AppDelegate and draw an arrow from AppDelegate pointing at it.
AppDelegate also has a dependency on API but you identified this earlier and already have it drawn on the map.
Within showLogin, you’ll see AppDelegate also has a dependency on LoginViewController. Draw an arrow pointing from AppDelegate to LoginViewController to indicate this.
Your diagram should now look like this:
Uh oh! you’ve discovered a dependency cycle! AppDelegate and LoginViewController mutually depend on each other. This may not be causing problems right now but it’s definitely a code smell and could cause issues in the future. You’ll deal with this in the next chapter.
That’s it for the dependencies of AppDelegate. So next, open API.swift.
This file starts by defining APIDelegate. Since its methods use all of the previously-identified models, it depends on them. Draw an arrow from APIDelegate to Models to show this.
Even though APIDelegate is defined within the same file as API, this doesn’t actually make API depend on APIDelegate. If needed, you could easily move APIDelegate to a separate file.
However, API later declares a delegate property of type APIDelegate and this created a dependency on it. Draw an arrow from API pointing to APIDelegate to show this.
API also declares a property for server, which is a String that it gets from the configuration on the AppDelegate. Hence, it depends on both Configuration and AppDelegate. Draw an arrow from API pointing at AppDelegate and another arrow pointing at Configuration to show this.
Your dependency map should now look like this:
Oh no! You’ve found another circular dependency between AppDelegate and API. Again, you’ll deal with this later.
API also has a new dependency on Token. Draw a new box to the top right of API for Token and draw an arrow from API pointing at it.
Lastly, API also has a dependency on URLSession. However, this is defined within Foundation. As you did before with the system dependency on UIKit, you don’t need to explicitly indicate this in the diagram.
The rest of this class doesn’t introduce any new dependencies, so you can move onto the next file you need to inspect: UIViewController+Alert.swift. This file declares an extension on UIViewController that has one new dependency on ErrorViewController.
Draw a new box for ErrorViewController below UIViewController+Alert and draw an arrow from the UIViewController+Alert box pointing at ErrorViewController box.
Now, your dependency map should look like this:
Deciding when to stop
You could iteratively walk all files and create a diagram for the entire app. While this might be interesting, it’d likely be too busy to be useful. The further you get from the type you’re trying to modify, the less likely you’ll find relevant dependencies. Should you find yourself making changes in files that aren’t on your diagram, of course, you can always include them later.
As a sanity check to verify you’ve gone far enough, do a text search for LoginViewController to see if any other files reference it. You’ll find ErrorViewController actually has a reference to LoginViewController.
Open ErrorViewController and you’ll see that it uses LoginViewController in secondaryAction(_:). Add another arrow pointing from ErrorViewController to LoginViewController to represent this.
This reveals an indirect cycle between LoginViewController, UIViewController+Alert and ErrorViewController.
ErrorViewController also has a property of type Skin. Add another arrow from ErrorViewController pointing at Skin.
Ultimately, your diagram should look like this:
What are problematic dependencies?
A type is coupled to another when it directly depends on it. However, this may or may not be problematic. For example, if a type is coupled to a delegate protocol (e.g. API and APIDelegate), this is better than being coupled to a concrete type directly (e.g. LoginViewController).
Tight coupling refers to a “problematic” dependency that cannot be easily swapped out. This begs the question: What is a problematic dependency? Simply put, a dependency is problematic if it prevents you from accomplishing your goal.
In this case, your goal is to pull login into a separate module. Anything that prevents this is a problematic dependency.
Practically speaking, how can you identify these problematic dependencies? Ask the following of each direct dependency of LoginViewController:
-
Is the dependency on the
AppDelegate? By definition, theAppDelegaterepresents the app, so it cannot be pulled into the module. Hence, it’s going to be problematic. -
Is the dependency circular? If so, you may need to break one or both sides.
-
Does the dependency have many secondary dependencies? If so, it’s going to be difficult to pull it into the module.
-
Does it make sense for the dependency to be pulled into the same module? Even if it’s possible to pull the dependency into the same module, it may not be appropriate to do so.
It may make sense to create another module but you should carefully plan what’s best to do. This is especially true if the dependency is used in many places throughout the app.
Finding problematic dependencies
You can evaluate the relationships in the dependency map using these questions to find problematic dependencies.
First, are any dependencies on the AppDelegate? Yes, there are. The problematic relationships are the arrows that point to to the AppDelegate. This includes LoginViewController depending on the AppDelegate and API depending on the AppDelegate.
Got your red marker handy? Highlight both of these arrows in red to indicate they’re problematic.
You won’t ever be able to pull the AppDelegate into a module, so it’s problematic in general. Highlight the AppDelegate box in red to indicate this.
Are there any circular dependencies? Yep, there are those too. LoginViewController depends on API, which it gets from the AppDelegate. In turn, AppDelegate depends on LoginViewController.
You’ve already identified the LoginViewController-to-AppDelegate relationship as problematic. What about the AppDelegate-to-LoginViewController relationship? Would this prevent you from pulling login into a separate module?
No, actually. AppDelegate could depend on the new login module and, in turn, it could still set up the LoginViewController. Hence, it’s not problematic in terms of your goal.
What about the LoginViewController-to-API relationship? Yes, this is a problem for two reasons:
-
APIis used in other places throughout the app, so it would be difficult to pull it into the login module. -
APIdoesn’t conceptually make sense in the login module. It knows about all of the models and networking calls within the app. This is way beyond the scope that login should know about.
Hence, highlight the LoginViewController-to-API arrow and the API box in red to show these are problematic.
Does LoginViewController have any dependencies with many secondary dependencies? Yes, APIDelegate depends on a lot of models.
Does the login module really need to know about any of these models? The two APIDelegate methods that are related to login are loginFailed(error:) and loginSucceeded(userId:). Neither of these actually use these models!
Consequently, the LoginViewController-to-APIDelegate relationship, the LoginViewController-to-Models relationship and the APIDelegate box itself are all problematic. Highlight each of these in red.
Your diagram should now look like this:
There are three remaining direct dependencies of LoginViewController: Skin, Validators and UIViewController+Alert. Does it make sense to pull these into the same module as login?
If Skin were only used by LoginViewController, it might be okay to pull it into the same module. However, it’s also used by ErrorViewController, so it’s not okay to do this. Highlight the LoginViewController-to-Skin relationship and the Skin box itself in red.
Should Validators be moved into the same login module? Yes, actually! It’s only used by LoginViewController and its methods are explicitly related to login validation. Highlight this relationship and the Validators box in green to indicate it’s okay to move.
Does it make sense for UIViewController+Alert to be in the same login module? Nope, it’s a generic component and used in several places throughout the app. It may actually make sense for this to be in a separate module itself but it doesn’t belong in the login module. Therefore, highlight the LoginViewController-to-UIViewController+Alert relationship and the UIViewController+Alert box itself in red.
Ultimately, your dependency map should look like this:
Completing the map
If you find that a direct dependency is problematic, you don’t need to evaluate whether its secondary dependencies are problematic. Rather, you’ll need to refactor or fix this in some way first. Depending on what you do in this regard, however, you may later consider the secondary dependencies or may never do this.
If you find that a dependency is okay, you do need to evaluate its secondary dependencies. It could turn out some of the secondary dependencies are problematic and you’ll need to add them somehow.
Once you’ve completed this for all relevant dependencies, you’re done with evaluating the dependencies on your map!
In this case, you’ve actually completed both of these already, so your map is good as is.
Breaking up complex systems
You can use your dependency map as a blueprint to break up complex systems. It tells you exactly how types are related and which relationships are problematic!
Of course, there’s still the issue of actually addressing the problematic relationships. You usually cannot simply delete a relationship, as it’s providing some sort of useful functionality.
Practically speaking then, how can you fix these problems? Using TDD, of course! Yes, there’s more to it than simply “magically TDD code it” but you’ll learn all about this in the next chapter!
Key points
You learned about dependency maps in this chapter. Here are their key points:
- Dependency maps are a tool for visualizing your code dependencies.
- You can use them to discover problematic relationships.
- You can use them as a blueprint for breaking up a complex system.
Where to go from here?
In the next chapter, you’ll use this dependency map to actually pull out the login functionality into a new module! Of course, you’ll do this in a TDD fashion and learn tricks along the way for handling problematic relationships.
Continue onto the next chapter to learn all about it!