Занадто велика кількість абстракцій робить код важко розширити


9

У мене виникають проблеми з тим, що я відчуваю, що це занадто велика абстракція в кодовій базі (або принаймні справу з цим). Більшість методів у кодовій базі були абстраговані для того, щоб прийняти найвищий з батьків у кодовій базі даних, але дитина B цього батька має новий атрибут, який впливає на логіку деяких із цих методів. Проблема полягає в тому, що ці атрибути неможливо перевірити в цих методах, тому що вхід абстрагується на A, і A, звичайно, не має цього атрибута. Якщо я спробую створити новий метод для обробки B по-різному, він буде викликаний для дублювання коду. Моя технологічна пропозиція полягає в тому, щоб зробити спільний метод, що приймає булі параметри, але проблема в тому, що деякі люди розглядають це як "прихований керуючий потік", де спільний метод має логіку, яка може не бути очевидною майбутнім розробникам. , а також цей спільний метод одноразово зросте надмірно складним / згорнутим, якщо потрібно буде додати майбутні атрибути, навіть якщо він буде розбитий на менші спільні методи. Це також збільшує зв'язок, зменшує згуртованість та порушує принцип єдиної відповідальності, на який вказував хтось із моєї команди.

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


10
додати зразок коду, щоб неграмотна "проблема" допомогла б зрозуміти ситуацію набагато більше
Seabizkit

Думаю, тут порушено два принципи ТВЕРДОГО. Однозначна відповідальність - якщо ви перейдете в булеву функцію, яка повинна контролювати поведінку, ця функція більше не буде мати єдиної відповідальності. Інший - принцип заміни Ліскова. Уявіть, що існує функція, яка приймає в класі A параметр. Якщо ви перейдете до класу B замість A, чи буде порушена функціональність цієї функції?
bobek

Я підозрюю, що метод A досить довгий і робить більше ніж одне. Це так?
Rad80

Відповіді:


27

Якщо я спробую створити новий метод для обробки B по-різному, він буде викликаний для дублювання коду.

Не всі дублювання коду створюються рівними.

Скажімо, у вас є метод, який бере два параметри і додає їх разом під назвою total(). Скажіть, у вас є інша названа add(). Їх реалізація виглядає абсолютно однаково. Чи повинні вони бути об'єднані в один метод? НІ!!!

Принцип " Не повторюй сам" або " БУЙ" не полягає у повторенні коду. Йдеться про поширення рішення, ідеї навколо, щоб, якщо ви коли-небудь змінили свою думку, вам доведеться переписувати всюди, де ви поширюєте цю ідею навколо. Blegh. Це жахливо. Не робіть цього. Замість цього використовуйте DRY, щоб допомогти вам приймати рішення в одному місці .

Принцип НУМИ (не повторюйте себе) говорить:

Кожна частина знань повинна мати єдине, однозначне, авторитетне представлення в системі.

wiki.c2.com - Не повторюйтеся

Але DRY може бути пошкоджена звичкою сканувати код, шукаючи подібну реалізацію, схоже, що це копія та вставлення десь в іншому місці. Це мозкова мертва форма СУХОГО. Чорт, ти можеш це зробити за допомогою інструменту статичного аналізу. Це не допомагає, оскільки він ігнорує суть DRY, яка полягає в тому, щоб тримати код гнучким.

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

Скільки болю? Звичайно, я міг просто скопіювати код і зробити новий метод, коли мені це потрібно. Так що нічого не так? Маларкі! Якщо нічого іншого ти не коштував мені доброго імені! Хороші імена важко придумати і не реагувати добре, коли ви поспішаєте з їх значенням. Хороші імена, які роблять зрозумілий намір, важливіші, ніж ризик, що ви скопіювали помилку, яку, чесно кажучи, легше виправити, коли ваш метод має правильну назву.

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

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

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

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

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

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

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

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

Коли ви наполягаєте на тому, що абстракція працює таким чином, кількість рівнів позаду не має значення. Поки ти не дивишся за абстракцію. Ви наполягаєте на тому, що абстракція відповідає вашим потребам, не адаптуючись до її потреб. Для цього він повинен бути простим у використанні, мати гарне ім’я та не протікати .

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

Я міг би вас заглушити в принципах цілий день. І це здається, що вже є ваші колеги. Але ось у чому річ: на відміну від інших інженерних галузей, це програмне забезпечення має менше 100 років. Ми все ще розгадуємо це. Тож не дозволяйте, щоб хтось із залякуючим звучанням книги вчився знущатися над написанням коду, який важко читати. Слухайте їх, але наполягайте, що вони мають сенс. Не віри нічого на віру. Люди, які кодують якимось чином лише тому, що їм сказали цей шлях, не знаючи, навіщо робити найбільші помилки з усіх.


Я від усієї думки згоден. DRY - це абревіатура з трьох літер для трискладової фрази Не повторюй себе, що, у свою чергу, є статтею на 14 сторінках у вікі . Якщо ви все це сліпо бурмотіти ці три літери , не читаючи і розуміння 14 сторінкову статтю, ви будете працювати в неприємності. Він також тісно пов'язаний з " Один раз і тільки один раз" (OAOO) і більш вільно пов'язаний з " Єдиною точкою істини" (SPOT) / Єдиним джерелом істини (SSOT) .
Йорг W Міттаг

"Їх реалізація виглядає абсолютно однаково. Чи варто їх об'єднати в один метод? НІ !!!" - Справедливе і зворотне: тільки те, що два фрагменти коду різні, не означає, що вони не є дублікатами. На сторінці вікі OAOO є чудова цитата Рона Джеффріса : "Я колись бачив, як Бек оголошує два патчі майже зовсім іншого коду як" дублювання ", зміняйте їх, щоб вони МУЖЕ дублювати, а потім видаліть щойно вставлене дублювання, щоб вийти з чимось, очевидно, кращим ».
Йорг W Міттаг

@ JörgWMittag звичайно. Істотна річ - ідея. Якщо ви дублюєте ідею за допомогою іншого коду, ви все ще порушуєте сухе.
candied_orange

Я маю уявити статтю на 14 сторінок про те, що не повторювати себе, як правило, повторюється багато.
Чак Адамс

7

Звичайна приказка, яку ми всі читаємо тут і є:

Усі проблеми можна вирішити, додавши ще один шар абстракції.

Ну, це неправда! Ваш приклад це показує. Тому я пропоную трохи змінене твердження (не соромтесь повторно використовувати ;-)):

Кожну проблему можна вирішити, використовуючи ПРАВИЙ рівень абстракції.

У вашому випадку є дві різні проблеми:

  • надмірно узагальнення , викликане додаванням кожного методу на абстрактному рівні;
  • фрагментація з конкретного поведінки , які призводять до враженню не одержати велику картину і почуття втрачається. Трохи як у циклі подій для Windows.

Обидва є основними:

  • якщо ви абстрагуєте метод, коли кожна спеціалізація робить це по-різному, все добре. Ніхто не має проблем із розумінням того, що A Shapeможе обчислити його surface()спеціалізованим способом.
  • Якщо ви абстрагуєте деяку операцію, коли існує загальна загальна поведінкова модель, у вас є два варіанти:

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

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

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


5

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

public void sort(List values) {
    if (values instanceof LinkedList) {
        // do efficient linked list sort
    } else { // ArrayList
        // do efficient array list sort
    }
}

Я би зробив це:

values.sort();

// ...

class ArrayList {
    public void sort() {
        // do efficient array list sort
    }
}

class LinkedList {
    public void sort() {
        // do efficient linked list sort
    }
}

Ми переміщуємо поведінку на те місце, яке знає, коли її використовувати. Ми створюємо справжню абстракцію, де вам не потрібно знати типи чи деталі реалізації. У вашій ситуації може бути більше сенсу перемістити цей метод з початкового класу (який я зателефоную O), щоб ввести Aта змінити його за типом B. Якщо метод викликається doItна якомусь об'єкті, перейдіть doItдо Aта перейдіть з різною поведінкою в B. Якщо є біти даних, звідки doItвони спочатку викликаються, або якщо метод використовується в достатній кількості, ви можете залишити початковий метод і делегувати:

class O {
    int x;
    int y;

    public void doIt(A a) {
        a.doIt(this.x, this.y);
    }
}

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

public void doIt(A a, boolean isTypeB) {
    if (isTypeB) {
        // do B stuff
    } else { 
        // do A stuff
    }
}

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

o.doIt(a, a instanceof B);

або:

o.doIt(a, true); //or false

По-перше, точка виклику не має уявлення, який тип Aмає. Тому, чи повинні ми проходити буленів всю дорогу вниз? Це справді шаблон, який ми хочемо в усьому кодовій базі? Що станеться, якщо є третій тип, який нам потрібно враховувати? Якщо так називається метод, ми повинні перенести його на тип і дозволити системі вибрати поліморфно реалізацію для нас.

По-друге, ми вже повинні знати тип aв точці виклику. Зазвичай це означає, що ми або створюємо там екземпляр, або приймаємо екземпляр цього типу як параметр. Створення методу, Oщо займає Bтут, працювало б. Компілятор буде знати, який метод вибрати. Коли ми проводимо подібні зміни, дублювання краще, ніж створення неправильної абстракції , принаймні, поки ми не з'ясуємо, куди насправді йдемо. Звичайно, я припускаю, що нас насправді не зробили незалежно від того, що ми змінили до цього моменту.

Нам потрібно уважніше придивитися до відносин між Aта B. Як правило, нам кажуть, що ми повинні віддавати перевагу складу над спадщиною . Це не так у кожному випадку, але це правда у дивовижній кількості випадків, коли ми викопаємо. У Bспадок це Aозначає, що ми вважаємо B, що це A. Bслід використовувати так само A, за винятком того, що він працює трохи інакше. Але в чому ці відмінності? Чи можемо ми дати різниці більш конкретну назву? Це не Bє A, але насправді Aмає Xте, що могло б бути A'чи B'? Як виглядав би наш код, якби ми це зробили?

Якщо ми перенесли метод, Aяк було запропоновано раніше, ми могли б ввести екземпляр Xу Aта делегувати цей метод X:

class A {
    X x;
    A(X x) {
        this.x = x;
    }

    public void doIt(int x, int y) {
        x.doIt(x, y);
    }
}

Ми можемо реалізувати A'та B'позбутися B. Ми вдосконалили код, давши ім’я концепції, яка могла б бути більш неявною, і дозволили собі встановити таку поведінку під час виконання замість часу компіляції. Aфактично також став менш абстрактним. Замість розширеного відносини успадкування він викликає методи на делегованому об'єкті. Цей об'єкт абстрактний, але більш орієнтований лише на відмінності в реалізації.

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

B b = new B();
o.doIt(b, true);

Ми припускали раніше при складанні, що Aмає Xте, що є A'або B'. Але, можливо, навіть це припущення є невірним. Це єдине місце, де ця різниця між Aі Bмає значення? Якщо це так, то, можливо, ми можемо використовувати трохи інший підхід. Ми по- , як і раніше мати Xщо або A'або B', але це не відноситься до A. Тільки O.doItтурботи про нього, так що давайте тільки передати його O.doIt:

class O {
    int x;
    int y;

    public void doIt(A a, X x) {
        x.doIt(a, x, y);
    }
}

Зараз наш сайт для викликів виглядає так:

A a = new A();
o.doIt(a, new B'());

Знову Bзникає, а абстракція переходить у більш цілеспрямований X. Цей час, проте, Aще простіший, знаючи менше. Це навіть менш абстрактно.

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


1
Мене вражає, що приклад "поганого" коду, який ви наводите тут, схожий на те, що я схильний би робити мовою, що не є ОО. Цікаво, чи навчилися вони неправильних уроків і внесли їх у світ ОО як спосіб кодування?
Baldrickk

1
@Baldrickk Кожна парадигма має свої власні способи мислення, зі своїми унікальними перевагами та недоліками. У функціональному Haskell кращим підходом буде відповідність шаблонів. Хоча такою мовою, деякі аспекти початкової проблеми також не були б можливими.
cbojar

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

0

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

Існує альтернатива: точки розширення , строгі абстракції та багаторівневе налаштування. Скажіть одне налаштування урядових клієнтів, виходячи з цієї настройки для конкретного міста.

Попередження: На жаль, це найкраще працює, коли всі (або більшість) класів зроблені розширенням. Немає варіанту для вас, можливо, в малому.

Ця розширюваність працює завдяки наявності базового класу об'єкта, що розширюється, містить розширення:

void f(CreditorBO creditor) {
    creditor.as(AllowedCreditorBO.class).ifPresent(allowedCreditor -> ...);
}

Всередині є ліниве відображення об'єкта на розширені об'єкти за класом розширення.

Для класів та компонентів GUI однакова розширюваність, частково з успадкуванням. Додавання кнопок тощо.

У вашому випадку валідація повинна виглядати, чи розширена вона та валідація щодо розширень. Введення точок розширення лише для одного випадку додає незрозумілий код, а не добре.

Тож рішення не існує, як намагатися працювати в поточному контексті.


0

"прихований контроль потоку" звучить для мене занадто ручно.
Будь-яка конструкція або елемент, вирваний із контексту, може мати таку характеристику.

Абстракції хороші. Я загартовую їх двома рекомендаціями:

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

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

В обох випадках "більше" і "занадто багато" не є фіксованими числами. Це залежить. Ось чому це важко.

Мені також подобається цей запис від Санді Меца

https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstraction

дублювання набагато дешевше, ніж неправильна абстракція,
і
віддають перевагу дублюванню над неправильною абстракцією

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