Лучший дизайн, чтобы не нарушать принцип замещения Лисков.

У меня возникла проблема с принципом замещения Лискова, и я не совсем уверен, как лучше всего это обойти.

Код под вопросом

class BaseModel:
    def run(self, base_model_input: BaseModelInput) -> BaseModelOutput:
        """Throws NotImplemented or @abstractmethod"""
        pass

class SpecificModel(BaseModel):
    def run(self, specific_input: SpecificModelInput) -> SpecificModelOutput:
        # do things...

Я хорошо понимаю, почему это плохой код и почему он нарушает принцип подстановки Лискова. Мне интересно, как лучше спроектировать мою систему, чтобы избежать этой проблемы в первую очередь.

По сути, у меня есть класс BaseModel, который действует как интерфейс, предоставляя некоторые методы, такие как run, которые должны реализовывать расширяющие классы. Но расширяющие классы также имеют дело с определенным вводом/выводом, которые также являются расширениями базовых классов ввода/вывода (то есть SpecificModelInput наследуется от BaseModelInput и добавляет некоторые поля и функциональные возможности, такие же, как и при выводе).

Какой подход здесь лучше?


comment
Технически это решает проблему, когда mypy выдает ошибку, но меня больше интересует, какой дизайн будет лучше в таком случае использования, как мой.   -  person mkserge    schedule 09.03.2021
comment
Если вы действительно можете ограничить входы и выходы подтипами базового ввода и базового вывода, я не вижу никаких проблем с этой моделью.   -  person jsbueno    schedule 10.03.2021
comment
@jsbueno это классический случай нарушения принципа замены Лискова, но я не могу найти лучшего способа обойти это. Подкласс переопределяет метод родителя, делая входные аргументы более узкими, чем родительский метод. Логически кажется разумным, но я понимаю, почему это нарушает принцип.   -  person mkserge    schedule 11.03.2021


Ответы (1)


Как я заявил в комментариях: если вы действительно можете ограничить входные и выходные данные подтипами базового ввода и базового вывода, я не вижу никаких проблем с этой моделью.

Если под нарушением вы подразумеваете, что при ограничении параметров ввода подкласс больше не может использоваться везде, где может использоваться базовый класс, я бы сказал, что это тот случай, когда вы принимаете принцип замены Лискова как догму, где это должен быть очень хороший совет.

Видите ли, принцип Лисков.. является хорошим руководством и немного упрощенным, так как он даст вам действительно хорошее представление о том, каким должно быть наследование.

Но в реальных условиях ограничение входных параметров (и типов атрибутов) не во всех случаях является проблемой. В любом случае, конкретный пример сослужит нам хорошую службу: подумайте об абстрактном классе Vehicle с методом board, который позволяет вам садиться в Transportables, а конкретные Transportables и некоторые допустимые подклассы Transportable — это Persons, Dogs, Grocery Bag, Pianos и Слоны. Если ваш класс Транспортного средства — Корабль, он может взять все это. Если класс вашего транспортного средства — «Автомобиль», некоторые из них отсутствуют.

Кажется, это ваш вариант использования.

Так что, конечно, вы нарушаете принцип здесь. Формулировка в большинстве мест, объясняющая это, заключается в том, что если вы замените экземпляр подкласса везде, где появляется суперкласс, программа не должна сломаться. Таким образом, наиболее правильным будет модифицировать контракты суперкласса таким образом, чтобы любой вход может или не работал, и позволить ему вызывать исключение во время выполнения ( или вернуть объект, сигнализирующий об ошибке), даже если ввод действителен.

Каждый, кто вызывает этот метод, должен обрабатывать состояние «не работает» — в приведенном выше примере легко увидеть, что если у меня есть метод в несвязанном классе, который вызывает vehicle.board, он должен быть ответственным за то, чтобы он не пытался поместить внутрь слона. машина. Если везде вызывается метод, то эти проверки выполняются, принцип соблюдается!

Если разработка этого является излишним для любой вашей задачи, я бы сказал ... практичность превосходит чистоту в этом случае, и просто установите аннотации, чтобы отключить проверку статического типа.

Конечно, рассказать об этом средству проверки статического типа - это другое дело - я думаю, что использование Generics, как указано в ответе, указанном в комментариях, может сделать: Как мне аннотировать тип параметра abstractmethod, когда параметр может иметь любой тип производным от определенного базового типа?

person jsbueno    schedule 11.03.2021