0

I've got these classes:

enum Environment {
    case staging
    case production
    
    static func current() -> Environment {
        #if STAGING
        return .staging
        #elseif PROD
        return .production
        #endif
    }
}

struct HostEnvironment {
    
    private let urlStrings: [Environment: String] = [
        .staging: "staging.website.com",
        .production: "www.website.com"
    ]
    
    let name: String
    
    init(env: () -> Environment) {
        name = urlStrings[env()]!
    }
}

struct StripeEnvironment {
    
    private let publishableKeys: [Environment: String] = [
        .staging: "pk_test_abc",
        .production: "pk_live_abc"
    ]
    
    private let clientIds: [Environment: String] = [
        .staging: "ca_abc",
        .production: "ca_def"
    ]
    
    let publishableKey: String
    let clientId: String
    
    init(env: () -> Environment) {
        publishableKey = publishableKeys[env()]!
        clientId = clientIds[env()]!
    }
}

These classes should be Entities, right? Because they are the business rules that will be the same across the enterprise among different apps. Now, these are actually being used only in the Interface-Adapter / Gateway classes which should reside in the outer layer of the Entities and UseCases like so:

protocol AuthApiInterface {
    func authenticate(params: AuthParam) -> Observable<AuthInfo>
}

final class AuthApiAdapter: AuthApiInterface {
    private let hostEnv: () -> HostEnvironment
    init(hostEnv: () -> HostEnvironment) {
        self.hostEnv = hostEnv
    }

    func authenticate(params: AuthParam) -> Observable<AuthInfo> {
        let hostName = hostEnv().name
        let url = URL(string: "https://\(hostName)/auth"
        return ...
    }
}

protocol PaymentGateway {
    func pay(params: PaymentParam) -> Observable<PaymentInfo>
}

final class StripePayment: AuthApiInterface {
    private stripeEnv: () -> StripeEnvironment
    init(stripeEnv: () -> StripeEnvironment) {
        self.stripeEnv = stripeEnv
    }

    func pay(params: PaymentParam) -> Observable<PaymentInfo> {
        let publishableKey = stripeEnv().publishableKey
        let clientId = stripeEnv().clientId
        return ...
    }
}

Is this okay? Considering the Interface-Adapter and Gateway are using something ouside of their boundary. Is there any better way to do it?

Thanks.

0

2 Answers 2

4

The More General Part of the Answer

I'm going to answer the question you stated in your closing line, but there's a caveat - see below.

Is this okay? Considering the Interface-Adapter and Gateway are using something outside of their boundary.

The interface adapters and gateways are supposed to use/reference something from an inner layer. They can, of course, use things from their own layer, but to actually do anything useful, they have to cross the layer boundary at some point. If you're following the dependency rule, the dependencies should only go towards the inner layers (and, in the more strict approach, only towards the next layer in). To achieve this, you either use a thing from an inner layer (to tell it to do something), or you implement an interface from an inner layer (so that that inner-layer code can call a thing in the outer layer, without depending on any types defined there).

This image sort of summarizes Clean Architecture.

enter image description here

Look closely at the inset:

enter image description here

The presenter (an interface adapter) implements an interface defined in the application business rules layer (the next layer in). This is precisely what enables dependency inversion. The interface (the "output port") is defined for the purposes of the interactor (the methods on the interface are designed to serve the needs of the specific interactor; it's not designed as some generic abstraction of a presenter). It's the interactor that "owns" the interface. The presenter then forms a bridge between the interface required by the interactor, and the interface(s) exposed by the GUI library you're using - in other words, it adapts one interface to work with a different one.

Similarly, the controller just makes use of a type defined in the application business rules layer. You can think of it as adapting the interfaces in the other direction - it translates from whatever general-purpose API the frameworks you're using have, to the interface that's more application-specific, expressed in terms that make more sense for the design of the interactor.

Now, this is just an example; you don't have to have this exact structure - the number and kinds of elements in each layer could vary (maybe you merge or split some objects that are in the same layer, maybe you have an abstract base class instead of an interface, etc.). What matters is the general principle - for classes that need to interact across the layer boundary, be conscious of which layer is the more higher level one, which objects should define/own types (not just interfaces, but also things like methods parameters, etc.), and control the direction of your dependencies.

This same basic idea repeats across every layer boundary. That doesn't mean you should have a large, elaborate web of interfaces and classes - keep it as simple as possible. It also doesn't mean that the exact same elements should appear in every layer. So, don't automatically apply this as a generic pattern to every class you create - this is meant for the classes that are at layer boundaries; internally, you could factor your code as you see fit. E.g. maybe your interactor manipulates the entities directly, or maybe it orchestrates two or three other, intermediary classes, that manipulate entities in turn - that's for you to figure out (maybe over project lifetime), as you wrestle with your problem domain, and with the inflow of new feature and change requests.

The Caveat

struct HostEnvironment {
    ...
}

struct StripeEnvironment {
    ...
}

These classes should be Entities, right? Because they are the business rules that will be the same across the enterprise among different apps.

Actually, these don't really look like business rules to me (as the other answer mentioned, they are more like config that belongs in the infrastructure layer). You could maybe argue that one of them (StripeEnvironment) is an entity in an anemic domain model, but even then, it doesn't actually express/encapsulate any business rules. Some data on its own does not business rule make (I hope that didn't sound condescending, I just wanted to make a point).

By "business rule", we don't mean any arbitrary rule vaguely related to the business. A business rule is something that's part of the higher-level rules that govern how things work in the domain, the rules of how the actual day-to-day business reality is put into practice, that we need to embody in our application. It's a distilled understanding of how people who are going to be using and benefiting from your application think about and go about their work - or some aspect of their work.

(Some of) your business rules are actually represented by the code you haven't shown; e.g. in case of payments, it's the code that decides when and how to call pay (and probably how to handle any thrown exceptions):

// Look at whatever calls this - that'll be your business logic 
protocol PaymentGateway {
    func pay(params: PaymentParam) -> Observable<PaymentInfo>
}

Note, for example, that you can't infer the rules (the logic) expressed there (in the calling code) by simply looking at StripeEnvironment - that's what I mean when I say that StripeEnvironment and HostEnvironment don't really capture any business rules - the relevant code is quite literally not in there.

It's possible that you haven't factored out that code into its own thing; if not, than what's there would constitute the basis of your as-of-yet nonexisting interactor. Now, the interactor code might be simple and straightforward (one or two lines - perfectly fine), or it might be more complicated. E.g., it could have a number of conditionals, or it could delegate parts of its work to more specialized classes - and some of those narrowly specialized classes would be Clean Architecture's entities.

You see, the entities in the context of Clean Architecture aren't all simple data structures with no behavior. A good chunk of the code in the innermost layer is meant to consist of proper, encapsulated objects that allow you to ask them to do stuff for you - by calling methods, and passing (sometimes callable) arguments. Note that this is different from what ORMs call entities, and also different (but in less significant way) from what entities refer to in DDD. Just something to be aware of. You can find material online that explores each of these further, but you'll have to do some sifting, as there's a lot of subpar content out there; try to find/obtain original sources where you can.

Now, you don't have to do it that way. You could go down the anemic domain model route, and have your logic mostly reside in the surrounding classes and functions. That works better for some people for some types of applications. Also, some frameworks might kind of force you to adopt an approach that's more like that. So, there are practicalities to consider that might cause you to deviate from the ideal - whatever that ideal might be for you.

1
  • Thank you for the pretty thorough explanation. As you can see, I have a hard time separating these into their correct categorizations. I don't know what to look at to determine which on is which. I'll have a look on this more. Commented Sep 15, 2022 at 11:26
3

No.

What are the structures you asked about? Unless the application you're developing is a deployment tool, those are not entities because they are not part of the application domain.

I would classify that data as configuration rather than entities. I'd explore the possibility of reading that data from the OS in some way (config file, environment variable) so that it lives in your deployment scripts rather than application code.

3
  • Thanks. I was thinking that everything that is consistent on every app are Entities. So Entities should be the items that will reflect the business process in reality, is that it? I have a hard time separating these into their correct categorizations. I don't know what to look at to determine which on is which. Commented Sep 15, 2022 at 11:26
  • 1
    Entities are a component of the business logic. Configuration options are data structures too and you might use the same syntactic constructs to implement them, but the way you think about (and then treat) your "business" entities is different. As a general rule if the people that pay you to develop the software talk among themselves about the thing, it's part oif the domain. If it's stuff that is in the application because you need it, it is not.
    – Rad80
    Commented Sep 16, 2022 at 7:28
  • I see. I'll take a not on that. BTW, so in that case, I can actually use Entities as a parameter or dependency for the Interface-Adapters / Gateway then? And in the View/ViewModels or the Presentation Layer itself then? I always map the entity to a DTO called Request Model for the Interface-Adapter (especially for API) and View Param for the Views. Because I was under the impression that we should really separate these layers. Commented Sep 18, 2022 at 10:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.