Как должна быть построена базовая иерархия классов? [закрытый]

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

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

я программирую в основном на C++, C# и Python, но я, вероятно, пойму простые примеры на большинстве языков.

Если какой-либо из терминов кажется смешанным или так, не стесняйтесь редактировать мой вопрос. Я не местный и не уверен во всех словах.

5 ответов


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


построение базового класса.

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

мы работаем над управлением ОС здесь, так что давайте начнем с простого класса, это будет Button. Теперь, каковы атрибуты на нашей кнопке? Очевидно, ему нужно position на окне. Кроме того, мы не хотим, чтобы каждая кнопка была точно такого же размера, так size является другим атрибутом. Кнопка также "нуждается" в label (текст, нарисованный на кнопке). Это то, что вы делаете с каждым классом, вы разработайте его, а затем Закодируйте. Теперь я знаю, какие атрибуты мне нужны, поэтому давайте построим класс.

class Button
{
    private:
        Point m_position;
        Size m_size;
        std::string m_label;
}

обратите внимание, как я опустил все геттеры и сеттеры и другие методы ради более короткого кода, но вам также придется включить их. Я также ожидаю, что у нас будет Point и Size классы, обычно мы должны были бы структурировать их сами.


переход на следующий класс.

теперь у нас есть один класс (Button) закончили, мы можем перейти в следующий класс. Пойдем с Slider, баре, например, позволяет прокручивать веб-страницы вверх и вниз.

давайте начнем, как мы сделали на кнопке, что нужно нашему классу слайдера? У него есть местоположение (position) и size ползунка. Кроме того, он имеет минимальные и максимальные значения (минимум означает, что скроллер установлен в верхней части слайдера, а максимум-внизу). Нам также нужно текущее значение, т. е. где скроллер находится на момент. Пока этого достаточно, мы можем построить наш класс:

class Slider
{
    private:
        Point m_position;
        Size m_size;
        int m_minValue;
        int m_maxValue;
        int m_currentValue;
}

создание базового класса.

теперь, когда у нас есть два класса, первое, что мы замечаем, это то, что мы просто определили Point m_position; и Size m_size; атрибуты для обоих классов. Это означает, что у нас есть два класса с общими элементами, и мы просто написали один и тот же код дважды, было бы здорово, если бы мы могли написать код только один раз и сказать обоим нашим классам использовать этот код вместо рерайтинг? Ну, мы можем.

создание базового класса" всегда " (есть исключения, но новички не должны беспокоиться о них) рекомендуется, если у нас есть два похожих класса с общими атрибутами, в этом случае Button и Slider. Они оба управляют нашей ОС с помощью size и position. Из этого мы получаем новый класс, называемый Control:

class Control
{
    private:
        Point m_position;
        Size m_size;
}

наследование аналогичных классов от общего базового класса.

теперь у нас есть наши!--20-- > класс, который включает общие детали для каждого контроля, мы может сказать наше Button и Slider наследовать от него. Это сэкономит нам время, память компьютера и, в конечном счете, время. Вот наши новые классы:

class Control
{
    private:
        Point m_position;
        Size m_size;
}

class Button : public Control
{
    private:
        std::string m_label
}

class Slider : public Control
{
    private:
        int m_minValue;
        int m_maxValue;
        int m_currentValue;
}

теперь некоторые могут сказать, что писать Point m_position; Size m_size; дважды намного проще, чем писать дважды : public Control и создании Control класса. Это может быть верно в некоторых случаях, но все равно рекомендуется не писать один и тот же код дважды, особенно когда создание классов.

кроме того, кто знает, сколько общих атрибутов мы в конечном итоге найдем. Позже мы могли бы понять, что нам нужно Control* m_parent члена Control класс, который указывает на окно (или панель или такое), в котором находится наш элемент управления.

другое дело, если мы позже поймем, что на вершине Slider и Button нам тоже нужны TextBox, мы можем просто создать элемент управления TextBox, сказав class TextBox : public Control { ... } и только напишите переменные - члены textbox, вместо размера, позиции и родителя снова и снова в каждом классе.


последние мысли.

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

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


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

class animal:
    #something
class dog(animal):
    #something
class cat(animal):
    #something

здесь есть два класса, собака и кошка, которые имеют атрибуты класса animal. Здесь свою роль играет наследование.

class parent:
    #something
class child(parent):
    #something

здесь родитель и ребенок-это два класса, где ребенок зависит от родителя, где ребенок имеет атрибуты родителя и свои собственные уникальные. Итак, здесь используется наследование.


это зависит от языка.

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

class Dog:
    def __init__(self, name):
        self.name = name

    def sing(self):
        return self.name + " barks"

class Cat:
    def __init__(self, name):
        self.name = name

    def sing(self):
        return self.name + " meows"

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

в C++ вместо этого вы будете вынуждены добавить базу класс (например Animal) и объявить эти два класса производными.

конечно, наследование реализовано и полезно в Python тоже, но во многих случаях, когда это необходимо, скажем, C++ или Java, вы можете просто избежать этого благодаря "утиному набору".

однако, если вы хотите, например, наследовать реализацию некоторых методов (в данном случае конструктор), то наследование может использоваться с Python тоже с

class Animal:
    def __init__(self, name):
        self.name = name

class Dog(Animal):
    def sing(self):
        return self.name + " barks"

class Cat(Animal):
    def sing(self):
        return self.name + " meows"

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

кто-то сказал, что с объектно-ориентированным программированием (на самом деле классовое Программирование) иногда вам просто нужен банан, и вместо этого вы получаете гориллу, держащую банан и целые джунгли с ним.


Я бы начал с определения класса Википедия:

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

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

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

что вам нужно при создании HTTP-запроса? Сервер, порт, протокол, заголовки, URI... Вы можете поместить все это в дикт, как {'server': 'google.com'} но когда вы используете класс для этого, вы просто сделаете его явным, что вам нужны эти атрибуты вместе, и вы будете использовать их для выполнения этой конкретной задачи.

для методов. Вы можете снова создать метод fetch(dict_of_settings), но вся функциональность привязана к атрибутам HTTP class и просто не делает смысл без них.

class HTTP:
    def __init__(self):
        self.server = ...
        self.port = ...
        ...

    def fetch(self):
        connect to self.server on port self.port
        ...

r1 = HTTP(...)
r2 = HTTP(...)
r1.port = ...
data = r1.fetch()

разве это не мило и читабельно?


абстрактные классы/интерфейсы

этот момент, просто быстро... Предположим, вы хотите реализовать инъекции зависимостей в вашем проекте для этого конкретного случая: вы хотите, чтобы ваше приложение было независимым от Database engine.

таким образом, вы предлагаете интерфейс (представленный абстрактным классом), который должен реализовывать каждый соединитель базы данных а затем полагайтесь на общие методы в своем приложении. Допустим, вы определяете DatabaseConnectorAbstract (вам не нужно фактически определять в python, но вы делаете в C++/C# при предложении интерфейса) с методами:

class DatabaseConnectorAbstract:
    def connect(): raise NotImplementedError(  )
    def fetch_articles_list(): raise NotImplementedError(  )
    ...

# And build mysql implementation
class DatabaseConnectorMysql(DatabaseConnectorAbstract):
   ...

# And finally use it in your application
class Application:
    def __init__(self,database_connector):
        if not isinstanceof(database_connector, DatabaseConnectorAbstract):
            raise TypeError()

        # And now you can rely that database_connector either implements all
        # required methods or raises not implemented exception

иерархия классов

исключения в Python. Просто взгляните на секунду на иерархию.

ArithmeticError и generic Exception и в некоторых случаях это может быть так же конкретно, как сказать FloatingPointError. Это чрезвычайно полезно при обработке исключений.

вы можете понять это лучше на .NET forms, когда объект должен быть экземпляром Control при добавлении в форму, но может быть практически ничего. Весь смысл в том, что объект DataGridView в то же время Control (и реализация всех методов и свойств). Это тесно связано с абстрактными классами и интерфейсами и одним из многочисленных примеров может быть HTML элементы:

class HtmlElement: pass # Provides basic escaping
class HtmlInput(HtmlElement): pass # Adds handling for values and types
class HtmlSelect(HtmlInput): pass # Select is input with multiple options
class HtmlContainer(HtmlElement): pass # div,p... can contain unlimited number of HtmlElements
class HtmlForm(HtmlContainer): pass # Handles action, method, onsubmit

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


с вас в первую очередь интересует общая картина, а не механика класса, вы, возможно, захотите ознакомиться с С. О. Л. И. Д. принципы объектно-ориентированного проектирования. Это не строгая процедура, а набор или правила для поддержки вашего собственного суждения и вкуса.

  1. суть в том, что класс представляет собой один ответственность (S). Он делает одно и делает это хорошо. Он должен представлять Ан абстрагирование, предпочтительно один, представляющий часть логики вашего приложения (инкапсуляция как поведение, так и данные для поддержки этого поведения). Это также может быть абстракция агрегации нескольких связанных полей данных. Класс является единицей такой инкапсуляции и отвечает за поддержание инварианты ваших абстракций.

  2. Способ построения классов должен быть как открыта для расширения и закрыты для модификации (O). Определите вероятные изменения в зависимостях вашего класса (типы или константы, которые вы использовали в его интерфейсе и реализации). Вы хотите интерфейс для завершения достаточно, чтобы он мог расширяться, но вы хотите его реализация должна быть надежной достаточно, чтобы его не пришлось менять для этого.

    Это два принципа, о классе как основной строительный блок. Сейчас в здании иерархии, который представляет отношения классов.

  3. иерархии строятся через наследование или состав. Ключевым принципом здесь является то, что вы используете наследование только для модели strict Лисков-взаимозаменяемость (L). Это причудливый способ сказать, что вы используете наследование только для is-a отношения. Для чего-либо еще (за исключением некоторых технических исключений, чтобы получить некоторые незначительные преимущества реализации) вы используете состав. Это сохранит вашу систему как слабосвязанных как это возможно.

  4. в какой-то момент многие разные клиенты могут зависеть от ваших классов по разным причинам. Это приведет к росту иерархии классов, а некоторые из классов ниже в иерархии могут стать слишком большими ("жир") интерфейсы. Когда это происходит (а на практике это вопрос вкуса и суждения), вы seggregate ваш универсальный интерфейс класса в многие клиентские интерфейсы (I).

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

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