1

I'm interpreting commands from a wire protocol. I have a class that encapsulates the command structure with something basic like

class Command:
    def __init__(self, commandType, body):
        self.commandType = commandType
        self.body = body

    @classmethod
    def decode(cls, someBytes):
        return cls(commandType=someBytes[0], body=someBytes[1:])

    def dispatch(self):
        pass # but could magic happen here?

    def command1(self):
        print("command1 stuff goes down")

    def command42(self):
        print("command42 stuff goes down")

What I'd like to be able to do is to somehow link the different commandN methods with the commandType so they could run in the dispatch method. A very naive/inefficient of dispatch could look like:

    def dispatch(self):
        methodName = f'command{self.commandType}'
        method = getattr(self, methodName, None)
        if method is not None:
            method(self)

But that only works as long as I name methods commandN. It would be nicer to use names that are monikers of what that command represents. That led me down the path of decorators. But a regular decorator wouldn't be enough, I would need a parameterized one. But after reading this question, I wasn't sure I wanted that. And some of the comments seemed to indicate it might be troublesome around instance methods.

Is there a different pattern that I'm missing that I can use to accomplish the association of instance methods with the different byte values?

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
Travis Griggs
  • 21,522
  • 19
  • 91
  • 167

2 Answers2

2

Would a simple translating dictionary be enough for you? Modify your Command class to include a translation dictionary as a class attribute that has the commandTypes as keys, and the method names as values. Then, inside your dispatch method, get the actual name of the command from the translation dictionary and run that.

class Command:
    commandNames = {1: "print_command_type", 
                    2: "print_body"}

    def __init__(self, commandType, body):
        self.commandType = commandType
        self.body = body

    @classmethod
    def decode(cls, someBytes):
        return cls(commandType=someBytes[0], body=someBytes[1:])

    def dispatch(self):
        methodName = self.commandNames[self.commandType]
        method = getattr(self, methodName, None)
        if method is not None:
            method() # No need to pass `self`. It already is `self.method`

    def print_command_type(self):
        print(f"Command Type: {self.commandType}")

    def print_body(self):
        print(f"Body: {self.body}")

a = Command(1, 0)
b = Command(2, 0)
a.dispatch()
b.dispatch()

For more features, you can modify dispatch to accept *args and **kwargs to pass to your method, if needed.

SyntaxVoid
  • 2,501
  • 2
  • 15
  • 23
0

I thought I'd post what I ended up doing (for now, until someone tells me a slicker way of doing this). Because it's a wire protocol, it's common to find these command codes in the form of an enum. And I wanted to leverage that so it showed up throughout the code. The enum is a sort of registering thing, if I have to add a new element (because of protocol revisions), I don't want to have to update the table initialization code like some header file. So I came up with the following that takes the (currently) accepted answer to just use a dict() and ran with it.

from enum import IntEnum, unique

@unique
class CommandCode(IntEnum):
    yes = 1
    no = 2
    maybe = 3

class Command(object):
    MethodTable = {}
    def __init__(self, code, message):
        super().__init__()
        self.code = code
        self.message = message

    def dispatch(self):
        # no "unknown code" protection here, left for student
        self.MethodTable[self.code](self)

    def yes(self):
        print("yes", self.message)

    def no(self):
        print("no", self.message)

    def maybe(self):
        print("maybe", self.message)

# registration phase that binds the enum to the class's MethodTable
for code in CommandCode:
    Command.MethodTable[code.value] = getattr(CommandCode, code.name)  


Command(code=1, message="bob").dispatch()
Command(code=2, message="fred").dispatch()
Command(code=3, message="george").dispatch()
Command(code=1, message="BOBBY").dispatch()

Update: I had to fix the first revision which was storing BoundMethods in the MethodTable rather than unbound methods.

Update 2: Realized I didn't need to be so clever with a dynamically growing MethodTable (too much time working with JIT languages I guess)

Travis Griggs
  • 21,522
  • 19
  • 91
  • 167