Як це було б запрограмовано в не-OO? [зачинено]


11

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

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

Від: http://www.smashcompany.com/technology/object-oriented-programming-is-an-expensive-disaster-which-must-end

// Consider the case where “SimpleProductManager” is a child of
// “ProductManager”:

public class SimpleProductManager implements ProductManager {
    private List products;

    public List getProducts() {
        return products;
    }

    public void increasePrice(int percentage) {
        if (products != null) {
            for (Product product : products) {
                double newPrice = product.getPrice().doubleValue() *
                (100 + percentage)/100;
                product.setPrice(newPrice);
            }
        }
    }

    public void setProducts(List products) {
        this.products = products;
    }
}

// There are 3 behaviors here:

getProducts()

increasePrice()

setProducts()

// Is there any rational reason why these 3 behaviors should be linked to
// the fact that in my data hierarchy I want “SimpleProductManager” to be
// a child of “ProductManager”? I can not think of any. I do not want the
// behavior of my code linked together with my definition of my data-type
// hierarchy, and yet in OOP I have no choice: all methods must go inside
// of a class, and the class declaration is also where I declare my
// data-type hierarchy:

public class SimpleProductManager implements ProductManager

// This is a disaster.

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

Що я конкретно запитую, - як би цей приклад був змодельований / запрограмований мовою FP (фактичний код, а не теоретично)?


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

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

20
Я отримав 10 речень у цій розпусті статті і здався. Не звертайте уваги на людину за цією завісою. Іншими новинами я не мав уявлення, що справжні шотландці - це насамперед програмісти ООП.
Роберт Харві

11
Ще одна розпуста від того, хто пише процедурний кодекс мовою ОО, то дивується, чому ОО не працює на нього.
TheCatWhisperer

11
Хоча це, безсумнівно, правда, що OOP - це катастрофа дизайнерських помилок від початку до кінця - і я пишаюся тим, що є її частиною! - ця стаття не читабельна, і приклад, який ви наводите, в основному робить аргумент про погано розроблену ієрархію класів.
Ерік Ліпперт

Відповіді:


42

У стилі FP Productбуло б незмінним класом, product.setPriceне мутуватиме Productоб'єкт, а повертає новий об'єкт, а increasePriceфункція - це "автономна" функція. Використовуючи схожий на вигляд синтаксис, як ваш (C # / Java як), еквівалентна функція може виглядати так:

 public List increasePrice(List products, int percentage) {
    if (products != null) {
        return products.Select(product => {
                double newPrice = product.getPrice().doubleValue() *
                    (100 + percentage)/100;
                return product.setPrice(newPrice);     
               });
    }
    else return null;
}

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


7
Способи зробити це "більше FP": 1) Використовуйте типи "Можливо / Необов'язково" замість зведення нанівець, щоб полегшити запис загальних функцій замість часткових функцій та використовувати допоміжні функції вищого порядку для абстрагування "if (x! = Null)" логіка. 2) Використовуйте лінзи, щоб визначити підвищення ціни на один продукт з точки зору застосування відсоткового збільшення в контексті лінзи на ціну товару. 3) Використовуйте часткове застосування / склад / каррі, щоб уникнути явної лямбда для карти / Виберіть виклик.
Джек

6
Треба сказати, що я ненавиджу, що ідея колекції може бути нульовою, а не просто порожньою за дизайном. Функціональні мови з рідною функцією кортежу / колекції працюють таким чином. Навіть в OOP я ненавиджу повернення, nullде колекція є типом повернення. / rant over
Berin Loritsch

Але це може бути статичний метод, наприклад, у клас утиліти на мовах OOP, таких як Java або C #. Цей код частково коротший, тому що ви просите перейти до списку, а не мати його самостійно. Оригінальний код також містить структуру даних, і просто переміщення його зробить початковий код коротшим без зміни понять.
Марк

@ Марк: звичайно, і я думаю, що ОП це вже знає. Я розумію питання як "як це висловити функціонально", не обов'язкове мовою, що не є ООП.
Док Браун

@Mark FP і OO не виключають жодного іншого.
Пітер Б

17

Що я конкретно запитую, - як би цей приклад був змодельований / запрограмований мовою FP (фактичний код, а не теоретично)?

Мова "FP"? Якщо будь-якого достатньо, я підбираю Emacs lisp. У нього є поняття типів (роду, роду), але лише вбудовані. Тож ваш приклад зводиться до "як ви множите кожен елемент у списку на щось і повертаєте новий список".

(mapcar (lambda (x) (* x 2)) '(1 2 3))

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

incPrice :: (Num) -> [Num] -> [Num]  
incPrice _ [] = []  
incPrice percentage (x:xs) = x*percentage : incPrice percentage xs  

(Або щось подібне, це було віками ...)

Я хочу бути відкритим до аргументів автора,

Чому? Я спробував прочитати статтю; Мені довелося відмовитись після сторінки і просто швидко просканував решту.

Проблема статті не в тому, що вона проти OOP. Ні я наосліп "про OOP". Я запрограмований з логічними, функціональними і парадигмами OOP, досить часто однією і тією ж мовою, коли це можливо, і часто без будь-якої з трьох, суто необхідних або навіть на рівні асемблера. Я б ніколи не сказав, що будь-яка з цих парадигм у всіх аспектах надзвичайно перевершує іншу. Чи можу я стверджувати, що мені подобається мова X краще, ніж Y? Звичайно, я б! Але це не про що йдеться у цій статті.

Проблема статті полягає в тому, що він використовує велику кількість риторичних засобів (помилок) від першого до останнього речення. Зовсім марно навіть починати описувати всі помилки, які він містить. Автор дає чітко зрозуміти, що у нього немає нульового інтересу до дискусій, він на хрестовому поході. Так навіщо турбуватися?

Зрештою, всі ці речі - це лише інструменти для отримання роботи. Можуть бути робочі місця, де OOP кращий, а можуть бути й інші роботи, де FP кращий, або де обидва є надмірними. Важливо - підібрати потрібний інструмент для роботи та виконати його.


4
"Цілком очевидно, що він не має інтересу до дискусій, він перебуває у хрестовому поході".
Ейфорія

Вам не потрібне обмеження на числення вашого коду Haskell? як можна подзвонити (*) в іншому випадку?
jk.

@jk., я зробив віки, що я робив Хаскелл, це просто для задоволення обмежень ОП щодо відповіді, яку він шукає. ;) Якщо хтось хоче виправити мій код, не соромтеся. Але звичайно, я переключу його на Num.
AnoE

7

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

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

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

Це легка частина об'єктно-орієнтованого дизайну. Ми просто робимо клас для всіх перерахованих вище "об'єктів", і ми добре, правда? Складіть Productклас, щоб об’єднати кілька з них разом?

Але зачекайте, ви можете мати колекції та агрегати деяких із цих типів: Set [категорія], (код знижки -> ціна), (кількість -> сума знижки) тощо. Де вони вміщуються? Чи ми створюємо окремий, CategoryManagerщоб відслідковувати всі різні види категорій, чи ця відповідальність належить до Categoryвже створеного нами класу?

А що робити з функціями, які дають вам знижку на ціну, якщо у вас є певна кількість предметів з двох різних категорій? Це іде в Productкласі, Categoryкласі, DiscountRuleкласі, CategoryManagerкласі, чи нам потрібно щось нове? Ось так ми закінчуємо подібні речі DiscountRuleProductCategoryFactoryBuilder.

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

def mapPrices(f: Int => Int)(products: Traversable[Product]): Traversable[Product] =
  products map {x => x.copy(price = f(x.price))}

def increasePrice(percentage: Int)(price: Int): Int =
  price * (percentage + 100) / 100

mapPrices(increasePrice(25))(products)

Я міг би додати інші функції цін , пов'язані тут , як decreasePrice, applyBulkDiscountі т.д.

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

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


2

Просто розділення даних і функцій, як автор натякав, може виглядати так у F # ("мова FP").

module Product =

    type Product = {
        Price : decimal
        ... // other properties not mentioned
    }

    let increasePrice ( percentage : int ) ( product : Product ) : Product =
        let newPrice = ... // calculate

        { product with Price = newPrice }

Ви можете здійснити підвищення цін у списку товарів таким чином.

let percentage = 10
let products : Product list = ...  // load?

products
|> List.map (Product.increasePrice percentage)

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

Я включив деякі анотації типу, але вони повинні бути непотрібними. getter / setter тут непотрібні, оскільки модуль не володіє даними. Йому належить структура даних і доступні операції. Це можна побачити і з тим List, що виставляє mapна виконання функції на кожному елементі списку і повертає результат у новий список.

Зауважте, що модуль Product не повинен знати нічого про циклічне циклічне використання, оскільки ця відповідальність лежить на модулі List (який створив потребу в циклі).


1

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

Це в Typescript (звідси всі анотації типу). Шрифт (наприклад, JavaScript) - це багатодоменна мова.

export class Product extends Object {
    name: string;
    price: number;
    category: string;
}

products: Product[] = [
    new Product( { name: "Tablet", "price": 20.99, category: 'Electronics' } ),
    new Product( { name: "Phone", "price": 500.00, category: 'Electronics' } ),
    new Product( { name: "Car", "price": 13500.00, category: 'Auto' } )
];

// find all electronics and double their price
let newProducts = products
    .filter( ( product: Product ) => product.category === 'Electronics' )
    .map( ( product: Product ) => {
        product.price *= 2;
        return product;
    } );

console.log( newProducts );

Докладно (і знову ж таки, не експерт з ПП), що потрібно розуміти, що не так багато заздалегідь визначеної поведінки. Не існує методу "підвищення ціни", який застосовує підвищення цін у всьому списку, оскільки, звичайно, це не є ООП: не існує класу, який би визначав таку поведінку. Замість того, щоб створити об’єкт, який зберігає список продуктів, ви просто створите масив продуктів. Потім ви можете використовувати стандартні процедури FP для маніпулювання цим масивом будь-яким способом: фільтрувати для вибору конкретних елементів, карту для коригування внутрішніх даних тощо. Ви закінчуєте більш детальний контроль над своїм списком продуктів, не обмежуючись цим API, який надає SimpleProductManager. Деякі можуть вважати це перевагою. Це також правда, що ви не ' не потрібно турбуватися про будь-який багаж, пов’язаний з класом ProductManager. Нарешті, про "SetProducts" або "GetProducts" не варто хвилюватися, оскільки немає жодного об'єкта, який би приховував ваші продукти: натомість у вас просто список продуктів, з якими ви працюєте. Знову ж таки, це може бути перевагою чи недоліком залежно від обставин / людини, з якою ви спілкуєтесь. Також очевидно немає ієрархії класів (на що він скаржився), оскільки в першу чергу немає занять. це може бути перевагою чи недоліком залежно від обставин / людини, з якою ви спілкуєтесь. Також очевидно немає ієрархії класів (на що він скаржився), оскільки в першу чергу немає занять. це може бути перевагою чи недоліком залежно від обставин / людини, з якою ви спілкуєтесь. Також очевидно немає ієрархії класів (на що він скаржився), оскільки в першу чергу немає занять.

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

Особисто мені важко сприймати когось серйозно, що робить такий сильний аргумент для "X просто жахливо. Завжди використовуй Y". Програмування має різноманітні інструменти та парадигми, оскільки існує широкий спектр проблем для вирішення. FP має своє місце, OOP має своє місце, і ніхто не стане чудовим програмістом, якщо вони не можуть зрозуміти недоліки та переваги всіх наших інструментів і коли їх використовувати.

** Примітка. Очевидно, що в моєму прикладі є один клас: клас Product. У цьому випадку, хоча це просто тупий контейнер даних: я не думаю, що моє використання його порушує принципи FP. Це скоріше помічник для перевірки типу.

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


2
Це насправді не приклад ООП у класичному розумінні. У справжньому OOP дані поєднуються з поведінкою; ось, ви розділили два. Це не обов'язково погано (я насправді вважаю його чистішим), але це не те, що я б назвав класичним OOP.
Роберт Харві

0

Мені не здається, що SimpleProductManager - це дитина (розширює або успадковує) щось.

Його просто реалізація інтерфейсу ProductManager, який в основному є договором, що визначає, які дії (поведінку) об'єкт повинен робити.

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

class SimpleProductManager extends ProductManager {
    ...
}

Отже, автор каже:

У кого є якийсь об'єкт, поведінка якого: setProducts, povećaPrice, getProducts. І нам байдуже, чи є в об’єкта інша поведінка або як поведінка реалізована.

Клас SimpleProductManager реалізує його. В основному, він виконує дії.

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

Але ми також можемо реалізувати інший клас: ValuePriceIncreaser, поведінка якого буде:

public void increasePrice(int number) {
    if (products != null) {
        for (Product product : products) {
            double newPrice = product.getPrice() + number;
            product.setPrice(newPrice);
        }
    }
}

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

Оскільки в FP немає такого поняття, як інтерфейси, це було б важко реалізувати. Наприклад, у C ми можемо утримувати покажчики на функції та викликати відповідний, виходячи з наших потреб. Зрештою, в OOP він працює дуже схожим чином, але його "автоматизує" компілятор.

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