Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to store pattern of enum with associated value in array?

Tags:

enums

swift

Let's say we have simple enum with message types:

enum MessageType {
    case audio
    case photo
    case text
}

There is Handler class which handles messages with specific types only:

class Handler {
    let allowed: [MessageType]

    init(_ allowed: [MessageType]) { self.allowed = allowed }

    func canHandle(_ messageType: MessageType) -> Bool {
        return allowed.contains(messageType)
    }
}

Basic usage example:

let handler = Handler([.audio, .photo])
print(handler.canHandle(.text)) // Prints false

I want to upgrade my MessageType and add associated value for some of message types.

class Audio {}

enum MessageType {
    case audio(Audio)
    case photo
    case text
}

Problem is that I can't store enum's pattern in allowed array for the future check in canHandle:

// error: '_' can only appear in a pattern or on the left side of an assignment
let handler = Handler([.audio(_), .photo])

Is it possible to resolve such case in a "clean" way?

  • It's not possible to modify MessageType because it's in third-party library (e.g. making arguments optional and passing nil)
  • It's not possible to init MessageType.audio(Audio) with fake Audio because it could have private initializer
  • I hope to avoid switch, case let or other hard-coded checks in canHandle

Any suggestions? Thanks

do androids dream of electric enums

like image 333
Leo Avatar asked Nov 08 '22 09:11

Leo


1 Answers

If instantiating the enum with a default (or garbage) value isn't a big deal

enum MessageType {
    case audio(String)
    case photo
    case text
}

protocol SneakyEquatableMessage {
    func equals(message: MessageType) -> Bool
}

extension MessageType: SneakyEquatableMessage {
    func equals(message: MessageType) -> Bool {
        switch (self, message) {
        case (.audio(_), .audio(_)),
             (.photo, .photo),
             (.text, .text):
            return true
        default:
            return false
        }
    }
}

class Handler {
    let allowed: [MessageType]

    init(_ allowed: [MessageType]) { self.allowed = allowed }

    func canHandle(_ messageType: MessageType) -> Bool {
        return allowed.contains { $0.equals(message: messageType) }
    }
}

Basic Usage

let handler = Handler([.audio(""), .photo])
print(handler.canHandle(.text)) // Prints false
print(handler.canHandle(.audio("abc")) //Prints true


Default (or garbage) values are unrealistic

This particular section is more specific in this context, but ultimately you're going to have breakdown your enum somehow as of Swift 4. So this is my suggestion: Dependency Injection of the Factory Pattern inside Handler. This solves all of your problems pretty cleanly without having to touch switch within Handler or optionals.

enum DisassembledMessage {
    case audio
    case photo
    case text
}

protocol MessageTypeFactory {
    func disassemble(message: MessageType) -> DisassembledMessage
    func disassemble(messages: [MessageType]) -> [DisassembledMessage]
}

class Handler {
    let allowed: [MessageType]
    let factory: MessageTypeFactory

    init(allowed: [MessageType], with factory: MessageTypeFactory) {
        self.allowed = allowed
        self.factory = factory
    }

    func canHandle(_ messageType: DisassembledMessage) -> Bool {
        return factory
            .disassemble(messages: allowed)
            .contains { $0 == messageType }
    }
}

Basic Usage

let audioValue: Audio = //...
let audioMessage = MessageType.audio(audioValue)
let factory: MessageTypeFactory = //...
let handler = Handler(allowed: [audioMessage, .photo], with: factory)
print(handler.canHandle(.text)) // Prints false
print(handler.canHandle(factory.disassemble(message: audioMessage))) //Prints true

You may be asking: wait... you just created another enum (also this is just an example, you could convert it to whatever you want in that protocol). Well I say: the enum you're using is from a library... see my notes section. Also, you can now use that factory anywhere you need to break down the library type to something, including inside Handler. You could easily expand the protocol MessageTypeFactory to convert your enum to other types (hopefully behaviors) you've created, and basically just distance yourself from the library type when you need to. I hope this helps clarify what I was getting at! I don't even think you should store MessageType in your class. You should store your own type which is some kind of mapped version of MessageType, like DisassembledType.

I hope this helps!


Notes

A few things:

  • I'm sorry your soul is owned by a library, really.
  • Use the Adapter Pattern. Clean C++ is one of many places you can learn about it. Don't pollute your entire code base with one of their types! That's just a tip.
  • I know you said you didn't want to use a switch... but you're working with enums... At least it's within the enum!
  • Use your own types! (Did I say that?)
  • Use the Adapter Pattern! (Stop it.)
like image 181
Benjamin Avatar answered Nov 15 '22 07:11

Benjamin