Значну кількість часу я не можу придумати причину мати об'єкт замість статичного класу. Чи мають об’єкти більше переваг, ніж я думаю? [зачинено]


9

Я розумію поняття об’єкта, і як програміст Java, я відчуваю, що парадигма ОО на практиці приходить досить природно до мене.

Однак нещодавно я задумався:

Зачекайте секунду, які насправді практичні переваги використання об'єкта над використанням статичного класу (при належній практиці інкапсуляції та ОО)?

Я міг би подумати про дві переваги використання об'єкта (обидва значущі та потужні):

  1. Поліморфізм: дозволяє динамічно та гнучко міняти функціональність під час виконання. Також дозволяє легко додавати нові функціональні «частини» та альтернативи до системи. Наприклад, якщо є Carклас, призначений для роботи з Engineоб'єктами, і ви хочете додати новий двигун до системи, якою може користуватися Автомобіль, ви можете створити новий Engineпідклас і просто передати об’єкт цього класу в Carоб'єкт, не маючи необхідності щось змінити Car. І ви можете вирішити це під час виконання.

  2. Можливість передачі функціоналу навколо: ви можете динамічно передавати об'єкт навколо системи.

Але чи є більше переваг для об’єктів над статичними класами?

Часто, коли я додаю нові "частини" до системи, я роблю це, створюючи новий клас і створюючи з нього об'єкти.

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

Наприклад, я працюю над додаванням механізму збереження / завантаження файлів у свій додаток.

З об'єктом, рядок виклику коду буде виглядати так: Thing thing = fileLoader.load(file);

З статичним класом це виглядатиме так: Thing thing = FileLoader.load(file);

Яка різниця?

Досить часто я просто не можу придумати привід для створення об'єкта, коли звичайний старий клас статики діяв би так само. Але в системах OO статичні класи досить рідкісні. Тому я, мабуть, чогось не вистачаю.

Чи є ще якісь переваги для інших, ніж два, які я перерахував? Будь ласка, поясніть.

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

Мелодії теж були об’єктами, оскільки їх корисно передавати навколо. Так само були акорди та шкали.

А як щодо "статичних" частин системи - які не будуть передаватися навколо? Наприклад - механізм "збереження файлу". Чому я повинен реалізовувати його в об'єкті, а не в статичному класі?


Отже, замість об'єкта з полями та, можливо, методами, жодних об’єктів, ніяких записів, просто ціла маса скалярних значень передана та повернута зі статичних методів? Редагувати: Ваш новий приклад пропонує інше: Об'єкти, лише без методів екземпляра? Інакше, що Thing?

Що відбувається, коли вам потрібно поміняти свій FileLoaderна той, який читається з розетки? Або макет для тестування? Або той, який відкриває zip-файл?
Бенджамін Ходжсон


1
@delnan Гаразд, я вирішив, що відповідь допоможе мені зрозуміти: навіщо ви реалізуєте "статичну" частину системи як об'єкт? Як приклад у питанні: механізм збереження файлів. Що я отримую, реалізуючи його в об’єкті?
Авів Кон

1
За замовчуванням має бути використання нестатичного об'єкта. Використовуйте статичні класи лише тоді, коли відчуваєте, що це дійсно краще виражає ваш намір. System.Mathв .NET - це приклад чогось, що має набагато більше сенсу як статичного класу: вам ніколи не потрібно буде поміняти його чи знущатися над ним, і жодна з операцій не може логічно бути частиною екземпляра. Я дійсно не думаю, що ваш приклад "заощадження" відповідає цьому рахунку.
Бенджамін Ходжсон

Відповіді:


14

хаха, ти звучиш як команда, над якою я працював;)

Java (і, можливо, C #), безумовно, підтримує цей стиль програмування. І я працюю з людьми, у яких перший інстинкт: "Я можу зробити це статичним методом!" Але є деякі тонкі витрати, які з часом наздоганяють вас.

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

Переходячи до класу лише статичними методами, ви порушуєте "стан" частини рівняння. Але державі все-таки треба десь жити. Тож, що я бачив з часом, це те, що клас із усіма статичними методами починає мати все більш складні списки параметрів, оскільки стан переходить з класу і викликає функцію.

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

2) Правила щодо ОО досить добре вивчені. Через деякий час ви можете подивитися дизайн класу і побачити, чи відповідає він таким критеріям, як SOLID. І після безлічі тестування одиничних практик ви розвиваєте добре розуміння того, що робить клас "правильного розміру" та "когерентним". Але не існує хороших правил для класу зі всіма статичними методами, і немає реальної причини, чому ви не повинні просто згрупувати все там. Клас відкритий у вашому редакторі, так що, до біса? Просто додайте туди свій новий метод. Через деякий час ваше додаток перетворюється на ряд конкуруючих «Божих об’єктів», кожен намагається домінувати над світом. Знову ж таки, рефакторинг їх на менші одиниці є дуже суб'єктивним і важко сказати, чи правильно ви це зробили.

3) Інтерфейси - одна з найпотужніших особливостей Java. Успадкування класів виявилося проблематичним, але програмування з інтерфейсами залишається одним з найпотужніших хитрощів мови. (ditto C #) Всестатичні класи не можуть вкласти себе у цю модель.

4) Він ляскає дверима у важливих техніках ОО, якими ви не можете скористатися. Тож ви можете роками працювати лише з молотком у вашій панелі інструментів, не усвідомлюючи, наскільки легше було б, якби у вас також була викрутка.

4.5) Це створює найважчі, найбільш нерозбірні залежності компіляції за часом. Так, наприклад, якщо у вас FileSystem.saveFile()тоді немає способу змінити це, окрім підробки вашого JVM під час виконання. Це означає, що кожен клас, який посилається на ваш статичний функціональний клас, має жорстку залежність у компіляції від конкретної реалізації, що робить розширення майже неможливим, і надзвичайно ускладнює тестування. Ви можете перевірити статичний клас ізольовано, але стає дуже важко перевірити класи, які посилаються на цей клас ізольовано.

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

Коли я на мові, я завжди намагаюся добре використовувати мову. Так, наприклад, коли я перебуваю на Java, я використовую хороший дизайн OO, бо тоді я дуже використовую мову для того, що робить. Коли я перебуваю в Python, я поєдную функції модульних рівнів із випадковими класами - я можу писати класи лише в Python, але тоді я думаю, що я б не використовував мову для того, у чому це добре.

Ще одна тактика - погано використовувати мову, і вони скаржаться на всі проблеми, що викликають її. Але це стосується майже будь-якої технології.

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

Хоча це важко. Я бачив, як люди отримують "всю статичну" помилку, і важко говорити про них. Але я бачив, як вони відчувають велике полегшення, коли переживають це.


Ви в основному говорите про збереження дрібних та модульних речей та збереження стану та функціональності разом. Ніщо не заважає мені це робити зі статичними класами. Вони можуть мати державу. А можна інкапсулювати їх за допомогою геттерів-сетерів. І ви можете розділити систему на невеликі класи, які містять функціональність та стан. Все це стосується як класів, так і об’єктів. І це саме моя дилема: скажіть, я додаю нову функціональність у свій додаток. Очевидно, що це буде йти в окремий клас, щоб підкорятися SRP. Але чому саме я повинен інстанціювати цей клас? Це я не розумію.
Авів Кон

Я маю на увазі: іноді зрозуміло, чому я хочу предмети. Я буду використовувати приклад із редагування до свого запитання: у мене був додаток, який складає мелодії. Існували різні ваги, які я міг підключати до MelodyGenerators для створення різних мелодій. А «Мелодії» були об’єктами, тому я міг би їх скласти в стек і повернутися, щоб грати на старих тощо. У таких випадках мені зрозуміло, чому я повинен використовувати об'єкти. Я не розумію, чому я повинен використовувати об'єкти для представлення "статичних" частин системи: наприклад, механізм збереження файлів. Чому це не повинно бути просто статичним класом?
Авів Кон

Клас статичними методами повинен зовнішній стан. Що іноді є дуже важливою методикою факторингу. Але більшу частину часу ти хочеш інкапсулювати стан. Конкретний приклад: роки тому співробітник написав "Вибір дат" у Javascript. І це був кошмар. Замовники нарікали на це постійно. Тож я переробив його на «календарні» об’єкти, і раптом я створив декілька з них, поклавши їх набік, стрибаючи між місяцями та роками тощо. Це було настільки багато, що нам насправді довелося вимкнути функції. Моментальність дає вам таку шкалу.
Роб

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

1
@Doval Я не розумію, чому кожне обговорення OO обов'язково перетворюється на обговорення функціонального програмування.
Роб

6

Ти запитав:

Але чи є більше переваг для об’єктів над статичними класами?

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

Однак це не є перевагами. Переваги:

  1. Кращі абстракції
  2. Краща ремонтопридатність
  3. Краща заповідність

Ти сказав:

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

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

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

Для другої категорії перетворень статичний клас такий же хороший, як Об'єкт, якщо не краще. Математичні функції - класичні приклади цієї категорії. Більшість стандартних функцій бібліотеки С також підпадають під цю категорію.

Ти запитав:

З об'єктом, рядок виклику коду буде виглядати так: Thing thing = fileLoader.load(file);

З статичним класом це виглядатиме так: Thing thing = FileLoader.load(file);

Яка різниця?

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

Ти запитав:

Чи є ще якісь переваги для інших, ніж два, які я перерахував? Будь ласка, поясніть.

Я перерахував переваги (переваги) використання парадигми ОО. Я сподіваюся, що вони пояснюють себе. Якщо ні, я буду радий детальніше.

Ти запитав:

А як щодо "статичних" частин системи - які не будуть передаватися навколо? Наприклад - механізм "збереження файлу". Чому я повинен реалізовувати його в об'єкті, а не в статичному класі?

Це приклад, коли статичний клас просто не буде робити. Існує багато способів збереження даних програми у файл:

  1. Збережіть його у файлі CSV.
  2. Збережіть його у XML-файлі.
  3. Збережіть його у файлі json.
  4. Збережіть його як двійковий файл із прямим скиданням даних.
  5. Збережіть його безпосередньо в якійсь таблиці в базі даних.

Єдиний спосіб надати такі варіанти - це створити інтерфейс та створити об’єкти, що реалізують інтерфейс.

У висновку

Скористайтеся правильним підходом до наявної проблеми. Краще не бути релігійними щодо одного підходу проти іншого.


Крім того , якщо багато різних типів об'єктів (класи) повинні бути збережені (наприклад Melody, Chord, Improvisations, SoundSampleта іншими), то це буде економічніше спростити реалізацію через абстракції.
rwong

5

Іноді це залежить від мови та контексту. Наприклад, PHPсценарії, які використовуються для обслуговування запитів, завжди мають один запит на обслуговування та одну відповідь для генерації протягом усього періоду дії сценарію, тому статичні методи для дії на запит та генерування відповіді можуть бути доречними. Але на сервері, написаному на Node.js, може бути багато різних запитів і відповідей одночасно. Тож перше питання - чи ви впевнені, що статичний клас дійсно відповідає однотонному об'єкту?

По-друге, навіть якщо у вас є одиночні кнопки , використання об'єктів дозволяє скористатися поліморфізмом за допомогою таких методів, як Dependency_injection та Factory_method_pattern . Це часто використовується в різних моделях Inversion_of_control і корисно для створення макетних об'єктів для тестування, ведення журналів тощо.

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


Дякую за відповідь. На вашу думку: при створенні "статичної" частини системи щось не буде "передано навколо" - наприклад, частина системи, яка відтворює звуки, або частина системи, яка зберігає дані у файл: я повинен реалізувати його в об'єкті або в статичному класі?
Авів Кон

3

Окрім посади Роб Я


Поки функціональність Вашого load(File file)можна чітко відокремити від усіх інших функцій, якими Ви користуєтесь, добре використовувати статичний метод / клас. Ви екстерналізуєте стан (що не є поганим), а також не отримуєте надмірності, оскільки можете, наприклад, частково застосувати або викривити свою функцію, щоб не довелося повторювати себе. (це фактично те саме або схоже на використання factoryшаблону)

Однак, як тільки дві ці функції починають мати спільне використання, ви хочете якось згрупувати їх. Уявіть, що у вас є не тільки loadфункція, але й hasValidSyntaxфункція. Що ти робитимеш?

     if (FileLoader.hasValidSyntax(myfile))
          Thing thing = FileLoader.load(myfile);
     else
          println "oh noes!"

Дивіться дві згадки myfileтут? Ви починаєте повторюватися, тому що ваш зовнішній стан повинен передаватися для кожного дзвінка. Роб Y описав, як ви інтерналізуєте стан (тут file), щоб ви могли це зробити так:

     FileLoader myfileLoader = new FileLoader(myfile)
     if (myfileLoader.hasValidSyntax())
          Thing thing = myfileLoader.load();
     else
          println "oh noes!"

Перехід в один і той же об'єкт двічі - поверхневий симптом; актуальною проблемою, на яку вона може вказувати, є те, що деяка робота виконується надмірно - файл, можливо, доведеться відкрити двічі, проаналізувати двічі та закрити два рази, якщо hasValidSyntax()і load()заборонено повторно використовувати стан (результат частково проаналізованого файлу).
rwong

2

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

public Person(String name)

Що ж, судячи з підпису, людині просто потрібно ім’я, правда? Ну, не якщо реалізація:

public Person(String name) {
    ResultSet rs = DBConnection.getPersonFilePathByName(name);
    File f = FileLoader.load(rs.getPath());
    PersonData.setDataFile(f);
    this.name = name;
    this.age = PersonData.getAge();
}

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

public void testPersonConstructor() {
    Person p = new Person("Milhouse van Houten");
    assertEqual(10, p.age);
}

Це має пройти, правда? Я маю на увазі, ми інкапсулювали всі інші речі, правда? Ну, насправді це вибухає за винятком. Чому? О так, приховані залежності, про які ми не знали, мають глобальний стан. Ці DBConnectionпотреби не започатковано і зв'язно. Ці FileLoaderпотреби инициализируется з FileFormatоб'єктом (наприклад , XMLFileFormatчи CSVFileFormat). Ніколи про них не чули? Ну, в цьому і справа. Ваш код (і ваш компілятор) не можуть сказати вам, що вам потрібні ці речі, оскільки статичні дзвінки приховують ці залежності. Я сказав тест? Я мав на увазі, що новий молодший розробник щойно поставив щось подібне у своєму останньому випуску. Зрештою, складений = працює, правда?

Крім того, скажіть, що ви працюєте в системі, де не працює екземпляр MySQL. Або, скажімо, ви перебуваєте в системі Windows, але ваш DBConnectionклас виходить лише на ваш сервер MySQL на вікні Linux (з контурами Linux). Або, скажімо, ви перебуваєте в системі, де шлях, повернутий DBConnectionдля вас, не читається / пише. Це означає, що спроба запустити або протестувати цю систему в будь-якому з цих умов не вдасться, не через помилку коду, а через помилку в дизайні, яка обмежує гнучкість коду і прив’язує вас до реалізації.

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

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


Приховані залежності - надзвичайно хороший момент. Я майже забув свою фразу, що "Якість коду слід судити з того, як він використовується, а не як він реалізований".
Ейфорія

Але приховані залежності можуть існувати і в нестатичному класі? Тому я не бачу, чому це буде недоліком статичних класів, але не нестатичних класів.
валентери

@valenterry Так, ми також можемо приховати нестатичні залежності, але ключовим моментом є те, що приховувати нестатичні залежності - це вибір . Я міг би newстворити купу об’єктів у конструкторі (що, як правило, не доцільно), але я також можу їх вставити. При статичних класах немає способу їх вводити (як не можна легко в Java, і це була б цілком інша дискусія для мов, які це дозволяють), тому вибору немає. Ось чому я підкреслюють ідею бути змушені в залежності укриттів так сильно в моїй обороні.
cbojar

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

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

1

Всього два мої копійки.

Для вашого питання:

А як щодо "статичних" частин системи - які не будуть передаватися навколо? Наприклад - механізм "збереження файлу". Чому я повинен реалізовувати його в об'єкті, а не в статичному класі?

Ви можете запитати себе , якщо механізм тієї частини системи , яка відіграє звуки, або частина системи , яка зберігає дані в файл буде змінюватися . Якщо ваша відповідь "так", це вказує на те, що слід абстрагувати їх за допомогою abstract class/ interface. Ви можете запитати, як я можу знати майбутні речі? Безумовно, ми не можемо. Отже, якщо річ без громадянства, ви можете використовувати "статичний клас", такий як java.lang.Math, в іншому випадку, використовувати об'єктно-орієнтований підхід.


Є порада, що стосується язика: три удари, і ви відскокуєте , що говорить про те, що можна затримати, поки зміна насправді не потрібна. Що я маю на увазі під «зміною», це: потрібно зберегти більше одного типу об’єктів; або потребує збереження до більш ніж одного формату файлу (тип зберігання); або різке збільшення складності збережених даних. Звичайно, якщо досить впевнено, що зміни відбудуться незабаром, можна передбачити більш гнучку конструкцію вперед.
rwong
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.