Обновлено: В нескольких ответах предлагалось изменить подпись функции RegisterHandler, однако в рамках исходного вопроса я спросил, можно ли дополнительно ограничить типы, а не предложения по дизайну. Изменение подписи изменит поведение функции, поэтому это неподходящее решение.
У меня есть функция, которая немного похожа
func (c *Context) RegisterHandler(f interface{}) error {
// Do lots of checking to make sure a function was passed in
// and it's arguments all implement a specific interface
}
func consumer(a ConcreteTypeA, b ConcreteTypeB) {}
func main() {
...
c.RegisterHandler(consumer)
}
что примерно сводится к
var a ConcreteTypeA
var b ConcreteTypeB
a.InitFromContext(c)
b.InitFromContext(c)
consumer(a, b)
Есть ли способ сделать это interface{}
более конкретным, не меняя его поведения? Если нет, то я вполне доволен своим решением. Я просто надеюсь, что смогу сократить количество проверок во время выполнения и возможных ошибок, не обнаруживаемых во время компиляции.
Моя текущая реализация очень похожа на приведенную выше. Я пытался сделать что-то вроде
func (c *Context) RegisterHandler(f func (...InitFromContexter)) error {
// Do lots of checking to make sure a function was passed in
// and it's arguments all implement a specific interface
}
Но это приводит к сбою всего существующего кода, поскольку он будет получать только вариационные функции.
Вы уже нашли путь со своим RegisterHandler(f interface{})
, другого действительно нет.
Дело в том, что если вы хотите поддерживать несколько подписей, вам также понадобится код, обрабатывающий все различные шаблоны подписей, которые вы поддерживаете, чтобы в конечном итоге можно было действительно сделать вызов.
Как вы уже писали, это будет спагетти из проверок во время выполнения, а также, по сути, проверок и вызовов на основе отражения. Возможно, было бы чище и понятнее (особенно позже) иметь что-то вроде RegisterHandler(handler interface{ Do() }
, где различные шаблоны отображаются как поля структуры, зависящие от реализации, если это возможно в вашем проекте.
Я оставлю вопрос открытым на некоторое время, чтобы посмотреть, получу ли я какие-либо другие ответы, и отмечу его как принятый, если не получу более убедительного ответа.
@MasaMaeda, вы можете использовать замыкания, если не хотите реализовывать дополнительный интерфейс: go.dev/play/p/V9rfQajQQJi
@Питер, это очень похоже на ответ, который дал Эйк stackoverflow.com/a/78691509/5302276. За исключением того, что ваш ответ не совсем делает все, что просил оригинал.
Я вроде как придумал близкое решение. Это некрасиво, но, похоже, работает так, как я ожидаю. Он удовлетворяет требованию, согласно которому исходный код обработчика не нужно редактировать. И весь регистрационный код находится в одном месте, поэтому с этим должно быть значительно проще справиться, чем менять все функции сами.
Я не полностью удовлетворен, но думаю, что это лучшее решение, чем то, которое у меня было до того, как я обрабатывал все это во время выполнения. Однако это делает регистрационный код значительно более шумным. Надеюсь, что можно найти лучшую золотую середину.
package main
import "fmt"
type Handler interface {
GenerateHandler() func(*Context)
}
type Context struct {
name string
handler func(*Context)
}
func (ctx *Context) RegisterHandler(handler Handler) {
ctx.handler = handler.GenerateHandler()
}
type InitFromContext interface {
InitFromContext(ctx *Context)
}
type ConcreteTypeA struct{}
type ConcreteTypeB struct{}
func (c ConcreteTypeA) InitFromContext(ctx *Context) {
fmt.Println("ConcreteTypeA InitFromContext", ctx.name)
}
func (c ConcreteTypeB) InitFromContext(ctx *Context) {
fmt.Println("ConcreteTypeB InitFromContext", ctx.name)
}
type OneArgHandler[T InitFromContext] func(a T)
// These new functions are so we can get type errasure.
// Go doesn't support this for types only for functions
func NewOneArgHandler[T InitFromContext](
f func(a T),
) OneArgHandler[T] {
return OneArgHandler[T](f)
}
func (f OneArgHandler[T]) GenerateHandler() func(*Context) {
return func(ctx *Context) {
var a T
a.InitFromContext(ctx)
f(a)
}
}
type TwoArgHandler[T, T2 InitFromContext] func(a T, b T2)
func NewTwoArgHandler[T, T2 InitFromContext](
f func(a T, b T2),
) TwoArgHandler[T, T2] {
return TwoArgHandler[T, T2](f)
}
func (f TwoArgHandler[T, T2]) GenerateHandler() func(*Context) {
return func(ctx *Context) {
var a T
a.InitFromContext(ctx)
var b T2
b.InitFromContext(ctx)
f(a, b)
}
}
type ThreeArgHandler[T, T2, T3 InitFromContext] func(a T, b T2, c T3)
func NewThreeArgHandler[T, T2, T3 InitFromContext](
f func(a T, b T2, c T3),
) ThreeArgHandler[T, T2, T3] {
return ThreeArgHandler[T, T2, T3](f)
}
func (f ThreeArgHandler[T, T2, T3]) GenerateHandler() func(*Context) {
return func(ctx *Context) {
var a T
a.InitFromContext(ctx)
var b T2
b.InitFromContext(ctx)
var c T3
c.InitFromContext(ctx)
f(a, b, c)
}
}
func main() {
ctx := &Context{name: "test"}
ctx.RegisterHandler(NewOneArgHandler(func(a ConcreteTypeA) {
fmt.Println("OneArgHandler ConcreteTypeA")
}))
ctx.handler(ctx)
ctx.RegisterHandler(NewOneArgHandler(func(a ConcreteTypeB) {
fmt.Println("OneArgHandler ConcreteTypeB")
}))
ctx.handler(ctx)
ctx.RegisterHandler(
NewTwoArgHandler(func(a ConcreteTypeA, b ConcreteTypeB) {
fmt.Println("TwoArgHandler ConcreteTypeA ConcreteTypeB")
}),
)
ctx.handler(ctx)
ctx.RegisterHandler(
NewThreeArgHandler(
func(a ConcreteTypeA, b ConcreteTypeB, c ConcreteTypeA) {
fmt.Println("ThreeArgHandler ConcreteTypeA ConcreteTypeB ConcreteTypeA")
},
),
)
ctx.handler(ctx)
}
Первый вопрос для меня был бы: зачем использовать обработчик как функцию с неизвестной сигнатурой?
То, что вы делаете, ничем не отличается от определения
func (ctx *Context) RegisterHandler(handler func(*Context)) {
ctx.handler = handler
}
а потом просто звоню
func main() {
ctx := &Context{name: "test"}
ctx.RegisterHandler(func(context *Context) {
ConcreteTypeA{}.InitFromContext(context)
fmt.Println("OneArgHandler ConcreteTypeA")
})
ctx.handler(ctx)
ctx.RegisterHandler(func(context *Context) {
ConcreteTypeB{}.InitFromContext(context)
fmt.Println("OneArgHandler ConcreteTypeB")
})
ctx.handler(ctx)
ctx.RegisterHandler(func(context *Context) {
ConcreteTypeA{}.InitFromContext(context)
ConcreteTypeB{}.InitFromContext(context)
fmt.Println("TwoArgHandler ConcreteTypeA ConcreteTypeB")
})
ctx.handler(ctx)
ctx.RegisterHandler(func(context *Context) {
ConcreteTypeA{}.InitFromContext(context)
ConcreteTypeB{}.InitFromContext(context)
ConcreteTypeA{}.InitFromContext(context)
fmt.Println("ThreeArgHandler ConcreteTypeA ConcreteTypeB ConcreteTypeA")
})
ctx.handler(ctx)
}
только то, что последнее гораздо более читабельно. Проблема в том, что в вашем примере вы можете передать только свой *Context
, а вызывающая функция (контекст) должна знать, какие параметры передавать, что делает выбор явно ограниченным.
Поэтому я бы предложил вместо этого использовать замыкания.
Вероятно, мне следует упомянуть, что это унаследованная кодовая база, и что дизайнерское решение было принято задолго до того, как я начал работать над проектом. Еще следует отметить, что подпись не только влияет на обработчик во время вызова, но также меняется при выполнении кода, или, другими словами, первоначальный разработчик написал это, чтобы обеспечить декларативный синтаксис в подписи. (и в этом случае другое мое решение тоже не совсем работает). Это целая паутина дерьма. Я думаю, что лучшее, что можно сделать на данный момент, если честно, — это просто оставить это в покое.
Но какую информацию может передать обработчик, если он не знает подписи? Можете ли вы как-нибудь сделать больше кода доступным?
Благодаря рефлексии он может получить конкретные типы и использовать их для создания группы обеспечения доступности баз данных, в которой два обработчика могут выполняться параллельно, если они не работают с одними и теми же данными. Обработчик не передает типы, которые передает регистрационный код. И есть набор правил для типов более высокого порядка (Ref, Query, Commands, Logger и т. д.). Другими словами, если два обработчика имеют Ref[int]
, они должны выполняться последовательно, но два обработчика, которые имеют Ref[int]
и Ref[string]
соответственно, могут делать параллельно. Я мог бы создать замыкание и передать типы, но это было бы излишним.
Если вы все равно используете отражение, any
кажется правильным выбором. Кроме того, похоже, что вы создаете некую структуру внедрения зависимостей. Как уже было сказано, это трудно понять, не видя больше кода.
Вот чего я боялся. Мне нравится ваше предложение, однако я думаю, что в моем случае вы просто перенесете беспорядок со стороны вызывающего абонента на вызывающую сторону. Из-за того, что вам нужно будет реализовать разные действия для каждого шаблона. Именно этого я и пытался избежать в первую очередь.