Hacker News new | past | comments | ask | show | jobs | submit login
A Square Is Not A Rectangle (elharo.com)
67 points by raganwald on Sept 14, 2009 | hide | past | favorite | 66 comments



Computer programming is young enough as a field that the mainstream hasn't realized it isn't possible to fit everything into one unified taxonomy. (Biologists and librarians have both spent over a century trying, and they have a lot of insight on the matter.) Putting types into a hierarchy is a useful abstraction sometimes, but there's not some Grand Truth in it -- often, attempts at cramming disparate elements into a hierarchy just create extra complexity. Most systems will keep trying to turn back into graphs unless you anchor the hierarchy in a specific context (what does 'a IS-A b' mean, really?), and the context tends to shift with the problem definition.

Worse still, it's the sort of complexity that looks like work, even though it's usually more like flailing in quicksand.

I think not having to deal with class hierarchies is a big reason why some people find dynamically-typed languages so freeing. I'm still kind of new to Erlang, but I think its push towards modeling things via a graph of communicating processes, with a relatively flat type structure, is worth considering as an alternative. It explicitly focuses on the message protocol between actors, rather than a tree of subtypes. Granted, I haven't worked a large enough project to see it break down and get ugly yet. (C++ style OO sounds good on paper, too.)

Regardless, it isn't classes per se that lead to OO's problems, but the tendency to overuse inheritance. Among statically-typed languages, Haskell's typeclasses seem like another good solution - they break the hierarchy of inheritance up into composition of properties.

Also, the late-binding in OOP means that any class can potentially be responsible for maintaining its invariants even after any of its methods has been overridden by any possible future subclasses. That's not necessarily true even with dynamically-typed languages.


It isn't because programming is too young, it's because programming is commercial. OOP retardation is new thing, it's not something we've been trying to do forever are are just now realizing is problematic. It was pushed by manager types who just wanted as many reports as possible and something that let them divide arbitrarily simple jobs among all of them, and it was allowed to grow by a class of programmers who didn't know any better because they were taught just enough to churn out the sort of programs that industry figured it needed. Now it's everywhere and even people who known it's crap just have to deal with it to get anything done.


Your points concerning inheritance are good but "the field is young" seems like just a pseudo-profound explanation for the situation. CS as a field really can't be compared in this way to other fields - it's not really a science since it doesn't deal with the discovery of verifiable facts. It is more like some combination of mathematics, humanities, management and engineering.

CS has to connect very fuzzy things, human beings, with very exacting things, computers. The problems of CS aren't really about the age of the discipline but difficulty of the domain. For example, when you start to get into biology-as-programming-with-DNA, you are dealing with an even more complex domain and biologists look to CS for clues in this situation.


That's why I mentioned librarians. Professional catalogers have spent over a century trying to figure out how to organize resources so that people doing research can find relevant information, and classifications that are useful for everyone prove surprisingly elusive. The Dewey Decimal system is from the 1870s, after all. (Its flaws are well-known, now, and there are several other cataloging systems in use.)

I was thinking about a quote (I think it's from Philip Greenspun or Joe Armstrong, though in ten minutes of googling I wasn't able to source it. Anyone?) observing that programming seems to be in a pre-scientific phase (in the Kuhn-ian sense) -- there still isn't a broad consensus about major ideas, so advances often come via books and manifestos, rather than gradually through organized research.

(Edit: The closest match I'm getting is regarding AI. I've been reading about Prolog, so that might be where I read the quote...)


Hmm,

The thing is that the phrase "pre-scientific" implies that CS is destined to reach a scientific phase. I don't know if that's at all certain.

... Another thing, just to think about, is how Google today often lets one find more cross-links than any hierarchical system would allow you to find, yet Google is, itself, not a well defined system using the semantics of the items involved but rather a "dumb" algorithm that only looks at the links between things...


I don't know if programming (as opposed to computer science, in the research sense) will ever attain "science" status because we don't stay in one place long enough to be settled. If we were all still writing terminal apps on 80x25 green screens, the science would be pretty well settled on how to do that by now, with recipes for every occurrence that any normal programmer would ever hit readily available, and easy access to people who could do the somewhat harder cases.

"What about the really hard cases?" you might ask, and the entire point of my hypothetical is that if we can't do it in 80x25, we don't do it.... and the contrast to the real world where we kept going is exactly my point. So, when will programming finally stabilize enough to become a science? I don't know, but I don't see it even in the long term (30+ years).


That's kind of how I think of the goal of finding a grand unified theory in physics. It seems to me that if such a thing is ever found, it means there is some unifying and "correct" organization to the universe and that'd revolutionize a lot of stuff when the same ideas and structures are applied to all other fields. It'd be a huge win if this worked out - and not just for physics. In my warped and uninformed mind, the grand unified theory, the perfect programming model, P=NP, and countless other such edges of theory are all different facets of the same entity that we have yet to identify - assuming it even exists in an identifiable way in our reality.


Exactly.

One of the handy things about physics or math is that you can read a paragraph like your clever example above and immediately recognize it as a joke.

The Sokal experiment demonstrates the dark alleys you'll head down once you lose that skill.


Problem is from historical conventions of what a square and rectangle are.

A square is not really a thing, its a description of a thing. In this case, proportions of the sides of a shape.

So when you translate this into object oriented language, you are subclassing a concept with one of its potential variations. It would be like making a class called MusicalNote and then a subclass named MusicalNoteC, even though MusicalNote already has it covered.

So "square" shouldn't be a subclass because its just a proportion of the data within "rectangle", so more appriopriately, 'square' would be a method of Rectangle since it deals with internal data.


This just comes down to the difference between identity that changes with mutation, and identity which is constant with mutation.

All squares are rectangles. Some rectangles are squares. If the object used to represent squares and rectangles is mutable in width and height, then its squareness cannot be immutably associated with its identity, and thus cannot be encoded as a type, since objects in static languages have immutably associated types.

There's some deep point about naming objects after their attributes, without paying attention to whether the attributes may change. Experienced programmers know this well, but laypeople don't need to make the distinction often as their language usage is usually only temporary, not needing to account for changes over time. It's the statement that must always be true, when all the right qualifiers and conditions have been included, that's hard to make.


No, there is no easy answer to this problem in a way that jives with all doctrines and also works in practice.

OO is ambiguous as to whether a "subclass" of an object is specialization of the parent object or a generalization. In practice, most subclasses are a generalizations and specializations introduce all sorts of problems, especially enforcing constraints.

This naturally comes down to OO trying to shoe-horn (at least) four related-but-not-necessarily-identifical activities together: code/data encapsulation, program design, code-reuse, and modeling (the translation of real-world objects-and-categories to code).

The net result is that OO languages, tools and methods can be useful for writing large program and can give you effects that are better than the alternatives but that the pronouncement of OO gurus wind-up sounding idiotic and following the book to the letter can create situations that negate the particular benefits of OO.


By Liskov substitution principle, or with the set view of types, subclasses are always specializations / subsets, unless you're introducing some kind of home-baked type system built on top of the existing type system.

I would classify the parts of OO differently. I think your second and fourth activities are the same, but one models abstract concepts, and the other models concepts of concrete things. I wouldn't bring in code reuse at this level; every practical programming paradigm tries to include code reuse. It's the mechanism that's a distinguishing feature, and the mechanism OO uses is a combination of subtype polymorphism and dynamic dispatch. It's a baked-in version of the function table / union|variant record idiom from procedural programming, and it has a transposed expression in sum types and pattern matching in functional programming: OO limits the addition of new (virtual) methods (on the base) but accepts the creation of new types (even at runtime), while FP limits the creation of new types (in most implementations I've seen) but accepts the addition of new methods (which costs some in discoverability in modern IDEs, and has namespace implications).


Hmm...

By Liskov substitution principle, or with the set view of types, subclasses are always specializations / subsets, unless you're introducing some kind of home-baked type system built on top of the existing type system.

So, does that mean that subclasses shouldn't have extra member variable in the way that a square doesn't have any extra member variables in inheriting?? I know that there are some systems which try to do something like this - Darwin and Date's Third Manifesto, for example, though they don't try to say they're doing OO. And, of course, this is far away from what most people do in defining objects.

The various tools and approaches are useful and your particular take on it might make it consistent if it was generally accepted. But at this point, the many confusions about what OO are set in people's minds. What's being defined now is Agile - which is naturally another semi-original, semi-useful, semi-contradictory toolset.

But hey, I guess my conclusion is it's all good if you don't take it too seriously and do stupid things just to satisfy some absurd constraint - like in the hay-day of OO when people let go of common sense and built these huge, fragile multiple inheritance trees that wound up not having the slight understanding of - or used ten separate, nested classes to define the complex numbers...


> does that mean that subclasses shouldn't have extra member variable

Not sure what you meant here. Extra members, whether they be properties, fields, methods, types, whatever, should be fine in subclasses. If it was removing members from the superclass, that would be problematic from a subtyping / polymorphic perspective, but adding members makes it more specific.


This just comes down to the difference between identity that changes with mutation, and identity which is constant with mutation.

What you say is true RE the OP, but the OP is not very good. Mutability is a red herring. Uncle Bob points this out in his response.

Assume immutability. Now the names 'square' and 'rectangle' actually make sense and the real issue becomes clear: the correct number and names of variables needed for representation. And the definition of the 'area' method.

Circle-ellipse is a better illustration because a circle has a 'radius' property that an ellipse doesn't have.


Ellipses have major and minor radii; in circles, both are the same length. Attributes in these kinds of objects are a bit of a red herring, though, as one set of attributes can be calculated from another, and vice versa. A circle can be described as a parametric function of angle, x = r * cos(t), y = r * sin(t); or the more familiar r^2 = x^2 + y^2. Which attributes you choose as the canonical, and which you choose to calculate, is just an implementation detail.

FWIW, I've felt for some time that the benefits of object orientation have been over-emphasized, though the general pattern of industry and language development has been moving in a better direction of late (say past 5 years).

I also have no well of respect for Uncle Bob - that's not to say he isn't a very intelligent, wise guy with lots of insightful things to say, it's just that I haven't read much of his stuff, and had never heard of him until a few months ago. But when I read him try to make an analogy between types in a program representing things in the world, and two lawyers advocating for a divorcing couple, and saying that since it's not the lawyers that are divorcing, you shouldn't model your types in an isomorphic correspondence to the things in the world you're modelling, well then, I think he's overreaching. If all your world is immutable rectangles, and you need to handle squares differently, and you're solidly orthodox in your subscription to OO, then I think modelling the relationship with inheritance is perfectly fine. In static languages, the fact that inheritance creates a subtyping relationship and introduces the possibility of polymorphism and dynamic dispatch is usually more interesting than the inherited scope of member lookup.


Ellipses have major and minor radii; in circles, both are the same length.

This is exactly the issue. According to specialization by constraint when an ellipse has equal radii then it is both an ellipse and a circle, therefore a circle isa ellipse, therefore a circle is a subtype of ellipse.

Hence a square isa rectangle. The specialization by constraint argument is that any type system that requires a rectangle to be a subtype of square or both to be subtypes of polygon is inherently broken.

LSP says that if a circle is a subtype of ellipse then it can be used in place of an ellipse. But it can't, so a circle can't be a subtype of ellipse. LSP and specialization by constraint are not compatible.


LSP says that if a circle is a subtype of ellipse then it can be used in place of an ellipse. But it can't

You haven't shown that - you've merely asserted it. If you live in a world of immutable ellipses, and you need different behaviour from circles and non-circle ellipses, you can make circles subtypes of ellipses without any problems, and still satisfy LSP - because we're not specializing by constraint. The most important point: circleness is attribute of an ellipse, and if the ellipse is immutable - its radii can't be changed - then that circleness can be contained in the type without problem.


You haven't shown that

I don't understand. An ellipse contains more information than a circle. You can't ask a circle for the length of its minor radius, so how can it substitute for an ellipse? I'm not sure we're speaking the same language.


I believe you're assuming more things about the programmatic interface of a circle type than is necessarily true. An ellipse with equal major and minor radii is a circle. That much is mathematical fact.

Equivalently, a circle is a special kind of ellipse. As an ellipse, you can certainly ask it for major and minor radii. But "more information" is an implementation detail. If Circle were a subclass of AbstractEllipse, which itself also had a concrete Ellipse subclass, Circle might implement both major and minor radii "properties" in terms of a single field. But again, this is just an implementation detail.

All this is only practically useful if there's a reason for considering circles as ellipses, or for specializing behaviour for circles. For example, the circumference calculation for circles is much simpler than for ellipses, so we might want to specialize it. Since our values are immutable, there's no problem in capturing this optimization in the type and overriding the appropriate method.

(I'm assuming immutable values throughout, to make it clear.)


You seem to be saying that circles are not subtypes of ellipses. That circleness is just a property of ellipses. If we accept that then the whole issue is moot, and substitutability is true by tautology.

A circle is a triple: (x,y,radius). An ellipse is a 5-tuple: (x,y,major,minor,angle).

Maybe we should be talking about angle instead? It can't be derived from a circle. What happens when you feed a circle to selectEllipsesByAngle()?


If isEllipseAtAngle(someAngle): boolean were a method, a Circle type could return true for every angle.


I have a simpler dictum: Square was-a Rectangle. In other words, we wish to implement a square using the rectangle class, but we do not acknowledge that a square is a rectangle at all. Implementation inheritance should sometimes be independent of interface inheritance.

If I was forced to use interface inheritance for this, I would say that both squares and rectangles are abstract quadrangles, but rectangles allow individual sides to be set and squares do not.


The article is good at pointing the logic faults.

As you correctly say, the abstraction is wrong. If you were abstracting classes for a graphics program the abstraction could even go one level higher in abstracting p o l y g o n s, rather than rectangles and squares. You can even have crossed rectangles (http://en.wikipedia.org/wiki/Rectangle).

Using such an abstraction IMHO any polygon shape would consist of line objects which would consist of points (x,y and even z)!

OOP abstractions can sometimes be masochistic! ;)


> OOP abstractions can sometimes be masochistic!

You think?

The article is a train wreck of analysis piled on the train wreck of "OO Design", which is itself a train wreck of an attempt to promote reuse by modeling code with data types, which is itself a train wreck of an attempt to imagine what hypothetical programmers in some hypothetical universe might find "useful" as masturbatory aids, hypothetically speaking. (This is not actually true - OO[D] is really an attempt to commoditize programming.) Most of the comments here are just dumping more trains on the pile. The abstraction shitheap is so high at this point that we can barely even remember that we are supposed to be solving some kind of actual problem.

The best part of the article is when the author acknowledges that the code might not actually need to be thread safe and so avoids wasting his time with that particular assumption. There is nothing else to be salvaged. A square is not a rectangle? Goodbye mathematics - what is the point of a type system again? More to the point: what planet are the users of this API from? Obviously not this one, and just as obviously no one knows or cares. Not ONCE does the author stop to wonder what services the actual users actually need; it's only about what this guy thinks MIGHT comply with some random "design" rules. One wonders if the users even exist as anything other than just another abstraction, and wondering about what problems those users might actually be trying to solve is right out.

Object Oriented Design can be seen as a case of reusing a cliche and butchering it at the same time, with the cliche being the shooting of ones self in the foot, and the butchery being the placement of said foot squarely upon the face of beauty immediately before pulling the trigger.


I think you're going way overboard with your cynicism and criticism of OOP - cynicism bordering on paranoia. I've read your other comment, alleging that OOP "was pushed by manager types who just wanted as many reports as possible".

The dominant paradigm before OOP was procedural programming, and it had (and has) real problems: tendency towards storing data in globals, lack of a machine-checked mechanisms for subtyping, less discoverable APIs (often a namespacing issue), and difficulty in sharing code when representing abstractions where many kinds of values need to be treated very similarly but with little differences, GUI widgets in particular.

OOP is unmistakably a better approach for a large class of problems, primarily because it encodes good procedural practice into well-defined concepts that are checked by the machine, either at compile time, run time, or both.

Functional programming has a different kind of benefit: whereas OOP helped modelling values, FP helps modelling algorithms. Because strict FP demands pure functions and immutable data, you can reason about complex functions and expressions knowing that there are no side-effects that can bite you. The benefits of FP over OOP aren't as clear as OOP over procedural programming, as taking the strict approach makes state management problematic and limits freedom in data structure design and ultimately algorithmic complexity, but the benefits are very clear in certain areas.


I note that every advantage of OOP you cite is a modularization feature (even subtyping, since OOP basically conflates modules with types).

Clearly you are not entertained by rants based on my experiences, so maybe I should just cut 'n paste pg:

"Object-oriented programming is exciting if you have a statically-typed language without lexical closures or macros. To some degree, it offers a way around these limitations. (See Greenspun's Tenth Rule.)

Object-oriented programming is popular in big companies, because it suits the way they write software. At big companies, software tends to be written by large (and frequently changing) teams of mediocre programmers. Object-oriented programming imposes a discipline on these programmers that prevents any one of them from doing too much damage. The price is that the resulting code is bloated with protocols and full of duplication. This is not too high a price for big companies, because their software is probably going to be bloated and full of duplication anyway.

Object-oriented programming generates a lot of what looks like work. Back in the days of fanfold, there was a type of programmer who would only put five or ten lines of code on a page, preceded by twenty lines of elaborately formatted comments. Object-oriented programming is like crack for these people: it lets you incorporate all this scaffolding right into your source code. Something that a Lisp hacker might handle by pushing a symbol onto a list becomes a whole file of classes and methods. So it is a good tool if you want to convince yourself, or someone else, that you are doing a lot of work."

http://www.paulgraham.com/noop.html


Modularization is independent of programming discipline; you can have modules in procedural programming, OOP, FP, etc.

I didn't argue that OOP is exciting, just that it is not a conspiracy perpetrated by a business elite to reduce the productivity of the average programmer.

And appeal to authority won't work on me, sorry. FWIW, I think pg goes too far in his rant - I think he mistakes a certain strand of Java enterprise development in large corporations for all business OOP.

The programming languages I use most often outside of C - Delphi (I maintain the compiler, written in C) and C# - both support closures, and of course C supports textual macros. As a compiler engineer I'm not oblivious to the fact that code is data and vice versa: turning code into data, and data into code is my meat and potatoes, as is symbolic manipulation of code trees (I implemented Delphi's closure support). But there's a downside to treating all the world as a list, and building all your structures out of conses: lack of documentation, encapsulation, machine checking. These things matter when you're trying to build an economy of software modules in a closed source world.

In my own past in business development, I even used call/cc for certain web client / server dialog scenarios, using a DSL especially built for GUI data binding and events over an AJAX protocol. Assuming that all, or even the majority, of business developers are inefficient bumblers performing make-work is foolish, insulting and extremely myopic.


Modularization is indeed independent of programming discipline. That is basically my point. Breaking a program down into the correct components is all-important, but most of this happens well below the module level. Unless the system has been over-engineered to death, as OOP encourages.

Three cheers for not being swayed by appeal to authority, but I'm curious about what you think counts as "business OOP" that doesn't count as "enterprise," as "enterprise" seems vague enough to encompass pretty much anything.

Your critique of lists and conses is again centered on the notion that documentation and encapsulation should be done in code and enforced by the machine. These things may well be necessary to build an economy of software modules in a closed source world, but the original article is just one example of the brain damage that this sort of world creates.

I'm glad you've been exposed to functional concepts, and I don't think that business developers are idiots, but I do think they live in an environment that is by and large toxic to good programming practices. I'm not paranoid, I just see a lot of evidence for the validity of Conway's Law.


Beyond just the obvious "this is a bad way to design things" message, another good takeaway is just the fact that most OO-design tutorials/discussions/books use the worst possible examples that teach people bad habits.

When done well, the useful part of OO design is encapsulation and data-hiding, not inheritance, but everyone gets hung up on inheritance because it's easy to cobble together "is-a" examples from real-world-like things, which just trains people to abuse the model.

In the real world outside OO-design books and freshman CS classes people don't have separate classes for different shapes, they just have "Polygon" or whatever the graphics buffer requires. And no one has separate subtypes of "AbstractVehicle" for "PassengerCar" and "Motorcycle" and "CommercialTruck," they just have a "Vehicle" class with a bunch of data on it and rely on the client code knowing what to do with that data.


This is why Eric Evans' "Domain-Driven Design" should be on every programmer's reading list. He applies object-oriented principles (though not exclusively) to real-world-sized problems, frequently pointing out the traps inherent in the enterprisey languages most big systems will use.


I understand the author is trying to show pitfalls of OO design. But the square/rectangle example doesn't do it for me.

1 - "There are actually several problems here. Thread safety is one, but let’s assume the class doesn’t need to be thread-safe."

The Rectangle implementation has a thread issue without regard to the Square subclass:

   public double getPerimeter() {
    return 2*width + 2*height;
  }
2 - "In object oriented programming, it is necessary that a subclass be able to fulfill the contract of its superclass. In this case, that means the square has to respond to setHeight() and setWidth() calls. However doing so enables the square to violate the nature of a square. A square cannot stand in for a rectangle.

You can try to work around this by overriding the setHeight() and setWidth() methods. For example, one might call the other:...

However this is fundamentally unsatisfying because there is no reasonable expectation that calling one of setHeight() on a Rectangle object will also invoke the setWidth() method or vice versa."

This is where things unwind. It is perfectly expected that calling setHeight() on a Square will also change its width.

3 - "We could instead just forbid setHeight() and setWidth() completely by throwing UnsupportedOperationException"

Yep, you could do that and it does show you are doing more work in working around using Square as a subclass of Rectangle. So either don't subclass it or accept the fact that its perfectly normal that if you have a Square, changing either its width or height will effect the other.

The author is trying too hard here. Its decent writing and a good attempt to show how subclassing can lead to unintended consequences. But this is not a great example.


"It is perfectly expected that calling setHeight() on a Square will also change its width."

If you're writing code that operates on Rectangles you wouldn't expect setHeight to have the side-effect of changing the width too.

It's only "perfectly expected" if you know about the implementation details of Square, which may not even have existed until after a lot of Rectangle code was already in place.


>It's only "perfectly expected" if you know about the implementation details of Square, which may not even have existed until after a lot of Rectangle code was already in place.

I don't quite agree. You don't need to know implementation details of Square (e.g. does SetHeight set the height and call SetWidth, or does it just call SetSide? Or does it, as well as SetWidth, set some new property called side?), you just need to know how a Square differs from a Rectangle (width == height).

Because Squares are a subset of Rectangles, any Rectangle code should also work with Squares. Even without Square, prevents you instantiating a Rectangle and always calling SetWidth(x) and SetHeight(x) one after the other.


your correct. And this is why the example is not good. Your saying someone might not expect the "side effect" of width changing when height changes. But this is a semantic setup. People do know how a Square is supposed to behave. One of the characteristics of a "first class object" is that there is common agreement on behavior. If we don't have this shared agreement, lots of things about OO design fall apart.


If the method isn't final, I would consider the possibility of implementation differences in subclasses. I am trying to think of a case where it would cause a problem, and most of those involve methods that probably ought to be in the Rectangle class anyway (and thus could be overridden in square).

To me the cleanest solution is :

setDimensions(height, width)

where the contract for square enforces the equality of height and width.


Yes, it's true that "it is perfectly expected that calling setHeight() on a Square will also change its width," which is why Square has its own class. However, if Square is subclassed from Rectangle, some Squares will be called in a context where setHeight is intended to be called on a Rectangle.

Consider if you have a collection of rectangles, and you called SetWidth on all of them (say you selected a column of rectangles and wanted to narrow them), you would not expect that two out of nine of your collection also changed its height.

If you have cached computations that depend on the rectangles' height (e.g. totalHeight of the column), your cached computations are now wrong and you are none the wiser.

Sure at this point we both know that there is a Square subclass of Rectangle that does different things, and we know I threw in a couple Squares into my array of Rectangles, but the key point is this:

The programmer who created the array of Rectangles might not know about Squares.

The Square class could have been written much later than her code, and suddenly you have violated the assumptions she was relying on, and guess who is going to be blamed for breaking the app?


So. Squares violate the assumptions that Rectangles established. Where did these "assumptions" come from? Surely the programmers are not looking at the implementation of a class rather than its interface?

If we have a language and a system where you have an elaborate vocabulary for constraining the types of messages we can send to an object and the parameters we can associate with those methods but we have absolutely zero ways to constrain what those messages will return and what changes of state those messages will cause...

Well then there are going to be these kinds of arguments. I think tools can help here. For one thing, we could use languages like Eiffel that give us Design by Contract that is checked by the compiler. If setting the width is not supposed to set the height, we can make that part of the contract for the setWidth(...) method of a Rectangle and our compiler will not allow us to write a Square class that is-a Rectangle.

And alternate approach with less strict languages is to write tests for Rectangle. I'm a big fan of Strict Liskov Equivalence (http://weblog.raganwald.com/2008/04/is-strictly-equivalent-t...). In a project with tests, I believe that any test written for the Rectangle class should work for instances of all of its subclasses.

Therefore if we write a test for the rectangle class and we include a check that setting width does not affect height, then a Sqauare cannot be a Rectangle whether the compiler barks at us or not. The test in our project establishes that expectation for all Rectangle subclasses.

OTOH, if no such test exists, than the programmer making that assumption is wrong. She should either drop the assumption or add the appropriate tests to the project to document her understanding of the contract of a Rectangle.


I see what your getting at. But this whole example is a setup. Let me go through some of your arguments to show just how easily they can be turned.

1 - "you would not expect that two out of nine of your collection also changed its height."

If two of them were Squares, I would expect height and width to mirror each other. Your saying you wouldn't expect it. Well, that is a problem if two different programmers using the same classes have two different views of the object's behavior. This can happen with any implementation of any class.

2 - "If you have cached computations that depend on the rectangles' height"

Your violating encapsulation of the object's attributes by "caching" this data elsewhere. If you need to do such things, this is what MVC was invented to handle: an attribute changes, it fires an event and dependents refresh themselves, caches invalidate, etc.

3 - "The Square class could have been written much later than her code"

Its still a Square (by my definition). The new Squares she has are still Rectangles too. Why did this programmer assume that changing a rectangle's length would never effect its width? This invalid assumption could have been made prior or after the introduction of Squares.

In short, the original argument is bad because almost everyone agrees about the behavior of a Square. Key to use of any code is agreement on how it works. For example, If I use jQuery and can't wrap my mind on how John Resig thinks I should be using his library, its my problem.


> This can happen with any implementation of any class.

From the definition of a square (a polygon with four sides of equal length) and we are trying to infer behaviour. But the definition does not require a square to remain a square after transformation; a square with a skew applied need not be a square nor a rectangle. Treating the square as a concrete class-based entity cannot capture this reality.

You are correct, this could happen with any class.


Actually you kind of miss the point.

If squares are a subclass or specialization of rectangle that the consumer does not recognize then the consumer will be lied to.

They are using "rectangles" rectangles can change height apart from width and still be rectangles. But if you are a squre then you can't change height apart from width.

The consumer code has every right to expect all rectangles to work like rectangles. But squares break the rectangle contract.

As far as state goes squares are rectangles. But when it comes to behaviour or valid operations squares aren't rectangles. This is the authors point in fact and is why he says immutability makes it go away. When you bundle both state and operations into an object you make it that much more difficult to model your domain.

Another solution to the articles problem would be to have objects that only contain state. Quadrangle state objects. And other objects that contain only operations. A quadrangle resizer object. resizers know that a square which has it's height changed apart from it's width is now just a rectangle This far more accurately models the problem domain.

Squares shouldn't be responsible for knowing whether they can become just rectangles. That should be your knife operators job.


"If two of them were Squares, I would expect height and width to mirror each other. Your saying you wouldn't expect it."

No, I wouldn't expect it, because I don't know about Squares. Don't be misled because this example are uses shapes we all learn in kindergarten. The real lesson is about all the abstractions we make up all day long building applications in the real world. Think of it as Frobulator and the false subclass MagicFrobulator.

Caches happen everywhere. Going back to squares and rectangles, I stated the collection was a column. Each rectangle rests on the one before it in the collection. When I narrow the column, I set the width of every Rectangle in the collection, but now unbeknownst to me some elements have changed their height. If the column happened to be in a collection of columns sorted by height, it would now be out of sort order. The original programmer would make a reasonable assumption (well, duh) that the column sort order would not be affected by a SetWidth call.

"Its still a Square (by my definition). The new Squares she has are still Rectangles too. Why did this programmer assume that changing a rectangle's length would never effect its width? This invalid assumption could have been made prior or after the introduction of Squares."

The definition of Rectangle states that changing the width would not change the height. I don't understand your objection.

I think using something as familiar as shapes is confusing the point.

Again, go back to Frobulator and MagicFrobulator. In 2005, I invent Frobulator which has two properties foo and bar that are mutually exclusive. I release Frobulator to the world and make a million dollars.

In 2008 you have a revelation that you could make a billion dollars if you could just simplify Frobulator. You decide that you only really need foo, and you can kind of obsolete bar by making it dependent on foo. You then market this as MagicFrobulator with the pitch, "Throw out your Frobulators! MagicFrobulator is the perfect replacement."

Customers buy your MagicFrobulators and plug them in and discover to their horror that they explode in a shower of Star Trek induced sparks. As it turns out, other systems have regulated themselves by setting and getting foo and bar independently. Now, Widgets set foo and screw up Gadgets who did not expect bar to have changed. Bar just happens to control the amperage, and so the Gadgets explode.


If you use setFoo() and getFoo() then you are not using OOP at all.

Encapsulation is simple thing - just write setters and getters for all your internal data and you done. ;-)

PS.

«If the classes are immutable, there is no problem with this state of affairs. Circles satisfy all invariants of ellipses; and an (immutable) circle can be used in any context where an immutable ellipse is expected.»


I agree. You can't have a rectangle with a null width, so why should you be able to instantiate one?


Sigh.

Those who do not read Quine are doomed to reproduce his results, as I remind people every time Uncle Bob or someone else of the cabal starts going off on this problem:

http://www.reddit.com/r/programming/comments/7zy40/writing_u...




This smells like a job for Type Classes.


Studying functional programming can really illumniate certain concepts in object oriented programming. The solution is a sum type which you can simulate in OOP using an abstract base class (in C# parlance) and a set of sibling classes that derive from it. So, as other have mentioned, you could have an abstract Polygon (or quadralateral) base class


I had an instructor once who started a lecture with this example, and then argued that rectangle should be a subclass of square.


Mirrored from Google's cache: http://jottit.com/qykre/


Well, the problem in my opinion is, that a square is a rectangle, indeed. However, this is not square is-a rectangle;, but rather: square is-a rectangle with rectangle.height == rectangle.width;

This is overall something I have been pondering (and recently planning and implementing) to extend the standard object orientation: adjectives (opposed to 'nouns' (objects) and 'verbs' (methods)).

An adjective would simply describe a noun furhter and simplify a dispatch inside a method depending on the state of an object.

Some pseudosyntax from the current state of ideas would go like this:

  adjective_group rectangle_type(R) {
     if(R.width == R.height) {
       return square;
     } else if(R.width * 4 = R.height * 3) {
       return widescreen;
     }
   }

  draw(square rectangle R) {
     // specialized draw for squares
   }
   draw(widescreen rectangle R) {
     // specialized draw for widescreen rectangles
   }
   draw(rectangle R) {
     // general drawing method for rectangles
   }
This goes pretty deep if one starts to think about it: * Buffers: empty, full, something in between * Abstract syntax trees: well-typed, ill-typed * Accounts: banned, active, moderator, admin

Basically, it simplifies the pattern

  method(object) {
    if (predicate_1(object.state)) {
      // stuff
    } else if(predicate_2(object.state)) {
      // other stuff
    } else {
      // default stuff
    }
  }
I am just currently working on a dispatch mechanism which is at least a bit intuitive :)


Have you ever used a language that makes heavy use of pattern matching? Prolog, in particular, or to a lesser extent (due to increasing amounts of distraction by other novel features) Erlang, OCaml/SML, or Haskell. It sounds like you're essentially describing functional dispatch based on structural attributes or current characteristics, rather than a (generally fixed) object identity.


yes, this is where a large part of the idea comes from.


My Haskell way of thinking of it would be that Square and Rectangle are distinct (non-inheriting) data types, Square and Rectangle are both instances of the type-class Rectangular (which inherits and provides default functions for Closed and Polygonal), Square has a transformation into Rectangle, and Rectangle has a transformation into Maybe Square.


OO is an incredibly powerful tool but is unnecessary for so many tasks. The contrived examples of many books show beautifully, as does this example, simple inheritance hierarchies that get at the gist of it but are not really an appropriate use of the technology. Everything does not need to be modeled as an object, oftentimes with only one or two methods, no internal state, etc. Some of the newer multi-paradigm languages (F# and Scala come to mind at first glance) seem poised to rescue us from improper use of OOP when you are stuck in an OO environment with a non-OO pattern or problem to solve. You get the OO stuff when you need it, with a more flexible syntax, without being strapped into a design straightjacket.


"Another is that it’s possible to give the sides negative lengths."

Is that really a problem that needs fixing? Also the with/height problem - one could argue the fault would lie with the code that tries to do both...

A while ago I had a realization as to what defines the Java developer mindset: it is fear driven development (FDD).

(Taking Java as an example because it is the only world I know a little bit better).

Maybe sometimes FDD is being called for, or maybe it really is the superior development style. Personally, I am quite sick of it, and it definitely makes it harder to get things done in my opinion.


If you believe OOP is "making physical objects' memory maps", instead of "related data put together" then yes there are some problems on some conditions. But this is caused by unrelated physical mapping need. If you can let go about this belief and think a bit lower level, OOP is a good enough methodology for thinking "like" objects. But it's not perfect. I don't know if this mapping need is good for software engineering.


I really like this comment from Uncle Bob: Inheritance is not ISA. It is a redeclaration of variables and functions in a sub-scope!

http://cafe.elharo.com/programming/a-square-is-not-a-rectang...


Funny I remember seeing a library (used somewhere internally) where the Rectangle inherited from square. It was a surprising discovery when reading the codebase but the implementation as odd tough.


Make sure to check out Uncle Bob's comment (it's first after the post.)


403 Forbidden

Damn. Did I miss it?


Okay, so I came back finally and checked again to see if I could get in.

My thought, now, is that a functional approach to making square a "subclass" (by stretching the term "subclass" out of the confines of its traditional Java-style OOP usage) of rectangle.

    (define (square s)
      (rectangle s s))
Simple. Yes?


a square is indeed not a rectangle therefore i see this as the only flaw that needs to be pointed out:

public class Square extends Rectangle

inheritance for the sake of implementation is bad OO design.


It is an old concept that got ignored somewhere along the road to java. Inheritance for the sake of implementation is called WAS_A. In C++, this is what private inheritance does. In languages like Java and Ruby, WAS-A is often implemented using delegation, so that a square HAS-A rectangle.


What about the rhombus?!




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: