Как разработать подкласс с функциями, недоступными в базовом классе? - PullRequest
5 голосов
/ 17 апреля 2009

Например, предположим, у меня есть класс Vehicle, и я хочу иметь подкласс ConvertibleVehicle, который имеет дополнительные методы, такие как foldRoof (), turboMode (), foldFrontSeats () и т. Д. Я хочу создать его экземпляр следующим образом

Vehicle convertible = new ConvertibleVehicle()

так что у меня все еще есть доступ к обычному методу, такому как openDoor (), startEngine () и т. Д. Как мне разработать такое решение?

Чтобы прояснить два моих начальных решения, ни одно из которых меня не устраивает:

  1. Имеют фиктивные методы foldRoof (), turboMode (), foldFrontSeats (), которые я переопределяю только в ConvertibleVehicle, оставляя их бездействующими в других подклассах
  2. Имеют абстрактные методы foldRoof (), turboMode (), foldFrontSeats () и заставляют каждый подкласс предоставлять реализацию, даже если он будет пустым во всех случаях, кроме ConvertibleVehicle

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

После прочтения некоторых ответов, возможно, в моем дизайне есть какой-то фундаментальный недостаток. Предположим, у меня есть класс VehicleFleet, который принимает транспортные средства и инструктирует их управлять следующим образом:

public VehicleFleet(Vehicle[] myVehicles) {

    for (int i=0; i < myVehicles.length; i++) {
        myVehicles[i].drive();
    }
}

Предположим, это работает для десятков подклассов Vehicle, но для ConvertibleVehicle я также хочу сложить крышу перед поездкой. Для этого я создаю подкласс VehicleFleet следующим образом:

public ConvertibleVehicleFleet(Vehicle[] myVehicles) {

    for (int i=0; i < myVehicles.length; i++) {
        myVehicles[i].foldRoof();
        myVehicles[i].drive();
    }
}

Это оставляет меня с грязной функцией foldRoof (), застрявшей в базовом классе, где она на самом деле не принадлежит, которая переопределяется только в случае ConvertibleVehicle и ничего не делает во всех остальных случаях. Решение работает, но кажется очень не элегантным. Эта проблема поддается лучшей архитектуре?

Я использую Java, хотя я надеюсь, что можно найти общее решение, которое будет работать на любом объектно-ориентированном языке, и что мне не нужно будет полагаться на специфические особенности языка

Ответы [ 8 ]

5 голосов
/ 17 апреля 2009

Любые объекты, которые используют Vehicle, не должны знать о ConvertibleVehicle и его специализированных методах. В надлежащем слабо связанном объектно-ориентированном дизайне, водитель будет знать только об интерфейсе автомобиля. Драйвер может вызывать startEngine () для Vehicle, но подклассы Vehicle могут переопределять startEngine () для обработки различных реализаций, таких как поворот ключа или нажатие кнопки.

Подумайте о рассмотрении следующих двух ссылок, которые должны помочь объяснить эту концепцию: http://en.wikipedia.org/wiki/Liskov_substitution_principle http://en.wikipedia.org/wiki/Open/closed_principle

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

3 голосов
/ 17 апреля 2009

Я делал это в похожих ситуациях.

Вариант А)

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

class Vehicle { 
     public abstract void drive();
}

class ConvertibleVehicle { 
     public void drive() { 
         this.foldRoof();
         .... // drive 
     }
     private void foldRoof() { 
         ....
     }
 }

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

 for( Vehicle v : vehicleFleet ) { 
      v.drive();
 }

Специализированный метод не предоставляется в открытом интерфейсе объекта, но вызывается при необходимости.

Вариант B)

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

class Vehicle { 
    public void drive() { 
        ....
    }
}
class ConvertibleVehicle extends Vehicle { 
         // specialized version may override base operation or may not.
        public void drive() { 
          ... 
         }

         public void foldRoof() { // specialized operation 
             ...
         }
 }

Почти так же, как и в предыдущем примере, только в этом случае foldRoof также является публичным.

Разница в том, что мне нужен специализированный клиент:

// Client ( base handler ) 
public class FleetHandler { 
     public void handle( Vehicle [] fleet ) { 
           for( Vehicle v : fleet ) {  
               v.drive();
            }
     }
}

// Specialized client ( sophisticate handler that is )  
 public class RoofAwareFleetHandler extends FleetHandler { 
      public void handle( Vehicle [] fleet ) { 
           for( Vehicle v : fleet ) { 
              // there are two options.
              // either all vehicles are ConvertibleVehicles (risky) then
              ((ConvertibleVehicles)v).foldRoof();
              v.drive();

              // Or.. only some of them are ( safer ) .
              if( v instenceOf ConvertibleVehicle ) { 
                  ((ConvertibleVehicles)v).foldRoof();
              } 
              v.drive();
            }
       }
  }

Это instaceof выглядит там довольно некрасиво, но это может быть подчеркнуто современной виртуальной машиной.

Суть в том, что только специализированный клиент знает и может вызывать специализированные методы. То есть только RoofAwareFleetHandler может вызвать foldRoof () при ** ConvertibleVehicle **

Финальный код не меняется ...

 public class Main { 
     public static void main( String [] args ) { 
         FleetHandler fleetHandler = .....
         Vehicles [] fleet =  ....

          fleetHandler.handle( fleet );
      }
 }

Конечно, я всегда проверяю совместимость автопогрузчика и множества транспортных средств (возможно, с использованием abstrac фабрики или производителя)

Надеюсь, это поможет.

3 голосов
/ 17 апреля 2009

Это хороший вопрос. Это означает, что у вас есть (или вы ожидаете иметь) код, который запрашивает у Vehicle (например) foldRoof (). И это проблема, потому что большинство транспортных средств не должны складывать свои крыши. Только код, который знает, что имеет дело с ConvertibleVehicle, должен вызывать этот метод, что означает, что это метод, который должен быть только в классе ConvertibleVehicle. Так будет лучше; как только вы попытаетесь вызвать Vehicle.foldRoof (), ваш редактор сообщит вам, что это невозможно сделать. Это означает, что вам нужно либо расположить код так, чтобы вы знали, что имеете дело с ConvertibleVehicle, либо отменить вызов foldRoof ().

1 голос
/ 17 апреля 2009

Чтобы добавить к отличному ответу Кайла У. Картмелла и, возможно, немного упростить ответ Оскара Рейеса ...

Возможно, вы захотите, чтобы базовый класс определил метод с именем prepareToDrive(), где унаследованные классы могут помещать любые задачи установки, которые необходимо выполнить перед запуском. Вызов drive() был бы способом запустить все с точки зрения пользователя, поэтому нам нужно было бы реорганизовать привод в фазу «настройки» и фазу «запуска».

public class Vehicle {
    protected void prepareToDrive() {
        // no-op in the base class
    }

    protected abstract void go();

    public final void drive() {
        prepareToDrive();
        go();
    }
}

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

Теперь ваш унаследованный класс может выглядеть так:

public class ConvertableVehicle extends Vehicle {

    // override setup method
    protected void prepareToDrive() {
        foldRoof();
    }

    protected void go() {
        // however this works
    }

    protected void foldRoof() {
        // ... whatever ...
    }
}

Эта структура также поможет, если вы столкнетесь с классом TractorTrailerRig, который должен убедиться, что прицеп загружен и правильно прикреплен, прежде чем он сможет двигаться.

1 голос
/ 17 апреля 2009

Я думаю, что большинство людей не понимают сути вопроса Дельты. Мне кажется, что он / она не спрашивает о наследовании. Он / она спрашивает о подклассах, реализующих функциональность, которая не является естественной для базового класса, и о возможной путанице. То есть продвижение определенных методов / функциональных возможностей вверх по иерархической цепочке или требование, чтобы подклассы реализовывали контракт для функциональных возможностей, которые не являются естественным соответствием.

Существует также вопрос, имеет ли смысл иметь возможность обрабатывать базовый класс как подкласс в каждом случае (чтобы избежать приведения и использовать их взаимозаменяемо). * edit - это называется принцип подстановки Лискова (спасибо, что напомнил, Кайл).

1 голос
/ 17 апреля 2009

Это то, что делает подклассы: добавляет функциональность, отсутствующую в базовом классе.

class MyVehicle : public Vehicle {

public: 
  void MyNewFunction()
...

Существует два (на самом деле просто) разных вида наследования: публичное и частное, отражающие отношения Is-A и Has-A соответственно. С помощью публичного наследования вы напрямую добавляете материал в класс. Если у меня есть класс Animal с методами Eat () и Walk (), я могу создать подкласс Cat с методом Purr (). Затем у Cat есть открытые методы Eat, Walk и Purr.

Однако в случае стека, основанного на LinkedList, я могу сказать, что Stack HAS-A LinkedList внутренне. Таким образом, я не раскрываю какие-либо функции базового класса публично, я сохраняю их как частные и должен явно предлагать все, что я выберу общедоступными. В списке может быть метод Insert (), но для стека я ограничиваю реализацию и переэкспонирую его как Push (). Предыдущие публичные методы не раскрываются.

В C ++ это определяется модификатором доступа, указанным перед базовым классом. Выше я использую публичное наследование. Здесь я использую частное наследование:

class MyVehicle : private Engine {

Это говорит о том, что MyVehicle имеет двигатель.

В конечном итоге, подклассы отбирают все доступное в базовом классе и добавляют к нему новые вещи.

EDIT: С этой новой информацией кажется, что вы действительно ищете интерфейсы, как указано в предыдущем (отклоненном) комментарии. Это одна из больших проблем с наследованием - гранулярность. Одна из больших претензий C ++ - это реализация множественного наследования (вариант для этого). Можете ли вы указать, какой язык вы используете, чтобы мы могли правильно посоветовать?

0 голосов
/ 12 февраля 2010

Наличие ConvertibleVehicle подкласса Vehicle и добавление его собственных методов, как вы описываете, прекрасно. Эта часть дизайна в порядке. Беда у тебя с флотом. ConvertibleFleet должен не быть подклассом VehicleFleet. Пример покажет вам, почему. Допустим, VehicleFleet выглядит так:

public class VehicleFleet {
    // other stuff...

    public void add(Vehicle vehicle) {
        // adds to collection...
    }
}

Это прекрасно и разумно, вы можете добавить любой Vehicle или его подкласс к VehicleFleet. Теперь, допустим, у нас есть еще один вид транспортного средства:

public class TruckVehicle extends Vehicle {
    // truck-specific methods...
}

Мы также можем добавить это к VehicleFleet, поскольку это транспортное средство. Проблема в следующем: если ConvertibleFleet является подклассом VehicleFleet, это означает, что мы также можем добавить грузовики к ConvertibleFleet. Это неверно. ConvertibleFleet является , а не надлежащим подклассом, поскольку операция, действительная для его родителя (добавление грузовика), недопустима для ребенка.

Типичным решением является использование параметра типа:

public class VehicleFleet<T extends Vehicle> {
    void add(T vehicle) {
        // add to collection...
    }
}

Это позволит вам определять автопарки, специфичные для определенных типов транспортных средств. Обратите внимание, что это также означает, что не существует «базового» класса VehicleFleet, который вы могли бы передать функциям, которым все равно, какое транспортное средство имеет автопарк. Это можно исправить, используя другой уровень базового класса (или интерфейса):

public interface VehicleFleetBase {
    Vehicle find(String name);
    // note that 'add' methods or methods that pass in vehicles to the fleet
    // should *not* be in here
}

public class VehicleFleet<T extends Vehicle> {
    void add(T vehicle) {
        // add to collection...
    }

    Vehicle find(String name) {
        // look up vehicle...
    }
}

Для методов, которые вытаскивают транспортные средства из парков и не заботятся о том, какого они типа, вы можете обойти VehicleFleetBase. Методы, которые должны вставлять транспортные средства, используют VehicleFleet<T>, который безопасно напечатан.

0 голосов
/ 17 апреля 2009

Как пользователь Vehicle узнает, что это ConvertibleVehicle? Либо им нужно динамическое приведение, чтобы убедиться, что оно правильное, либо вы предоставили метод в Vehicle, чтобы получить объекты реального типа.

В первом случае у пользователя уже есть ConvertibleVehicle как часть динамического приведения. Они могут просто использовать новый указатель / ссылку для доступа к методам ConvertiblVehicle

Во втором случае, когда пользователь проверяет тип объекта с помощью одного из методов Транспортных средств, он может просто привести Транспортное средство к ConvertibleVehicle и использовать его.

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

Все, что сказал. Мне нужны все методы производных классов из базового класса. Я мог бы привести к производному классу, но он был вовлечен в структуру и потребовал бы намного больше усилий. Старая поговорка «все проблемы можно решить с помощью еще одного слоя косвенности» - вот как я решил это. Я вызвал виртуальный метод в базовом классе с именем функции, которую я хотел вызвать. «Имя» может быть строкой или целым числом в зависимости от ваших потребностей. Это медленнее, но вам нужно будет делать это редко, если ваша иерархия классов достаточно выразительна.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...