Skip to content

Instantly share code, notes, and snippets.

@albertbori
Last active September 9, 2024 00:23
Show Gist options
  • Select an option

  • Save albertbori/0c21bf5af8c450a058657978a524028b to your computer and use it in GitHub Desktop.

Select an option

Save albertbori/0c21bf5af8c450a058657978a524028b to your computer and use it in GitHub Desktop.

Revisions

  1. albertbori revised this gist Sep 9, 2024. 1 changed file with 6 additions and 0 deletions.
    6 changes: 6 additions & 0 deletions CPDI-docs.md
    Original file line number Diff line number Diff line change
    @@ -479,6 +479,12 @@ The problem with this approach is that the original type alias no longer describ

    **Every file that uses a dependency should define its own “Dependencies” type alias.** This includes any “Spliced” dependencies type, if required. This keeps dependencies well-scoped and easy to reason about.

    **Good Example**

    ```swift
    struct ProductDetailView<Dependencies: ProductDetailViewDependencies>: View { ... }
    ```

    ---

    ### Following Compiler Prompts Instead of CPDI
  2. albertbori revised this gist Apr 22, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion CPDI-docs.md
    Original file line number Diff line number Diff line change
    @@ -297,7 +297,7 @@ Using the above concepts, the following table can help you determine if you need
    > * **Entire Feature** - A widely-used dependency that is instantiated and destroyed along with the entire feature it's associated with.
    > * **Shared with Several Non-Adjacent Descendants** - This dependency is instantiated by a distance ancestor, being passed down by potentially several initializers that may or may not be directly connected.
    > * **Shared with Direct Descendants** - This dependency is used only by a parent type and its direct descendants or passed through very few initializers.
    > * **Single Type** - This dependency is used only within a single feature, view, component, or file and should be memory-scoped with that code.
    > * **Single Type** - This dependency is used only within a single piece of code or file and should be memory-scoped with that code.
    > * **Publicly Available** - Your dependency type is marked as "public" because you want to share this type with callers outside of your module, making it part of your module's public API.
    > * **Internally Available** - Your dependency type is marked as "internal" (or, by default is internal), and is intended for access across any file in the given module.
    > * **File / Privately Available** - Your dependency type is marked as "private" or "fileprivate" and is not intended for access outside of the given file.
  3. albertbori revised this gist Apr 22, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion CPDI-docs.md
    Original file line number Diff line number Diff line change
    @@ -212,7 +212,7 @@ struct SplicedProductDetailViewDependencies<ExternalDependencies: ProductDetailV
    }
    ```

    > Note: The spliced dependencies type _encapsulates_ the external dependencies. Avoid accessing the external dependencies type directly outside of this type.
    > Note: The spliced dependencies type _encapsulates_ the external dependencies. Avoid accessing the external dependencies property directly outside of this type.
    The spliced dependencies type above combines the broad and narrow scope dependencies. This can be a struct or a class, as needed. `SplicedProductDetailViewDependencies` will now act as the underlying dependencies type for `ProductDetailView`. We accomplish this by changing `ProductDetailView`'s generic dependency declaration like so:

  4. albertbori revised this gist Apr 22, 2024. 1 changed file with 4 additions and 1 deletion.
    5 changes: 4 additions & 1 deletion CPDI-docs.md
    Original file line number Diff line number Diff line change
    @@ -199,7 +199,7 @@ Now, we create the `SplicedProductDetailViewDependencies` type that allows us to

    ```swift
    struct SplicedProductDetailViewDependencies<ExternalDependencies: ProductDetailViewExternalDependencies>: ProductDetailViewDependencies {
    let externalDependencies: ExternalDependencies // Obtained from outside callers
    private let externalDependencies: ExternalDependencies // Obtained from outside callers
    let productDataRepository: ProductDataRepository // Created locally
    var tracking: Tracking { // Forwarded to children
    externalDependencies.tracking
    @@ -212,6 +212,8 @@ struct SplicedProductDetailViewDependencies<ExternalDependencies: ProductDetailV
    }
    ```

    > Note: The spliced dependencies type _encapsulates_ the external dependencies. Avoid accessing the external dependencies type directly outside of this type.
    The spliced dependencies type above combines the broad and narrow scope dependencies. This can be a struct or a class, as needed. `SplicedProductDetailViewDependencies` will now act as the underlying dependencies type for `ProductDetailView`. We accomplish this by changing `ProductDetailView`'s generic dependency declaration like so:

    ```swift
    @@ -295,6 +297,7 @@ Using the above concepts, the following table can help you determine if you need
    > * **Entire Feature** - A widely-used dependency that is instantiated and destroyed along with the entire feature it's associated with.
    > * **Shared with Several Non-Adjacent Descendants** - This dependency is instantiated by a distance ancestor, being passed down by potentially several initializers that may or may not be directly connected.
    > * **Shared with Direct Descendants** - This dependency is used only by a parent type and its direct descendants or passed through very few initializers.
    > * **Single Type** - This dependency is used only within a single feature, view, component, or file and should be memory-scoped with that code.
    > * **Publicly Available** - Your dependency type is marked as "public" because you want to share this type with callers outside of your module, making it part of your module's public API.
    > * **Internally Available** - Your dependency type is marked as "internal" (or, by default is internal), and is intended for access across any file in the given module.
    > * **File / Privately Available** - Your dependency type is marked as "private" or "fileprivate" and is not intended for access outside of the given file.
  5. albertbori revised this gist Apr 22, 2024. 1 changed file with 58 additions and 0 deletions.
    58 changes: 58 additions & 0 deletions CPDI-docs.md
    Original file line number Diff line number Diff line change
    @@ -241,6 +241,64 @@ ProductDetailView(

    Where the caller's `Dependencies` type alias conforms to our external `ProductDetailViewExternalDependencies` type alias that we just created.

    ### When to Splice

    Since "dependency splicing" brings some cognitive overhead and boilerplate, it's important to consider if you need to splice at all. In certain cases, it may make sense to pass the dependency as a separate parameter to your various call sites.

    The main things to take into account when deciding on "dependency splicing" are:

    1. The accessibility of the dependency (public, internal, file-private / private)
    2. The memory-scope of the dependency (When should the dependency be created/destroyed from memory?)
    3. The number of components / sub-components that share the dependency
    4. The contiguous (or non-contiguous) nature of the components that share the dependency
    5. The number of dependencies that share the same scope

    #### 1 - Splicing for Accessibility

    If you have a dependency that is rightfully internal or file-private / private, do not make it public to avoid splicing. Exposing internal dependencies that are not intended for external use is a shortcut that can propagate confusion and compiler complexity, and potentially introduce security considerations. Instead, evaluate the other factors above to determine if you need to use a "SplicedFooDependencies" type to encapsulate your internal or file-private / private dependency from your external APIs, or if you can just pass the dependency directly (internally) across component initializers or functions as a separate parameter.

    #### 2 - Splicing for Memory Scope

    You should not instantiate a dependency at a higher memory scope than is required. Your dependency should be instantiated with the exact memory-scope and life-span/life-cycle that is required for the use case. This may require that you splice that dependency into the type whose memory scope it shares. Consider the other factors above to determine if you need to use a "SplicedFooDependencies" type to ensure that your dependency is memory-scoped correctly to your feature, view, or component, or if you can just pass the dependency directly across component initializers or functions as a separate parameter.

    A helpful question is this: "Should this dependency only exist in memory for as long as the component that requires it? If not, what component should it be scoped with?" The answer to this question will help you know if you need to splice, and where you would splice the dependency.

    #### 3 - Splicing for Scope of Use

    If your dependency is only used once, by one file, do not splice the dependency. Instead, pass it directly to the type via a separate initializer or function parameter. However, if your dependency has many, many uses, even within the same file, a splice may be preferred over the boilerplate of passing an additional dependency around a dozen or so call-sites. Considering the other factors above may reinforce or dispel the need for splicing based on number of components (or call sites) needing the dependency.

    #### 4 - Splicing for Distance Between Components

    If your dependency is used by few (see above), contiguous components, it may make sense to pass that dependency directly as a separate parameter. However, if your dependency is used by 2+ components that are only distantly related, it may be considerably less work & cognitive load to splice that dependency at the closest shared ancestor, instead of passing the dependency as an extra parameter across many, many initializers and function calls. Consider the other factors above when making this decision.

    #### 5 - Splicing for Dependency Grouping

    If you have several dependencies that share the same memory scope or accessibility, even if they are narrowly used, it may make sense to use "dependency splicing". Consider the above factors and weigh the amount of boilerplate required for splicing vs passing them all directly via initializer or function parameters.

    #### Splicing Rubric

    Ultimately, you must weigh the time-savings of CPDI for each dependency. CPDI favors the more broadly-used dependencies, but has quickly diminishing returns for internal/private or narrowly-used dependencies. You'll want to use CPDI splicing except in situations where passing a separate parameter from file-to-file (via initializers or function arguments) requires significantly less effort than splicing. This can also be affected by the number of internal/private or narrowly-used dependencies that share the same scope.

    Using the above concepts, the following table can help you determine if you need to splice a dependency in your specific scenario. The left column represents the possible memory scopes of a given dependency. The right columns represent the accessibility of the dependency, and the values in the grid suggest whether you should splice in that scenario or pass the dependency as a separate parameter outside of CPDI.

    | Memory Scope ⬇️ | x | Publicly Available| Internally Available | File / Privately Available |
    |-:|-|:-:|:-:|:-:|
    | Global (Static or Near-Static) | | N/A | Splice | N/A |
    | Entire Feature | | Splice | Splice | N/A |
    | Shared with Non-Adjacent Descendants | | Maybe Splice | Maybe Splice | N/A |
    | Shared with Direct Descendants | | Separate Parameter | Separate Parameter | Separate Parameter |
    | Single Type | | Separate Parameter | Separate Parameter | Separate Parameter |

    > Note: Key Terms
    >
    > * **Global** - Static or near-static dependency that's instantiated at startup or when the module is first loaded or called.
    > * **Entire Feature** - A widely-used dependency that is instantiated and destroyed along with the entire feature it's associated with.
    > * **Shared with Several Non-Adjacent Descendants** - This dependency is instantiated by a distance ancestor, being passed down by potentially several initializers that may or may not be directly connected.
    > * **Shared with Direct Descendants** - This dependency is used only by a parent type and its direct descendants or passed through very few initializers.
    > * **Publicly Available** - Your dependency type is marked as "public" because you want to share this type with callers outside of your module, making it part of your module's public API.
    > * **Internally Available** - Your dependency type is marked as "internal" (or, by default is internal), and is intended for access across any file in the given module.
    > * **File / Privately Available** - Your dependency type is marked as "private" or "fileprivate" and is not intended for access outside of the given file.
    ### Public Encapsulation (Factory)

    To prevent from having to make all of your dependencies type aliases public, you can splice your dependencies at the top-most object/struct or call site of your feature. Regardless, it's usually a good idea to create a publicly accessible wrapper around your internal functionality to create a single entry point into your feature. Here is such an example:
  6. albertbori revised this gist Apr 15, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion CPDI-docs.md
    Original file line number Diff line number Diff line change
    @@ -130,7 +130,7 @@ ProductDetailView(dependencies: ..., sku: "ABC", disposition: ..., context: ...)

    ### Direct Child Dependencies

    If your code file instantiates a struct or class that has its own dependencies type alias, you must forward that child's Dependencies type alias along with your file's Dependencies type alias.
    If your code file instantiates a struct or class that has its own Dependencies type alias, you must forward that child's Dependencies type alias along with your file's Dependencies type alias.

    Consider a SwiftUI view named `ProductDetailView` that includes a sub-view called `ProductImageView`. Each view requires specific dependencies:

  7. albertbori revised this gist Apr 15, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion CPDI-docs.md
    Original file line number Diff line number Diff line change
    @@ -130,7 +130,7 @@ ProductDetailView(dependencies: ..., sku: "ABC", disposition: ..., context: ...)

    ### Direct Child Dependencies

    If your code file instantiates a struct or class that has its own dependencies type, you must forward that child's Dependencies type alias along with your file's Dependencies type alias.
    If your code file instantiates a struct or class that has its own dependencies type alias, you must forward that child's Dependencies type alias along with your file's Dependencies type alias.

    Consider a SwiftUI view named `ProductDetailView` that includes a sub-view called `ProductImageView`. Each view requires specific dependencies:

  8. albertbori revised this gist Apr 15, 2024. 1 changed file with 104 additions and 104 deletions.
    208 changes: 104 additions & 104 deletions CPDI-docs.md
    Original file line number Diff line number Diff line change
    @@ -76,7 +76,7 @@ func thunk(dependencies: ThunkDependencies) {
    In this simple example, we have a SwiftUI view that needs do to some tracking. We'll use CPDI to fulfill its requirements.

    ```swift
    struct PDPView: View {
    struct ProductDetailView: View {
    var body: some View {
    VStack {
    Text("Product Detail Page")
    @@ -88,12 +88,12 @@ struct PDPView: View {
    }
    ```

    To apply the CPDI pattern to `PDPView`, we add a `Dependencies` type _to the top of the same file where the dependencies are being used_. We are also careful to _name the dependencies type using the exact name of the type whose requirements it satisfies_: `PDPViewDependencies`. This location and naming of the dependencies type alias are critical for discoverability and maintainability. Following this pattern will help prevent any downstream confusion about the scope of a given type alias.
    To apply the CPDI pattern to `ProductDetailView`, we add a `Dependencies` type _to the top of the same file where the dependencies are being used_. We are also careful to _name the dependencies type using the exact name of the type whose requirements it satisfies_: `ProductDetailViewDependencies`. This location and naming of the dependencies type alias are critical for discoverability and maintainability. Following this pattern will help prevent any downstream confusion about the scope of a given type alias.

    ```swift
    typealias PDPViewDependencies = TrackingDependency
    typealias ProductDetailViewDependencies = TrackingDependency

    struct PDPView<Dependencies: PDPViewDependencies>: View {
    struct ProductDetailView<Dependencies: ProductDetailViewDependencies>: View {
    let dependencies: Dependencies
    var body: some View {
    VStack {
    @@ -106,18 +106,18 @@ struct PDPView<Dependencies: PDPViewDependencies>: View {
    }
    ```

    > Note: We also use a generic alias instead of using the `PDPViewDependencies` type alias directly within the class. We do this because it lets us add generic dependencies (later on) without any additional code changes.
    > Note: We also use a generic alias instead of using the `ProductDetailViewDependencies` type alias directly within the class. We do this because it lets us add generic dependencies (later on) without any additional code changes.
    ### "Dependencies" Parameter Order

    For readability, consistency, and convention, the `dependencies` parameter should always appear first in any parameter list. For example:

    ```swift
    // BAD
    PDPView(sku: "ABC", disposition: ..., context: ..., dependencies: ...)
    ProductDetailView(sku: "ABC", disposition: ..., context: ..., dependencies: ...)

    // GOOD
    PDPView(dependencies: ..., sku: "ABC", disposition: ..., context: ...)
    ProductDetailView(dependencies: ..., sku: "ABC", disposition: ..., context: ...)
    ```

    ## Sub-Dependencies
    @@ -132,23 +132,23 @@ PDPView(dependencies: ..., sku: "ABC", disposition: ..., context: ...)

    If your code file instantiates a struct or class that has its own dependencies type, you must forward that child's Dependencies type alias along with your file's Dependencies type alias.

    Consider a SwiftUI view named `PDPView` that includes a sub-view called `ProductImageView`. Each view requires specific dependencies:
    Consider a SwiftUI view named `ProductDetailView` that includes a sub-view called `ProductImageView`. Each view requires specific dependencies:

    - `ProductImageView` depends on various services and utilities specific to its functionality, grouped under a type alias `ProductImageViewDependencies`.
    - `PDPView` requires its own set of services, such as `TrackingDependency`, for tracking user interactions, grouped under a type alias `PDPViewDependencies`.
    - `ProductDetailView` requires its own set of services, such as `TrackingDependency`, for tracking user interactions, grouped under a type alias `ProductDetailViewDependencies`.

    To accommodate all requirements, `PDPViewDependencies` must integrate `ProductImageViewDependencies` into its own dependency structure. For example:
    To accommodate all requirements, `ProductDetailViewDependencies` must integrate `ProductImageViewDependencies` into its own dependency structure. For example:

    ```swift
    typealias PDPViewDependencies = TrackingDependency
    & ProductImageViewDependencies // Required by ProductImageView
    typealias ProductDetailViewDependencies = TrackingDependency
    & ProductImageViewDependencies // Required by ProductImageView

    struct PDPView<Dependencies: PDPViewDependencies>: View {
    struct ProductDetailView<Dependencies: ProductDetailViewDependencies>: View {
    let dependencies: Dependencies
    var body: some View {
    VStack {
    Text("Product Detail Page")
    ProductImageView(dependencies: dependencies) // Borrows PDPViewDependencies
    ProductImageView(dependencies: dependencies) // Borrows ProductDetailViewDependencies
    Button("Add to cart") {
    dependencies.tracker.trackAddToCart()
    }
    @@ -164,14 +164,14 @@ This principle applies in all code files that work with injected dependencies, n
    Similar to child dependencies, if you inherit a class that has its own dependencies requirement, you must forward that superclass' `Dependencies` type alias along with your file's `Dependencies` type alias. In the following example, a UIKit view controller depends on a base view controller, which has its own dependencies requirement.

    ```swift
    typealias PDPViewControllerDependencies = TrackingDependency
    & ShopBaseViewControllerDependencies // Required by ShopBaseViewController
    typealias ProductDetailViewControllerDependencies = TrackingDependency
    & ShopBaseViewControllerDependencies // Required by ShopBaseViewController

    struct PDPViewController<Dependencies: PDPViewControllerDependencies>: ShopBaseViewController {
    struct ProductDetailViewController<Dependencies: ProductDetailViewControllerDependencies>: ShopBaseViewController {
    let dependencies: Dependencies
    init(dependencies: Dependencies) {
    self.dependencies = dependencies
    super.init(dependencies: dependencies) // Borrows PDPViewControllerDependencies
    super.init(dependencies: dependencies) // Borrows ProductDetailViewControllerDependencies
    }
    }
    ```
    @@ -182,44 +182,44 @@ If you you have a dependency that should only be accessible within a certain str

    In this approach, we use a concrete struct or class to splice the narrowly scoped dependencies into the broader-scoped dependencies in a way that maintains the desired memory scope and accessibility.

    Continuing with our previous example, if we want to add a `PDPDataRepositoryDependency` to the `PDPView` and that dependency is only internally available (within the same module), and you want that repository to be instantiated and destroyed along with the `PDPView`, then we would add that dependency to the PDPViewDependencies, as you would expect.
    Continuing with our previous example, if we want to add a `ProductDataRepositoryDependency` to the `ProductDetailView` and that dependency is only internally available (within the same module), and you want that repository to be instantiated and destroyed along with the `ProductDetailView`, then we would add that dependency to the ProductDetailViewDependencies, as you would expect.

    ```swift
    typealias PDPViewDependencies = TrackingDependency
    & PDPDataRepositoryDependency
    typealias ProductDetailViewDependencies = TrackingDependency
    & ProductDataRepositoryDependency
    ```

    Then, we create a second dependencies type alias that splits out all of the broader-scope dependencies. This will be used to help construct the spliced dependencies concrete type.

    ```swift
    typealias PDPViewExternalDependencies = TrackingDependency
    typealias ProductDetailViewExternalDependencies = TrackingDependency
    ```

    Now, we create the `SplicedPDPViewDependencies` type that allows us to create and inject the `PDPDataRepository` into a single `Dependencies` type, like so:
    Now, we create the `SplicedProductDetailViewDependencies` type that allows us to create and inject the `ProductDataRepository` into a single `Dependencies` type, like so:

    ```swift
    struct SplicedPDPViewDependencies<ExternalDependencies: PDPViewExternalDependencies>: PDPViewDependencies {
    struct SplicedProductDetailViewDependencies<ExternalDependencies: ProductDetailViewExternalDependencies>: ProductDetailViewDependencies {
    let externalDependencies: ExternalDependencies // Obtained from outside callers
    let pdpDataRepository: PDPDataRepository // Created locally
    let productDataRepository: ProductDataRepository // Created locally
    var tracking: Tracking { // Forwarded to children
    externalDependencies.tracking
    }

    init(externalDependencies: ExternalDependencies, pdpDataRepository: PDPDataRepository) {
    init(externalDependencies: ExternalDependencies, productDataRepository: ProductDataRepository) {
    self.externalDependencies = externalDependencies
    self.pdpDataRepository = pdpDataRepository
    self.productDataRepository = productDataRepository
    }
    }
    ```

    The spliced dependencies type above combines the broad and narrow scope dependencies. This can be a struct or a class, as needed. `SplicedPDPViewDependencies` will now act as the underlying dependencies type for `PDPView`. We accomplish this by changing `PDPView`'s generic dependency declaration like so:
    The spliced dependencies type above combines the broad and narrow scope dependencies. This can be a struct or a class, as needed. `SplicedProductDetailViewDependencies` will now act as the underlying dependencies type for `ProductDetailView`. We accomplish this by changing `ProductDetailView`'s generic dependency declaration like so:

    ```swift
    struct PDPView<ExternalDependencies: PDPExternalDependencies>: View {
    let dependencies: SplicedPDPViewDependencies<ExternalDependencies>
    struct ProductDetailView<ExternalDependencies: ProductDetailViewExternalDependencies>: View {
    let dependencies: SplicedProductDetailViewDependencies<ExternalDependencies>
    var body: some View {
    VStack {
    Text(dependencies.pdpDataRepository.productName)
    Text(dependencies.productDataRepository.productName)
    Button("Add to cart") {
    dependencies.tracking.trackAddToCart()
    }
    @@ -228,53 +228,53 @@ struct PDPView<ExternalDependencies: PDPExternalDependencies>: View {
    }
    ```

    How and where the `SplicedPDPViewDependencies` type is initialized depends on several factors, but for SwiftUI, it usually must be done by whoever is instantiating the SwiftUI view in question. The previous example may be declared like so:
    How and where the `SplicedProductDetailViewDependencies` type is initialized depends on several factors, but for SwiftUI, it usually must be done by whoever is instantiating the SwiftUI view in question. The previous example may be declared like so:

    ```swift
    PDPView(
    dependencies: SplicedPDPViewDependencies(
    ProductDetailView(
    dependencies: SplicedProductDetailViewDependencies(
    externalDependencies: ...,
    pdpDataRepository: PDPDataRepository(dependencies: ...)
    productDataRepository: ProductDataRepository(dependencies: ...)
    )
    )
    ```

    Where the caller's `Dependencies` type alias conforms to our external `PDPViewExternalDependencies` type alias that we just created.
    Where the caller's `Dependencies` type alias conforms to our external `ProductDetailViewExternalDependencies` type alias that we just created.

    ### Public Encapsulation (Factory)

    To prevent from having to make all of your dependencies type aliases public, you can splice your dependencies at the top-most object/struct or call site of your feature. Regardless, it's usually a good idea to create a publicly accessible wrapper around your internal functionality to create a single entry point into your feature. Here is such an example:

    ```swift
    public typealias PDPFeatureDependencies = PDPViewExternalDependencies
    & PDPDataRepositoryDependencies
    // & Other encapsulated component dependencies
    public typealias ProductFeatureDependencies = ProductDetailViewExternalDependencies
    & ProductDataRepositoryDependencies
    // & Other encapsulated component dependencies
    ```

    First, we created a new `PDPFeatureDependencies` type alias in a new file where our feature's API will live. We declare this type alias as publicly accessible.
    First, we created a new `ProductFeatureDependencies` type alias in a new file where our feature's API will live. We declare this type alias as publicly accessible.

    > Note: You may need to declare your "External" Dependencies type aliases as public as well, as required, but no other type aliases should be made public. If you find yourself making all of your types public to fix compiler warnings, you should reevaluate how you are splicing your dependencies.
    Next, we create the public API using `PDPFeatureDependencies`:
    Next, we create the public API using `ProductFeatureDependencies`:

    ```swift
    public struct PDPFeature<Dependencies: PDPFeatureDependencies> {
    public struct ProductFeature<Dependencies: ProductFeatureDependencies> {
    let dependencies: Dependencies

    public init(dependencies: Dependencies) {
    self.dependencies = dependencies
    }

    public func pdpView(for sku: String) -> some View {
    PDPView(
    dependencies: SplicedPDPViewDependencies(
    public func productDetailView(for sku: String) -> some View {
    ProductDetailView(
    dependencies: SplicedProductDetailViewDependencies(
    externalDependencies: dependencies,
    pdpDataRepository: PDPDataRepository(dependencies: dependencies, sku: sku)
    productDataRepository: ProductDataRepository(dependencies: dependencies, sku: sku)
    )
    )
    }

    // Other PDP Feature functions
    // Other Product feature functions
    }
    ```

    @@ -283,10 +283,10 @@ This provides a succinct API for accessing all of our internal code, and a simpl
    Finally, callers can invoke your feature at any time using the following code:

    ```swift
    PDPFeature(dependencies: dependencies).pdpView(for: "ABC")
    ProductFeature(dependencies: dependencies).productDetailView(for: "ABC")
    ```

    Where the caller's `Dependencies` type alias conforms to our external `PDPFeatureDependencies` type alias that we just created.
    Where the caller's `Dependencies` type alias conforms to our external `ProductFeatureDependencies` type alias that we just created.

    ## All In One Reference

    @@ -321,7 +321,7 @@ The following uses of CPDI will land you into trouble. Avoid using these where p
    You may be tempted to create a new protocol that composes other dependencies into it. _Resist this urge._ Always use type aliases when composing and forwarding protocols.

    ```swift
    protocol PDPViewDependencies: TrackingDependency & ProductImageViewDependencies {
    protocol ProductDetailViewDependencies: TrackingDependency & ProductImageViewDependencies {
    // ...
    }
    ```
    @@ -332,13 +332,13 @@ The consequences of using the above approach are that when you compose this prot

    You should avoid passing dependencies directly into functions. Instead, you should pass the dependencies via the initializer of the thing that vends the helper function. However, if you must, the following pattern should be followed when passing dependencies into helper functions.

    Similar to child dependencies and superclass dependencies, if your file calls a helper function that has its own dependencies requirement, you must forward that function's `Dependencies` type alias along with your file's `Dependencies` type alias. In the following example, a SwiftUI view modifier requires a dependencies type alias that must be satisfied by the `PDPViewDependencies` type alias. The view modifier in this example auto-expands the product view when tapped.
    Similar to child dependencies and superclass dependencies, if your file calls a helper function that has its own dependencies requirement, you must forward that function's `Dependencies` type alias along with your file's `Dependencies` type alias. In the following example, a SwiftUI view modifier requires a dependencies type alias that must be satisfied by the `ProductDetailViewDependencies` type alias. The view modifier in this example auto-expands the product view when tapped.

    ```swift
    typealias PDPViewDependencies = TrackingDependency
    & View_autoExpandDependencies // Required by `.autoExpand(...)`, declared in `View+AutoExpand.swift`
    typealias ProductDetailViewDependencies = TrackingDependency
    & View_autoExpandDependencies // Required by `.autoExpand(...)`, declared in `View+AutoExpand.swift`

    struct PDPView<Dependencies: PDPViewDependencies>: View {
    struct ProductDetailView<Dependencies: ProductDetailViewDependencies>: View {
    let dependencies: Dependencies
    var body: some View {
    VStack {
    @@ -347,7 +347,7 @@ struct PDPView<Dependencies: PDPViewDependencies>: View {
    dependencies.tracker.trackAddToCart()
    }
    }
    .autoExpand(dependencies: dependencies) // Borrows PDPViewDependencies
    .autoExpand(dependencies: dependencies) // Borrows ProductDetailViewDependencies
    }
    }
    ```
    @@ -361,11 +361,11 @@ Otherwise, if you _must_ use this pattern, adhere to the following guidelines:
    If your type implements a protocol with an associated `Dependencies` type, you _must_ also forward that base class or protocol file's `FooDependencies` type alias along with your file's dependencies type alias, like so:

    ```swift
    typealias PDPViewDependencies = TrackingDependency
    & ProductImageViewDependencies
    & PDPComponentDependencies // Required by PDPComponent
    typealias ProductDetailViewDependencies = TrackingDependency
    & ProductImageViewDependencies
    & ProductComponentDependencies // Required by ProductComponent

    struct PDPView<Dependencies: PDPViewDependencies>: View, PDPComponent /* <- Implicitly uses PDPViewDependencies */ {
    struct ProductDetailView<Dependencies: ProductDetailViewDependencies>: View, ProductComponent /* <- Implicitly uses ProductDetailViewDependencies */ {
    let dependencies: Dependencies
    var body: some View {
    VStack {
    @@ -382,19 +382,19 @@ struct PDPView<Dependencies: PDPViewDependencies>: View, PDPComponent /* <- Impl
    }
    ```

    The above example implements and uses a protocol called `PDPComponent` by forwarding its `PDPComponentDependencies` type alias. `PDPComponent` is defined like so:
    The above example implements and uses a protocol called `ProductComponent` by forwarding its `ProductComponentDependencies` type alias. `ProductComponent` is defined like so:

    ```swift
    typealias PDPComponentDependencies = LoggingDependency
    typealias ProductComponentDependencies = LoggingDependency

    protocol PDPComponent {
    associatedtype Dependencies: PDPComponentDependencies
    protocol ProductComponent {
    associatedtype Dependencies: ProductComponentDependencies
    let dependencies: Dependencies
    }

    extension PDPComponent {
    extension ProductComponent {
    func log(message: String) {
    dependencies.logger.log(message: "PDPComponent: " + message)
    dependencies.logger.log(message: "ProductComponent: " + message)
    }
    }
    ```
    @@ -410,8 +410,8 @@ The following sections contain more information on what to CPDI patterns to avoi
    **Bad Examples**

    ```swift
    struct PDPView<Dependencies: HotDealsDependencies>: View { ... }
    struct PDPView<Dependencies: AllPDPDependencies>: View { ... }
    struct ProductDetailView<Dependencies: HotDealsDependencies>: View { ... }
    struct ProductDetailView<Dependencies: AllProductDependencies>: View { ... }
    ```

    The problem with this approach is that the original type alias no longer describes the dependency requirements of a given file, and it’s very, very difficult to figure out which files need which dependencies and which dependencies are _actually_ used. It also is a violation of the “least knowledge principle”, which indicates that code should not be exposed to or interact with distant or unrelated concepts.
    @@ -427,18 +427,18 @@ The problem with this approach is that the original type alias no longer describ
    **Bad Example**

    ```swift
    typealias PDPViewDependencies = TrackingDependency // Used by this file
    & LoggingDependency
    // 👆 LoggingDependency is used by ProductImageView, but not used by this file
    struct PDPView<Dependencies: PDPViewDependencies>: View {
    typealias ProductDetailViewDependencies = TrackingDependency // Used by this file
    & LoggingDependency
    // 👆 LoggingDependency is used by ProductImageView, but not used by this file
    struct ProductDetailView<Dependencies: ProductDetailViewDependencies>: View {
    let dependencies: Dependencies
    var body: some View {
    VStack {
    Text("Product Detail View")
    ProductImageView(dependencies: dependencies)
    // 👆 Gave us the compiler error: "PDPViewDependencies does not conform to LoggingDependency", so our knee-jerk reaction was to add LoggingDependency to PDPViewDependencies.
    // 👆 Gave us the compiler error: "ProductDetailViewDependencies does not conform to LoggingDependency", so our knee-jerk reaction was to add LoggingDependency to ProductDetailViewDependencies.
    }
    .onAppear { dependencies.tracker.track(Events.pdpViewAppeared) }
    .onAppear { dependencies.tracker.track(Events.productDetailViewAppeared) }
    }
    }
    ```
    @@ -450,18 +450,18 @@ The problem with this approach is that it breaks the automatic cascading of depe
    **Correct Example**

    ```swift
    typealias PDPViewDependencies = TrackingDependency
    & ProductImageViewDependencies
    // 👆 Forward the child's "Dependencies" type alias, not the individual dependencies of the child
    typealias ProductDetailViewDependencies = TrackingDependency
    & ProductImageViewDependencies
    // 👆 Forward the child's "Dependencies" type alias, not the individual dependencies of the child

    struct PDPView<Dependencies: PDPViewDependencies>: View {
    struct ProductDetailView<Dependencies: ProductDetailViewDependencies>: View {
    let dependencies: Dependencies
    var body: some View {
    VStack {
    Text("Product Detail View")
    ProductImageView(dependencies: dependencies)
    }
    .onAppear { dependencies.tracker.track(Events.pdpViewAppeared) }
    .onAppear { dependencies.tracker.track(Events.productDetailViewAppeared) }
    }
    }
    ```
    @@ -470,26 +470,26 @@ struct PDPView<Dependencies: PDPViewDependencies>: View {

    ### Type Alias Naming Pattern

    **🚫 Avoid: _PDPContainerBlock is basically the top-level component for PDP, so I’ll just name its ‘Dependencies’ type alias ‘PDPDependencies’.”_**
    **🚫 Avoid: _ProductContainerView is basically the top-level component for the "Product" feature, so I’ll just name its ‘Dependencies’ type alias ‘ProductDependencies’.”_**

    **Bad Example**

    ```swift
    typealias PDPDependencies = ...
    typealias ProductDependencies = ...

    struct PDPContainerBlock<Dependencies: PDPDependencies>: View { ... }
    struct ProductContainerView<Dependencies: ProductDependencies>: View { ... }
    ```

    The problem with this approach is that when engineers attempt to use `PDPContainerBlock`, they will have a tough time finding which “Dependencies” type alias belongs to that type.
    The problem with this approach is that when engineers attempt to use `ProductContainerView`, they will have a tough time finding which “Dependencies” type alias belongs to that type.

    **Each “Dependencies” type alias should be prefixed with the type name it represents.** E.g.: “PDPView” would have a dependencies type alias named “PDPViewDependencies". This makes child component “Dependencies” type aliases easy to find and grok.
    **Each “Dependencies” type alias should be prefixed with the type name it represents.** E.g.: “ProductDetailView” would have a dependencies type alias named “ProductDetailViewDependencies". This makes child component “Dependencies” type aliases easy to find and grok.

    **Correct Example**

    ```swift
    typealias PDPContainerBlockDependencies = ...
    typealias ProductContainerViewDependencies = ...

    struct PDPContainerBlock<Dependencies: PDPContainerBlockDependencies>: View { ... }
    struct ProductContainerView<Dependencies: ProductContainerViewDependencies>: View { ... }
    ```

    ---
    @@ -501,11 +501,11 @@ struct PDPContainerBlock<Dependencies: PDPContainerBlockDependencies>: View { ..
    **Bad Example**

    ```swift
    // In PDPViewDependencies.swift
    typealias PDPViewDependencies = ...
    // In ProductDetailViewDependencies.swift
    typealias ProductDetailViewDependencies = ...

    // In PDPView.swift
    struct PDPView<Dependencies: PDPViewDependencies>: View { ... }
    // In ProductDetailView.swift
    struct ProductDetailView<Dependencies: ProductDetailViewDependencies>: View { ... }
    ```

    While organizing types in separate files is good in many cases, doing so in this case makes it more difficult to make the mental connection between the sub-dependencies declared in the “Dependencies” type alias and the actual requirements of the file where it is used.
    @@ -536,11 +536,11 @@ extension ContextLoggableView {
    }
    }

    typealias PDPViewDependencies = ContextLoggableViewDependencies
    typealias ProductDetailViewDependencies = ContextLoggableViewDependencies

    struct PDPView<Dependencies: PDPViewDependencies>: ContextLoggableView {
    struct ProductDetailView<Dependencies: ProductDetailViewDependencies>: ContextLoggableView {
    let dependencies: Dependencies
    let context: String = "com.foo.pdp-view"
    let context: String = "com.foo.product-detail-view"

    var body: some View {
    VStack {
    @@ -573,11 +573,11 @@ struct ContextLogger<Dependencies: ContextLoggerDependencies>: ContextLogging {
    }
    }

    typealias PDPViewDependencies = ContextLoggerDependencies
    typealias ProductDetailViewDependencies = ContextLoggerDependencies

    struct PDPView<Dependencies: PDPViewDependencies>: View {
    struct ProductDetailView<Dependencies: ProductDetailViewDependencies>: View {
    let dependencies: Dependencies
    var logger: ContextLogger { ContextLogger(dependencies: dependencies, context: "com.foo.pdp-view") }
    var logger: ContextLogger { ContextLogger(dependencies: dependencies, context: "com.foo.product-detail-view") }

    var body: some View {
    VStack {
    @@ -602,11 +602,11 @@ extension Logging {
    }
    }

    typealias PDPViewDependencies = LoggingDependency
    typealias ProductDetailViewDependencies = LoggingDependency

    struct PDPView<Dependencies: PDPViewDependencies>: View, ContextProviding {
    struct ProductDetailView<Dependencies: ProductDetailViewDependencies>: View, ContextProviding {
    let dependencies: Dependencies
    let context = "com.foo.pdp-view"
    let context = "com.foo.product-detail-view"

    var body: some View {
    VStack {
    @@ -627,10 +627,10 @@ struct PDPView<Dependencies: PDPViewDependencies>: View, ContextProviding {
    **Bad Example**

    ```swift
    typealias PDPViewDependencies = ...
    typealias ProductDetailViewDependencies = ...

    struct PDPView: View {
    let dependencies: PDPViewDependencies
    struct ProductDetailView: View {
    let dependencies: ProductDetailViewDependencies
    ...
    }
    ```
    @@ -642,9 +642,9 @@ Doing this will cause explosion of upstream and downstream changes if a generic
    **Good Example**

    ```swift
    typealias PDPViewDependencies = ...
    typealias ProductDetailViewDependencies = ...

    struct PDPView<Dependencies: PDPViewDependencies>: View {
    struct ProductDetailView<Dependencies: ProductDetailViewDependencies>: View {
    let dependencies: Dependencies
    ...
    }
    @@ -659,7 +659,7 @@ struct PDPView<Dependencies: PDPViewDependencies>: View {
    **Bad Example**

    ```swift
    struct PDPView<ExternalDependencies: HotDealsExternalDependencies>: View {
    struct ProductDetailView<ExternalDependencies: HotDealsExternalDependencies>: View {
    let dependencies: SplicedHotDealsDependencies<ExternalDependencies>
    ...
    }
  9. albertbori revised this gist Apr 15, 2024. 1 changed file with 210 additions and 2 deletions.
    212 changes: 210 additions & 2 deletions CPDI-docs.md
    Original file line number Diff line number Diff line change
    @@ -407,56 +407,264 @@ The following sections contain more information on what to CPDI patterns to avoi

    **🚫 Avoid: _“It’s ok to reuse the dependencies type alias from another file, especially if it’s convenient.”_**

    The problem with this approach is that the type alias no longer describes the dependency requirements of a given file, and it’s very, very difficult to figure out which files need which dependencies and which dependencies are _actually_ used. It also is a violation of the “least knowledge principle”, which indicates that code should not be exposed to or interact with distant or unrelated concepts.
    **Bad Examples**

    ```swift
    struct PDPView<Dependencies: HotDealsDependencies>: View { ... }
    struct PDPView<Dependencies: AllPDPDependencies>: View { ... }
    ```

    The problem with this approach is that the original type alias no longer describes the dependency requirements of a given file, and it’s very, very difficult to figure out which files need which dependencies and which dependencies are _actually_ used. It also is a violation of the “least knowledge principle”, which indicates that code should not be exposed to or interact with distant or unrelated concepts.

    **Every file that uses a dependency should define its own “Dependencies” type alias.** This includes any “Spliced” dependencies type, if required. This keeps dependencies well-scoped and easy to reason about.

    ---

    ### Following Compiler Prompts Instead of CPDI

    **🚫 Avoid: _“If the compiler complains that ‘FooDependencies does not conform to BarDependency’ when passing dependencies into a child component, it’s ok to just add BarDependency to my file's type alias.”_**

    **Bad Example**

    ```swift
    typealias PDPViewDependencies = TrackingDependency // Used by this file
    & LoggingDependency
    // 👆 LoggingDependency is used by ProductImageView, but not used by this file
    struct PDPView<Dependencies: PDPViewDependencies>: View {
    let dependencies: Dependencies
    var body: some View {
    VStack {
    Text("Product Detail View")
    ProductImageView(dependencies: dependencies)
    // 👆 Gave us the compiler error: "PDPViewDependencies does not conform to LoggingDependency", so our knee-jerk reaction was to add LoggingDependency to PDPViewDependencies.
    }
    .onAppear { dependencies.tracker.track(Events.pdpViewAppeared) }
    }
    }
    ```

    The problem with this approach is that it breaks the automatic cascading of dependencies from file-to-file and level-to-level. If you add 'BarDependency' directly to your file because on if your child requires it, then if your child later removed the BarDependency requirement, your file would still needlessly be requiring it because you added it directly to your file's requirements instead of forwarding your child's `Dependencies` type alias.

    **Each “Dependencies” type alias should only include dependencies used within that file.** This includes forwarding the “Dependencies” type aliases of your direct children. This keeps dependencies well-scoped and easy to reason about.

    **Correct Example**

    ```swift
    typealias PDPViewDependencies = TrackingDependency
    & ProductImageViewDependencies
    // 👆 Forward the child's "Dependencies" type alias, not the individual dependencies of the child

    struct PDPView<Dependencies: PDPViewDependencies>: View {
    let dependencies: Dependencies
    var body: some View {
    VStack {
    Text("Product Detail View")
    ProductImageView(dependencies: dependencies)
    }
    .onAppear { dependencies.tracker.track(Events.pdpViewAppeared) }
    }
    }
    ```

    ---

    ### Type Alias Naming Pattern

    **🚫 Avoid: _“PDPContainerBlock is basically the top-level component for PDP, so I’ll just name its ‘Dependencies’ type alias ‘PDPDependencies’.”_**

    **Bad Example**

    ```swift
    typealias PDPDependencies = ...

    struct PDPContainerBlock<Dependencies: PDPDependencies>: View { ... }
    ```

    The problem with this approach is that when engineers attempt to use `PDPContainerBlock`, they will have a tough time finding which “Dependencies” type alias belongs to that type.

    **Each “Dependencies” type alias should be prefixed with the type name it represents.** E.g.: “PDPView” would have a dependencies type alias named “PDPViewDependencies". This makes child component “Dependencies” type aliases easy to find and grok.

    **Correct Example**

    ```swift
    typealias PDPContainerBlockDependencies = ...

    struct PDPContainerBlock<Dependencies: PDPContainerBlockDependencies>: View { ... }
    ```

    ---

    ### Type Alias Location

    **🚫 Avoid: _“I like to organize each type in a separate file. I’ll put my ‘Dependencies’ type alias in a file next to the file where it is used.”_**

    **Bad Example**

    ```swift
    // In PDPViewDependencies.swift
    typealias PDPViewDependencies = ...

    // In PDPView.swift
    struct PDPView<Dependencies: PDPViewDependencies>: View { ... }
    ```

    While organizing types in separate files is good in many cases, doing so in this case makes it more difficult to make the mental connection between the sub-dependencies declared in the “Dependencies” type alias and the actual requirements of the file where it is used.

    **Each “Dependencies” type alias should be defined in the same file where the types are being used.** This makes child component “Dependencies” type aliases easy to find, and makes it easier to know which dependencies are actually needed in the “Dependencies” type alias.

    ---

    ### Convenience & Helpers

    **🚫 Avoid: _“I’m going to extend this protocol with some conveniences, but those conveniences require some dependencies, so I’ll add a ‘Dependencies’ associated type to the protocol.”_**

    **Bad Example**

    ```swift
    typealias ContextLoggableViewDependencies = LoggingDependency

    /// Provides context-logging behavior to the implementing type
    protocol ContextLoggableView: View {
    associatedtype Dependencies: ContextLoggableViewDependencies
    var dependencies: Dependencies { get }
    var context: String { get }
    }

    extension ContextLoggableView {
    func log(_ message: String) {
    dependencies.logger.log("\(context): \(message)")
    }
    }

    typealias PDPViewDependencies = ContextLoggableViewDependencies

    struct PDPView<Dependencies: PDPViewDependencies>: ContextLoggableView {
    let dependencies: Dependencies
    let context: String = "com.foo.pdp-view"

    var body: some View {
    VStack {
    Text("Product Detail View")
    ...
    }
    .onAppear { log("View loaded!") }
    }
    }
    ```

    Protocol-Oriented Programming has a very particular scope of usefulness. It works best when extending protocols with reflexive behavior, or behavior that uses adjacent types (not needing dependency injection).

    More than this, attaching a Dependencies requirement to your protocol imposes opinionated implementation details to all implementers, which is a violation of properly scoped abstractions.

    **Use concrete, injected dependencies to provide convenience to your file.** This creates a very clear inversion-of-control pattern that works seamlessly with CPDI.

    **Good Example**

    ```swift
    typealias ContextLoggerDependencies = LoggingDependencies

    /// Provides context-logging behavior to any caller
    struct ContextLogger<Dependencies: ContextLoggerDependencies>: ContextLogging {
    let dependencies: Dependencies
    let context: String

    func log(_ message: String) {
    dependencies.logger.log("\(context): \(message)")
    }
    }

    typealias PDPViewDependencies = ContextLoggerDependencies

    struct PDPView<Dependencies: PDPViewDependencies>: View {
    let dependencies: Dependencies
    var logger: ContextLogger { ContextLogger(dependencies: dependencies, context: "com.foo.pdp-view") }

    var body: some View {
    VStack {
    Text("Product Detail View")
    ...
    }
    .onAppear { logger.log("View loaded!") }
    }
    }
    ```

    **Another Good Example**

    ```swift
    protocol ContextProviding {
    var context: String { get }
    }

    extension Logging {
    func log(_ message: String, source: some ContextProviding) {
    log("\(source.context): \(message)")
    }
    }

    typealias PDPViewDependencies = LoggingDependency

    struct PDPView<Dependencies: PDPViewDependencies>: View, ContextProviding {
    let dependencies: Dependencies
    let context = "com.foo.pdp-view"

    var body: some View {
    VStack {
    Text("Product Detail View")
    ...
    }
    .onAppear { logger.log("View loaded!", source: self) }
    }
    }
    ```

    ---

    ### Generic Dependencies

    **🚫 Avoid: _“To keep things simple, I’ll just use my Dependencies type alias directly as an existential type (any) instead of a generic type (some).”_**
    **🚫 Avoid: _“To keep things simple, I’ll just use my Dependencies type alias directly as an existential type (`any`) instead of a generic type (`some`).”_**

    **Bad Example**

    ```swift
    typealias PDPViewDependencies = ...

    struct PDPView: View {
    let dependencies: PDPViewDependencies
    ...
    }
    ```

    Doing this will cause explosion of upstream and downstream changes if a generic dependency is introduced in the future. With SwiftUI being based on protocols with associated types (View), generic dependencies will become very common as modularized features vend Views to each other through dependency injection.

    **Pass your Dependencies type alias generically (using a generic alias and constraint)** This saves a lot of work later on, if a generic dependency is introduced in the future.

    **Good Example**

    ```swift
    typealias PDPViewDependencies = ...

    struct PDPView<Dependencies: PDPViewDependencies>: View {
    let dependencies: Dependencies
    ...
    }
    ```

    ---

    ### Spliced Dependencies Reuse

    **🚫 Avoid: _“To keep things simple and DRY, I’ll just use this other Spliced Dependencies type to instantiate my narrow-scope dependency, even though it’s instantiated at a different scope than my feature.”_**

    **Bad Example**

    ```swift
    struct PDPView<ExternalDependencies: HotDealsExternalDependencies>: View {
    let dependencies: SplicedHotDealsDependencies<ExternalDependencies>
    ...
    }
    ```

    Doing this can cause circular references, memory leaks, high-memory usage, high CPU usage, etc.

    **Splice in narrow-scoped dependencies along with their corresponding components in the same file where the dependencies are used.** This keeps dependencies well-scoped and easy to reason about. It also protects against memory leaks and reduces overall memory footprint and CPU usage.
  10. albertbori revised this gist Apr 15, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion CPDI-docs.md
    Original file line number Diff line number Diff line change
    @@ -401,7 +401,7 @@ extension PDPComponent {

    ## Best Practices & Common Pitfalls

    The following sections contian more information on what to CPDI patterns to avoid and how to avoid them.
    The following sections contain more information on what to CPDI patterns to avoid and how to avoid them.

    ### "Dependencies" Reuse

  11. albertbori revised this gist Apr 13, 2024. 1 changed file with 63 additions and 1 deletion.
    64 changes: 63 additions & 1 deletion CPDI-docs.md
    Original file line number Diff line number Diff line change
    @@ -397,4 +397,66 @@ extension PDPComponent {
    dependencies.logger.log(message: "PDPComponent: " + message)
    }
    }
    ```
    ```

    ## Best Practices & Common Pitfalls

    The following sections contian more information on what to CPDI patterns to avoid and how to avoid them.

    ### "Dependencies" Reuse

    **🚫 Avoid: _“It’s ok to reuse the dependencies type alias from another file, especially if it’s convenient.”_**

    The problem with this approach is that the type alias no longer describes the dependency requirements of a given file, and it’s very, very difficult to figure out which files need which dependencies and which dependencies are _actually_ used. It also is a violation of the “least knowledge principle”, which indicates that code should not be exposed to or interact with distant or unrelated concepts.

    **Every file that uses a dependency should define its own “Dependencies” type alias.** This includes any “Spliced” dependencies type, if required. This keeps dependencies well-scoped and easy to reason about.

    ### Following Compiler Prompts Instead of CPDI

    **🚫 Avoid: _“If the compiler complains that ‘FooDependencies does not conform to BarDependency’ when passing dependencies into a child component, it’s ok to just add BarDependency to my file's type alias.”_**

    The problem with this approach is that it breaks the automatic cascading of dependencies from file-to-file and level-to-level. If you add 'BarDependency' directly to your file because on if your child requires it, then if your child later removed the BarDependency requirement, your file would still needlessly be requiring it because you added it directly to your file's requirements instead of forwarding your child's `Dependencies` type alias.

    **Each “Dependencies” type alias should only include dependencies used within that file.** This includes forwarding the “Dependencies” type aliases of your direct children. This keeps dependencies well-scoped and easy to reason about.

    ### Type Alias Naming Pattern

    **🚫 Avoid: _“PDPContainerBlock is basically the top-level component for PDP, so I’ll just name its ‘Dependencies’ type alias ‘PDPDependencies’.”_**

    The problem with this approach is that when engineers attempt to use `PDPContainerBlock`, they will have a tough time finding which “Dependencies” type alias belongs to that type.

    **Each “Dependencies” type alias should be prefixed with the type name it represents.** E.g.: “PDPView” would have a dependencies type alias named “PDPViewDependencies". This makes child component “Dependencies” type aliases easy to find and grok.

    ### Type Alias Location

    **🚫 Avoid: _“I like to organize each type in a separate file. I’ll put my ‘Dependencies’ type alias in a file next to the file where it is used.”_**

    While organizing types in separate files is good in many cases, doing so in this case makes it more difficult to make the mental connection between the sub-dependencies declared in the “Dependencies” type alias and the actual requirements of the file where it is used.

    **Each “Dependencies” type alias should be defined in the same file where the types are being used.** This makes child component “Dependencies” type aliases easy to find, and makes it easier to know which dependencies are actually needed in the “Dependencies” type alias.

    ### Convenience & Helpers

    **🚫 Avoid: _“I’m going to extend this protocol with some conveniences, but those conveniences require some dependencies, so I’ll add a ‘Dependencies’ associated type to the protocol.”_**

    Protocol-Oriented Programming has a very particular scope of usefulness. It works best when extending protocols with reflexive behavior, or behavior that uses adjacent types (not needing dependency injection).

    More than this, attaching a Dependencies requirement to your protocol imposes opinionated implementation details to all implementers, which is a violation of properly scoped abstractions.

    **Use concrete, injected dependencies to provide convenience to your file.** This creates a very clear inversion-of-control pattern that works seamlessly with CPDI.

    ### Generic Dependencies

    **🚫 Avoid: _“To keep things simple, I’ll just use my Dependencies type alias directly as an existential type (any) instead of a generic type (some).”_**

    Doing this will cause explosion of upstream and downstream changes if a generic dependency is introduced in the future. With SwiftUI being based on protocols with associated types (View), generic dependencies will become very common as modularized features vend Views to each other through dependency injection.

    **Pass your Dependencies type alias generically (using a generic alias and constraint)** This saves a lot of work later on, if a generic dependency is introduced in the future.

    ### Spliced Dependencies Reuse

    **🚫 Avoid: _“To keep things simple and DRY, I’ll just use this other Spliced Dependencies type to instantiate my narrow-scope dependency, even though it’s instantiated at a different scope than my feature.”_**

    Doing this can cause circular references, memory leaks, high-memory usage, high CPU usage, etc.

    **Splice in narrow-scoped dependencies along with their corresponding components in the same file where the dependencies are used.** This keeps dependencies well-scoped and easy to reason about. It also protects against memory leaks and reduces overall memory footprint and CPU usage.
  12. albertbori revised this gist Apr 13, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion CPDI-docs.md
    Original file line number Diff line number Diff line change
    @@ -283,7 +283,7 @@ This provides a succinct API for accessing all of our internal code, and a simpl
    Finally, callers can invoke your feature at any time using the following code:

    ```swift
    PDPFeature(dependencies: dependencies).view(for: "ABC")
    PDPFeature(dependencies: dependencies).pdpView(for: "ABC")
    ```

    Where the caller's `Dependencies` type alias conforms to our external `PDPFeatureDependencies` type alias that we just created.
  13. albertbori revised this gist Apr 13, 2024. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion CPDI-docs.md
    Original file line number Diff line number Diff line change
    @@ -241,13 +241,14 @@ PDPView(

    Where the caller's `Dependencies` type alias conforms to our external `PDPViewExternalDependencies` type alias that we just created.

    ### Public Encapsulation
    ### Public Encapsulation (Factory)

    To prevent from having to make all of your dependencies type aliases public, you can splice your dependencies at the top-most object/struct or call site of your feature. Regardless, it's usually a good idea to create a publicly accessible wrapper around your internal functionality to create a single entry point into your feature. Here is such an example:

    ```swift
    public typealias PDPFeatureDependencies = PDPViewExternalDependencies
    & PDPDataRepositoryDependencies
    // & Other encapsulated component dependencies
    ```

    First, we created a new `PDPFeatureDependencies` type alias in a new file where our feature's API will live. We declare this type alias as publicly accessible.
  14. albertbori revised this gist Apr 13, 2024. 1 changed file with 5 additions and 3 deletions.
    8 changes: 5 additions & 3 deletions CPDI-docs.md
    Original file line number Diff line number Diff line change
    @@ -246,7 +246,7 @@ Where the caller's `Dependencies` type alias conforms to our external `PDPViewEx
    To prevent from having to make all of your dependencies type aliases public, you can splice your dependencies at the top-most object/struct or call site of your feature. Regardless, it's usually a good idea to create a publicly accessible wrapper around your internal functionality to create a single entry point into your feature. Here is such an example:

    ```swift
    public typealias PDPFeatureDependencies = PDPExternalDependencies
    public typealias PDPFeatureDependencies = PDPViewExternalDependencies
    & PDPDataRepositoryDependencies
    ```

    @@ -264,14 +264,16 @@ public struct PDPFeature<Dependencies: PDPFeatureDependencies> {
    self.dependencies = dependencies
    }

    public func view(for sku: String) -> some View {
    public func pdpView(for sku: String) -> some View {
    PDPView(
    dependencies: SplicedPDPViewDependencies(
    externalDependencies: dependencies,
    pdpDataRepository: PDPDataRepository(dependencies: dependencies, sku: sku)
    )
    )
    }

    // Other PDP Feature functions
    }
    ```

    @@ -292,7 +294,7 @@ The following example shows the ideal setup for forwarding any kind of dependenc
    ```swift
    typealias FooDependencies = FooBaseDependencies // Superclass dependencies
    & LoggingDependency // Used directly in this file
    & BarDependencies // Direct child's dependencies
    & BarDependencies // Direct child's dependencies
    & Baz_trackDependencies // Helper/function call w/ dependencies

    class Foo<Dependencies: FooDependencies>: FooBase {
  15. albertbori revised this gist Apr 13, 2024. 1 changed file with 6 additions and 1 deletion.
    7 changes: 6 additions & 1 deletion CPDI-docs.md
    Original file line number Diff line number Diff line change
    @@ -265,7 +265,12 @@ public struct PDPFeature<Dependencies: PDPFeatureDependencies> {
    }

    public func view(for sku: String) -> some View {
    PDPView(dependencies: SplicedPDPViewDependencies(externalDependencies: dependencies, pdpDataRepository: PDPDataRepository(dependencies: dependencies, sku: sku)))
    PDPView(
    dependencies: SplicedPDPViewDependencies(
    externalDependencies: dependencies,
    pdpDataRepository: PDPDataRepository(dependencies: dependencies, sku: sku)
    )
    )
    }
    }
    ```
  16. albertbori revised this gist Apr 13, 2024. 1 changed file with 17 additions and 6 deletions.
    23 changes: 17 additions & 6 deletions CPDI-docs.md
    Original file line number Diff line number Diff line change
    @@ -108,28 +108,36 @@ struct PDPView<Dependencies: PDPViewDependencies>: View {

    > Note: We also use a generic alias instead of using the `PDPViewDependencies` type alias directly within the class. We do this because it lets us add generic dependencies (later on) without any additional code changes.
    ### Order of Dependencies Parameter
    ### "Dependencies" Parameter Order

    For readability, consistency, and convention, the `dependencies` parameter should always appear first in any parameter list. For example:

    ```swift
    // BAD
    PDPView(sku: "ABC", disposition: ..., context: ..., dependencies: ...)

    // GOOD
    PDPView(dependencies: ..., sku: "ABC", disposition: ..., context: ...)
    ```

    ## Sub-Dependencies

    `Dependencies` types _should never be reused or shared with other types_, except when forwarding those as sub-dependencies. Forwarding a `Dependencies` type alias should only happen in the following sub-dependencies situations:
    `Dependencies` type aliases _should never be reused or shared with other types_, except when forwarding them as sub-dependencies. Forwarding a `Dependencies` type alias should only happen in the following sub-dependencies situations:

    * Direct Child Dependencies
    * Superclass Dependencies
    * Helper or Static Function Dependencies

    ### Direct Child Dependencies

    If your code file instantiates a struct or class that has its own dependencies type, you must forward that child's `Dependencies` type alias along with your file's `Dependencies` type alias. The following `PDPView` example instantiates a sub-view called `ProductImageView`. `ProductImageView` has its own dependencies requirement declared alongside `ProductImageView` called `ProductImageViewDependencies`. The child's dependencies type alias (`ProductImageViewDependencies`) is forwarded as part of the parent's dependencies type alias (`PDPViewDependencies`), as follows:
    If your code file instantiates a struct or class that has its own dependencies type, you must forward that child's Dependencies type alias along with your file's Dependencies type alias.

    Consider a SwiftUI view named `PDPView` that includes a sub-view called `ProductImageView`. Each view requires specific dependencies:

    - `ProductImageView` depends on various services and utilities specific to its functionality, grouped under a type alias `ProductImageViewDependencies`.
    - `PDPView` requires its own set of services, such as `TrackingDependency`, for tracking user interactions, grouped under a type alias `PDPViewDependencies`.

    To accommodate all requirements, `PDPViewDependencies` must integrate `ProductImageViewDependencies` into its own dependency structure. For example:

    ```swift
    typealias PDPViewDependencies = TrackingDependency
    @@ -149,6 +157,8 @@ struct PDPView<Dependencies: PDPViewDependencies>: View {
    }
    ```

    This principle applies in all code files that work with injected dependencies, not just for SwiftUI views.

    ### Superclass Dependencies

    Similar to child dependencies, if you inherit a class that has its own dependencies requirement, you must forward that superclass' `Dependencies` type alias along with your file's `Dependencies` type alias. In the following example, a UIKit view controller depends on a base view controller, which has its own dependencies requirement.
    @@ -160,6 +170,7 @@ typealias PDPViewControllerDependencies = TrackingDependency
    struct PDPViewController<Dependencies: PDPViewControllerDependencies>: ShopBaseViewController {
    let dependencies: Dependencies
    init(dependencies: Dependencies) {
    self.dependencies = dependencies
    super.init(dependencies: dependencies) // Borrows PDPViewControllerDependencies
    }
    }
    @@ -188,9 +199,9 @@ Now, we create the `SplicedPDPViewDependencies` type that allows us to create an

    ```swift
    struct SplicedPDPViewDependencies<ExternalDependencies: PDPViewExternalDependencies>: PDPViewDependencies {
    let externalDependencies: ExternalDependencies
    let pdpDataRepository: PDPDataRepository
    var tracking: Tracking {
    let externalDependencies: ExternalDependencies // Obtained from outside callers
    let pdpDataRepository: PDPDataRepository // Created locally
    var tracking: Tracking { // Forwarded to children
    externalDependencies.tracking
    }

  17. albertbori created this gist Apr 12, 2024.
    381 changes: 381 additions & 0 deletions CPDI-docs.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,381 @@
    # Composed Protocol Dependency Injection (CPDI) Pattern

    This dependency injection pattern uses native Swift language features to provide a safe, concise, deterministic, and intentional approach to dependency injection.

    ## Overview

    The primary Swift language feature that drives CPDI is called "protocol composition". This feature allows you to create a type alias from any combination of protocols. For example:

    ```swift
    protocol Car {
    func drive()
    }

    protocol Airplane {
    func fly()
    }

    typealias Carplane = Car & Airplane

    func start(vehicle: Carplane) {
    vehicle.drive()
    vehicle.fly()
    }
    ```

    The above example shows how you can combine two protocols into a single type alias and then use that to describe a tangible requirement. Additionally, that type alias can be used as a requirement for another type alias, and so on. For example:

    ```swift
    protocol Motorcycle {
    func wheelie()
    }

    typealias Carplanecycle = Carplane & Motorcycle

    func start(vehicle: Carplanecycle) {
    vehicle.drive()
    vehicle.wheelie()
    vehicle.fly()
    }
    ```

    The above example uses the `Carplane` type alias from the previous example and combines it effortlessly with the `Motorcycle` protocol.

    **Using this approach, we can provide every dependency to every part of a modularized app using a single parameter.**

    If we create protocols that describe something we depend on, we can compose those protocols together into a type alias in each file to describe that file's dependencies. For example:

    ```swift
    protocol LoggingDependency {
    var logger: Logging { get }
    }

    protocol TrackingDependency {
    var tracker: Tracking { get }
    }
    ```

    If the above dependency protocols are defined somewhere accessible to our file's code, we can use those dependency protocols like so:

    ```swift
    typealias ThunkDependencies = LoggingDependency
    & TrackingDependency

    func thunk(dependencies: ThunkDependencies) {
    dependencies.logger.logDebug("Thunk was called!")
    dependencies.tracker.trackEvent(Events.thunk)
    }
    ```

    > Note: We recommend that you namespace your dependencies as shown above to avoid property and function name collisions in your dependencies. Instead of `dependencies.trackAddToCart()`, it should be `dependencies.tracker.trackAddToCart()`.
    > Note: One of the major benefits of CPDI is that it _cannot crash at runtime_ because all dependencies are satisfied explicitly and cannot be accessed outside of their intended runtime scopes. This inherent safety puts the burden of proof on the engineer (in the form of type safety) instead of putting the burden of proof on the end-user (in the form of crashes). It is for this and several other reasons that CPDI is preferred over the many other dependency injection approaches and libraries.
    ## Basic Feature

    In this simple example, we have a SwiftUI view that needs do to some tracking. We'll use CPDI to fulfill its requirements.

    ```swift
    struct PDPView: View {
    var body: some View {
    VStack {
    Text("Product Detail Page")
    Button("Add to cart") {
    // TODO: Track add to cart event
    }
    }
    }
    }
    ```

    To apply the CPDI pattern to `PDPView`, we add a `Dependencies` type _to the top of the same file where the dependencies are being used_. We are also careful to _name the dependencies type using the exact name of the type whose requirements it satisfies_: `PDPViewDependencies`. This location and naming of the dependencies type alias are critical for discoverability and maintainability. Following this pattern will help prevent any downstream confusion about the scope of a given type alias.

    ```swift
    typealias PDPViewDependencies = TrackingDependency

    struct PDPView<Dependencies: PDPViewDependencies>: View {
    let dependencies: Dependencies
    var body: some View {
    VStack {
    Text("Product Detail Page")
    Button("Add to cart") {
    dependencies.tracker.trackAddToCart()
    }
    }
    }
    }
    ```

    > Note: We also use a generic alias instead of using the `PDPViewDependencies` type alias directly within the class. We do this because it lets us add generic dependencies (later on) without any additional code changes.
    ### Order of Dependencies Parameter

    For readability, consistency, and convention, the `dependencies` parameter should always appear first in any parameter list. For example:

    ```swift
    // BAD
    PDPView(sku: "ABC", disposition: ..., context: ..., dependencies: ...)
    // GOOD
    PDPView(dependencies: ..., sku: "ABC", disposition: ..., context: ...)
    ```

    ## Sub-Dependencies

    `Dependencies` types _should never be reused or shared with other types_, except when forwarding those as sub-dependencies. Forwarding a `Dependencies` type alias should only happen in the following sub-dependencies situations:

    * Direct Child Dependencies
    * Superclass Dependencies
    * Helper or Static Function Dependencies

    ### Direct Child Dependencies

    If your code file instantiates a struct or class that has its own dependencies type, you must forward that child's `Dependencies` type alias along with your file's `Dependencies` type alias. The following `PDPView` example instantiates a sub-view called `ProductImageView`. `ProductImageView` has its own dependencies requirement declared alongside `ProductImageView` called `ProductImageViewDependencies`. The child's dependencies type alias (`ProductImageViewDependencies`) is forwarded as part of the parent's dependencies type alias (`PDPViewDependencies`), as follows:

    ```swift
    typealias PDPViewDependencies = TrackingDependency
    & ProductImageViewDependencies // Required by ProductImageView

    struct PDPView<Dependencies: PDPViewDependencies>: View {
    let dependencies: Dependencies
    var body: some View {
    VStack {
    Text("Product Detail Page")
    ProductImageView(dependencies: dependencies) // Borrows PDPViewDependencies
    Button("Add to cart") {
    dependencies.tracker.trackAddToCart()
    }
    }
    }
    }
    ```

    ### Superclass Dependencies

    Similar to child dependencies, if you inherit a class that has its own dependencies requirement, you must forward that superclass' `Dependencies` type alias along with your file's `Dependencies` type alias. In the following example, a UIKit view controller depends on a base view controller, which has its own dependencies requirement.

    ```swift
    typealias PDPViewControllerDependencies = TrackingDependency
    & ShopBaseViewControllerDependencies // Required by ShopBaseViewController

    struct PDPViewController<Dependencies: PDPViewControllerDependencies>: ShopBaseViewController {
    let dependencies: Dependencies
    init(dependencies: Dependencies) {
    super.init(dependencies: dependencies) // Borrows PDPViewControllerDependencies
    }
    }
    ```

    ## Scoping Dependencies

    If you you have a dependency that should only be accessible within a certain struct/class, file, or module, or if you need a dependency to be instantiated and destroyed along with a certain component, view, or other memory scope, you will have to use a technique called "Dependencies Splicing".

    In this approach, we use a concrete struct or class to splice the narrowly scoped dependencies into the broader-scoped dependencies in a way that maintains the desired memory scope and accessibility.

    Continuing with our previous example, if we want to add a `PDPDataRepositoryDependency` to the `PDPView` and that dependency is only internally available (within the same module), and you want that repository to be instantiated and destroyed along with the `PDPView`, then we would add that dependency to the PDPViewDependencies, as you would expect.

    ```swift
    typealias PDPViewDependencies = TrackingDependency
    & PDPDataRepositoryDependency
    ```

    Then, we create a second dependencies type alias that splits out all of the broader-scope dependencies. This will be used to help construct the spliced dependencies concrete type.

    ```swift
    typealias PDPViewExternalDependencies = TrackingDependency
    ```

    Now, we create the `SplicedPDPViewDependencies` type that allows us to create and inject the `PDPDataRepository` into a single `Dependencies` type, like so:

    ```swift
    struct SplicedPDPViewDependencies<ExternalDependencies: PDPViewExternalDependencies>: PDPViewDependencies {
    let externalDependencies: ExternalDependencies
    let pdpDataRepository: PDPDataRepository
    var tracking: Tracking {
    externalDependencies.tracking
    }

    init(externalDependencies: ExternalDependencies, pdpDataRepository: PDPDataRepository) {
    self.externalDependencies = externalDependencies
    self.pdpDataRepository = pdpDataRepository
    }
    }
    ```

    The spliced dependencies type above combines the broad and narrow scope dependencies. This can be a struct or a class, as needed. `SplicedPDPViewDependencies` will now act as the underlying dependencies type for `PDPView`. We accomplish this by changing `PDPView`'s generic dependency declaration like so:

    ```swift
    struct PDPView<ExternalDependencies: PDPExternalDependencies>: View {
    let dependencies: SplicedPDPViewDependencies<ExternalDependencies>
    var body: some View {
    VStack {
    Text(dependencies.pdpDataRepository.productName)
    Button("Add to cart") {
    dependencies.tracking.trackAddToCart()
    }
    }
    }
    }
    ```

    How and where the `SplicedPDPViewDependencies` type is initialized depends on several factors, but for SwiftUI, it usually must be done by whoever is instantiating the SwiftUI view in question. The previous example may be declared like so:

    ```swift
    PDPView(
    dependencies: SplicedPDPViewDependencies(
    externalDependencies: ...,
    pdpDataRepository: PDPDataRepository(dependencies: ...)
    )
    )
    ```

    Where the caller's `Dependencies` type alias conforms to our external `PDPViewExternalDependencies` type alias that we just created.

    ### Public Encapsulation

    To prevent from having to make all of your dependencies type aliases public, you can splice your dependencies at the top-most object/struct or call site of your feature. Regardless, it's usually a good idea to create a publicly accessible wrapper around your internal functionality to create a single entry point into your feature. Here is such an example:

    ```swift
    public typealias PDPFeatureDependencies = PDPExternalDependencies
    & PDPDataRepositoryDependencies
    ```

    First, we created a new `PDPFeatureDependencies` type alias in a new file where our feature's API will live. We declare this type alias as publicly accessible.

    > Note: You may need to declare your "External" Dependencies type aliases as public as well, as required, but no other type aliases should be made public. If you find yourself making all of your types public to fix compiler warnings, you should reevaluate how you are splicing your dependencies.
    Next, we create the public API using `PDPFeatureDependencies`:

    ```swift
    public struct PDPFeature<Dependencies: PDPFeatureDependencies> {
    let dependencies: Dependencies

    public init(dependencies: Dependencies) {
    self.dependencies = dependencies
    }

    public func view(for sku: String) -> some View {
    PDPView(dependencies: SplicedPDPViewDependencies(externalDependencies: dependencies, pdpDataRepository: PDPDataRepository(dependencies: dependencies, sku: sku)))
    }
    }
    ```

    This provides a succinct API for accessing all of our internal code, and a simple `Dependencies` type alias for external callers to grok and use.

    Finally, callers can invoke your feature at any time using the following code:

    ```swift
    PDPFeature(dependencies: dependencies).view(for: "ABC")
    ```

    Where the caller's `Dependencies` type alias conforms to our external `PDPFeatureDependencies` type alias that we just created.

    ## All In One Reference

    The following example shows the ideal setup for forwarding any kind of dependencies for a type called `Foo`.

    ```swift
    typealias FooDependencies = FooBaseDependencies // Superclass dependencies
    & LoggingDependency // Used directly in this file
    & BarDependencies // Direct child's dependencies
    & Baz_trackDependencies // Helper/function call w/ dependencies

    class Foo<Dependencies: FooDependencies>: FooBase {
    let dependencies: Dependencies
    let bar: Bar

    init(dependencies: Dependencies) {
    self.dependencies = dependencies // set own dependencies, if required
    self.bar = Bar(dependencies: dependencies) // pass child-dependencies
    super.init(dependencies: dependencies) // pass superclass dependencies
    Baz.track(dependencies: dependencies) // pass helper function dependencies
    dependencies.logger.log("foo Loaded!") // use dependencies directly
    }
    }
    ```

    ## Forbidden Patterns (Don'ts)

    The following uses of CPDI will land you into trouble. Avoid using these where possible, but if you use them, do it right.

    ### Composing Into Protocols

    You may be tempted to create a new protocol that composes other dependencies into it. _Resist this urge._ Always use type aliases when composing and forwarding protocols.

    ```swift
    protocol PDPViewDependencies: TrackingDependency & ProductImageViewDependencies {
    // ...
    }
    ```

    The consequences of using the above approach are that when you compose this protocol with other type aliases that forward the same sub-dependencies, you will get unresolvable compiler errors akin to, "duplicate conformance detected".

    ### Helper Functions

    You should avoid passing dependencies directly into functions. Instead, you should pass the dependencies via the initializer of the thing that vends the helper function. However, if you must, the following pattern should be followed when passing dependencies into helper functions.

    Similar to child dependencies and superclass dependencies, if your file calls a helper function that has its own dependencies requirement, you must forward that function's `Dependencies` type alias along with your file's `Dependencies` type alias. In the following example, a SwiftUI view modifier requires a dependencies type alias that must be satisfied by the `PDPViewDependencies` type alias. The view modifier in this example auto-expands the product view when tapped.

    ```swift
    typealias PDPViewDependencies = TrackingDependency
    & View_autoExpandDependencies // Required by `.autoExpand(...)`, declared in `View+AutoExpand.swift`

    struct PDPView<Dependencies: PDPViewDependencies>: View {
    let dependencies: Dependencies
    var body: some View {
    VStack {
    Text("Product Detail Page")
    Button("Add to cart") {
    dependencies.tracker.trackAddToCart()
    }
    }
    .autoExpand(dependencies: dependencies) // Borrows PDPViewDependencies
    }
    }
    ```

    ### Inherited Dependencies

    When building protocols that have extended behavior, using an `associatedtype` for Dependencies is generally discouraged because it can lead to difficult situations where confusing, circular, or unresolvable dependency loops can occur. It is also a conceptual POP violation to mix implementation constraints into an abstract description of functionality. Instead, stick to providing convenience directly through dependencies and not through protocol conformance of self. Protocol Oriented Programming favors extensions that provide reflexive or laterally-available concrete behavior. If your protocol extension needs a dependency, rethink your pattern.

    Otherwise, if you _must_ use this pattern, adhere to the following guidelines:

    If your type implements a protocol with an associated `Dependencies` type, you _must_ also forward that base class or protocol file's `FooDependencies` type alias along with your file's dependencies type alias, like so:

    ```swift
    typealias PDPViewDependencies = TrackingDependency
    & ProductImageViewDependencies
    & PDPComponentDependencies // Required by PDPComponent

    struct PDPView<Dependencies: PDPViewDependencies>: View, PDPComponent /* <- Implicitly uses PDPViewDependencies */ {
    let dependencies: Dependencies
    var body: some View {
    VStack {
    Text("Product Detail Page")
    ProductImageView(dependencies: dependencies)
    Button("Add to cart") {
    dependencies.tracker.trackAddToCart()
    }
    }
    .onAppear {
    log("appeared!")
    }
    }
    }
    ```

    The above example implements and uses a protocol called `PDPComponent` by forwarding its `PDPComponentDependencies` type alias. `PDPComponent` is defined like so:

    ```swift
    typealias PDPComponentDependencies = LoggingDependency

    protocol PDPComponent {
    associatedtype Dependencies: PDPComponentDependencies
    let dependencies: Dependencies
    }

    extension PDPComponent {
    func log(message: String) {
    dependencies.logger.log(message: "PDPComponent: " + message)
    }
    }
    ```