Как построить базовую иерархию классов?

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

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

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

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

8

Решение

Я буду использовать 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;
}

Наследование похожих классов от общего базового класса.

Теперь, когда мы получили Control класс, который включает в себя общие элементы для каждого элемента управления, мы можем сказать нашим 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мы можем просто создать текстовое поле управления, сказав class TextBox : public Control { ... } и только записывайте переменные-члены, специфичные для текстового поля, вместо размера, положения и родителя снова и снова в каждом классе.


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

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

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

9

Другие решения

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

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

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

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

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

1

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

Например, в 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"

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

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

1

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

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

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

Первый и самый прямой случай, когда вам нужен класс, — это когда вам нужно (или, скорее, вам нужно) объединить определенные функции и методы вместе, потому что они просто имеют смысл вместе. Давайте представим что-то простое: 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()

Разве это не приятно и читабельно?


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

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

Таким образом, вы предлагаете интерфейс (представленный абстрактным классом), который должен реализовывать каждый соединитель базы данных, а затем полагаться на универсальные методы в вашем приложении. Допустим, вы определяете 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 является общий Exception и в некоторых случаях это может быть так же, как поговорка FloatingPointError, Это чрезвычайно полезно при обработке исключений.

Вы можете понять это лучше на .NET формы когда объект должен быть экземпляром 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.O.L.I.D. принципы объектно-ориентированного проектирования. Это не строгая процедура, но набор или правила, чтобы поддержать ваше собственное суждение и вкус.

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

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

    Это два принципа в отношении класса как основного строительного блока. Теперь перейдем к построению иерархий, которые представляют классовые отношения.

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

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

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

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

1