
Why are objects so hard to debug? - luu
https://codewords.hackerschool.com/issues/one/why-are-objects-so-hard-to-debug
======
userbinator
_global variables make it harder to figure out which code modified the value
that you suspect is now incorrect_

I think this is largely a moot point with debuggers that can (conditionally)
breakpoint-on-write -- if you see a global variable being set to a wrong
value, put a breakpoint-on-write to that variable with condition to trigger
when that wrong value is written, and you'll stop exactly in the code that
wrote to it. Then trace back the computation from there to see where that
value came from.

What really makes programs hard to debug is their total size and complexity;
in object-oriented programs that tend to favour emphasis on reducing local
complexity, global complexity increases. Tracing through layers of indirection
whose only purpose is seemingly to call other layers is tedious, and the flow
of data --- something which is important to see --- becomes more obfuscated.

~~~
angersock
_Tracing through layers of indirection whose only purpose is seemingly to call
other layers is tedious, and the flow of data --- something which is important
to see --- becomes more obfuscated._

This right here is probably the one of the only two arguments I would've
accepted about why OOP is bad--the other being that certain problems just
don't map cleanly onto OOP.

EDIT:

Acton had a great presentation on why OOP is maybe not a good fit in graphics
code:

[http://macton.smugmug.com/gallery/8936708_T6zQX#!i=593426709...](http://macton.smugmug.com/gallery/8936708_T6zQX#!i=593426709&k=ZX4pZ)

~~~
sebastianconcpt
This is just an emotional appeal. You can equally have "layers" of many
dependant functions that have tedious flows of data obfuscating what's
important to see.

That aside, I'm curious about your comment on "certain problems just don't map
cleanly onto OOP"

FP and OOP are both Turing complete, which kind of problems you think "just
don't map cleanly onto OOP"?

~~~
AnimalMuppet
Things that are equivalent in theory are not necessarily equivalent in
practice.

For instance, think about Green's, Stokes', and Gauss's Theorems in vector
calculus. They say that the integral of a function over the boundary of a
region is equal to the integral of a derivative of the function over the whole
region. They're equal; you can't get much more equivalent than that. But in
practice, one of the integrals is often easier to do than the other, and so
these theorems are used in practice to transform a hard problem into an easier
one.

Both FP and OOP are Turing complete. But in practice, certain problems may be
easier to program in one approach or the other.

If I were writing a numerical analysis package, the functional approach might
work better. If I were writing an event-driven embedded system, I'd expect
that OOP would do more for me than FP.

~~~
angersock
Oh, that's absolutely true--but in OOP as practiced with deep inheritance
trees (as distinct from, say, actors with internal functional
implementations), it's not just possible but indeed is the preferred way of
doing things.

I'm not saying that it is wrong or even unnecessary--but the parent criticism
was that it _seemed_ more complicated than it had to be, and that it could
often be more important to look at the _data_ instead of the _objects_. I
agree with that criticism.

As for problems, I'll agree with my sibling poster--just because two things
are academically equivalent doesn't mean that they are practically equivalent.

Consider the link to Acton's talk...a lot of the testing and method calls are
redundant or can be pulled out at a higher level, and encapsulation doesn't
really win you anything because you know the problem domain concretely.

Numerical code is another case where OOP doesn't quite get you there. If
you're just finding the average of a list of numbers, is it more sensible to
have a function which maps elements of the space of lists of numbers into a
space of scalars (which happen to be averages), or to have a list of numbers
object that happens to know how to answer the "what is your average" message?

Now, let's say you wanted to represent a new space only of elements consisting
of averages plus some offset. That's again trivial in a functional language
with currying, but in OOP is kind of awkward.

The reason we like functions is because they represent a straightforward
mapping from one space to another, and can be implemented very performantly.
OOP principles, especially encapsulation, require that a function _cannot
know_ about the internals of the object it's working with, because it should
be able to be changed out at a moment's notice.

It is also because of the requirement of functional programming to know
concretely the details of the things passed into them that we see such demand
for static and strongly-typed and immutable systems.

------
pjc50
_From the perspective of debugging, an instance variable is not in the scope
of any of the methods on the stack_

Your debugger is broken. The call stack will have implicit "this"
pointers/references in each frame, and the instance variables can be found
there.

Now, the rant as a whole has a valid point that mutable state causes confusion
in terms of finding the code that mutated it. An argument for immutability or
time-reversible debuggers. But OOP at least gives you a chance to keep the
mutating methods close together where you can find them.

------
gmarx
I think the most important aspect of the object paradigm is that it models
real world entities. The real world has a logical consistency so code properly
modeled on it will have a logical consistency. This makes it easier to extend
as you try to solve more problems in the given domain.

The more you program the more you tend to design with the intent of making it
easier to debug. You end up doing a lot of things that borrow from other
paradigms like passing more variables to method calls while using fewer
instance variables and consciously avoiding side effects.

Honestly, I don;t know enough about the functional paradigm to argue whether
it is easier to debug. It looks like it has a lot of features that would make
it so.

Definitely agree that inheritance, instance variables, setters make things
more difficult. Inheriting from a framework is asking for trouble.

~~~
pjc50
_The real world has a logical consistency_

lolnope

(Have a look at, say, the UK benefit system and the failing Universal Credit
project that's supposed to work out what to pay people in only one place
rather than across a dozen agencies)

~~~
gmarx
i'm not sure how to contradict this since it seems orthogonal to my point. I
don't know those programs and can't tell which aspects of them you consider
unmodelable. A human designed system could be stupidly designed but as long as
the design doesn't change randomly, you can model it.

------
sago
I don't recognize the problem from this article (programming for 20 years,
including 10 doing R&D as a consultant).

Firstly I don't spend most of my time debugging. I do spend a fair amount of
time programming in a way that makes it less likely I'll have to debug
(asserting contracts, implementing tests). Whacking out some random spaghetti
code, then spending days navigating it isn't productive.

And secondly, thinking about the recent projects I've worked on, the debugging
I have done has not been about finding where a variable got the wrong value.
The only day-sapping bugs I've had recently have been figuring out why some
piece of code performs an incorrect calculation; and figuring out how to
ensure the correct order of different operations. The latter I suspect would
have been more difficult in an FP with a more complex stack trace and tail
call elimination. Most of the smaller debugging problems I have relate to
impedance mismatches between my code and 3rd party libraries.

Library interface problems seem less in FP, but mostly because there are fewer
libraries. It is easy to make a totally understandable, simple, programming
environment if you never have to do any heavy lifting with other people's
code.

I program in FP, but a lot of the time I end up in OO languages with good
library, tool, testing and debugging support.

I can't help but think this is another case of FP-zealots interpreting the
world in a way that maximises the problem they know how to solve.

------
Hermel
Objects are much easier to debug then their predecessors: methods making use
of global state variables.

With objects, state is encapsulated and not modified by the outside, making
them much more predictable and thus also easier to test.

~~~
rayiner
I'd rather deal with a procedural program modifying global variables than an
object-oriented one. Strewing an algorithm across a bunch of methods makes
control flow incomprehensible.

~~~
ionrock
I've felt the same way at times until I realized the issue is just my
perspective. OO provides an organizational tool for organizing functions and
variables. The same thing can often be accomplished by changing the way code
is organized.

For example, in Python, if I have two functions that implement a "download"
operation, but they accept different types of locations, I could use different
modules to implement them:

    
    
        from mypkg.endpoints import ftp
        from mypkg.endpoints import http
    
        ftp.download(ftp.info(some_ftp_info))
        http.download(http.info(some_http_info))
    

You can see that we pass the result of `ftp.info` to each, which is
essentially defining its type somewhat reliably.

In OO, you can simplify things a great deal by encapsulating data in an
object, as well as organizing functionality accordingly. I think (in Python at
least) the same benefit can be accomplished by using modules for namespaces
and grouping, like you would a class.

By using modules in this way, it is then possible to utilize types like you
might in haskell as well as enforce immutability, assuming the "types" you
create are immutable (like namedtuples).

All that said, I've yet to have the courage to enforce this sort of paradigm
in a project. I worry that it would be prohibitively expensive to my fellow
developers who may not appreciate the benefits. With that said, I would be
interested if the paradigm would be reasonable over time.

------
taeric
This has to be one of the first times I've seen functional programming called
imperative. I think it was just last week I was in a debate where people were
basically calling functional as the same as !imperative.

This one goes further and argues that oop is !imperative. Have popular
definitions changed while I wasn't paying attention?

~~~
efnx
As far as I know functional languages would be declarative and OO langs would
be procedural and imperative.

Though, people refer to languages like lisp and scala as functional even
though these langs are only functional if you use them a certain way, and I
think it dilutes the term and creates confusion.

~~~
sebastianconcpt
You can even use Smalltalk in a functional style which feels quite good
actually.

------
zak_mc_kracken
This whole article seems to be more about the author not having a good
debugger at his disposition than "OO programs being hard to debug".

Modern debuggers have conditional breakpoints, breakpoints on write,
breakpoints on count, sophisticated formatters and all kinds of facilities
which make the problem of debugging a method call that might go to a subclass
instead of the base class all but trivial.

Debugging complex programs is hard, regardless of the underlying methodology
that this code uses.

~~~
sebastianconcpt
Yes, completely biased. It takes "having to see implementors or senders in the
whole system" as if it where some kind of a problem. Same for looking a
hierarchy or debugging.

Author sounds like talking of OO tools frozen in the 60's, really poorly
investigated or just sharing his/her own rationalising and enthusiasm about
using functional programming instead.

------
lmm
Utterly backwards, as those who've tried it in practice know. If you attempt
to program in a functional way you replace mutation of values with e.g. State
monad constructs. And you go from having inspectable, debuggable state changes
(you can breakpoint the point in code where a value is modified, and this will
correspond to the point where it's actually modified, and you can inspect the
changes in your debugger) to having state changes that happen invisibly,
implicitly, later, when a function is actually called. A closure carries just
as much state as an object, but it carries it implicitly, invisibly,
undebuggably.

There are advantages to the functional approach - I use it for most of my
programming these days - but this article is pure FUD. Actually it goes beyond
that, making claims that are outright falsehoods.

------
CmonDev
It's interesting that author is avoiding talking about the Actor Model which
has an obvious overlap with the mainstream OOP in terms of stateful objects
communicating with messages yet is being embraced by functional developers
(Akka, Erlang).

~~~
danieldk
An actor in Erlang is not an object, but a (usually recursive) function where
_receive_ is called in each recursion. It's stateful in the sense that Haskell
functions are stateful: you pass a new state as an argument to the next
function.

~~~
rdtsc
> An actor in Erlang is not an object

And function with closure and/or with a state passed through and it is an as
good as an object.

Heck, look at one of the most popular languages -- Python. Method calls look
nice an explicit like def method(self,...). Others have self too (usually
called "this"), but it appears there implicitly.

At the memory level Erlang actors are even better objects because they
actually enforce memory isolation. Which is in spirit with Alan Kay's original
OO idea.

~~~
danieldk
_Heck, look at one of the most popular languages -- Python. Method calls look
nice an explicit like def method(self,...). Others have self too (usually
called "this"), but it appears there implicitly._

"Foolish pupil - objects are merely a poor man's closures." And visa versa.

However, this line of reasoning definitely differs from what one nowadays
considers an object-oriented language. Which was exactly what this article was
about.

------
aikah
To the OP,please to stop trying to make general points about OOP,because I
wonder what language you are talking about.

> The technical term for a programming paradigm which uses neither inheritance
> nor instance variables is imperative (or functional) programming.

what ?!?

~~~
AnimalMuppet
The technical term for that paradigm, in my day, was structured programming
(because it used block structure, rather than gotos, to control the flow of
execution). It meant C or Algol, not Lisp or Haskell.

------
blt
Yes, any system where you're not in control of the main loop becomes hard to
debug very quickly. Flow of control should be tree-like whenever possible.

Polymorphic virtual methods are also hard to debug because you must figure out
what type the object actually is. You must either run the code with
breakpoints, or run it mentally in your head.

Don't give up on knowing exactly who's in control and what the data structures
look like unless there is a clear benefit.

With heavy OOP in C++, you often end up with code that knows nothing about the
data except a heap pointer and a pointer to a virtual method table. Not only
does this make debugging a pain, it kills almost every opportunity for
compiler optimization.

Of course, this pattern can be really useful. But it should be selected as a
very deliberate design process, not "Oh, we put everything behind interfaces
because I heard it's good."

The only place where the pattern is really, truly necessary is when a library
needs to call user-supplied methods on user-supplied data. In C this looks
like:

    
    
        // library.h
        void set_callback(void (*myfunc)(void *, int), void *);
        
        // me.c
        struct S { ... };
        void update_s(void *s, int new_value); // must cast to struct S *
    
        struct S s;
        set_callback(&update_s, &s);
    

This is ugly but it makes it clear how much information is being lost in such
a pattern.

------
ajuc
Small things that I noticed hurt debugability a lot:

    
    
        a.getB().getC().getD().e() - NullPointerException can have 4 causes
    
        B b = a.getB();
        C c = b.getC();
        D d = c.getD();
        d.e(); // NPE can have only 1 reason, you can inspect b,c,d at debug time without stepping inside
    

Another thing that hurts debugability are non-reversible transformations in
code. If sth is non-reversible it means 1 effect can have many possible
causes, so when debugging you need to consider many possibilities why we are
here. When there are many non-reversible steps you get exponential increase of
possibile causes.

The most common things that cause this in code are defaults - when you have
defaults and need to debug you need to consider many possible reasons the end
effect is what it is.

It's not a big deal, but it makes debuging one step harder (instead of "I know
why we get there, let's go deeper" it's "We get there either because of X or
Y, let's debug to decide, or go both ways").

------
wyager
> If you believe (as I do) that the majority of effort a programmer expends is
> devoted to finding and fixing bugs

I think if you feel this way, you should definitely look for languages and
tools that don't admit as many bugs.

~~~
taeric
Or find a way to exert and capture more thinking ahead of time. Language may
help. May. Similar for tooling. (If you are convinced that they will both
automatically help, consider that you can't just give them to a 1 year old and
get results.)

It also helps tremendously to limit the scope of what you are doing. One of
the reasons the "add print statements" helps so many people is that they
reduced the scope of their thinking to be smaller than the entire problem. It
isn't, "how can I make this whole program behave," but rather "how can I make
this variable what I want it to be?"

There is a trap in this thinking, though. Many feel that if you can reduce
everything down to small functions that it will be easier to deal with. This
_can_ be true, but it isn't strictly true. If each function is called from
many places, then it can become hard to really understand the impact of a
change.

~~~
sebastianconcpt
This is the voice of the experience talking.

Spending "lots of time on debugging" is a signal of lots of reverse engineer
that is hard (not easy), probably due to poor method naming (very common
problem regardless of paradigm).

Is not the language fault that you design sucks.

------
spullara
Sadly, functional languages with lazy evaluation and data pipelines are harder
to debug as the stack has far less meaning. At least with N layers of
indirection in OOP you can see all the layers in the stack trace.

------
sebastianconcpt
Well Smalltalk was designed to be easily mastered by one individual so its
browser can look the entire hierarchy in two clicks and list the senders or
implementors of any method you want in one keystroke. Not to mention that its
debugger can restart the stack at any point, change code, recompile it on hot
and continue _in the same thread_.

So, I guess yeah, if you don't have great tools anything is "so hard to debug"

------
dusklight
This was a terribly written piece. It is terribly long winded, when what it
was trying to say could have been said much better in a few short paragraphs.
I don't think it is possible to take seriously the words of anyone who thinks
the "art of programming" is the art of debugging programs. That's like
thinking the art of astronomy is about cleaning telescopes.

------
vjeux
On a related subject, React has been designed to make tracing up bugs easy via
various techniques explained on those slides:
[https://speakerdeck.com/vjeux/why-does-react-scale-
jsconf-20...](https://speakerdeck.com/vjeux/why-does-react-scale-jsconf-2014)

------
erikb
You are on the right path, dear author, but you are still not at the point of
understanding debugging. One huge part of debugging is avoiding bugs, and
because this is often done poorly by the average coder, the next big part is
understanding what a piece of code does and specify what it should do. Before
you can fix a bug, you need to know what's wrong, not just where it happens.

Also, always using inheritance is bad, but so is never using inheritance.
(Does that need explanation?)

------
cratermoon
Alternately:
[http://c2.com/cgi/wiki?ForgetTheDebugger](http://c2.com/cgi/wiki?ForgetTheDebugger)

------
angersock
Many misleading statements here (paraphrased):

 _With imperative code, you can just hop up the stack!_

...unless something screws up your stack frames, in which case you win nothing
good day sir.

 _With OOP code, you alternate framework code with application code, which
never happens with imperative libraries!_

...except any sort of nontrivial library that has proper abstraction via
callbacks, in which case you very much do end up alternating application code
with libraries.

 _Inheriting from a framework object can do things you don 't expect!_

...except that in languages that aren't C++, it's usually pretty easy to
control whether or not you execute parent class code instead of/in addition to
your extended function.

 _The technical term for a programming paradigm which uses neither inheritance
nor instance variables is imperative (or functional) programming._

ahahahahha what? no.

Imperative != functional programming. At all. Had a CTO try to convince me
this was case, was dead wrong. Imperative code allows for mutation of global
state and perhaps no functions-as-first-class-citizens. Functional code (in
theory) allows for no mutation of global state and always has functions-as-
first-class-citizens.

Author has also left out _declarative_ languages, which also somewhat fit that
definition, but hey, who's counting?

 _The best way to illustrate the issue with debugging multi-threaded programs
is to describe the debugging scenario as consisting of multiple stacks._

What? Fuck no. The biggest issue with debugging multi-threaded programs is
_having shared mutable state_. The fact that thread A can write and screw up
something depended upon by thread B, which is totally supported in imperative
programming and is hard to protect against, is the problem. Second would be
the magic of race conditions, live locks, dead locks, and all of that jazz.

OOP is, other than _pure_ functional programming, perhaps the only sane way of
dealing with mutlithreaded programs when you aren't subscribing whole-hog to
actor-style message passing.

~

This is a bad article, and displays both a fundamental lack of understanding
as well as a sort of arrogance in the profession of the aforementioned
ignorance.

EDIT:

And no, OOP/actor programming is not a panacea for all of your problems
either.

There is quite a lot of room in the world for simple imperative programs
(which are basically objects interfaced with from the command line) or chained
functional programs (very useful for data transformation pipelines).

That said, there are _reasons_ why we invented OOP. There are _use cases_ for
it. There are places where it is _the right thing_ , and to pretend otherwise
makes it all the harder to know when _not_ to use it.

------
smrtinsert
lately in clojure i find closures incredibly difficult to debug. Not having a
step debugger is of course very annoying but the root problem is the same -
state is captured which means you have to find wherever the closure was
generated and recreate that flow...

~~~
sebastianconcpt
Yeah a step debugger is a must. I miss it in [http://amber-
lang.net/](http://amber-lang.net/)

------
mariusz79
Wouldn't it be easier to debug your classes not objects ?;)

~~~
delinka
It's a bit difficult to test an instance method at the class level. Once
you've instantiated an _object_ from a _class_ , now you're debuggin' with
objects.

~~~
sebastianconcpt
Depends. There are systems where everything is an object. Including classes
(and if you ask what's the class of the class object the answer is Metaclass).

In those systems, you technically always work on objects.

