3 min reading time
3 Mar 2021
SwiftUI & Combine with TDD
Building AI-Powered To-Do App - Part 2
I part 1 I created an App architecture diagram showing how dependencies will interact with each other. Now its time to get to the good stuff, building the App!
I want Done Remastered to be a modular App; hence I will build it one module at a time and, in the last part, build the UI and compose all the pieces together.
Building a Language Recognizer
Firstly I highly recommend you check out the GitHub repository of the project as it progresses. I will be pushing up updates to the repository as I go along bundling the Done Remastered App. The first module I built is Language Recognizer.
The main idea is to have a LanguageRecognizer interface(in red) with only one function at this stage called findTags, which takes TagType and String input parameters. The function returns a Publisher data stream of strings of TagType found within an input String. For example, if my input string is "Go buy bananas" and TagType is a noun, the publisher emits one value, "bananas", which is the only noun in the input string.
Making a platform independent framework
As I wanted to build the App's core logic as a platform-independent framework, I started with a Mac framework which I can late reuse inside a Mac, iPadOS or iOS App. The main benefit during development is that I can run its tests using my Mac as a host, which is the fastest way to do TDD in Xcode. You can see git diff for the Language Recognizer module here.
❌ → ✅ Test Driving
If you look at the current project and its NaturalLanguageRecognizerTests file, that is where I started developing the first LanguageRecognizer module. Test driving with the red-green-refactor process, I made sure that the main functionality of the framework is reliable. The framework can now find verbs and nouns in the given input string and emits them individually as it iterates over the input string.
Hiding implementation details
Next thing I believe is important when building a framework is to keep the interface consistent regardless of its implementation. The interface becomes the contract between the user of the framework, and the framework interface itself should ideally not change. Implementation details can then change without breaking the interface, and users of the framework having to change how they consume the framework.
For example, in the LanguageRecognizer interface, I created my TagType enum, which I then map to NaturalLanguage's NLTag type inside the NaturalLanguageRecognizer concrete implementation. In theory, I could replace NaturalLanguageRecognizer with a different implementation of the LanguageRecognizer framework without breaking the interface. The user of the framework wouldn't have to know of any changes in that case. Keeping interface consistent as in this implementation is the main power of modular design.
The above is also a good example of clean code following the dependency inversion principle from SOLID. The implementation could now also be replaced with a different implementation in production or with a Fake object in tests. I only tend to create a Fake or Spy object in tests if using a real framework for test purposes would not be reliable (i.e. URL requests). However, in this case, I know that the NaturalLanguage framework is reliable, and its results will always be the same, so it's OK to test the real production implementation directly.
Stay tuned for the next part 😃
I hope you enjoyed this article and learned something new on the way.