Существует два способа использования абстрактных базовых классов.
Вы специализируете свой абстрактный объект, но все клиенты будут использовать производный класс через его базовый интерфейс.
Вы используете абстрактный базовый класс, чтобы исключить дублирование объектов в вашем проекте, а клиенты используют конкретные реализации через свои собственные интерфейсы.!
Решение для 1 - шаблон стратегии
Если у вас первая ситуация, то у вас фактически есть интерфейс, определенный виртуальными методами в абстрактном классе, которые реализуют ваши производные классы.
Вам следует подумать о том, чтобы сделать это реальным интерфейсом, изменить конкретный класс на конкретный и использовать экземпляр этого интерфейса в его конструкторе. Затем ваши производные классы становятся реализациями этого нового интерфейса.
Это означает, что теперь вы можете тестировать свой ранее абстрактный класс, используя фиктивный экземпляр нового интерфейса, а каждую новую реализацию - через общедоступный интерфейс. Все просто и проверяемо.
Решение для 2
Если у вас вторая ситуация, тогда ваш абстрактный класс работает как вспомогательный класс.
Посмотрите на функциональность, которую он содержит. Посмотрите, может ли что-либо из этого быть помещено на объекты, которыми манипулируют, чтобы минимизировать это дублирование Если у вас все еще есть что-то, посмотрите, как сделать это вспомогательным классом, который ваша конкретная реализация использует в своем конструкторе, и удалите его базовый класс.
Это снова приводит к конкретным классам, которые просты и легко тестируемы.
Как правило
Пользуйтесь сложной сетью простых объектов над простой сетью сложных объектов.
Ключом к расширяемому тестируемому коду являются небольшие строительные блоки и независимая проводка.
Обновлено: как обрабатывать смеси обоих?
Можно иметь базовый класс, выполняющий обе эти роли ... т.е. он имеет открытый интерфейс и имеет защищенные вспомогательные методы. Если это так, то вы можете выделить вспомогательные методы в один класс (script2) и преобразовать дерево наследования в шаблон стратегии.
Если вы обнаружите, что у вас есть некоторые методы, которые ваш базовый класс реализует напрямую, а другие являются виртуальными, то вы все равно можете преобразовать дерево наследования в шаблон стратегии, но я бы также воспринял это как хороший показатель того, что обязанности не выровнены правильно и, возможно, потребуется рефакторинг.
Обновление 2: Абстрактные классы как трамплин (2014/06/12)
На днях у меня была ситуация, когда я использовал абстракцию, поэтому я хотел бы выяснить, почему.
У нас есть стандартный формат для наших файлов конфигурации. Этот конкретный инструмент имеет 3 файла конфигурации в этом формате. Я хотел иметь строго типизированный класс для каждого файла настроек, чтобы через внедрение зависимостей класс мог запрашивать настройки, о которых он заботился.
Я реализовал это, имея абстрактный базовый класс, который знает, как анализировать форматы файлов настроек и производные классы, которые предоставляют те же методы, но инкапсулируют местоположение файла настроек.
Я мог бы написать «SettingsFileParser», который обернул 3 класса и затем делегировал его базовому классу, чтобы показать методы доступа к данным. Я решил не делать это пока , так как это приведет к 3 производным классам с большим количеством кода Delegation , чем что-либо еще.
Однако ... по мере развития этого кода и выяснения потребителей каждого из этих классов настроек. Каждую настройку пользователи будут запрашивать некоторые настройки и каким-то образом преобразовывать их (поскольку настройки являются текстовыми, они могут обернуть их в объекты, преобразовать их в числа и т. Д.). Когда это произойдет, я начну извлекать эту логику в методы манипулирования данными и возвращать их обратно в строго типизированные классы настроек. Это приведет к более высокоуровневому интерфейсу для каждого набора настроек, который в конечном итоге перестанет осознавать, что имеет дело с «настройками».
На этом этапе строго типизированные классы настроек больше не будут нуждаться в методах «получения», которые предоставляют базовую реализацию «настроек».
В этот момент я бы больше не хотел, чтобы их общедоступный интерфейс включал методы доступа к настройкам; поэтому я изменю этот класс, чтобы инкапсулировать класс анализатора настроек, а не наследовать от него.
Таким образом, класс Abstract: для меня способ избежать кода делегирования на данный момент и маркер в коде, чтобы напомнить мне о необходимости изменить дизайн позже. Я, возможно, никогда не доберусь до этого, поэтому он может жить долго ... только код может сказать.
Я считаю, что это верно для любого правила ... типа "нет статических методов" или "нет частных методов". Они указывают запах в коде ... и это хорошо. Это заставляет вас искать абстракцию, которую вы пропустили ... и позволяет в то же время обеспечивать ценность для вашего клиента.
Я представляю себе подобные правила, определяющие ландшафт, в котором обслуживаемый код живет в долинах. Когда вы добавляете новое поведение, это похоже на дождь в вашем коде. Сначала вы помещаете его туда, где он приземляется. Затем вы делаете рефакторинг, чтобы позволить силам хорошего дизайна изменить поведение, пока все не окажется в долинах.