На самом деле вы немного превзошли xD
Наследование описывает отношение "есть", композиция описывает отношение "имеет".Так что в вашем случае использование композиции для таких атрибутов, как крылья и ноги, имеет смысл, но птицы, кошки и собаки - это животные - у них нет «животных» (ну, у них всех есть блохи, но это уже другая тема), поэтому онидолжен наследовать от Animal
.
Кроме того, у большинства птиц ноги слишком сильные, и многие из них вообще не летают вообще (но некоторые используют их, чтобы плавать, и делают это очень эффективно) ;-)
Рекомендуется ли сначала проверять, имеет ли объект определенный атрибут (в данном случае «ноги»), прежде чем вызывать функцию с этим атрибутом?
Зависит отконтекст, действительно.Как правило, нет, это не считается хорошей практикой (см. «Говори, не спрашивай» и «закон деметры»), но есть случаи, когда это законно.Кроме того, «хороший» дизайн также зависит от решаемой проблемы, и здесь мы достигаем предела примеров игрушек, которые никогда не являются реальными примерами использования.
Теоретически состав / делегирование должно быть прозрачным длякод клиента, поэтому вам нужно просто позвонить whatever_animal.walk()
и покончить с этим.Теперь вы (как «код клиента»), возможно, захотите узнать, что животное не может ходить, и в этом случае не ходячее животное должно вызывать исключение, когда ему говорят: «Это также означает, что Animal
должна иметь реализацию по умолчаниювсе возможные «действия» и то, что клиентский код должен быть подготовлен для исключений «UnsupportedAction» (или как вы хотите их называть).
по сравнению с реализацией, прозрачность делегирования может быть такой же простой, как и использование __getattr__()
, то есть:
class UnsupportedAction(LookupError):
pass
class Animal(object):
_attributes = ()
def __init__(self, name):
self.name = name
def make_sound(self):
print("silence...")
def __getattr__(self, name):
for att in self._attributes:
if hasattr(att, name):
return getattr(att, name)
else:
raise UnsupportedAction("{} doesn't know how to {}".format(type(self), name))
class Dog(Animal):
_attributes = (Legs(), )
class Bird(Animal):
_attributes = (Legs(), Wings())
Приятным моментом этого решения является то, что оно очень простое и очень динамичное.Менее приятным моментом является то, что он не является ни проверяемым, ни явным.
Другое решение - явное делегирование:
class UnsupportedAction(LookupError):
pass
class Animal(object):
_attributes = ()
def __init__(self, name):
self.name = name
def make_sound(self):
print("silence...")
def walk(self):
return self._resolve_action("walk")
def fly(self):
return self._resolve_action("walk")
# etc
def _resolve_action(self, name):
for att in self._attributes:
if hasattr(att, name):
return getattr(att, name)
else:
raise UnsupportedAction("{} doesn't know how to {}".format(type(self), name))
, которое является более многословным, гораздо менее динамичным, но гораздо более очевидным, задокументированнымчитаемый и проверяемый.
В приведенном выше примере вы могли бы фактически выделить лишний код с помощью пользовательского дескриптора:
class Action(object):
def __init__(self, name):
self.name = name
def __get__(self, obj, cls):
if obj is None:
return self
return obj._resolve_action(self.name)
def __set__(self, obj, value):
raise AttributeError("Attribute is readonly")
class Animal(object):
_attributes = ()
def __init__(self, name):
self.name = name
def make_sound(self):
print("silence...")
walk = Action("walk")
fly = Action("fly")
# etc
Но, опять же, ничего из этого не имеет смысла без реальной проблемырешить, что обычно определяет правильное решение.