Класс, который ничего не представляет - это правильно?

42

Я просто проектирую свое приложение, и я не уверен, правильно ли я понимаю SOLID и OOP. Классы должны делать одну вещь и делать это хорошо, но, с другой стороны, они должны представлять реальные объекты, с которыми мы работаем.

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

  1. FeatureExtractor
  2. DataSet
  3. анализатор

Но класс FeatureExtractor ничего не представляет, он делает то, что делает его скорее рутиной, чем классом. Он будет иметь только одну функцию, которая будет использоваться: extract_features ()

Правильно ли создавать классы, которые не представляют одну вещь, а делают одну?

РЕДАКТИРОВАТЬ: не уверен, если это имеет значение, но я использую Python

И если extract_features () будет выглядеть так: стоит ли создавать специальный класс для хранения этого метода?

def extract_features(df):
    extr = PhrasesExtractor()
    extr.build_vocabulary(df["Text"].tolist())

    sent = SentimentAnalyser()
    sent.load()

    df = add_features(df, extr.features)
    df = mark_features(df, extr.extract_features)
    df = drop_infrequent_features(df)
    df = another_processing1(df)
    df = another_processing2(df)
    df = another_processing3(df)
    df = set_sentiment(df, sent.get_sentiment)
    return df
Алиша Гловацкая
источник
13
Это выглядит отлично, как функция. С учетом трех вещей, которые вы перечислили как модули, все в порядке, и вы можете разместить их в разных файлах, но это не значит, что они должны быть классами.
Берги
31
Имейте в виду, что в Python довольно распространено и приемлемо использовать не OO-подходы.
jpmc26
13
Вы можете быть заинтересованы в Домене Управляемый Дизайн. «Классы должны представлять объекты из реального мира» на самом деле ложно ... они должны представлять объекты в домене . Домен часто тесно связан с реальным миром, но в зависимости от приложения некоторые вещи могут или не могут считаться объектами, или некоторые вещи, которые «на самом деле» являются отдельными, могут в конечном итоге либо быть связанными, либо идентичными внутри домена приложения.
Бакуриу
1
По мере знакомства с ООП, я думаю, вы обнаружите, что классы очень редко соотносятся один с другим с сущностями реального мира. Например, вот эссе, в котором утверждается, что попытка втиснуть всю функциональность, связанную с сущностью реального мира, в один класс очень часто является анти-паттерном: programmer.97things.oreilly.com/wiki/index.php/…
Кевин - Восстановите Монику
1
«Они должны представлять реальные объекты, с которыми мы работаем». не обязательно. Многие языки имеют потоковый класс, представляющий поток байтов, который является абстрактным понятием, а не «реальным объектом». Технически файловая система тоже не «реальный объект», это просто концепция, но иногда существуют классы, представляющие файловую систему или ее часть.
Pharap

Ответы:

96

Классы должны делать 1 вещь и делать это хорошо

Да, это вообще хороший подход.

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

Нет, это ИМХО распространенное недоразумение. Хороший доступ новичка к ООП часто «начинается с объектов, представляющих вещи из реального мира» , это правда.

Однако не стоит останавливаться на этом !

Классы могут (и должны) использоваться для структурирования вашей программы различными способами. Моделирование объектов из реального мира является одним из аспектов этого, но не единственным. Создание модулей или компонентов для конкретной задачи - еще один разумный пример использования классов. «Экстрактор функций», вероятно, является таким модулем, и даже если он содержит только один публичный метод extract_features(), я был бы удивлен, если бы он также не содержал много частных методов и, возможно, некоторое общее состояние. Таким образом, наличие класса FeatureExtractorпредставит естественное местоположение для этих частных методов.

Примечание: в таких языках, как Python, которые поддерживают концепцию отдельного модуля, FeatureExtractorдля этого также можно использовать модуль , но в контексте этого вопроса ИМХО это незначительная разница.

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

Док Браун
источник
33
Мне особенно нравится, что вы упомянули проблему внутреннего состояния. Я часто нахожу это решающим фактором для того, чтобы я сделал что-то классом или функцией в Python.
Дэвид З
17
Вы, кажется, рекомендуете «classitis». Не создавайте класс для этого, это антипаттерн в стиле Java. Если это функция, сделайте ее функцией. Внутреннее состояние не имеет значения: оно может быть у функций (через замыкания).
Конрад Рудольф
25
@KonradRudolph: вы, кажется, упустили, что речь идет не о выполнении одной функции. Речь идет о куске кода, который требует нескольких функций, общего имени и, возможно, некоторого общего состояния. Использование модуля для этого может быть целесообразно в языках с концепцией модуля, отличной от классов.
Док Браун
8
@DocBrown Мне придется не согласиться. Это класс с одним открытым методом. API мудрый, это неотличимо от функции. Смотрите мой ответ для деталей. Использование классов с одним методом может быть оправдано, если вам нужно создать определенный тип для представления состояния. Но здесь это не так (и даже в этом случае иногда можно использовать функции).
Конрад Рудольф
6
@DocBrown Я начинаю понимать, что вы имеете в виду: вы говорите о функциях, которые вызываются изнутри extract_features? Я просто предположил, что это публичные функции из другого места. Справедливо, я согласен, что если они являются частными, то, вероятно, они должны войти в модуль (но все же: не в класс, если они не разделяют состояние) вместе с extract_features. (Тем не менее, вы, конечно, могли бы объявить их локально внутри этой функции.)
Конрад Рудольф
44

Док Браун очень точен: классы не должны представлять объекты реального мира. Они просто должны быть полезными . Классы - это в основном просто дополнительные типы, и что соответствует intили stringсоответствует в реальном мире? Это абстрактные описания, а не конкретные, осязаемые вещи.

Тем не менее, ваш случай особенный. По вашему описанию:

И если extract_features () будет выглядеть так: стоит ли создавать специальный класс для хранения этого метода?

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

Злоупотребление классами связано с тем, что ООП стал распространенным явлением в Java в 1990-х годах. К сожалению, в то время в Java отсутствовали некоторые современные языковые возможности (такие как замыкания), что означало, что многие понятия было трудно или невозможно выразить без использования классов. Например, в Java до недавнего времени было невозможно иметь методы, которые переносят состояние (например, замыкания). Вместо этого вам пришлось написать класс для переноса состояния, в котором был представлен единственный метод (называемый чем-то вроде invoke).

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

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

Конрад Рудольф
источник
Было бы полезно добавить, что если функции, вызываемые в примере, на самом деле являются частными функциями, то их инкапсуляция в модуле или классе была бы вполне уместной. В остальном я полностью согласен.
Лелиэль
11
Также полезно помнить, что «ООП» не означает «написать кучу классов». Функции в Python являются объектами, поэтому нет необходимости рассматривать это как «не ООП». Скорее, это просто повторное использование встроенных типов / классов, и «повторное использование» является одним из святых Грааля в программировании. Обертывание этого в классе будет препятствовать повторному использованию, поскольку ничто не будет совместимо с этим новым API (если __call__не определено, в этом случае просто используйте функцию!)
Warbo
3
Также на тему «классы против автономных функций»: eev.ee/blog/2013/03/03/…
Joker_vD
1
Разве это не так, хотя в Python «свободные функции» также являются объектами с типом, представляющим метод __call__()? Это действительно так отличается от анонимного экземпляра внутреннего класса? Синтаксически, конечно, но из языкового дизайна, это кажется менее значимым отличием, чем вы здесь представляете.
JimmyJames
1
@JimmyJames Верно. Все дело в том, что они предлагают те же функции для конкретной цели, но проще в использовании.
Конрад Рудольф
36

Я просто проектирую свое приложение, и я не уверен, правильно ли я понимаю SOLID и OOP.

Я был в этом более 20 лет, и я тоже не уверен.

Классы должны делать 1 вещь и делать это хорошо

Трудно ошибиться здесь.

они должны представлять реальные объекты, с которыми мы работаем.

Да неужели? Позвольте мне познакомить вас с одной самыми популярным и успешным классом всех времен: String. Мы используем это для текста. И объект реального мира, который он представляет, таков:

Рыбак держит 10 рыб, подвешенных на веревке

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

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

Теперь, конечно, вы можете думать об этом так:

праздничные письма, подвешенные на веревочке, гласят: «ДАВАЙ ДЕЛАТЬ!»

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

candied_orange
источник
1
Красивая серия фотографий ...
Зев Шпиц
На данный момент я не уверен, что «строка» даже метафорична. Это просто имеет определенное значение в области программирования, как и слова, подобные classи tableи column...
Kyralessa
@ Kyralessa, вы, эфир, учите новичков метафоре, или вы позволите им быть волшебными для них. Пожалуйста, спасите меня от кодеров, которые верят в магию.
candied_orange
6

Осторожно! Нигде SOLID не говорит, что класс должен «делать только одну вещь». Если бы это было так, у классов был бы только один метод, и между классами и функциями не было бы никакой разницы.

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

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

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

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

JacquesB
источник
+1 за «не нужно создавать класс, если вы можете решить его с помощью функции». Иногда кому-то нужно говорить правду.
tchrist
5

Правильно ли создавать классы, которые не представляют одну вещь, а делают одну?

В целом это нормально.

FeatureExtractorТрудно сказать, без точного описания того, что именно должен делать класс.

В любом случае, даже если FeatureExtractorвыставляется только публичная extract_features()функция, я мог бы подумать о том, чтобы настроить ее с помощью Strategyкласса, который определяет, как именно следует выполнить извлечение.

Другой пример - класс с функцией Template .

И есть больше Поведенческих Шаблонов Проекта , которые основаны на моделях класса.


Как вы добавили код для уточнения.

И если extract_features () будет выглядеть так: стоит ли создавать специальный класс для хранения этого метода?

Линия

 sent = SentimentAnalyser()

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

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

πάντα ῥεῖ
источник
2
Я не вижу смысла добавлять сложность ( FeatureExtractorкласс) просто для того, чтобы еще больше усложнить (интерфейс для SentimentAnalyserкласса). Если развязка желательна, то функция extract_featuresможет принимать get_sentimentфункцию в качестве аргумента ( loadвызов кажется независимым от функции и вызывается только для ее эффектов). Также обратите внимание, что Python не имеет / поощряет интерфейсы.
Варбо
1
@warbo - даже если вы предоставляете функцию в качестве аргумента, делая ее функцией, вы ограничиваете потенциальные реализации, которые будут вписываться в формат функции, но если необходимо управлять постоянным состоянием между одним вызовом и затем (например, a CacheingFeatureExtractorили a TimeSeriesDependentFeatureExtractor) тогда объект будет гораздо лучше подходить. Просто потому , что нет никакой необходимости для объекта в данный момент не означает , что никогда не будет.
Жюль
3
@Jules Во-первых, вам это не нужно (YAGNI), во-вторых, функции Python могут ссылаться на постоянное состояние (замыкания), если вам это нужно (вы не собираетесь), в-третьих, использование функции ничего не ограничивает, поскольку любой объект с __call__Метод будет совместим, если вам это нужно (вы не собираетесь), в-четвертых, добавив оболочку, как будто FeatureExtractorвы делаете код несовместимым со всем другим когда-либо написанным кодом (если только вы не предоставите __call__метод, в этом случае функция будет явно проще). )
Warbo
0

За исключением паттернов и всех причудливых языков / концепций: вы наткнулись на работу или пакетный процесс .

В конце концов, даже чистая программа ООП должна как-то чем-то руководствоваться, чтобы фактически выполнять работу; как-то должна быть точка входа. Например, в шаблоне MVC контроллер «C» получает события щелчка и т. Д. От GUI, а затем организует другие компоненты. В классических инструментах командной строки «главная» функция будет делать то же самое.

Правильно ли создавать классы, которые не представляют одну вещь, а делают одну?

Ваш класс представляет сущность, которая делает что-то и организует все остальное. Вы можете назвать это « Контроллер» , « Работа» , « Главное» или как вам на ум.

И если extract_features () будет выглядеть так: стоит ли создавать специальный класс для хранения этого метода?

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

Anoe
источник
3
Обратите внимание, что в подобных сценариях вызов отдельных процедур «методов» может привести к путанице. Большинство языков, включая Python, называют их «функциями». «Методы» - это функции / процедуры, которые привязаны к конкретному экземпляру класса, что противоположно использованию вами термина :)
Warbo
Правда, @Warbo. Или мы могли бы назвать их процедурой или defun или sub или ...; и они могут быть методами класса (sic), не связанными с экземпляром. Я надеюсь, что нежный читатель сможет абстрагироваться от предполагаемого значения. :)
AnoE
@ Варбо, это приятно знать! В большинстве учебных материалов, с которыми я сталкивался, говорится, что термины «функция» и «метод» являются взаимозаменяемыми и что это всего лишь языковые предпочтения.
Дом
@Dom В общем случае ("pure") "function" - это отображение входных значений в выходные значения; «процедура» - это функция, которая также может вызывать эффекты (например, удаление файла); оба статически рассылаются (то есть ищутся в лексическом контексте). «Метод» - это функция или (обычно) процедура, которая динамически отправляется (ищется) из значения (называемого «объектом»), которое автоматически связывается с (неявным thisили явным self) аргументом метода. Методы объекта взаимно «открыто» рекурсивны, поэтому замена fooвызывает все self.fooвызовы для использования этой замены.
Warbo
0

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

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

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

Именно на этой стадии «объединения вместе» мы могли бы написать несколько классов. Мы пишем классы, когда нет существующего объекта, который ведет себя так, как мы хотим. Например, наш домен может содержать «foos», коллекции foos, называемые «bars», и коллекции баров, называемые «bazs». Мы можем заметить, что foos достаточно просты для моделирования со строками, поэтому мы делаем это. Мы находим, что бары требуют, чтобы их содержимое подчинялось определенному ограничению, которое не соответствует ни одному из представленных Python, и в этом случае мы могли бы написать новый класс для обеспечения соблюдения этого ограничения. Возможно, у баз нет таких особенностей, поэтому мы можем просто представить их списком.

Обратите внимание, что мы могли бы написать новый класс для каждого из этих компонентов (foos, bars и bazs), но нам это не нужно, если уже есть что-то с правильным поведением. В частности, для того, чтобы класс был полезен, он должен что-то «предоставлять» (данные, методы, константы, подклассы и т. Д.), Поэтому даже если у нас много слоев пользовательских классов, мы в конечном итоге должны использовать некоторую встроенную функцию; например, если бы мы написали новый класс для foos, он, вероятно, просто содержал бы строку, так почему бы не забыть класс foo и вместо этого иметь класс bar, содержащий эти строки? Имейте в виду, что классы также являются встроенным объектом, они просто очень гибкие.

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

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


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


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

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

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

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

Warbo
источник
0

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

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

Дом
источник