Нарушил ли я принцип LSP в этом примере?

У меня есть этот код, который реализует 2 типа дверей. В одной двери есть замок, а в другой нет.

Интерфейс Door прост:

public interface Door {
    void open();
    void close();
}

Затем у меня есть реализации: LockedDoor и RegularDoor.

public class LockedDoor implements Door {
    private Lock lock;
    private boolean isOpen;

    @Override
    public void open() {
        if(!lock.isLocked()) {
            this.isOpen = true;
        }
    }

    @Override
    public void close() {
        this.isOpen = false;
    }
}

public class RegularDoor implements Door {
    private boolean isOpen;

    @Override
    public void open() {
        isOpen = true;
    }

    @Override
    public void close() {
        isOpen = false;
    }
}

Как видите, функция открытия LockedDoor открывает дверь, только если замок разблокирован.
Вы можете разблокировать замок, получив его от LockedDoor и вызвав его функцию разблокировки.

Является ли это нарушением принципа замещения Лискова?
Если да, то какая альтернатива будет хорошей?


person JacobKreynin    schedule 26.04.2018    source источник
comment
Ваш вопрос более уместен здесь: Software Engineering SE   -  person KarelG    schedule 26.04.2018
comment
Почему вы думаете, что вам нужны два разных класса? Будет ли одного класса Door недостаточно? Вам нужно только проверить состояние isOpen, заперта дверь или нет :)   -  person KarelG    schedule 26.04.2018
comment
@KarelG, ссылаясь на другие сайты, часто полезно указать, что кросс-постинг не одобряется   -  person gnat    schedule 26.04.2018


Ответы (2)


Немного сложно ответить на этот вопрос, так как ваш интерфейс для Door кажется неполным в том смысле, что неясно, что должны делать open() и close(). Давайте проясним это, добавив метод isOpen() и определив, что после вызова open() последующий вызов isOpen() должен возвращать true (и я намеренно игнорирую вопрос о том, что произойдет, если вы попытаетесь открыть и уже открыли дверь, только для для краткости).

В этом случае вы определенно нарушаете принцип LSP — если вы попытаетесь открыть запертую дверь, вы потерпите неудачу, и дверь останется закрытой.

Один из способов решить эту проблему — добавить возвращаемое значение к методам open() и close(), чтобы они могли сообщить, успешно ли выполнена операция:

public interface Door {
    /**
     * Checks if the door is open.
     * @return {@code true} if the door is open, {@code false} if not.
    boolean isOpen();

    /**
     * Attempt to open the door.
     * @return {@code true} if the door was successfully opened, 
     * {@code false} if not.
     * In other words, if a call to {@code open} returns {@code true}, a
     * subsequent call to {@link #isOpen} will return {@code true}.
     */
    boolean open();

    /**
     * Attempt to close the door.
     * @return {@code true} if the door was successfully closed, 
     * {@code false} if not.
     * In other words, if a call to {@code close} returns {@code true}, a
     * subsequent call to {@link #isOpen} will return {@code false}.
     */
    void close();
}

public class RegularDoor implements Door {
    private boolean isOpen;

    @Override
    public boolean isOpen() {
        return isOpen;
    }

    @Override
    public boolean open() {
        return isOpen = true;
    }

    @Override
    public boolean close() {
        return isOpen = false;
    }
}


public class LockedDoor implements Door {
    private Lock lock;
    private boolean isOpen;

    @Override
    public boolean isOpen() {
        return isOpen;
    }

    @Override
    public boolean open() {
        if (!lock.isLocked()) {
            return isOpen = true;
        }
        return false;
    }

    @Override
    public boolean close() {
        return isOpen = false;
    }

    // Not shown here - methods to lock and unlock the door
}
person Mureinik    schedule 26.04.2018
comment
Я думаю, что было бы лучше сделать так, чтобы open() выбрасывал исключение, когда не мог открыть. Имея возвращаемое значение из метода команды, вы нарушаете CQS, хотя все еще есть преимущество в различии между успешным и неудачным выполнением open(). Исключения дают понять, что либо команда выполняется успешно, либо мы должны перейти к другому потоку выполнения. - person Filip Malczak; 26.04.2018

Нет, вы (вероятно) не нарушаете LSP.

Более длинный ответ: зависит, конечно, от вашего «определения» метода open() в интерфейсе Door. Если вы определяете метод как "пытается открыть дверь, если это возможно", то вы в безопасности.

Можно возразить, что метод open() следует называть tryOpen(), чтобы прояснить ваше намерение вызывающему абоненту, что дверь может быть закрыта после звонка.

Однако, если вы определяете метод open(), чтобы всегда открывать дверь, то, конечно же, вы нарушаете свой контракт (и LSP) в LockedDoor.

Другая проблема в том, что в интерфейсе чего-то не хватает. На данный момент нет никакого эффекта, который оказывает открытое/закрытое состояние на любой из доступных методов open()/close(). Я предполагаю, что у вас есть какой-то другой метод в Door, для которого важно состояние двери, например walkThrough() или что-то подобное.

person Robert Bräutigam    schedule 26.04.2018