Если кратко, то лучше
вообще забыть быть втройне осторожным с protected и особенно protected virtual, когда вы что-то проектируете. Проблемы будут как раз из-за того, что это не входит в интерфейс и почти всегда нарушает два принципа из
SOLID, а именно Open/closed principle и Liskov substitution principle.
Нужно отличать расширение от модификации. Например, в родительском классе у вас есть метод, который пишет сообщение в поток. Если дочерний класс сначала вызовет метод родительского класса, а потом еще добавит сообщение в лог — это определенно расширение. Если дочерний класс добавит валидацию сообщения, которое пишется в поток, то это тоже расширение. Если же дочерний класс изменит само сообщение перед записью в поток, то это уже модификация.
В итоге клиентское приложение будет вести себя по-разному в зависимости от того, использует ли оно дочерний или родительский класс. Если бы этот метод входил в интерфейс, то в такой ситуации более приемлемым вариантом было бы расширение интерфейса — добавление второго метода записи в поток, чтобы клиентское приложение видело оба метода и могло выбрать соответствующий.
Если этот метод был protected, то ни о каком интерфейсе тут речи уже нет. Разработчик должен гарантировать, что для клиента родительский класс и дочерний класс с перегруженным protected методом будут работать не просто одинаково хорошо, а вообще одинаково, так как клиент может получать экземпляр через конструктор и по интерфейсу и вообще быть не в курсе его реального типа.