ООП дизайн проблема
Что хорошего дизайна в этом простом случае:
Допустим, у меня есть базовый класс автомобилей с методом FillTank(Fuel fuel)
где топливо также является базовым классом, который имеет несколько классов листьев, дизельное топливо, этанол и т. д.
На моем листе авто класс DieselCar.FillTank(Fuel fuel)
разрешен только определенный вид топлива (никаких сюрпризов там нет:)). Теперь я обеспокоен тем, что, согласно моему интерфейсу, каждая машина может заправляться любым топливом, но это кажется мне неправильным во всех FillTank()
Внедрение проверяют на входе топлива для правильного типа и, если нет, сбрасывают ошибку или что-то.
Как я могу сделать такой случай более точным, возможно ли это? Как разработать базовый метод, который принимает базовый класс для ввода без получения этих "странных результатов"?
9 ответов
Если существует жесткая граница между типами автомобилей и видами топлива, то FillTank()
не имеет никакого дела в базе Car
класс, так как знание того, что у вас есть машина, не говорит вам, что за топливо. Итак, для того, чтобы обеспечить правильность во время компиляции, FillTank()
должны быть определены в подклассах и должны принимать только Fuel
подкласс, который работает.
Но что, если у вас есть общий код, который вы не хотите повторять между подклассами? Затем вы пишете защищенный FillingTank()
метод для базового класса, который вызывает функция подкласса. То же самое касается Fuel
,
Но что, если у вас есть какая-то волшебная машина, работающая на нескольких видах топлива, скажем, дизель или газ? Тогда этот автомобиль становится подклассом обоих DieselCar
а также GasCar
и вам нужно убедиться, что Car
объявлен как виртуальный суперкласс, поэтому у вас нет двух Car
случаи в DualFuelCar
объект. Заполнение танка должно просто работать с небольшими изменениями или без них: по умолчанию вы получите оба DualFuelCar.FillTank(GasFuel)
а также DualFuelCar.FillTank(DieselFuel)
, давая вам перегруженную по типу функции.
Но что, если вы не хотите, чтобы подкласс имел FillTank()
функционировать? Затем вам нужно переключиться на проверку времени выполнения и сделать то, что вы думали: проверка подкласса Fuel.type
и либо сгенерировать исключение, либо вернуть код ошибки (предпочтительнее последнего), если есть несоответствие. В C++, RTTI и dynamic_cast<>
это то, что я бы порекомендовал. В Python isinstance()
,
Используйте базовый базовый класс (если ваш язык поддерживает его (ниже C#)):
public abstract class Car<TFuel> where TFuel : Fuel
{
public abstract void FillTank(TFuel fuel);
}
По сути, это позволяет любому классу, который наследуется от автомобиля, указывать, какой тип топлива он использует. Кроме того, Car
класс налагает ограничение на TFuel
должен быть некоторый подтип абстрактного Fuel
учебный класс.
Допустим, у нас есть некоторый класс Diesel
что просто:
public class Diesel : Fuel
{
...
}
И машина, которая работает только на дизеле:
public DieselCar : Car<Diesel>
{
public override void FillTank(Diesel fuel)
{
//perform diesel fuel logic here.
}
}
Объектно-ориентированное программирование само по себе не может справиться с этой проблемой. Что вам нужно, это общее программирование (решение C++ показано здесь):
template <class FuelType>
class Car
{
public:
void FillTank(FuelType fuel);
};
Ваша дизельная машина - это просто конкретная машина, Car<Diesel>
,
Для этого может быть использована двойная отправка: примите немного топлива перед заправкой. Имейте в виду, что на языке, который не поддерживает его напрямую, вы вводите зависимости
Похоже, вы просто хотите ограничить тип топлива, которое идет в ваш дизельный автомобиль. Что-то вроде:
public class Fuel
{
public Fuel()
{
}
}
public class Diesel: Fuel
{
}
public class Car<T> where T: Fuel
{
public Car()
{
}
public void FillTank(T fuel)
{
}
}
public class DieselCar: Car<Diesel>
{
}
Сделал бы трюк, например
var car = new DieselCar();
car.FillTank(/* would expect Diesel fuel only */);
По сути, то, что вы делаете здесь, позволяет Car
иметь конкретные виды топлива. Это также позволяет вам создать автомобиль, который будет поддерживать любой тип Fuel
(шанс был бы прекрасной вещью!). Однако, в вашем случае, DieselCar, вы просто извлекаете класс из автомобиля и ограничивает его использованием Diesel
только топливо.
Я думаю, что принятый метод будет иметь ValidFuel(Fuel f)
метод в вашем базовом классе, который выбрасывает NotImplementedException
(разные языки имеют разные термины), если "листовые" автомобили не перекрывают его.
FillTank
может быть тогда полностью в базовом классе и вызвать ValidFuel
чтобы увидеть, если это действительно.
public class BaseCar {
public bool ValidFuel(Fuel f) {
throw new Exception("IMPLEMENT THIS FUNCTION!!!");
}
public void FillTank(Fuel fuel) {
if (!this.ValidFuel(fuel))
throw new Exception("Fuel type is not valid for this car.");
// do what you'd do to fill the car
}
}
public class DieselCar:BaseCar {
public bool ValidFuel(Fuel f) {
return f is DeiselFuel
}
}
Вы можете расширить свой оригинальный интерфейс автомобиля
interface Car {
drive();
}
interface DieselCar extends Car {
fillTank(Diesel fuel);
}
interface SolarCar extends Car {
chargeBattery(Sun fuel);
}
}
В CLOS-подобной системе вы можете сделать что-то вроде этого:
(defclass vehicle () ())
(defclass fuel () ())
(defgeneric fill-tank (vehicle fuel))
(defmethod fill-tank ((v vehicle) (f fuel)) (format nil "Dude, you can't put that kind of fuel in this car"))
(defclass diesel-truck (vehicle) ())
(defclass normal-truck (vehicle) ())
(defclass diesel (fuel) ())
(defmethod fill-tank ((v diesel-truck) (f diesel)) (format nil "Glug glug"))
давая вам такое поведение:
CL> (fill-tank (make-instance 'normal-truck) (make-instance 'diesel))
"Dude, you can't put that kind of fuel in this car"
CL> (fill-tank (make-instance 'diesel-truck) (make-instance 'diesel))
"Glug glug"
Что, на самом деле, является версией двойной отправки Common Lisp, как упомянул stefaanv.
Использовать is
Оператор для проверки с принятыми классами, и вы можете бросить исключение в конструкторе