Swift - соответствие стороннего типа моему собственному протоколу с конфликтующими требованиями
Вот ситуация со сложившейся ситуацией:
Допустим, сторонний фреймворк, написанный Алисой Аллман, предоставляет очень полезный класс:
public class AATrackpad {
public var cursorLocation: AAPoint = .zero
}
и другой фреймворк, написанный Бобом Беллом, предоставляет другой класс:
public class BBMouse {
public var where_is_the_mouse: BBPoint = .zero
}
Во время выполнения может потребоваться один из этих классов в зависимости от того, какое аппаратное обеспечение пользователь решил использовать. Поэтому, в соответствии с принципом инверсии зависимости, я не хочу, чтобы мои собственные типы зависели от AATrackpad
или же BBMouse
непосредственно. Скорее я хочу определить протокол, который описывает поведение, которое мне нужно:
protocol CursorInput {
var cursorLocation: CGPoint { get }
}
и затем мои собственные типы используют этот протокол вместо этого:
class MyCursorDescriber {
var cursorInput: CursorInput?
func descriptionOfCursor () -> String {
return "Cursor Location: \(cursorInput?.cursorLocation.description ?? "nil")"
}
}
Я хочу иметь возможность использовать экземпляр BBMouse
в качестве ввода курсора, вот так:
let myCursorDescriber = MyCursorDescriber()
myCursorDescriber.cursorInput = BBMouse()
но для того, чтобы это скомпилировать, я должен задним числом соответствовать BBMouse
к моему протоколу:
extension BBMouse: CursorInput {
var cursorLocation: CGPoint {
return CGPoint(x: self.where_is_the_mouse.x, y: self.where_is_the_mouse.y)
}
}
Теперь, когда я соответствовал BBMouse
к моему CursorInput
протокол, мой код компилируется, и моя архитектура - это то, что я хочу. Причина, по которой у меня нет проблем, заключается в том, что я думаю, что where_is_the_mouse
это ужасное название для этого свойства, и я очень рад, что никогда не буду использовать это имя снова. Однако с AATrackpad
это другая история. Мне кажется, что Алиса назвала ее cursorLocation
свойство отлично, и, как вы можете видеть, я хочу иметь возможность использовать то же имя для моего требования протокола. Моя проблема в том что AATrackpad
не использует CGPoint
в качестве типа этого свойства, но вместо этого использует собственный тип точки под названием AAPoint
, Тот факт, что мое требование протокола (cursorLocation
) имеет то же имя, что и существующее свойство AATrackpad
но другой тип означает, что я не могу задним числом соответствовать CursorInput
:
extension AATrackpad: CursorInput {
var cursorLocation: CGPoint { // -- Invalid redeclaration
return CGPoint(x: self.cursorLocation.x, y: self.cursorLocation.y) // -- Infinite recursion
}
}
Как говорится в комментариях к этому фрагменту, этот код не компилируется, и даже если бы он это сделал, я столкнулся бы с бесконечной рекурсией во время выполнения, потому что у меня нет возможности специально ссылаться на AATrackpad
версия cursorLocation
, Было бы здорово, если бы что-то подобное сработало (self as? AATrackpad)?.cursorLocation
, но я не верю, что это имеет смысл в этом контексте. Однако, опять же, соответствие протокола даже не будет компилироваться, поэтому устранение неоднозначности для решения бесконечной рекурсии является вторичным.
Учитывая весь этот контекст, мой вопрос:
Если я создаю свое приложение с использованием протоколов (что широко рекомендуется по уважительной причине), действительно ли это правда, что моя способность использовать конкретный сторонний конкретный тип зависит от надежды, что сторонний разработчик не разделяет моего вкуса соглашения об именах?
ПРИМЕЧАНИЕ. Ответ "Просто выберите имя, которое не конфликтует с типами, которые вы хотите использовать", не будет удовлетворительным. Может быть, в начале у меня было только BBMouse
и не было никаких конфликтов, а затем год спустя я решил, что хочу добавить поддержку AATrackpad
также. Изначально я выбрал отличное имя, и теперь оно широко используется в моем приложении. Должен ли я менять его везде ради одного нового конкретного типа? Что происходит позже, когда я хочу добавить поддержку CCStylusTablet
Что сейчас конфликтует с любым новым именем, которое я выбрал? Должен ли я снова изменить название моего требования к протоколу? Я надеюсь, вы понимаете, почему я ищу более здравый ответ, чем этот.
1 ответ
Вдохновленный комментариями Джонаса Майера, я нашел то, что я считаю архитектурно адекватным решением этой проблемы. Как сказал Джонас, перегрузка функций демонстрирует поведение, которое я ищу. Я начинаю думать, что, возможно, требования к протоколу должны быть только функциями, а не свойствами. Следуя этой мысли, мой протокол теперь будет:
protocol CursorInput {
func getCursorLocation () -> CGPoint
func setCursorLocation (_ newValue: CGPoint)
}
(Обратите внимание, что в этом ответе я также настраиваю его, в отличие от оригинального сообщения.)
Теперь я могу задним числом соответствовать AATrackpad
к этому протоколу без конфликтов:
extension AATrackpad: CursorInput {
func getCursorLocation () -> CGPoint {
return CGPoint(x: self.cursorLocation.x, y: self.cursorLocation.y)
}
func setCursorLocation (_ newValue: CGPoint) {
self.cursorLocation = AAPoint(newValue)
}
}
Важно - это все равно будет компилироваться, даже если AATrackpad
уже имеет функцию func getCursorLocation () -> AAPoint
, который имеет то же имя, но другой тип. Такое поведение именно то, что я хотел от моей собственности в оригинальном посте. Таким образом:
Основная проблема с включением свойства в протокол состоит в том, что он может сделать определенные конкретные типы буквально неспособными соответствовать этому протоколу из-за коллизий пространства имен.
После такого решения у меня появилась новая проблема: была причина, по которой я хотел cursorLocation
быть собственностью, а не функцией. Я определенно не хочу быть вынужденным использовать getPropertyName()
синтаксис по всему моему приложению. К счастью, это может быть решено, как это:
extension CursorInput {
var cursorLocation: CGPoint {
get { return self.getCursorLocation() }
set { self.setCursorLocation(newValue) }
}
}
Это то, что так здорово в расширениях протокола. Все, что объявлено в расширении протокола, ведет себя аналогично аргументу по умолчанию для функции - используется только в том случае, если ничто другое не имеет приоритета. Из-за этого другого способа поведения это свойство не вызывает конфликта, когда я соответствую AATrackpad
в CursorInput
, Теперь я могу использовать семантику свойств, которую я изначально хотел, и мне не нужно беспокоиться о конфликтах пространства имен. Я удовлетворен.
"Подождите секунду - теперь, когда AATrackpad
соответствует CursorInput
Разве у него нет двух версий cursorLocation
? Если бы я должен был использовать trackpad.cursorLocation
будет ли это CGPoint
или AAPoint
?
Это работает так: если в этой области известно, что объект AATrackpad
тогда используется исходное свойство Алисы:
let trackpad = AATrackpad()
type(of: trackpad.cursorLocation) // This is AAPoint
Однако, если тип известен только как CursorInput
тогда используется свойство по умолчанию, которое я определил:
let cursorInput: CursorInput = AATrackpad()
type(of: cursorInput.cursorLocation) // This is CGPoint
Это означает, что если я случайно узнаю, что тип AATrackpad
тогда я могу получить доступ к любой версии свойства следующим образом:
let trackpad = AATrackpad()
type(of: trackpad.cursorLocation) // This is AAPoint
type(of: (trackpad as CursorInput).cursorLocation) // This is CGPoint
и это также означает, что мой вариант использования точно решен, потому что я специально хотел не знать, является ли мой cursorInput
случается быть AATrackpad
или BBMouse
- только то, что это какая-то CursorInput
, Поэтому везде, где я использую cursorInput: CursorInput?
его свойства будут иметь типы, которые я определил в расширении протокола, а не исходные типы, определенные в классе.
Есть одна возможность, что протокол с только функциями в качестве требований может вызвать конфликт пространства имен - Джонас указал на это в своем комментарии. Если одним из требований протокола является функция без аргументов, и соответствующий тип уже имеет свойство с таким именем, тогда тип не сможет соответствовать протоколу. Вот почему я убедился, что назвал мои функции, включая глаголы, а не только существительные (func getCursorLocation () -> CGPoint
) - если какой-либо сторонний тип использует глагол в имени свойства, то, вероятно, я все равно не хочу его использовать:)