8

I'm reading Clean Code

In Chapter 6, Objects and data structure

First talk about Data Abstraction:

Hiding implementation is not just a matter of putting a layer of functions between the variables. Hiding implementation is about abstractions! A class does not simply push its variables out through getters and setters. Rather it exposes abstract interfaces that allow its users to manipulate the essence of the data, without having to know its implementation.

We do not want to expose the details of our data. Rather we want to express our data in abstract terms.

And there is an example:

Concrete Point:

public class Point { 
    public double x; 
    public double y;
}

Abstract Point:

public interface Point {
    double getX();
    double getY();
    void setCartesian(double x, double y); 
    double getR();
    double getTheta();
    void setPolar(double r, double theta); 
}

And then continue with:

The beautiful thing about Abstract Poin is that there is no way you can tell whether the implementation is in rectangular or polar coordinates. It might be neither! And yet the interface still unmistakably represents a data structure.

But it represents more than just a data structure. The methods enforce an access policy. You can read the individual coordinates independently, but you must set the coordi- nates together as an atomic operation.

So I wonder how would it be a Rectangular Point class without violating 'I' in SOLID, Interface Segregation???

Does Rectangular Point Really need to implement getTheta and setPolar Functions???

0

6 Answers 6

6

To understand the Clean-Code example correctly, you need to assume a code base where modules which do something with those points require both, cartesian and polar coordinates, for the same point object, so the code can switch between those two representations "on-the-fly".

(This is not an artificial situation, there are lots of real-world applications where switching between these two representations is really a requirement).

Then, the ISP is not violated, since users of "points" require the interface in full, and obviously implementers must provide the interface implementation in full.

Of course, when your code base looks different, you may also separate the above interface into a CartesianPoint and a PolarPoint, and also provide an interface Point which derives from both. In a context where certain modules exclusively use the "cartesian part" of the interface, and others only the "polar part", not providing this two interfaces could indeed be seen as a ISP violation.

Also providing a two separate interfaces might allow to provide optimized implementations for each kind of point interface. Or, one would not even make use of data abstraction, and simply provide and use two different point classes where the internal representation is transparent to the user.

Don't forget the ISP (as well as the usage of interfaces) is not an end in itself, it is a means to an end. If you go too far with it, you end up with an overdesigned system with lots of boilerplate code.

3

As presented this is a really bad example.

The given Point class is not an implementation of the given interface that has the same name so nothing is demonstrated, it is just confusing. Instead of listing everything that is wrong with the example, here's a general observation.

A class could have multiple behaviors and still comply with the single responsibility principle. Like a cat can do many things, it can run, jump, scratch, eat and poop. You cannot split that up on the class level without losing your cat.

But you can have your cat class implement multiple interfaces, each of which represents a particular behavior: IRunner, IJumper, IScratcher, IEater and IPooper. That is the interface segregation principle: when it comes to implementing interfaces you split them up as far as you can. ICat would be a violation of ISP.

2
  • @GregBurghardt As a non-native speaker I am not sure I get the joke. It does occur to me now though that poop and run can both mean either escape or empty one's colon (in a controlled manner or not). Mar 12, 2022 at 22:13
  • Sorry about that. Admittedly it was a juvenile joke. I deleted the comment. But in all seriousness your answer is a good illustration of splitting interfaces up based on behavior. Mar 13, 2022 at 12:47
3

This is a perfect example of how Robert Martin is mostly wrong about things.

He usually starts out being right in theory. Hiding implementation. Don't expose data. Expose the abstract concept. Excellent points (no pun).

But then, when it comes to code, he completely misses the mark. This interface does expose the underlying data! He argues that you don't know which method exactly, so it's all good. That does not make any sense. Let's be real, these/some are getters, the exact same things he argued just a minute ago should not be there.

Also, supposedly we get to decide how to "actually" store the point. Does that help us at all? How should we decide? The only way to decide what an appropriate internal representation is to know how it is used. Even then, if you change the internal representation you'll have to track down every piece of code that uses it to see whether that change makes sense or not. That leads us to the next point:

So what abstractions would be appropriate for a Point? Well, the exact things you need for your application! You usually don't create generic objects. You create an object that fits in the requirements space of your application.

The question becomes, what do you need the point for? Let's assume you need some geometric stuff:

public interface Point {
   Point add(Point b);

   Point multiply(double scalar);

   Line to(Point b);
}

That is an abstraction! Now you can truly change whats inside. And you can decide whether that change is meaningful based on this one class, since all the use-cases are local.

Maybe this is more of a rant and not an answer, but hope it helps anyway. :)

12
  • "The only way to decide what an appropriate internal representation is to know how it is used." You seem to have missed the entire point of Martin's example, which is quite the polar (no pun intended) opposite of your claim here. By enforcing that any implementation of Point provides both a Cartesian and polar set of coordinates; you don't need to know how external callers will be using your Point type. The interface is precisely the thing that prescribes all ways in which the Point type can be used. So long as your class provides both Cartesian and polar coordinates, its job is done.
    – Flater
    Mar 17, 2022 at 10:57
  • "Even then, if you change the internal representation you'll have to track down every piece of code that uses it to see whether that change makes sense or not." Again the polar opposite of what the interface in Martin's example achieves. Abstraction specifically avoids this tight coupling because it uses the intermediary interface. The only reason you'd need to hunt down all usages is when the interface (not the implementation) changes - which is logically inevitable regardless of whether you abstract or not.
    – Flater
    Mar 17, 2022 at 10:59
  • "Let's assume you need some geometric stuff" You're moving the goalposts here. Instead of defining a point, you've sidestepped into calculations that you can perform on a point (whose concept must logically already be defined, because otherwise it wouldn't exist). This is completely unrelated to the topic at hand, i.e. defining the concept of a point in and of itself.
    – Flater
    Mar 17, 2022 at 11:01
  • @Flater "defining the concept of a point in and of itself". There is no such thing. That is my whole argument. There are no designs, interfaces, concepts, etc. without a specific and preferably narrow context. Also: "So long as your class provides both Cartesian and polar coordinates, its job is done.". How do you know? You don't even know what I want to do with it, therefore you can't possibly know whether either of those is appropriate, let alone optimal for my uses. Mar 17, 2022 at 11:35
  • "How do you know?" Because the interface prescribes it. That is precisely the point. An interface acts as the definition of a contract. At the stage where your class implements said interface, all that's left to do is to provide an implementation that complies with the contract - not to second guess the contract that you've already been given. If the contract should've been different, then that should be addressed in the definition of the interface. But, very intentionally, the interface design precedes and is separate from the design of the classes that will implement said interface.
    – Flater
    Mar 17, 2022 at 11:42
2

Does Rectangular Point Really need to implement getTheta and setPolar Functions???

Yes absolutely, if that’s needed. If not, why would you do that?

The I in SOLID actually says if this interface provides more than you need you’re within your rights to make a simpler one. So using code could be completely unaware that getR() exists. That doesn’t mean it must not exist. So long as something somewhere uses it having it is fine. Not knowing about it means you don’t care either way.

The point here isn’t that every point must be designed like this point. It’s that every point should be designed to support how it’s used. It should not be designed to force you to use it in only a certain way that’s based only on how it happens to store the information.

Abstraction gives you some helpful distance from that. With this code you don’t know how it’s stored. That’s a good thing.

Unless we’re talking about money. Don’t do this with money.

1

No.

If we look at your entire question body, you have contradictory expectations, and it is this contradiction that leads you to observe this alleged violation.

public interface Point 
{
    double getX();
    double getY();
    void setCartesian(double x, double y); 
    double getR();
    double getTheta();
    void setPolar(double r, double theta); 
}

This interface's design communicates a very clear expectation: any implementation of Point must provide both a Cartesian and polar set of coordinates.

Does Rectangular Point Really need to implement getTheta and setPolar Functions???

I cannot answer what an application must and mustn't do. That is obviously subjective to the specific application's requirements.
However, what I can state for a fact is that the Point interface explicitly demands that any implementation of Point must provide both Cartesian and polar coordinates.

If we take the Point interface as the correct documentation of our requirements, then yes, a Cartesian point (what you call "rectangular") must also expose its polar coordinates.

So I wonder how would it be a Rectangular Point class without violating 'I' in SOLID, Interface Segregation???

ISP states that you should subdivide separate contracts into separate interfaces, and not needlessly keep them tied together.

However, your Point interface explicitly demands that the Cartesian and polar coordinate contracts are kept together; which is an explicit requirement and therefore overrides ISP, which only applies in cases where the specific structure has not already been prescribed explicitly.

ISP simply doesn't apply here because the requirements specifically demand the two coordinate systems to be kept together.

0

Abstraction and ISP are complementary and should reinforce each other:

  • Abstraction is about generalizing a concept/idea (e.g. a type, a collaboration pattern) by getting rid of its implementations.
  • OOP interfaces are an important part of the toolbox that facilitates abstraction.
    Caution: Interfaces are not only abstractions: they define an abstractions yet at the same impose a contract about how the abstraction shall be used.
  • Separation of concerns is an essential ingredient to powerful abstraction: unrelated concepts that just happened to be used in the same implementation should not accidentally be mixed togehter (a horror film dramatizes the importance of this principle: The Fly).
  • ISP aims to enforce separation of concerns in interfaces.

Looking at the many geometric operations possible on a Point independently of its coordinates, and considering the many more coordinate systems that exist beyond the most popular CartesianCoordinate PolarCoordinate, ea can conclude that ISP should reinforce abstraction, but the complexity of geometry does not make this example the most suitable to pass that message.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.