Чи є проблемами лише класи з одним (загальнодоступним) методом?


51

Зараз я працюю над програмним проектом, який виконує стиснення та індексацію кадрів із відеоспостереження. Стиснення працює шляхом розбиття фонових та переднього плану, а потім збереження фону як статичного зображення, а переднього плану - як спрайту.

Нещодавно я приступив до перегляду деяких класів, які я призначив для проекту.

Я помітив, що існує багато класів, які мають лише єдиний публічний метод. Деякі з цих класів:

  • VideoCompressor (з compressметодом, який приймає вхідне відео типу RawVideoта повертає вихідне відео типу CompressedVideo).
  • VideoSplitter (з splitметодом, який приймає вхідне відео типу RawVideoта повертає вектор із 2 вихідних відео, кожен із типів RawVideo).
  • VideoIndexer (з indexметодом, який приймає вхідне відео типу RawVideoта повертає відеоіндекс типу VideoIndex).

Я вважаю себе інстанцірованія кожного класу тільки здійснювати дзвінки , як VideoCompressor.compress(...), VideoSplitter.split(...), VideoIndexer.index(...).

На перший погляд, я думаю, що назви класів достатньо описують призначену функцію, і вони насправді є іменниками. Відповідно їх методами є також дієслова.

Це насправді проблема?


14
Це залежить від мови. У такій мові мульти-парадигми, як C ++ або Python, у цих класів немає діла: їхні "методи" повинні бути вільними функціями.

3
@delnan: навіть у C ++ ви зазвичай використовуєте класи для створення модулів, навіть якщо вам не потрібні повні "OO". Дійсно, існує альтернатива використанню просторів імен замість того, щоб групувати "вільні функції" разом в модуль, але я не бачу в цьому жодних переваг.
Док Браун

5
@DocBrown: C ++ має для цього простори імен. Насправді в сучасному C ++ ви часто використовуєте безкоштовні функції для методів, оскільки вони можуть бути (статично) перевантажені для всіх аргументів, тоді як методи можуть бути перевантажені лише для виклику.
Jan Hudec

2
@DocBrown: Це дуже суть питання. Модулі, що використовують простори імен, що мають лише один "зовнішній" метод, є ідеальними, коли функція є досить складною. Оскільки простори імен не претендують на те, що нічого представляють, і не претендують на об'єктно-орієнтовану. Класи є, але такі класи справді просто простори імен. Звичайно, на Java ви не можете мати функції, тому це результат. Сором Яві.
Ян Худек

2
Пов’язані (майже дублікат): programmers.stackexchange.com/q/175070/33843
Heinzi

Відповіді:


87

Ні, це не проблема, а навпаки. Це ознака модульності та чіткої відповідальності класу. Користувач інтерфейсу легко зрозуміти з точки зору користувача цього класу, і це сприятиме слабкому з'єднанню. Це має багато переваг, але майже не має недоліків. Я хочу, щоб більше компонентів було б розроблено саме так!


6
Це правильна відповідь, і я її заперечував, але ви можете зробити це краще, пояснивши деякі переваги. Я думаю, що інстинкт ОП говорить йому, що це проблема - це щось інше ... тобто те, що VideoCompressor - це справді інтерфейс. Mp4VideoCompressor - це клас, який повинен бути взаємозамінним з іншим VideoCompressor без копіювання коду для VideoSplitter до нового класу.
pdr

1
Вибачте, але ви помиляєтесь. Клас з єдиним методом повинен бути просто окремою функцією.
Майлз Рут

3
@MilesRout: ваш коментар показує, що ви неправильно зрозуміли питання - ОП писало про клас з одним публічним методом, а не з одним методом (а він справді мав на увазі клас з декількома приватними методами, як він сказав нам тут в одному коментарі).
Док Браун

2
"Клас з одним методом повинен просто бути окремою функцією". Приблизне припущення про те, що це за метод. У мене є клас з одним публічним методом. ПІД ІТ - це декілька методів у цьому класі плюс 5 інших класів, які абсолютно не мають уявлення про клієнта. Відмінне застосування SRP дасть чистий, простий інтерфейс шаруватої структури класу, при цьому простота проявляється аж до вершини.
radarbob

2
@Andy: питання було "це проблема", а не "чи відповідає це певним визначенням ОО"? Більше того, в питанні абсолютно немає ознаки, що ОП означає класи без стану.
Док Браун

26

Він більше не орієнтований на об’єкти. Оскільки ці класи нічого не представляють, вони є лише суднами для функцій.

Це не означає, що це неправильно. Якщо функціональність є досить складною або коли вона є загальною (тобто аргументи - це інтерфейси, а не конкретні кінцеві типи), має сенс розмістити цю функціональність в окремому модулі .

Звідти це залежить від вашої мови. Якщо мова має вільні функції, вони повинні бути модулями, які експортують функції. Навіщо робити вигляд, що це клас, коли його немає. Якщо мова не має вільних функцій, наприклад, Java, то ви створюєте класи за допомогою одного публічного методу. Це добре показує межі об'єктно-орієнтованого дизайну. Іноді функціонал просто краще відповідає.

Є один випадок, коли вам може знадобитися клас з єдиним публічним методом, оскільки він повинен реалізовувати інтерфейс з єдиним публічним методом. Будь то для моделі спостереження чи ін'єкції залежності чи будь-чого іншого. Тут це знову залежить від мови. У мовах, які мають функціонери першого класу (C ++ ( std::functionабо параметр шаблону), C # (делегат), Python, Perl, Ruby ( proc), Lisp, Haskell, ...), ці шаблони використовують типи функцій та не потребують класах. Java не має (поки що буде у версії 8) типів функцій, тому ви використовуєте інтерфейси одного методу та відповідні класи одного методу.

Звичайно, я не прихильник написання однієї величезної функції. Він повинен мати приватні підпрограми, але вони можуть бути приватними до файлу реалізації (статичного рівня або анонімного простору імен на рівні файлів на C ++) або до приватного класу помічників, який лише інстанціюється у межах загальнодоступної функції ( Зберігати дані чи ні? ).


12
"Навіщо робити вигляд, що це клас, коли його немає". Наявність об'єкта замість вільної функції дозволяє стан і підтипу. Це може виявитися цінним у світі, де вимоги змінюються. Рано чи пізно виникає необхідність грати з налаштуваннями стиснення відео або забезпечувати альтернативні алгоритми стиснення. Перед цим слово "клас" просто говорить нам, що ця функція належить до легко розширюваного та взаємозамінного програмного модуля з чіткою відповідальністю. Чи не на це дійсно спрямований ОО?
ВІД

1
Ну, якщо він має лише один публічний метод (і такий спосіб не передбачає жодної захищеної), його реально не можна продовжити. Звичайно, упаковка параметрів стиснення може мати сенс, і в цьому випадку вона стає об'єктом функції (деякі мови мають окрему підтримку, а деякі - ні).
Ян Худець

3
Це фундаментальний зв'язок між об'єктами та функціями: функція ізоморфна об'єкту єдиним методом і не має полів. Замикання ізоморфне об'єкту єдиним методом та деякими полями. Селекторна функція, що повертає одну з декількох функцій, які всі закриваються над одним і тим же набором змінних, є ізоморфною для об'єкта. (Насправді, таким чином об’єкти кодуються в JavaScript, за винятком того, що ви використовуєте структуру даних словника замість функції селектора.)
Jörg W Mittag,

4
"Це вже не об'єктно-орієнтоване. Оскільки ці класи нічого не представляють, вони є лише суднами для функцій." - Це неправильно. Я б стверджував, що цей підхід БІЛЬШЕ об’єктно-орієнтований. ОО не означає, що він являє собою об'єкт реального світу. Велика кількість програмування стосується абстрактних понять, але чи це означає, що ви не можете застосувати підхід ОО для її вирішення? Точно ні. Йдеться більше про модульність та абстракцію. Кожен об’єкт несе єдину відповідальність. Це дозволяє обернути голову навколо програми та внести зміни. Не вистачає місця, щоб перелічити численні переваги OOP.
Деспертар

2
@Phoshi: Так, я розумію. Я ніколи не стверджував, що функціональний підхід також не буде працювати. Однак це явно не тема. Відеокомпресор або відеомагнітофон або все ще є абсолютно допустимим кандидатом на об'єкт.
ПРИЙДАЄТЬСЯ

13

Можуть бути причини для вилучення заданого методу у виділений клас. Однією з таких причин є дозволити введення залежностей.

Уявіть, що у вас є клас під назвою, VideoExporterякий, зрештою, повинен мати можливість стиснути відео. Чистим способом було б мати інтерфейс:

interface IVideoCompressor
{
    Stream compress(Video video);
}

що буде реалізовано так:

class MpegVideoCompressor : IVideoCompressor
{
    // ...
}

class FlashVideoCompressor : IVideoCompressor
{
    // ...
}

і використовується так:

class VideoExporter
{
    // ...
    void export(Destination destination, IVideoCompressor compressor)
    {
        // ...
        destination = compressor(this.source);
        // ...
    }
    // ...
}

Погана альтернатива була б мати , VideoExporterякий має багато спільних методи і робить всю роботу, в тому числі в стискає. Це швидко перетвориться на кошмар технічного обслуговування, що ускладнить додавання підтримки для інших форматів відео.


2
Ваша відповідь не розрізняє державні та приватні методи. Це можна зрозуміти як рекомендацію щодо введення всього коду класу лише одним методом, але я думаю, це не те, що ви мали на увазі?
Док Браун

2
@DocBrown: Приватні методи не стосуються цього питання. Їх можна поставити на внутрішній клас помічників чи будь-що інше.
Ян Худек

2
@JanHudec: в даний час текст є "поганою альтернативою було б мати VideoExporter, який має безліч методів" - але це повинно бути "поганою альтернативою було б мати VideoExporter, який має безліч публічних методів".
Док Браун

@DocBrown: Погодьтесь тут.
Ян Худець

2
@DocBrown: дякую за зауваження. Я відредагував свою відповідь. Спочатку я вважав, що передбачається, що питання (і так моя відповідь) стосується лише публічних методів. Здається, це не так очевидно.
Арсеній Муренко

6

Це знак того, що ви хочете передати функції як аргументи іншим функціям. Я здогадуюсь, ваша мова (Java?) Не підтримує її; якщо це так, це не стільки невдача у вашому дизайні, скільки недолік вашої мови вибору. Це одна з найбільших проблем з мовами, які наполягають на тому, що все повинно бути класом.

Якщо ви фактично не передаєте ці штучні функції, тоді ви просто хочете вільну / статичну функцію.


1
З Java 8 у вас є лямбдаси, тому ви майже можете передавати функції.
Сільвіу Бурча

Справедливий момент, але це офіційно не вийде трохи довше, і деякі робочі місця повільно переходять на новіші версії.
Doval

Дата виходу - у березні. Крім того, версії EAP були дуже популярні для JDK 8 :)
Сільвіу Бурча

Хоча лямбди Java 8 - це просто короткий спосіб визначення об'єктів лише одним загальнодоступним методом, крім збереження трохи кодового коду, тут вони взагалі не мають ніякої різниці.
Жуль

5

Я знаю, що я спізнююсь на вечірку, але, як видається, кожен пропустив це:

Це добре відома модель дизайну під назвою: Стратегічний шаблон .

Шаблон стратегії застосовується, коли існує кілька можливих стратегій вирішення підзадачі. Зазвичай ви визначаєте інтерфейс, який застосовує контракт щодо всіх реалізацій, а потім використовуєте певну форму введення залежностей для надання конкретної стратегії для вас.

Наприклад, у цьому випадку ви можете мати, interface VideoCompressorа потім мати кілька альтернативних реалізацій, наприклад, class H264Compressor implements VideoCompressorта class XVidCompressor implements VideoCompressor. З ОП незрозуміло, що тут задіяний інтерфейс, навіть якщо його немає, може бути просто те, що оригінальний автор залишив двері відкритими, щоб застосувати шаблон стратегії, якщо це необхідно. Що саме по собі є хорошим дизайном.

Проблема, з якою ОП постійно виявляє примушення класів викликати метод, - це проблема з її неправильним використанням ін'єкцій залежності та схемою стратегії. Замість того, щоб інстанціювати його там, де вам це потрібно, клас, що містить, повинен мати члена з об'єктом стратегії. І цей член повинен бути введений, наприклад, в конструктор.

У багатьох випадках схема стратегії призводить до класів інтерфейсів (як ви показуєте) лише одним doStuff(...)методом.


1
public interface IVideoProcessor
{
   void Split();

   void Compress();

   void Index();
}

Те, що у вас є, є модульним і це добре, але якби ви згрупували ці обов'язки в IVideoProcessor, це, мабуть, має більше сенсу з точки зору DDD.

З іншого боку, якби розщеплення, стискання та індексація ні в якому разі не пов'язані, я б зберігав їх як окремі компоненти.


Оскільки причина кожної з цих функцій, яка потребує змін, дещо інша, я б стверджував, що об'єднання їх таким чином порушує SRP.
Жуль

0

Це проблема - ви працюєте з функціонального аспекту дизайну, а не даних. Насправді у вас є 3 самостійні функції, які були встановлені OO.

Наприклад, у вас є клас VideoCompressor. Чому ви працюєте з класом, призначеним для стиснення відео - чому у вас немає відеокласу з методами на ньому для стиснення даних (відео), які містять кожен об'єкт цього типу?

При проектуванні OO систем найкраще створювати класи, що представляють об’єкти, а не класи, які представляють діяльність, яку ви можете застосувати. За старих часів класи називались типами - ОО було способом розширення мови з підтримкою нових типів даних. Якщо ви думаєте про OO таким чином, ви отримаєте кращий спосіб проектувати свої класи.

Редагувати:

дозвольте мені спробувати трохи краще пояснити себе, уявіть клас струн, у якому є метод concat. Ви можете реалізувати таку річ, коли кожен об'єкт, ініційований з класу, містить рядкові дані, так що ви можете сказати

string mystring("Hello"); 
mystring.concat("World");

але ОП хоче, щоб це працювало так:

string mystring();
string result = mystring.concat("Hello", "World");

тепер є місця, де клас може бути використаний для зберігання колекції пов'язаних функцій, але це не ОО, це зручний спосіб використання функцій мови мови OO, щоб допомогти краще керувати вашою кодовою базою, але це жоден спосіб "OO Design". Об'єкт у таких випадках є абсолютно штучним, просто використовується таким чином, оскільки мова не пропонує нічого кращого для управління подібними проблемами. напр. У таких мовах, як C #, ви б використовували статичний клас, щоб забезпечити цю функціональність - він повторно використовує механізм класу, але вам більше не потрібно інстанціювати об'єкт, щоб викликати його методи. Ви закінчуєте такі методи, як, на string.IsNullOrEmpty(mystring)мою думку, погані порівняно з mystring.isNullOrEmpty().

Отже, якщо хтось запитує "як я проектую свої класи", я рекомендую продумати дані, які буде містити клас, а не функції, які він містить. Якщо ви переходите до "класу - це купа методів", тоді ви закінчите писати код стилю "кращий C". (що не обов'язково погано, якщо ви вдосконалюєте код C), але це не дасть вам найкращої програми, розробленої ОО.


9
-1, я абсолютно не згоден. Дизайн початківців OO працює лише з об'єктами даних типу a Videoі має тенденцію переповнювати такі класи з функціональністю, що часто закінчується безладним кодом> 10K LOC на клас. Розширений дизайн OO розбиває функціональність на менші одиниці, такі як VideoCompressor(і дозволяє Videoкласу просто бути класом даних або фасадом для VideoCompressor).
Док Браун

5
@DocBrown: На даний момент він, однак, вже не є об'єктно-орієнтованим дизайном, оскільки VideoCompressorне представляє об'єкт . У цьому немає нічого поганого, він просто показує межу об'єктно-орієнтованого дизайну.
Ян Худець

3
ах, але початківці ОО, де 1 функція перетворюється на клас, насправді взагалі не є OO, і лише заохочує масову розв'язку, що закінчується в тисячі файлів класів, які ніхто не може підтримувати. Я думаю, що найкраще думати про клас як про "обгортку даних", а не про "функціональну обгортку", що дасть новачкам краще зрозуміти, як думати про програми OO.
gbjbaanb

4
@DocBrown насправді точно підкреслив мою стурбованість; У мене дійсно є Videoклас, але методологія стиснення аж ніяк не тривіальна, тому я б фактично розбив compressметод на кілька інших приватних методів. Це є частиною причини мого запитання, прочитавши « Не створювати класи дієслів»
yjwong

2
Я думаю, що може бути нормально мати Videoклас, але він би складався з класів a VideoCompressor, a VideoSplitterта інших пов'язаних з ним класів, які, в хорошій формі OO, мають бути згуртованими окремими класами.
Ерік Кінг

-1

ISP (інтерфейс принцип сегрегації) говорить , що ні один клієнт не повинен бути змушений залежати від методів його не використовує. Переваги багаторазові і зрозумілі. Ваш підхід повністю поважає ISP, і це добре.

Інший підхід, що стосується також ISP, це, наприклад, створити інтерфейс для кожного методу (або набір методів з високою когезією), а потім мати єдиний клас, що реалізує всі ці інтерфейси. Краще рішення це чи ні, залежить від сценарію. Перевага цього полягає в тому, що при використанні ін'єкції залежності ви можете мати клієнта з різними колабораторами (по одному на кожен інтерфейс), але врешті-решт всі співпрацівники вказуватимуть на один і той же екземпляр об'єкта.

До речі, ви сказали

Я вважаю, що інстанціюю кожен клас

, ці класи здаються послугами (і, таким чином, без громадянства). Ви думали зробити їх одинаковими?



3
Для решти, хоча принцип розділення інтерфейсу є гарною схемою, це питання стосується реалізації, а не інтерфейсів, тому я не впевнений, що це актуально.
Ян Худець

3
Я не збираюся вступати в дискусію про те, чи є одиночний зразок, чи антипаттерн. Я запропонував можливе рішення (у нього навіть назва) своєї проблеми, вирішувати, підходить воно чи ні.
diegomtassis

Що стосується того, чи відповідний ISP чи ні, то предметом питання є "чи є класи з лише одним методом проблемою?", Протилежне цьому класу з багатьма методами, що стає проблемою в момент, коли ви робите від цього безпосередньо залежить клієнт ... саме той ксероксний приклад Роберта Мартіна знадобився йому для формулювання провайдера.
diegomtassis

-1

Так, є проблема. Але не важкий. Я маю на увазі, що ви можете структурувати свій код так, і нічого поганого не станеться, це є ремонтом. Але в такому вигляді структуризації є деякі слабкі сторони. Наприклад, врахуйте, що якщо представлення вашого відео (у вашому випадку воно згруповане в класі RawVideo) зміниться, вам потрібно буде оновити всі свої операційні класи. Або вважайте, що у вас може бути кілька зображень відео, які відрізняються за часом виконання. Тоді вам доведеться співставити представлення з певним класом "операції". Також суб'єктивно роздратовано перетягувати залежність для кожної операції, яку ви хочете зробити на smth. а також оновлювати список залежностей, пройдених кожен раз, коли ви вирішите, що операція більше не потрібна або вам потрібна нова операція.

Також це фактично є порушенням SRP. Деякі люди просто розглядають СРП як керівництво для розподілу обов'язків (і переносять його далеко, розглядаючи кожну операцію окремо відповідально), але вони забувають, що СРП також є керівництвом щодо групування відповідальності. Відповідно до обов'язків SRP, які змінюються з тієї ж причини, слід згрупувати разом, щоб у разі зміни вони були локалізовані на якомога менше класів / модулів. Що стосується великих класів, це не проблема наявності кількох алгоритмів в одному класі, якщо ці алгоритми пов'язані (тобто поділитися деякими знаннями, які не повинні бути відомі поза цим класом). Проблема - це великі класи, у яких є алгоритми, які ніяк не пов’язані між собою і змінюються / змінюються з різних причин.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.