in General

Single responsibility can be a real headache!

The trade of keeping it simple stupid vs keeping code clean for Swift mobile apps

Single responsibility apps are a headache to code, to remember, to even parse visually; when it comes to simple apps — isn’t it just easier to keep it simple, stupid?


So I’ve been reading up on the idea of Single responsibility, which is aligned with the principal of SOLID, as popularised by “Uncle Bob”

Single responsibility is defined as:

The single-responsibility principle (SRP) is a computer-programming principle that states that every module or class[1] should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class, module or function.

Wikipedia – https://en.wikipedia.org/wiki/Single-responsibility_principle

Visually illustrated;

Clean Architecture
Clean Architecture diagram
Source: CleanCoder

The idea behind SOLID, and that of single responsibility, is to ensure that your apps are de-coupled where business logic, the use-cases that sit behind your application are de-coupled from your models and thinly applied.

It is in-effect, separation of concerns.

I’ve seen a few VIPER design patterns that try to follow SOLID too; and to be honest, they are a real pain to understand, visually follow — and the same can be true for single responsibility.

The issue I have is that it can easily become really, really hard to visually read, understand or try to ascertain what is responsible for what.

So; I tried to do an experiment on a simple task – a Wallet class where the following two use cases apply:

  • A Wallet can be credited coins (Int)
  • A Wallet can be debited coins (Int)
  • Validation of coins added, spent are outside of scope

To keep it very simple, the Wallet simply is a struct with cash represented by an Int


struct Wallet {
   var cash: Int
}

This is a model of a Wallet which holds coins/cash, represented by Ints.

Now, in the past I’d actually put my functions in this model; and it can become quite a mess.

But upon reading and trying to get my head around the concepts and ideas of Single Responsibility I’ve realised that putting functions, use-cases inside my models is not a good idea; and should be handled elsewhere.

Okay, so lets make a class where we have our functions.

class WalletHandler {
    var wallet: Wallet
    init(wallet: Wallet) {
        self.wallet = wallet
    }
}

extension WalletHandler {
    func credit(amount: Int) {
     wallet.cash += amount
    }

    func debit(amount: Int) {
       wallet.cash -= amount
    } 
}

Okay, cool — I’ve omitted any validation, and normally I’d put the functions into their own extensions.

But how is this any different to what we had before? All I’ve done is moved the code somewhere else?

So, as I understand single responsibility; the functionality of this module/code should follow seperation of concerns.

Okay, so this means we need something to do the work; I believe this is called an Interactor in some design patterns.

The Interactor is the thing that handles the business use cases; so let’s code that; not only that, we’ll seperate the use cases of credit and debit into their own delegate patterns.

The following code is visual pseduocode only and may not work.

class WalletInteractor {
   var wallet: Wallet
   var creditDelegate: CreditDelegate?
   var debitDelegate: DebitDelegate?
...
   func credit(amount: Int) {
       self.creditDelegate?.credit(amount: Int)
    ....
}

So the delegate (which could be itself via extensions) could handle the work

But this still isn’t good enough. The interactor knows about the model directly, and shouldn’t. The delegate patterns mutate the model directly, and shouldn’t — and so forth.

So that means we need more stuff.

We need a class that only do the Credit, another just to do Debit.

We need to inject the interactor with a protocol so that it has no knowledge of what model its working with; only that it has a protocol composition and now we must undertake actions against it.

So it would look something like this (again, pseduocode)

class WalletInteractor {
    var walletHandler : WalletHandler // reference to a class that holds the struct
    var creditDelegate ...
     var debitDelegate ...
}   

The credit delegate would perform actions against the `WalletHandler` which is just a class that holds the model.

Example:

class WalletHandler {
    var wallet: Wallet 
    init(wallet) ....
}

But this still isn’t good enough. The Wallet Interactor has knowledge of a class and is still is interacting with it, mutating it — and again; it shouldn’t.

Protocol all the things!

So; okay — lets go a little bit more extreme — and use protocol composition to make the Interactor only deal with protocols that I inject into it; the WalletHandler must also conform to protocols. This is going to get very messy!

We know we have 2 use cases: Credit and Debit

So let’s make protocols just for that: And we’ll add throwing too

// 2 use cases now added
protocol CreditUseCase {
    func credit(amount: Int) throws
}
protocol DebitUseCase {
    func debit(amount: Int) throws
}

We know that we need to credit and debit a wallet; that means we need a reference to functions to do this

protocol WalletCreditUseCase {
    func credit(walletHandler: WalletUseCase, amount: Int) throws
}
protocol WalletDebitUseCase {
    func debit(walletHandler: WalletUseCase, amount: Int) throws
}

But we can’t reference the class here or even the struct; that’d break stuff — so we need another protocol; a wallet use case:

protocol WalletUseCase {
    var wallet: Wallet { get }
    var balance: Int { get }
    var creditDelegate: CreditUseCase? { get }
    var debitDelegate: DebitUseCase?  { get }
}

The wallet use case gives us a get access to the Wallet struct; also a variable balance we can use to get to the cash value; plus we need to reference the protocols in variables for usage.

We’re going to use this stuff in our WalletHandler class;

class WalletHandler : WalletUseCase {
    private(set) internal var wallet: Wallet
    private(set) var creditDelegate: CreditUseCase?
    private(set) var debitDelegate: DebitUseCase?

    var balance: Int {
        return self.wallet.cash
    }

    init(wallet: Wallet) {
        self.wallet = wallet
        self.creditDelegate = self
        self.debitDelegate = self
    }
}

So what this now means is we can inject the Wallet Interactor with anything that conforms to the WalletUseCase; rather than the class directly.

Oops, seeing as we have declared creditDelegate and debitDelegate to be self, we need to add these functions:

extension WalletHandler : CreditUseCase {
    func credit(amount: Int) {
        self.wallet.cash += amount
    }
}

extension WalletHandler : DebitUseCase {
    func debit(amount: Int) {
        self.wallet.cash -= amount
    }
}

So here we have the actual meat and potatoes of actually adding coins or subtracting them; inside a class which holds a reference to the original struct.

But, what about the Interactor I hear you ask?

We know that the Interactor will have use cases of its own, it needs a reference to the wallethandler via the protocol named WalletUseCase; and we also need a reference to the two protocols WalletCreditUseCase and WalletDebitUseCase. Why these? Why can’t we just use CreditUseCase?

The reason is because the wallet has its own usage of the use cases; plus we need to know what wallet we are interacting with.

So lets code up a protocol to hold all the use cases that the Interactor will use:

/*
 * Protocol that defines the Interactor's use case.
 */
protocol WalletInteractorInput: class, CreditUseCase, DebitUseCase {
    var walletHandler: WalletUseCase? { get }
    var creditHandler: WalletCreditUseCase? { get }
    var debitHandler: WalletDebitUseCase? { get }

    var balance: Int { get }
}

The Interactor can now be created.

*
 * The Interactor responsible for implementing
 * the business logic of the module.
 */
class WalletInteractor : WalletInteractorInput {
    internal var walletHandler: WalletUseCase?
    internal var creditHandler: WalletCreditUseCase?
    internal var debitHandler: WalletDebitUseCase?

    var balance: Int {
        return walletHandler?.balance ?? 0
    }

    init(walletHandler: WalletUseCase) {
        self.walletHandler = walletHandler
        self.creditHandler = CreditUseCaseHandler()
        self.debitHandler = DebitUseCaseHandler()
    }
     ... // code trimmed
}

The wallet interactor will have 2 functions;

  • credit and,
  • debit

In both cases they will call a handler (CreditUseCaseHandler and DebitUseCaseHandler) respectively.

So something like this (pseduocode follows)

// inside the Interactor
func credit(amount: Int) throws { 
   do {   
      try creditHandler.credit(walletHandler, amount)
   }
    catch {
      throw error
    } 
}

The above code isn’t complete; its just representative.

Okay, so now we need 2 more classes; the credit and debit class.

// Credit Handler
class CreditUseCaseHandler : WalletCreditUseCase {
    func credit(walletHandler: WalletUseCase, amount: Int) throws {
         if try canCredit(walletHandler, amount) { 
             try walletHandler.creditDelegate.credit(amount)
         }
     }
}

Both classes follow a similar pattern, and again; the code is not representative of functioning/working code.

Phew.

A gist of the actual code used can be found via:

https://git.io/JfxAE

So we have lots of code, and now it’s a real headache to figure out what the hell is going on; who is responsible for what; and where the business logic is actually happening.

If I were to draw it out; it’d look something like this:

A messy visualisation of all the things involved in a credit/debit against a simple struct

It feels like we are kicking the ball further and further away from the original model.

The wallet struct needs to be fed into an interactor; but the interactor can’t know “too much”; so we end up stuffing the Interactor full of protocols and delegates (which isn’t the issue).

There are now 2 models; one, the original struct and on the right is the object as used and manipulated by the app which I guess is fine?

What makes this poor for me is that you can simply just call the struct directly and make your manipulations – thereby by-passing all the nice throwable validation you’ve written. Or, you could call the WalletHandler class

This seems like a mess — and it is!

So whats the insight here? What have I learnt?

Sometimes its just easier to make things stupid and simple; have my interactor but that does all the work and ignore all this other junk.

But; for bigger projects I can sort of see how it might be used; and that’s the real responsibility of this design pattern.

For me, the biggest insight is that this design pattern concept is just that — its just a concept; its not a hard rule, its a CHOICE to do this; for me, I just want to get my app done rather than getting it “right”.

References / Sources: