WidgetCenter: Installed Widgets And Unit Tests (Part 2) | Applying Design Principles
Applying Design Principles to get installed Widgets for iOS 14+ and correctly add Unit Tests for it.
I welcome you to the second part of my series on writing Unit Tests for tracking the Installed Widgets for iOS+14. In the first part, I explained my approach and my learnings. For this second part, I will show how I refactored my previous implementation.
You can find the complete code in this repository.
Coming Back to the Basics
I always knew about the Software Design Principles, and I thought I truly understood them until I realized that I was not consistently applying them to my code. Once, I’ve heard, “if you don’t know how to apply, you don’t understand it.” That was a wake-up call, and I invite you to reflect on how many concepts you think you understand but fail to implement. Usually, we fail to shift from the abstract idea to its implementation in Swift. One of my goals with this blog is to show you how to apply them in concrete examples.
The Single Responsibility and Dependency Inversion Principles guided the Refactoring of my previous implementation.
Single Responsibility Principle (SRP)
“The single-responsibility principle is a computer-programming principle that states that every module, class or function in a computer program should have responsibility over a single part of that program’s functionality, and it should encapsulate that part."
Dependency Inversion Principle (DIP)
- “High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g., interfaces)."
- “Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions."
First learning (SPR and DIP involved)
Consider the naming of what the class and methods of the Interactor do without leaking infrastructure implementation details.
Let’s compare the previous Naming and the new Naming by comparing the architecture diagrams.
Widget Architecture Diagram in Part 1
New Widget Architecture Diagram in Part 2
Improvements
- I renamed WidgetCenterService to WidgetTracker. Following the SRP, this class should only have one responsibility (to track the Widgets); thus, the naming should reflect this intention. Additionally, following DIP, the name “WidgetCenter” (Infrastructure detail) shouldn’t leak in the naming. Furthermore, usually adding “Service,” “Manager” could indicate that this class does everything related to the WidgetCenter, but what does this “everything” mean? I think it does not add much clarity and the new postfix “Tracker” indicates better its purpose better.
- Furthermore, Manager and Service are an invitation to add more functionality to them. Who hasn’t ended up with a Manager class with tons of responsibilities? (raise your hand if you are guilty of this ✋🏽) I consider we can do it better, and naming Tracker will make you think twice whether you would add any extra functionality, e.g., delete or load.
- The TrackingAPI remained the same.
One small detail to mention. In WidgetCenterService the method to track the installed Widgets was: func trackInstalledWidgetInfo()
. However, WidgetInfo is also a leaked detail because it’s the name of a WidgetKit class. That’s why for the WidgetTracker, I renamed it to: func trackInstalledWidgets()
.
Second Learning (DIP involved)
The infrastructure should depend on the Interactor, not the other way around (DPI). Create the infrastructure layer outside of the Interactor (The WidgetTracker and WidgetStore in this case).
Improvements
- WidgetStore is the Interface that the WidgetTracker will use to fetch the Widgets. As a result, the WidgetTracker does not know about WidgetCenterProtocol nor WidgetInfoProtocol. It requests the Widgets, and it does not care how the WidgetStore fetched them.
- WidgetSize is my enum to use instead of WidgetFamily. It brought the benefit that the WidgetStore and WidgetTracker do not need to import WidgetKit. Remember, we are trying to decouple any implementation details from our Interactors (WidgetStore and WidgetTracker). Secondly, I consider that WidgetSize is a term that the Business side can also understand. Necessary to align the words you use with the business requirements. Try telling your PO or client WidgetFamily and see whether they know it 😅.
- WidgetCenterStore is the one and the only one responsible for talking with the Infrastructure (WidgetCenter).
Now, I could write better Unit Tests for the WidgetTracker and also for the WidgetCenterStore. Before, they were all in the WidgetCenterService.
Example of test for the WidgetTracker
Example of test for WidgetStore
Conclusion
I believe in applying the Design Principles leads to writing better code. Now, in theory, you could track the Widgets from any class that implements WidgetStore without leaking infrastructure details. Maybe you could either have your cache, so you don’t need to always talk to WidgetCenter. Finally, consider the importance of naming your classes. If they would only have one responsibility, how would you call them? In the case of WidgetTracker, its sole responsibility was to track. You will for sure think twice before you extend its functionality.
Next steps
In these two parts, I covered mainly Unit Tests for the WidgetCenter. In the future, I’d also like to show how to add Unit Tests when using TimelineProvider, WidgetConfiguration, especially the Widget’s classes related to creating the UI for Widget.
I thank you for reading the article!