Previous Lecture Complete and continue  

  Key Takeaways to Maximize Your Development Output

Congratulations 🎉! You have completed the first two modules!

Up to this point, you have access to 10.5 hours of software development video content, which by our estimation would translate to a maximum of 2 days of work in an office environment.

We ended up with 80 commits so far. As you may have gathered, we are fond of small, frequent commits that contain meaningful messages and provide accurate feedback. By taking a minute or two to author a good commit message not only we help out our future self to understand what our thoughts behind the committed changes were, but we also provide documentation for other team members that might get involved in the project. We can assure you that these long commit messages weren’t because the project is open source or a course; we take real pride by doing it in our everyday operations.

Learning outcomes up to now

  • Basic depiction of dependencies, abstractions and concrete types in diagrams
  • How diagrams translate into code and vice versa
  • How the SOLID principles and composition are applied through examples
  • Differences and similarities between closures and protocols as abstractions in Swift (unnamed type signatures vs. strictly named types)
  • Representing component and module relationships in a diagram
  • How concrete or modular a system is from its diagram representation
  • Understand that every system needs to be tailored, rather than fit a predefined template
  • The process of modularization needs to be applied incrementally
  • How good architecture is a byproduct of good team processes
  • The importance of good communication and well-defined requirements
  • How to deal with lousy requirements
  • How to represent and communicate requirements into different formats
  • How contracts enable teams to develop independently (even when key parts of the system such as the UI or the backend API are not yet implemented.)
  • Establishing processes promoting detailed documentation.
  • The tradeoffs of the project’s starting point (should we start with abstractions or concrete components?!).
  • How to speed up the development process using macOS frameworks.
  • How to test-drive an API layer implementation
  • Modular Design
  • Singletons: When and Why
  • Singletons: Better alternatives
  • Singletons: Refactoring steps to gradually remove tight coupling created by singletons
  • Controlling your dependencies: Locating globally shared instances (Implicit) vs. Injecting dependencies (Explicit)
  • Controlling your dependencies: Dependency injection
  • Understand the trade-offs of access control for testing purposes
  • Expand behavior checking (and coverage) using test spy objects
  • Handling network errors
  • Differences between stubbing and spying when unit testing
  • How to extend code coverage by using samples of values to test specific test cases
  • Design better code with enums to make invalid paths unrepresentable
  • Differences and trade-offs between mocking vs. testing collaborators in integration
  • Mapping JSON data to native models using the `Decodable` protocol
  • How to protect your architecture abstractions by working with domain-specific models
  • How to simplify tests leveraging factory methods and test helper functions
  • Automating memory leak detection with tests
  • Preventing common async bugs
  • Protecting the production code from test details
  • Maintain modularity by protecting high-level abstractions from low-level implementation details
  • Dealing with potential issues when using the `Swift.Error` protocol
  • Pattern matching using Swift enums
  • Assert asynchronous behavior with `XCTestCase` expectations
  • Learn various testing strategies for network requests and their trade-offs
  • Subclass and protocol-based mocking of classes we don’t own (e.g., the Foundation `URLSession` class)
  • The (little-known) URL Loading System
  • Intercepting and handling URL requests with `URLProtocol`
  • Avoiding the downsides of mocking/stubbing
  • The economics of test feedback
  • Minimizing risk in the codebase and maximizing learning by testing all scenarios (including error, invalid and unhappy paths)
  • Refactoring techniques for abstracting tests from implementation details
  • Extra testing configurations (running tests in random order, parallelizing test execution, and gathering code coverage)
  • Testing client and server integration (end-to-end) even when the backend is still in development
  • Economics of end-to-end tests vs unit tests
  • Setting up a CI pipeline

Key takeaways to maximize your development output

Before you start coding

As we did throughout the case study, we recommend you to gather as much information as possible about the system you want/need to build before starting to code.

To do so in a Lean/Agile fashion, we recommend you to focus on delivering customer value in small iterations with fast feedback. In collaboration with other parts of the business, you can ask “what features the customers of the app would like to have?” and come up with lean solutions (help the business deliver simpler versions of the feature, learn and improve iteratively). In functional teams, you won’t have to *imply,* what the customers need instead learn through customer research exercises. Many developers don’t participate in customer research and consequently, feature planning. When you don’t participate, it’s much harder to come up with achievable features, estimations, and deadlines, so conflicts with the business might occur. As a proactive professional, you can join those activities and bring your expertise to help the nontechnical colleagues plan and deliver better software products. The collected information will then help us draft BDD-like scenarios and Use Cases.

In our case study, after we were confident of the primary customer experience features, we then dived into a more technical level and started outlining some use cases, expressing the scenarios at a component level.

Finally, we drew the modules we could think of at the time, based on the information we had, in an architecture diagram. This initial diagram is a rough scaffolding for the feature (we’re sure it’ll change over time), and it shouldn’t take more than 10 minutes to come up with. When not feeling confident about how to structure the feature yet, we can even skip this step and spike/play around with code first and come up with the design as we go (evolutionary design) by following our development process disciplines.

The development process

We followed a disciplined test-first approach as a mechanism for implementing and validating the Scenarios and Use Cases drafted in the requirements analysis phase. Such automated tests give us the confidence to move fast. We talked extensively about the value of a fast, reliable and expressive test suite brings to the team and the business that’s why we tried our best to make the tests as precise as possible and as decoupled as possible to ensure they only test the behavior of the system instead of depending on too many implementation details.

Moreover, perhaps you’ve noticed that we never named the architecture we ended up using. This was no accident from our part. We are interested in solving our specific challenges, instead of prematurely letting an architecture design pattern/template dictate where we’re going. Although we may end up with an architecture that matches a common design pattern, we are not interested in a specific architecture template; rather we are interested in solving the presented problem as efficiently as possible, taking care of the project’s short but long-term needs as well. We achieved that by using a clear separation of concerns, establishing proper boundaries and utilizing dependency inversion whenever necessary.

Finally, we also made sure to set up an automated Continuous Integration pipeline to improve the code integration (merge) process and feedback. Along with our initially proposed modular architecture and reliable tests, automation enables us to happily build modules, components, and features in parallel with other developers (independent development and deployment), maximizing the team output.

We’re now ready to start the next module!