Какова реальная ответственность класса?

42

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

Чтобы объяснить проблему чуть подробнее, в статье говорится, что не должно быть, например, FileWriterкласса, но поскольку написание является действием, оно должно быть методом класса File. Вы поймете, что это часто зависит от языка, поскольку программист на Ruby, скорее всего, будет против использования FileWriterкласса (Ruby использует метод File.openдля доступа к файлу), тогда как программист на Java этого не сделает.

Моя личная (и да, очень скромная) точка зрения заключается в том, что это нарушит принцип единой ответственности. Когда я программировал на PHP (потому что PHP, очевидно, лучший язык для ООП, верно?), Я часто использовал такой тип фреймворка:

<?php

// This is just an example that I just made on the fly, may contain errors

class User extends Record {

    protected $name;

    public function __construct($name) {
        $this->name = $name;
    }

}

class UserDataHandler extends DataHandler /* knows the pdo object */ {

    public function find($id) {
         $query = $this->db->prepare('SELECT ' . $this->getFields . ' FROM users WHERE id = :id');
         $query->bindParam(':id', $id, PDO::PARAM_INT);
         $query->setFetchMode( PDO::FETCH_CLASS, 'user');
         $query->execute();
         return $query->fetch( PDO::FETCH_CLASS );
    }


}

?>

Насколько я понимаю, суффикс DataHandler не добавляет ничего значимого ; но суть в том, что принцип единой ответственности диктует нам, что объект, используемый в качестве модели, содержащей данные (может быть, она называется записью), также не должен отвечать за выполнение запросов SQL и доступа к базе данных. Это как-то делает недействительным шаблон ActionRecord, используемый, например, Ruby on Rails.

Я наткнулся на этот код C # (да, четвертый объектный язык, используемый в этом посте) только на днях:

byte[] bytes = Encoding.Default.GetBytes(myString);
myString = Encoding.UTF8.GetString(bytes);

И я должен сказать, что для меня не имеет особого смысла, что класс Encodingor на Charsetсамом деле кодирует строки. Это должно быть просто представление о том, что такое кодировка.

Таким образом, я склонен думать, что:

  • Это не Fileкласс ответственность открывать, читать или сохранять файлы.
  • Это не Xmlответственность класса, чтобы сериализовать себя.
  • Это не Userкласс ответственности за запрос базы данных.
  • и т.п.

Однако, если экстраполировать эти идеи, почему бы Objectиметь toStringкласс? Это не ответственность Автомобиля или Собаки, чтобы преобразовать себя в строку, не так ли?

Я понимаю, что с прагматической точки зрения избавление от toStringметода для красоты следования строгой форме SOLID, которая делает код более легким в обслуживании, делая его бесполезным, не является приемлемым вариантом.

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

Какова ответственность класса?

Пьер Арло
источник
Объекты toString в основном удобны, а toString обычно используется для быстрого просмотра внутреннего состояния во время отладки
ratchet freak
3
@ratchetfreak Я согласен, но это был пример (почти шутка), чтобы показать, что единственная ответственность очень часто нарушается так, как я описал.
Пьер Арло
8
В то время как принцип единоличной ответственности имеет своих ярых сторонников, на мой взгляд, это в лучшем случае руководство, а в подавляющем большинстве вариантов выбора оно очень слабое. Проблема с SRP состоит в том, что вы получаете сотни классов со странными именами, что делает действительно трудным найти то, что вам нужно (или выяснить, существует ли то, что вам нужно, уже существует) и понять общую картину. Я предпочитаю иметь меньше хорошо названных классов, которые мгновенно говорят мне, чего ожидать от них, даже если у них довольно много обязанностей. Если в будущем все изменится, я разделю класс.
Данк

Ответы:

24

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

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

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

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

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

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

Однако, если мы экстраполируем эти идеи, почему у Object будет класс toString? Это не ответственность Автомобиля или Собаки, чтобы преобразовать себя в строку, не так ли?

Теперь позвольте мне остановиться здесь, чтобы кое-что здесь сказать toString: есть фундаментальная вещь, которой обычно пренебрегают, когда кто-то делает такой переход от модулей к классам и размышляет над тем, какие методы должны принадлежать классу. И дело в динамической рассылке (иначе, позднее связывание, «полиморфизм»).

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

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

Тиаго Сильва
источник
31

[Примечание: я собираюсь говорить об объектах здесь. Объекты - это объектно-ориентированное программирование, а не классы.]

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

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

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

Вот пример, который также очень популярен во вводных ОО курсах: банковский счет. Который обычно моделируется как объект с balanceполем и transferметодом. Другими словами: баланс счета - это данные , а перевод - операция . Какой разумный способ смоделировать банковский счет.

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

Что касается вашего конкретного вопроса о toString, я согласен. Я очень предпочитаю решение Haskell Showable класса типов . (Scala учит нас, что классы типов прекрасно сочетаются с ОО.) То же самое с равенством. Равенство очень часто является не свойством объекта, а контекстом, в котором этот объект используется. Просто подумайте о числах с плавающей запятой: каким должен быть эпсилон? Опять же, у Haskell есть Eqкласс типов.

Йорг Миттаг
источник
Мне нравятся ваши сравнения с haskell, это делает ответ немного более свежим. Что бы вы тогда сказали о проектах, которые позволяют ActionRecordбыть одновременно моделью и запросчиком базы данных? Вне зависимости от контекста, похоже, нарушается принцип единоличной ответственности.
Пьер Арло
5
«... потому что это зависит от того, как вы сформулируете предложение». Ура! Ох, и мне очень нравится ваш банковский пример. Никогда не знал, что еще в восьмидесятые годы меня уже учили функциональному программированию :) (дизайн, ориентированный на данные и независимое от времени хранилище, где остатки и т. Д. Вычислимы и не должны храниться, а чей-то адрес не должен изменяться, а храниться как новый факт) ...
Марьян Венема
1
Я бы не сказал, что методология глагола / существительного является «нелепой». Я бы сказал, что упрощенные проекты потерпят неудачу и что это очень трудно сделать правильно. Потому что, контрпример, не является ли RESTful API методологией глагола / существительного? Где, для дополнительной степени сложности, глаголы чрезвычайно ограничены?
user949300
@ user949300: тот факт, что набор глаголов является фиксированным, в точности исключает проблему, о которой я говорил, а именно то, что вы можете формулировать предложения несколькими различными способами, делая таким образом то, что является существительным, а что глаголом, зависит от стиля письма, а не от предметного анализа ,
Йорг Миттаг
8

Слишком часто Принцип Единой Ответственности становится Принципом Нулевой Ответственности, и в итоге вы получаете Анемичные Классы, которые ничего не делают (кроме сеттеров и геттеров), что приводит к катастрофе в Королевстве Существительных .

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

В вашем примере с Encoding, IMO, он определенно должен иметь возможность кодировать. Как вы думаете, что он должен делать вместо этого? Просто иметь имя «utf» - это нулевая ответственность. Теперь, возможно, имя должно быть Encoder. Но, как сказал Конрад, данные (какая кодировка) и поведение (делая это) принадлежат друг другу .

user949300
источник
7

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

Если вы хотите написать что-то для записи в файл, подумайте обо всем, что вам нужно указать (для ОС): имя файла, метод доступа (чтение, запись, добавление), строка, которую вы хотите написать.

Таким образом, вы создаете объект File с именем файла (и методом доступа). Объект File теперь закрыт по имени файла (в этом случае он, вероятно, примет его как значение только для чтения / const). Экземпляр File теперь готов принимать вызовы метода «write», определенного в классе. Это просто принимает строковый аргумент, но в теле метода write, реализации, также есть доступ к имени файла (или дескриптору файла, который был создан из него).

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

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

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

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

Часто эти инкапсуляции внешне выглядят как объекты реального мира / объекты в проблемной области, но не обманывайте себя. Когда вы создаете класс Car, на самом деле это не автомобиль, а набор данных, которые формируют платформу для достижения определенных видов вещей, которые вы, возможно, захотите сделать на автомобиле. Формирование строкового представления (toString) является одной из таких вещей - выплевывание всех этих внутренних данных. Помните, что иногда класс Car может не подходить, даже если проблемная область связана с автомобилями. В отличие от царства существительных, это операции, глаголы, на которых должен основываться класс.

Бенедикт
источник
4

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

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

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

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

Telastyn
источник
4

Я наткнулся на этот код C # (да, четвертый объектный язык, используемый в этом посте) только на днях:

byte[] bytes = Encoding.Default.GetBytes(myString);
myString = Encoding.UTF8.GetString(bytes);

И я должен сказать, что для меня не имеет особого смысла, что класс Encodingor на Charsetсамом деле кодирует строки. Это должно быть просто представление о том, что такое кодировка.

В теории, да, но я думаю, что C # делает оправданный компромисс ради простоты (в отличие от более строгого подхода Java).

Если бы Encodingтолько представляла (или идентифицировала) определенную кодировку - скажем, UTF-8 - тогда вам, очевидно, понадобился бы и Encoderкласс, чтобы вы могли реализовать GetBytesего - но тогда вам нужно управлять отношением между Encoders и Encodings, поэтому мы в конечном итоге с нашим добрым старым

EncodersFactory.getDefaultFactory().createEncoder(new UTF8Encoding()).getBytes(myString)

так блестяще озлоблен в той статье, на которую вы ссылались (кстати, отлично читал).

Если я вас хорошо понимаю, вы спрашиваете, где линия.

Что ж, на противоположной стороне спектра находится процедурный подход, который не сложно реализовать в языках ООП с использованием статических классов:

HelpfulStuff.GetUTF8Bytes(myString)

Большая проблема в том, что когда автор HelpfulStuffуходит, а я сижу перед монитором, у меня нет возможности узнать, где GetUTF8Bytesэто реализовано.

Должен ли я искать его в HelpfulStuff, Utils, StringHelper?

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

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

EncodersFactory.getDefaultFactory().createEncoder(new UTF8Encoding()).getBytes(myString)

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

Так что я бы пошел с умеренным подходом и был бы либеральным в отношении единой ответственности. Я бы предпочел, чтобы Encodingкласс идентифицировал тип кодирования и выполнял реальное кодирование: данные неотделимы от поведения.

Я думаю, что это проходит как шаблон фасада, который помогает смазать механизмы. Этот законный образец нарушает SOLID? Смежный вопрос: нарушает ли шаблон Facade SRP?

Обратите внимание, что это может быть то, как получение закодированных байтов реализовано внутри (в библиотеках .NET). Классы просто не видны публично, и только этот «фасадный» API доступен снаружи. Не так сложно проверить, правда ли это, я просто слишком ленив, чтобы сделать это прямо сейчас :) Возможно, это не так, но это не лишает меня смысла.

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

Конрад Моравский
источник
1

В Java абстракция, используемая для представления файлов, является потоком, некоторые потоки являются однонаправленными (только для чтения или только для записи), другие являются двунаправленными. Для Ruby класс File - это абстракция, представляющая файлы ОС. Таким образом, возникает вопрос, какова его единственная ответственность? В Java ответственность FileWriter заключается в предоставлении однонаправленного потока байтов, который подключен к файлу ОС. В Ruby File отвечает за предоставление двунаправленного доступа к системному файлу. Они оба выполняют SRP, у них просто разные обязанности.

Это не ответственность Автомобиля или Собаки, чтобы преобразовать себя в строку, не так ли?

Конечно, почему нет? Я ожидаю, что класс Car будет полностью представлять абстракцию Car. Если имеет смысл использовать абстракцию car там, где необходима строка, я полностью ожидаю, что класс Car будет поддерживать преобразование в строку.

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

stonemetal
источник
0

Класс может иметь несколько обязанностей. По крайней мере, в классической ООП, ориентированной на данные.

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

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

Эти извлекаемые классы должны иметь очень описательные имена , основанные на операциях они содержат, например <X>Writer, <X>Reader, <X>Builder. Такие имена, как <X>Manager, <X>Handlerвообще не описывают операцию, и если они называются так, потому что они содержат более одной операции, то неясно, чего вы достигли. Вы отделили функциональность от ее данных, вы все еще нарушаете принцип единой ответственности даже в этом извлеченном классе, и если вы ищете определенный метод, вы можете не знать, где его искать (если в <X>или <X>Manager).

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

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

климат
источник