Структури vs. обекти

Има нещо некомфортно в тезата „всичко е обект“. Ето едно любопитно разсъждение, на което попаднах наскоро (в Clean Code, която препоръчвам):

Обектите скриват данните си зад абстракции и предлагат функции, които работят с тях. Структурите предлагат директен достъп до данните и нямат смислени функции. На практика са противоположности.

Изтъркан пример в Ruby:

class Circle
  def initialize(center, radius)
    @center = center
    @radius = radius
  end

  def area
    Math::PI * radius * radius
  end
end

class Square
  def initialize(top_left, side)
    @top_left = top_left
    @side = side
  end

  def area
    side * side
  end
end

Клиентският код изглежда така:

Circle.new(Point.new(0.0, 0.0), 2.0).area
Square.new(Point.new(0.0, 0.0), 2.0).area

Всяка фигура отговаря за собствените си операции. Обектно-ориентирано програмиране по учебник.

Ето и структурния подход:

class Circle
  attr_accessor :center, :radius

  def initialize(center, radius)
    @center = center
    @radius = radius
  end
end

class Square
  attr_accessor :top_left, :side

  def initialize(top_left, side)
    @top_left = top_left
    @side = side
  end
end

module Geometry
  def self.area(shape)
    case shape
      when Circle then Math::PI * shape.radius * shape.radius
      when Square then shape.side * shape.side
    end
  end
end

…и клиентския код:

Geometry.area(Circle.new(Point.new(0.0, 0.0), 2.0))
Geometry.area(Square.new(Point.new(0.0, 0.0), 2.0))

Сега, сигурно искате да ме замерите с Refactoring и да престанете да четете. Но дайте шанс на процедурния код за момент. В някои случаи има интересни предимства.

Например, какво става ако искате да добавите нова фигура? В обектния подход е лесно: създавате нов клас и имплементирате операциите. В структурния е по-трудно – освен новия клас, трябва да промените всяка операция в Geometry. Очевидно тук обектите са по-подходящи.

Но ако искате да добавите нова опреция? В обектния подход трябва да промените всяка една от фигурите. В структурния е лесно – добавяте нов метод в Geometry. Тук структурите са по-подходящи.

Накратко

ОО кодът ни позволява да добавяме нови типове, без да променяме съществуващите операции. За сметка на това, когато добавяме нова операция се налага да променим всички съществуващи типове. Процедурният код ни позволява да добавяме нови операции, без да променяме съществуващите типове. За сметка на това, когато добавяме нов тип се налага да променим всички съществуващи операции.

Прочетете го пак.

Заигравка с Ruby

Има нещо много досадно в процедурния подход – трябва да пишете Geometry навсякъде. Ruby ви позволява избегнете това:

module Geometry
  def method_missing(message, *args, &block)
    if Geometry.singleton_methods.include?(message.to_s)
      Geometry.send(message, self, *args, &block)
    else
      super
    end
  end
end

class Circle
  include Geometry
end

class Square
  include Geometry
end

Така клиентския става:

Circle.new(Point.new(0.0, 0.0), 2.0).area
Square.new(Point.new(0.0, 0.0), 2.0).area

От друга страна, част методите на Square са имплементирани в друг модул (Geometry) и то по крехък начин. Не съм сигурен, че допълнителната сложност си заслужава.

Visitor

Ако приложите visitor design pattern (или какъво и да е double-dispatch) в ОО подхода, може да извъртите нещата така, че добавянето на нови операции да не предизвиква промяна във всички типове. Гъвкавостта на Ruby ви позволява да направите това с малко код. От друга страна, това (1) прави кода доста по-сложен и (2) го кара да изглежда по-процедурен. Рядко попадам на случаи, в които е оправдано.

Една генерализация

Това ръзсъждение може се генерализира лесно. Въпросът опира до това дали да държите операците заедно с типовете или отделно от тях. В обектно-ориентирания подход те са заедно, докато в процедурния – отделно. Така може да приложите тази логика за Haskell или LISP.

Обобщение

Във всяка достатъчно сложна система се срещат и двата случая. На места обектите са по-подходящи, на други – структурите. Не всичко е обект – понякога имате нужда от прости структури и процедури, които боравят с тях.

От друга страна, много неща са обекти.

18 thoughts on “Структури vs. обекти

  1. Здрасти 🙂 Позволи ми да изкажа алтернативна хипотеза по темата. Т.е. съгласен съм с твоето мнение относно разликата между структури/класове в Руби, но мисля че в C++(и по-важно – в C) причината на някои места да се ползват структури е различна.

    Скорост и Памет. Структурите ползват по-малко от тях в сравнение с обектите. На цената на по-сложен код. Като се замислиш, обектите просто връзват всичките данни с някакви методи, нищо повече. Е, все някъде трябва да пише кои са тези методи, т.е. трябва да се подават някакви указатели в някакви стекове.

    Това е лошо. Т.е. лошо е в случая на(примерно) математически код. Всякакви геометрии служат за нещо като „Асемблер“ на всичко от по-високо ниво. Т.е. в някой момент ще искаш да намериш общото лице на 10000 правоъгълника, по 10 пъти в секунда. Ти НЕ ИСКАШ този код да е бавен. Буквално ако можеше да бъде написан(работещ) на Асемблер, щеше да е на Асемблер.

    Да повторим: Структурите се ползват тогава, когато паметта ти е скъпа, искаш да оптимизираш за L2 кеш достъп и искаш всичко възможно да стане по време на компилация. Поради това тези Geometry.send неща са нечовешки бавни. Всъщност, дори индексирането на таблицата Geometry е бавно. Направо ти трябва глобална функция AreaCircle.

    Няма да ти трябва полиморфизъм за тези неща. Никога. Освен това НЯМА да ти трябва да добавяш нови методи в модули с процедурен код(най-много един-два пъти ЕВЪР). Не е като да бъде открита утре нова математическа операция.

    Ако евентуално ще ти трябва да променяш каквото и да е или спецификациите не са замразени, или скоростта не е от значение – ползвай обекти.

  2. @Михаил:

    Факт. Но това, на което исках да наблегна е чистия код, не бързодействието. Когато гониш performance съображенията винаги са други.

    Демек ако се абстрахираш от циклите, разсъждението е изцяло валидно за C++ (и C). Забележи, че „струкутра“ (в моята „терминология“) може да значи и клас, който дава публичен достъп до член-променливите си (или дори ги обвива зад getter и setter). Като цяло, аргумента е за това кога операциите да бъдат пакетирани с типа (обект) и кога отделно (структура).

  3. Здрасти Стефане,

    От известно време и аз клоня към разните функционални езици, в които няма обекти (без да броим Скала), а само структури и се замислих как би се решил проблема като искаш да добавиш нова структура или опрация.

    1. Както ти каза – добавянето на операция във функционалните езици е лесно – няма какво да спорим :).

    2. Добавянето на нова структура:

    Стефан: „За сметка на това, когато добавяме нов тип се налага да променим всички съществуващи операции“.

    Вариантите са два:

    2.1 Съществуващите структури са твой код – просто добавяш функции, които да обработват новата структура. Във функционалните езици има читав петърн мачинг, който обикновенно се прави на аргументите на функциите, а не вътре в тях – така че случаят не е много по-различен от ОО.

    2.2. Съествуващите структури са чужди (идват от фреумуърк или друго място) – тогава метода също ми се струва лесен – добавяш си твоята структура и функциите за нея и пишеш обвивки около бибклиотеката, които са по 1 ред – не е много елегантно но е приемливо.

    Примерчета (на Erlang, но се четат):

    -record(circle, {r}).
    -record(rect, {a, b}).
    area(C = #circle) -> C#circle.r * C#circle.r * 3.14.
    area(R = #rect) -> R#rect.a * R#rect.b.
    

    Да добавим квадрат:

    -record(square, {a}).

    Сега добавяме новата функция в случай 2.1:

    area(S = #square) -> S#square.a * S#square.a.

    В случай 2.2 си пишем и една опаковка:

    my_area(S = #square) -> S#square.a * S#square.a.
    my_area(Other) -> framework:area(Other).
    

    Започва все повече да ми харесва тоя функционален сок. 🙂

  4. Специално за добавянето на нова функция си мисля, че по-добрия вариант не е да я добавяш към всеки клас поотделно, ами да си си дефинирал някакъв абстрактен, който обектите да онаследяват и който да предостави тази функция. Естествено това не е решение във всички случаи, а и самото добавяне на клас във веригата на онаследяване може така или иначе да доведе до промяна на всички класове, ползващи новата функция.

  5. Като се абстрахирам от митовете за бързодействието на структури и класове, на мен лично идеята в примера ти не ми допада. Това вероятно е приложимо за малки програми, където няма много структури (или иначе казано типове/класове). С времето и добавянето на нови обекти, операциите които ще прилагаш върху тях, ще стават все по-големи и по-сложни, а от там по-трудни за поддръжка отколкото операциите на ниво клас. Аз лично съм по-близо до мисленето на Стефан Ковачев, защото ако ми се наложи да добавя нова операция, то ще го направя във възможно най-основния (абстрактен) клас от йерархията, така че само тези наследници на които наистина им е нужна да си я дефинират. Колко трудно е това? Във всяка добра йерархия основните (базови) класове са абстрактни.

    Аз препрочитам да мисля, за класовете като затворени черни кутии, които знаят точно какво правят и го правят добре. Модула Geometry в примера по-скоро ми напомня на „майстор по всичко“.

    БТВ Мисля, че по-точното заглавие в случая е „Структури vs Класове“, защото структурата както класа е тип, докато обектите са инстанции на типовете.

  6. @Георги:

    Разбира се, съгласен съм с идеята „добави операцията във възможно най-основния клас от йерархията“. Но лошото е, че тя е частен случай. Ако вземеш наивния пример с фигури, нямаш никакъв шанс да имплементираш

    area
    и
    perimeter
    в базов клас. Демек, не можеш да я приложиш в примера ми и ми е чудно защо въобще я повдигна.

    Впрочем, аз също предпочитам да мисля за обектите като затворени черни кутии. В крайна сметка, една от основните идеи на ООП-то е да скрива имплементационни детайли зад абстракция. Обаче всичко става на някаква цена – в случая, добавянето на нови операции върху хетерогенно множество от типове е по-скъпо.

    И щях да се съглася с теб за името, ако всички бяхме C++ програмисти. Но в множеството от езици с които се занимаваме, понятията са доста по-размити. Какво е структура в Java или Python например? А каква е разликата между клас и обект в JavaScript?

  7. @Стефан Кънев

    Защо да не мога да направя

    area
    и
    perimeter
    в един базов клас!? Че нали това са базови геометрични операции – напълно приложимо е в примера ти. Повдигнах темата, защото не съм съгласен, че е необходима промяна на всички операции при ОО – можеш в базов (но не абстрактен) клас да се даде някаква фиктивна дефиниция (напр.
    return 0
    ), която само в някои от наследниците да се предефинира, според нуждите.

    Не съм съгласен, че добавянето на нови операции в ООП е по-скъпо, защото не взимам в предвид само момента на писането. Може тогава да е по-скъпо, но в дългосрочен план ще бъде по-евтино, защото поддръжката на по-кратки и определени методите ще бъде по-лесна. Представи си, че някой след теб (дори самия ти) промени операция

    area
    , чупейки я за всички структури/обекти. При ООП това ще бъде ограничено само до определен клас и евентуално наследниците му (големината на проблема все пак зависи от това в коя част на йерархията е).

    Добре, хвана ме, че изхождам от C++ 🙂 Все пак си мисля, че темата е общо за ООП и терминологията трябва да не е обвързана с конкретен език

  8. Имаш

    Shape
    ,
    Circle
    и
    Square
    . Последните наследяват от първия. Как ще дефинираш
    perimeter
    и
    area
    в Shape?. Код, моля.

    (имам и добро разсъждение за останалата част от коментара ти, но първо искам да видя това :))

  9. Съжалявам за забавянето, но трябва и да работя 🙂 Отговора на въпроса ти е, че просто няма да дефинирам

    area
    и
    perimeter
    в
    Shape
    – ще ги оставя чисти виртуални методи. След това всеки от наследниците ще си даде точно определение. Ето примерен код:

    Paste link

    Разширих примера с 3D фигури, за да имам разклонена йерархия. Kласа

    Shape3D
    наследява от Shape и определя операцията
    perimeter
    , като един вид я „премахва“, защото няма голям смисъл за повечето 3D фигури. Също така той добавя операцията
    volume
    , защото тя има доста смисъл за 3D фигури. Надявам се примера да изясни предходните ми коментари относно това, защото мисля че добавянето/премахването на операции в ООП не е трудно и скъпо.

    Очаквам разсъжденията ти 🙂

  10. Първо, през 2010 нямаш оправдание да пействаш огромно парче код във форма, вместо линк в pastie или github. Дори това, че още пишеш на C++ не оправдание 🙂 . Редактирах ти коментара и го направих по-човешко. Моля те не прави това никога повече никъде.

    Второ, примерът ти ми показва единствено, че си противоречиш. По-горе казваш „Защо да не мога да направя area и perimeter в един базов клас!? … напълно приложимо е в примера ти.“. Сам демонстрираш, че не e – имплементираш

    area
    веднъж в
    Circle
    и веднъж в
    Square
    . Именно, подкрепяш ми тезата – ако се наложи да промениш смисъла на операцията
    area
    , ще се наложи да промениш няколко различни класа. Така една промяна (върху операцията) се разпростира върху няколко различни места. Ако не се лъжа, термина е Shotgun Surgery.

    (Разбира се, вселенски закони едва ли ще се променят скоро и демек няма да ти се наложи да промениш семантиката на „лице на фигура“. Но когато имаш проблемна област с термини от реалния свят, такива промени са ежедневие.)

    Като цяло имам чувството, че не вникваш в това, за което говоря. Не съм сигурен къде точно текстът ми се проваля в комуникацията, обаче 🙂

    И понеже си ми дал толкова много код, чувствам се длъжен да го коментирам:

    В 110 реда си успял да сложиш ред неща, които са ужасяващо лоши практики, в ООП-то или в програмирането като цяло. Ето ти част от тях:

    • Point3D
      наследява
      Point2D
      . Това е грубо нарушение на Liskov Substitution Princple. Аналогично
      Shape2D
      и
      Shape3D
    • Класът
      Sphere
      е ужасяваш. Представяш сфера чрез точка в пространството и окръжност. Далеч по-адекватно би било да я представиш чрез точка в пространството (център) и радуис.
    • Аналогично за
      Cube
    • Това
      Shape3D
      да има
      area
      е грешно. Зад този метод си скрил две неща – лице на фигура (area) и площ на тяло (surface area).
      Shape2D
      трябва да има само
      area
      , докато
      Shape3D
      – само
      surface
      . Впрочем, генерализацията на лице в пространстово е обем, а не площ (линк). Разбира се, това е пролем на това да наследяваш фигури в пространството от фигури в равнината.
    • Да имплементираш
      perimeter
      да връща 0 в базов клас е почти престъпно. Ако случайно забравя да добавя такъв метод в наследник, искам да получа грешка при компилация (ако пиша на такъв език) или поне изключение (ако това беше Ruby или Python). Ако връщаш 0 просто каниш някой да въведе бъг в кода ти без да иска
    • Съвсем инцидентно, „обиколка“ (perimeter) няма смисъл в 3D фигури. Още едни симптом, че йерархията ти е лоша
    • И разбира се, ако вместо
      main
      функция беше дал unit test, щеше дори да ме накараш да ти пробвам кода. Обаче тотално ме мръзи да смятам дали операциите ти са верни 🙂

    Щях още да кажа, че (1) избягвам дълбоките йерархии и (2) когато само малка част от наследниците ми предефинират метод на родителя, третирам това като симптом, че един класът прави твърде много. В последното обикновено успявам да извлеча нов клас и да делегирам натам.

  11. Тъй, разбирам какво иска да каже Стефан, виждам идеята му, но не разбирам защо мисли така. Хм, малко оксиморон се получи май? Обяснение:

    Той разделя проблема на 2 области: Добавяне на нови класове, и добавяне на нова функционалност към съществуващи класове. В последния коментар добави и още една област, промяна на семантичния смисъл на съществуваща функционалност, за което не говори в статията, и мисля е съществено различен проблем, но ще обърна внимание и на него накрая.

    Какво печелим и губим във всеки от трите случая при процедурен и ооп код?

    1. Добавяне на нови класове

    При ООП подхода, онаследяването ни дава всичката обща функционалност за новия клас, и също ни дава хинтове какво специфично трябва да имплементираме. Което са само хубави работи

    При процедурния, за да постигнем такава структура ни е необходим catch-all default в switch statement-ите, което е достатъчно тривиално при плитките йерархии, но става доста тежко при по-дълбоките. Лошо. Но пък имаме същото (или почти същото) ниво на подсказки какво точно да имплементираме – ако имаме default клауза, която хвърля изключение NotYetImplementedException ще хванем какво сме пропуснали при първия unit test. При ООП подхода бихме получили същото (абстрактия базов клас реализира по този начин тази функционалността. Освен ако функцията не е pure virtual, или не го онаследяваме от interface, когато ще получим грешката още от компилатора, но смея да кажа, че двете са доста еквивалентни)

    1. Добавяне на нова функционалност

    ООП: Добавяме нов метод в базов клас, реализираме го като pure virtual (или съответния аналог от друг език, или го правим да хвърля NotYetImplementedException). Отваряме клас дървото, за всеки дъщерен клас имплементираме функционалността. Не е най-лесното и просто нещо на света, но дали процедурния подход е по-лесен?

    Процедурно: Добавяме нов метод, switch за всеки клас. Дефаулт клауза, която хвърля въпросното изключение, отваряме клас дървото, за всеки relevant клас имплементираме функционалността. Някак си, струват ми се доста еднакви – дали ще е ООП или процедурно, стъпките които предприемаме са до голяма степен еднакви. Но ако функционалността е малко по-сложна, метода при процедурния подход ще стане 100 и кусур реда, което рядко е хубаво нещо.

    Според мен, двата подхода са горе-долу еквивалентни

    1. Промяна на семантичния смисъл на операцията

    ООП: Тук вече, ООП sux. При този проблем, трябва да обходим всеки клас, който бива impact-нат. Тежка и error prone операция, защото това рядко са всички класове, и често името на класа не е достатъчно да преценим дали там има нужда от промяна. Трябва да сканираме всички методи, за да вземем решение. Лошо.

    Процедурно: Тук всичко е лесно. Имаме един метод, с един поглед се вижда кой от случайте има нужда от промяна. Процедурния подход определено има голямо предимство.

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

  12. @Георги

    Много хубав коментар! Мерси за което.

    Малка бележка: не разделям на „добавяне на нови класове“ и „добавяне на функционалност към същестуващи класове“. Разделям на „добавяне на типове“ и „добавяне на операции“. Откривам, че тези термини правят мисленето ми по тази тема много по-чисто. Иначе:

    Засягаш две неща: (1) разликата при добавянето на нова функционалност и (2) разликата между промяна и добавяне. В този ред:

    Разликата при добавяне на нова функционалност. Повторил си каквото аз съм казал, само дето си го облякъл в повече подробности. Съгласен съм със всичко, освен последния ред. Не са горе долу еквиваленти. Въпроса не е в това каква логика ще добавиш, а каква е козехията ѝ. В „лошия“ случай логиката е разхвърляна на различни места, докато в добрия е на едно. Когато за да добавиш една логична единица се налага да пишеш на няколко различни места, отдалечени помежду си, това се нарича Shotgun Surgery и е Code Smell. Принципната разлика е, че при обекти получаваш shotgun surgery като добавяш операция, докато при структури – като добавяш тип. Това е причината да не са „горе-долу еквивалентни“ и точно това обяснявам в поста.

    Разбира се, ако имаш метод 100 реда, това също е лошо и трябва да се избягва. Ако процедурния подход резултира в това, значи имаш да рефакторираш. Впрочем, най-вероятно решението ще е някакъв Visitor, а не да минеш към обекти – ако си избрал структури на първо място си го направил защото имаш много операции и ги добавяш често. Класове с по 1000 реда също не са добро нещо.

    Промяна на семантичния смисъл на операция. За мен двете неща са „горе-долу еквиваленти“. Погледни другия вариант – променяш семантиката (или дори имплементацията) на типа. Първоначално си го направил лошо, открил си че не отговаря на реалния свят и сега искаш да отразиш новото познание в кода. Тогава лошия вариант ще е при структурите – там ще трябва да минеш през всяка операция и да прегледаш нейния switch/case внимателно. За това казвам, че са еквивалентни – shotgun surgery-то е на същите места, както и когато добавяш.

    И една друга бележка:

    Дълбоки йерархии. За мен това почти винаги е code smell. Ако имаш дълбока йерархия и се налага да правиш промени по нея, вероятно използваш наследяването лошо (погледни по-горния линк към pastie за пример). Всяка дълбока йерархия която съм виждал е ставала далеч по-добра, когато голяма част от наследяванията са били превръщани в делегация. Дори и GUI. А ако все пак дълбоката йерархия е добро решение (ще ми е интересно да видя пример за добра дълбока йерархия), тогава този въпрос въобще не е налице. 🙂

    Терминологията. Колкото повече мисля, толкова повече се уверявам, че „ООП“ и „Процедурно програмиране“ са лоши термини за нещата в тая дискусия. Вместо това предпочитам да си мисля за обекти, които скриват данните си зад операции (което наричам „обекти“) и други обекти, които са далеч по-глупави и предлагат данните си публично (които наричам структури).

  13. Първоначално тръгнах да пиша доста по-дълъг коментар, но колкото повече се замислих, стигнах до извода, че общо взето говорим за едни и същи неща. Ако разбирам правилно мисълта ти, това за което говориш е добавяне на логична единица, която се държи много сходно, но над хетерогенни данни? Ако съм прав, то тогава съм съгласен с теб – ползването на структури (по твоята дефиниция, не C/C++ структури) е многократно по-добро решение от използването на множество интерфейси.

    Намирам Visitor патърна и това което описваш са прекалено сходни, и няма да коментирам Visitor-а отделно – предполагам във всички случай, когато се колебаеш кой вариант(структура или обект) да използваш, има страни на данните ти, които е добре да се държат като обект и такива, за които е полезно да са структура. Тогава така или иначе ще реализираш някакъв визитор. Когато данните ти са очевидно структура, няма причина да го енкапсулираш като обект, и тогава ще ползваш варианта, който пряко си описал.

    За семантичния смисъл: И ти си прав. Подходих към въпроса от гледна точка на вече съществуващи класове, които са се доказали до голяма степен. Но си напълно прав, че понякога рефактор на класовете ти е по-доброто решение за промяна на семантичния смисъл на операциите.

    За дълбоките йерахии е относително – за един човек дълбока са 4 онаследявания, за друг са 20. Конкретно в примера ми имах предвид под дълбока между 3 и 5 наследника, което се случва сравнително често в определени области. Само като пример – контейнери за различни структури от данни.

    Така че, съгласен съм с теб: Деца, не правете обекти, само защото са обекти, когато няма нужда от тях.

  14. Обичам съгласието 🙂 . Два малки коментара.

    1. За мен четири нива на наследяване (A <- B <- C <- D) е дълбоко. Всъщност, за мен дори три е дълбоко. Мирише ми на липсващи интерфейси и делегати, но това е друга тема.
    2. Visitor-а, разбира се, е читаво решение, но носи допълнителна сложност. Понякога (дори твърде често) тая сложност не е оправдана.
  15. @ Кънев # 10

    Първо, избора на какво да пиша е мой и това, че не сменям езика всяка година едва ли е порок. Това, че не ползвам точно това което и ти (pastie или github) отново не е порок. Още повече Makefile е развален в pastie (липсват табулациите). Мислех да сложа кода някъде, но ми се стори маловажно къде точно… Между другото въобще не мисля, че си интересен като „обиждаш” хората, че не са „съвременни”… и на лекции правиш същата грешка.

    Второ, не мисля, че си противореча по какъвто и да е начин. Това което съм изпълнил като пример е напълно нормално в ООП. Ако е замислена добре като абстрактна, операцията

    area
    едва ли ще се нуждае от промяна в бъдеще, а още повече за запазване на API-то ще е по-добре да се дефинира нова (напр.
    surface_area
    ), вместо да се променя стара. Това отново е свързано с по-лесната поддръжка на кода, но не е свързано с целта на примера.

    Разговора не върви на добре, ако ще ме обвиняваш, че не съм вникнал в това което говориш. Имам пълното право да не съм разбрал, което за съжаление е само твоя интерпретация.

    Ще коментирам кода си, за да е напълно ясно какви са (простите) идеите заложени в него:

    • Точка в равнината се определя по координати x и y => клас
      Point2D
      ;
    • Точка в тримерното пространство добавя координата z за да се определи => клас
      Point3D
      , който разширява
      Point2D
      . Не виждам където точно прегрешението да не се съобразя с LSP в този прост пример ще окаже влияние – никъде нямам методи които да работят върху обекти от различни нива на йерархията;
    • Фигурата има една основна точка (за кръг и сфера това е центъра им) от двумерното или тримерното пространство, но може да има необходимост от допълнителни – това обаче не е предмет на примера => Клас
      Shape2D
      ;
    • Двумерната фигура има радиус за кръг и страна за квадрат => Класове
      Circle
      и
      Square
      , разширяващи
      Shape2D
      ;
    • Тримерната фигура се представя като в нея се включва двумерна => Класове
      Sphere
      и
      Cube
      , наследяващи от
      Shape3D
      и включващи съответно
      Circle
      и
      Square
      . Какво става когато завъртя един кръг (който има радиус) на 360 градуса по оста z? Не намирам нищо чак толкова „ужасяващо” в това представяне;
    • Лице на фигура има повече смисъл в тримерното пространство като площ, затова съм го определил отново. Лицето на включената двумерна фигура също може да бъде намерено
      Shape2D::area()
      ;
    • Разбира се мога да оставя
      perimeter
      като чисто виртуален в
      Shape3D
      и така ще получиш грешка при компилацията ако го използваш, но идеята ми беше да го определя като безполезен;
    • Обиколка на триизмерна фигура може да се определи различно – бях написал в един коментар в
      Cube
      – това е обиколка по страните или по страните и диагоналите? Ами призма, конус, цилиндър? Затова е безполезно да се продължава само с метода
      perimeter
      . По-добре би било да се определят нови методи;
    • Разбира се, за да пробваш кода ми ти трябва само Unix/Linux среда, около 4К място и 2 сек за изграждане с make. CppUnit отново е в страни от темата. Операциите са верни – никога не пиша код, без да го проверявам, така че правилно си си спестил времето.

    Мненията ми предизвикват доста писане от твоя страна, само че това ни отдалечава от темата. Нека да припомня, че ставаше въпрос за това, че добавянето на нови операции в ООП било по-скъпо и изисквало промени на „всички съществуващи операции“. Къде виждаш скъпото в метода

    volume
    , който се появява в класа
    Shape3D
    и ти въобще не си му обърнал внимание? Мога и въобще да не го определям в класовете в които не е нужен.

    Мерси за оценките ти, но в крайна сметка искам да ти кажа, че просто изразявам мнение. Най-малко имам желание да се заяждам с теб, нито пък да те обиждам. Ти не си привърженик на дълбоките йерархии (въпреки, че моята въобще не е дълбока), но пък аз не съм привърженик на събирането на кода на различни обекти в процедурния

    module Geometry
    , защото съм виждал много като него. Това е мнението ми и тази статия едва ли ще го промени.

  16. Понеже виждам, че мненията по темата са се развили искам да допълня относно промяна на смисъла на операция. Пак не виждам предимството в процедурния подход, защото този един единствен метод ще бъде неимоверно голям ако имаме повече обекти и по-сложна операция. В крайна сметка ще се стигне до разбиването му на отделни функции, което обезсмисля реализирането му за сметка на OOP подхода, където така или иначе ще имаме отделни функции. Нека да припомня мотото „Разделяй и владей“.

    Съгласен съм с мнението изразено от Георги Попов, че не е нужно да се правят класовете само защото са класове, но практиката е нещо много по-различно от това което пише и се дава като пример в книгите.

    Йерархия от 3-ма наследници въобще, ама въобще не е дълбока.

  17. @Георги Сотиров

    Мисля, че не ме разбра. Не виждам причина да преразказвам поста си още веднъж. Предвид кода ти и коментарите на моите коментари, дори не мисля, че ще излезем на глава за това какво е ООП. Съответно, не виждам защо да продължавам тази дискусия.

  18. Понеже се зачетох в спора, само да вметна, че периметър (или по общо – дължина) в двумерно пространство е повърхност в тримерното пространство.

    И всъщност математически погледнато сфера не трябва да наследява окръжност. Сферата е 3Д окръжност , т.е. един и същ клас , дефиниран с точка , радиус и степен на свобода. RR = xx + y*y + ……. (необходимия брой измерения)

    И всъщност никаква промяна на операции не е нужна – обем и площ (независимо от размерността) се изчислява с интеграл , повърхност и периметър (дължина) – също.

    Всъщност геометрията изобщо не е подходяща за илюстриране на ООП , защото всички геометрични обекти с описват с f(x,y,….) , a свойствата им (дължина,повърхност ; площ,обем) са интеграл на f(x,y,…)

    Ето и затова не обичам ООП , там където нещата могат да се опишат просто , любителите на ООП ще дефинират един куп класове и наследници 🙂

Вашият коментар

Вашият имейл адрес няма да бъде публикуван.