должен ли базовый класс иметь атрибуты? - PullRequest
2 голосов
/ 09 августа 2010

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

Мое мнение таково: «Это не эмпирическое правило». Я привел следующий тривиальный пример:

class Shape
{
    protected:
        double Area;
        double Perimeter ;
    public:
        Shape():Area(0.0), Perimeter(0.0){}
        double getArea(){return Area;}
        double getPerimeter (){return Perimeter;}
        virtual void setArea () = 0 ;
        virtual void setPerimeter () = 0 ;
};

class Rectangle : public Shape
{
    private :
        double length ;
        double breadth ;
    public:
        Rectangle ():length(0.0),breadth(0.0){}
        Rectangle (int _length, int _breadth):length(_length),breadth(_breadth){}
        void setArea ()
        {
            Area = length * breadth ;
        }
        void setPerimeter ()
        {
            Perimeter = 2 * (length + breadth) ;
        }
} ; 

С примером, приведенным выше, я чувствую, что любая форма имеет следующие атрибуты 'Площадь' и 'Периметр' и, следовательно, если мы не предоставляем эти атрибуты в классе, Класс Shape не будет подходящим представлением 'Форма реального мира'

Пожалуйста, дайте мне знать ваше мнение по этому поводу.

EDIT:

Первое, что я бы сказал, действительно, мой код наивен и не имеет особого смысла. дизайн. Ранее я думал, что добавлю строку, в которой говорится, что это всего лишь пример, и весь смысл этого поста состоит в том, чтобы знать, должен ли «Базовый класс НИКОГДА иметь атрибуты». Но потом я подумал, что получу несколько хороших дизайнерских предложений, и у меня действительно есть много :))

Переходя к вопросу, со всеми постами ниже, я понимаю, что это не практическое правило, а выбор дизайна (именно это я и хотел подчеркнуть). Сказав, что я также признаю, что было бы желательно НЕ иметь атрибуты в базовом классе, если эти атрибуты могут быть вычислены (или получены) как Площадь = Длина * Ширина. Большое спасибо за все ваши ответы (хотелось бы иметь возможность принять более одного ответа).

Ответы [ 10 ]

12 голосов
/ 09 августа 2010

Я не согласен с вашим примером. Хотя, конечно, у прямоугольника есть площадь и периметр, не имеет смысла хранить их отдельно от длины и ширины, когда вы можете просто вычислить их на лету в геттерах. Зачем заставлять людей вызывать эти функции «Set» для получения точных результатов, когда вы можете предоставить гораздо более приятный интерфейс?

Я бы сделал Shape чистым интерфейсом:

struct Shape
{
    virtual ~Shape(); // probably need this
    virtual double getArea () = 0 ;
    virtual double getPerimeter () = 0 ;
};

class Rectangle : public Shape
{
    double length ;
    double breadth ;
public:
    Rectangle ():length(0.0),breadth(0.0){}
    Rectangle (int _length, int _breadth):length(_length),breadth(_breadth){}
    double getArea ()
    {
        return length * breadth ;
    }
    double getPerimeter ()
    {
        return 2 * (length + breadth) ;
    }
};

Мне кажется, что любая форма имеет следующие атрибуты 'Площадь' и 'Периметр', и, следовательно, если мы не предоставим эти атрибуты в классе, класс Shape не будет подходящим представлением 'Форма реального мира'

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

Обеспечение внешнего мира красивым интерфейсом - это, скажем, 85% дизайна класса. Остальные 15% следят за тем, чтобы интерфейс, который подходит вызывающей стороне, был реально осуществим. Заставить детали внутренней реализации следовать тому же шаблону, что и внешний интерфейс, в той мере, в которой каждое свойство объекта, возвращаемого функцией, обязательно сохраняется в поле, а не просто с низким приоритетом, это бессмысленно.

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

Теперь, если это был не прямоугольник, а какая-то очень сложная фигура, где вычисление площади и периметра идет медленно, то вы можете кэшировать область и периметр в объекте (и либо пересчитать их, либо отметить устаревшие значения кэша). всякий раз, когда определяющие параметры изменяются). Но это специфическое свойство сложных форм, не имеющее ничего общего с простыми формами, такими как Rectangles, и поэтому не относится к базовому классу Shape. Базовые классы должны содержать только те вещи, которые являются общими для всех производных классов или, по крайней мере, общими для достаточного количества из них, и вы готовы действовать так, как если бы они были общими для всех (и переопределять или игнорировать эти вещи в подклассах, к которым они не применяются. к). В вашем примере, способность сообщать площадь и периметр являются общими для всех фигур. Факт сохранения площади и периметра в элементах данных не обязательно должен быть общим для всех фигур, а в случае Rectangle занимает дополнительную память и требует дополнительного кода, что практически бесполезно.

вы никогда не должны иметь атрибутов в базовом классе

Я тоже не согласен с этим, хотя, думаю, я ценю, куда они могут пойти с этим.

Поля, которые может иметь базовый класс Shape, включают координаты положения и ориентацию (если мы говорим о реальных прямоугольниках и т. Д., Которые должны отображаться на экране, а не прямоугольники в абстрактном виде), цвет, теги (если мы твердо привержены краудсорсинговым таксономиям и, вероятно, унаследованы от другого базового класса, а не определены в самой Shape), и тому подобное, что будет обрабатываться одинаково, независимо от того, какой производный класс задействован.

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

Это не слишком плохо, если вы придерживаетесь неизменных объектов. Квадрат - это Прямоугольник (ширина и ширина которого равны), поэтому он может быть подклассом Прямоугольника. Но затем через месяц вы хотите ввести общие четырехугольники. Где вы должны добавить новый класс? Упс - в top иерархии классов, где это потенциально нарушает оба ваших существующих класса. Добавление четырехугольников заставило вас переосмыслить квадраты. При прочих равных вы хотели бы, чтобы такого рода широкомасштабные изменения были необязательными, а не навязывали их себе.

А как насчет изменчивых объектов? Изменяемый квадрат не является изменяемым прямоугольником, поскольку изменяемые прямоугольники должны обеспечивать возможность устанавливать ширину 2 и высоту 3. Квадраты не должны предоставлять эту способность. Изменяемый прямоугольник также не является изменяемым квадратом (поскольку изменяемые квадраты должны обладать свойством, что при изменении ширины высота изменяется. Изменяемые прямоугольники не должны иметь этого свойства). Таким образом, изменяемые прямоугольники, ширина и высота которых равны, совсем не то же самое, что изменяемые квадраты. Классы не могут быть связаны по наследству. Теперь Rectangle(2,2) == Square(2) должно быть истинным или ложным? Что бы мы ни решили, мы кого-нибудь удивим. Таким образом, мы обнаруживаем, что должны оставить их как несвязанные, несравненные классы. То есть не используйте наследование.

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

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

9 голосов
/ 09 августа 2010

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

Существует много разных мнений по этому вопросу, но в C ++ я думаю, что базовые классы довольно часто хранят данные.

6 голосов
/ 09 августа 2010

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

2 голосов
/ 10 августа 2010

Вообще говоря, когда кто-то говорит вам, чтобы НИКОГДА не делать что-то, умные деньги говорят, что он либо «зеленый», либо упрощает проблему.

Ваша цель как программиста - найти самый простой способ решения проблемы (часто этоэто не то же самое, что самый простой способ!).Если вам приходится прилагать большие структурные усилия для удовлетворения принципа «наилучшей практики», есть большая вероятность, что в данном конкретном случае это не лучшая практика.Более того, соображения производительности могут дать реализации, которые профессор CS мог бы осмеивать, хотя они должны быть хорошо документированы и применяться разумно.

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

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

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

class base {
public:
  base(int x);
  virtual ~base();

  inline int x() const { return x_; }

  // virtual interface declared here

private:
  int x_;
};

class derived : public base { ... };

может стать

class base {
public:
  virtual ~base();
  virtual int x() const = 0;

  // ...
};

class base_with_static_x : public base {
public:
  base_with_static_x(int);
  virtual ~base_with_static_x();

  inline int x() const { return x_; }

  // ...

private:
  int x_;
};

class derived : public base_with_static_x { ... };

На самом деле очень мало смысла писать один и тот же код снова и снова.

1 голос
/ 09 августа 2010

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

class Shape
{
    private:
        double Area;
        double Perimeter ;
    public:
        Shape(double the_area, double the_perimeter): Area(the_area), Perimeter(the_perimeter){}
        double getArea() const {return Area;}
        double getPerimeter () const {return Perimeter;}
};

class Rectangle : public Shape
{
    private :
        double length ;
        double breadth ;
    public:
        Rectangle (int _length, int _breadth):Shape(length * breadth, 2 * (length + breadth)), length(_length),breadth(_breadth){}
} ; 
1 голос
/ 09 августа 2010

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

Обратите внимание, что в вашем примере вы нарушили инварианты; у прямоугольника определенной длины и ширины может быть публично изменена его площадь из-под ног. (Потому что это еще не пересчитано) Беспорядок! Определите интерфейс только для атрибутов:

class shape
{
    public:
        ~shape(){}

        virtual double area(void) const = 0;
        virtual double perimeter(void) const = 0;
};

И пусть формы возвращают эту информацию по-своему:

class square : public shape
{
public:
    square(double pSide) : mSide
    {}

    double area(void) const
    {
        return mSide * mSide;
    }

    double perimeter(void) const
    {
        return 4 * mSide;
    }

private:
    double mSide;
};

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

template <typename Shape>
class shape_attribute_cache : public shape
{
public:
    // provide shape interface
    double area(void) const
    {
        return cached_area();
    }

    double perimeter(void) const
    {
        return cached_perimeter();
    }

protected:
    shape_attribute_cache() : mArea(0), mPerim(0), mChanged(false) {} 
    ~shape_attribute_cache(){} // not to be used publically 

    double cached_area(void) const { update_area(); return mArea; }
    void cached_area(double pArea) { mArea = pArea; }

    double cached_perimeter(void) const { update_perimeter(); return mPerim; }
    void cached_perimeter(double pPerim) { mPerim = pPerim; }

    void changed(void) { mAreaChanged = true; mPerimChanged = true; }

private:
    void update_area(void)
    {
        if (!mAreaChanged) return;

        // delegate to derived shapes update method
        static_cast<Shape*>(this)->update_area();
        mAreaChanged = false;
    }

   void update_perimeter(void)
    {
        if (!mPerimChanged) return;

        // delegate to derived shapes update method
        static_cast<Shape*>(this)->update_perimeter();
        mPerimChanged = false;
    }

    double mArea;
    double mPerim;
    bool mAreaChanged;
    bool mPerimChanged;
};

// use helper implementation
class expensive_shape : public shape_attribute_cache<expensive_shape>
{
public:
    // ...

    void some_attribute(double pX)
    {
        mX = pX;
        changed(); // flag cache is bad, no longer callers responsibility
    }

private:
    // ...

    void update_area(void)
    {
        double newArea = /* complicated formula */;
        cached_area(newArea);
    }

    void update_perimeter(void)
    {
        double newPerim = /* complicated formula */;
        cached_perimeter(newPerim);
    }
}; 

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

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

1 голос
/ 09 августа 2010

Пользователи класса могут расширять его способами, о которых вы никогда не задумывались.Например, представьте прямоугольник без вершины (только 3 другие стороны).Я бы назвал это формой, даже если нет смысла говорить о его области, и даже его периметр может быть нечетким.Почему я должен включать код для установки свойств, которые не имеют смысла?И что более того, что происходит с кодом, который ожидает, что эти свойства что-то значат?

1 голос
/ 09 августа 2010

Я думаю, что переоценка происходит от стилистических проблем. Считается чище иметь в качестве базы класс протокола (интерфейса), который публикует только методы класса. Реализация должна быть максимально скрытой от классов клиентов. Если вы также храните данные в базовом классе, то реализующие классы не свободны в выборе лучшего способа хранения данных. Таким образом, базовый класс ограничивает возможности реализации. Возможно, одна из реализаций хотела бы рассчитать уже сохраненное значение, чтобы пространство не требовалось, но у интервалов нет возможности освободить (не динамическую) выделенную память.

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

0 голосов
/ 10 августа 2010

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

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

Возьми свой пример. Класс Shape может быть интерфейсом, определяющим только методы для получения области и периметра, которые должны иметь все Shapes (= классы, реализующие Shape).

После этого вам нужно определить две фигуры: прямоугольник и квадрат. Квадрат имеет только одно свойство - ширину, которая также является его высотой. Прямоугольник имеет ширину и высоту. Теперь вы можете реализовать свой Rectangle как отдельный класс, который имеет Height и Width, но тогда у вас будет два класса с Width - Square и Rectangle. Вместо этого вы можете создать подкласс Square, добавить высоту и получить прямоугольник.

Вероятно, не лучший из примеров, но эх.

0 голосов
/ 09 августа 2010

Чтобы ответить на ваш вопрос .... да, используйте атрибуты в базовом классе, где они применимы ко всем подклассам.Например, если вы знаете, что форма (POLYGON!) Всегда имеет длину и ширину, используйте их в качестве атрибутов базового класса.

Я не понимаю, почему вы захотите воссоздать их для каждой реализацииформу (ПОЛИГОН), которую вы создаете ...

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

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