What is the best practice when it comes to writing classes that might
have to know about the user interface. Wouldn't a class knowing how to
draw itself break some best practices since it depends on what the
user interface is (console, GUI, etc)?
That depends on the class and the use case. A visual element knowing how to draw itself is not necessarily a violation of the single responsibility principle.
In many programming books I've come across the "Shape" example that
shows inheritance. The base class shape has a draw() method that each
shape such as a circle and square override. This allows for
polymorphism. But isn't the draw() method very much dependent on what
the user interface is?
Again, not necessarily. If you can create an interface (drawPoint, drawLine, set Color etc.), you can pretty much pass any context for drawing things onto something to the shape, for example within the shape's constructor. This would enable shapes to draw themselves on a console or any canvas given.
If we write this class for say, Win Forms, then
we cannot re-use it for a console app or web app. Is this correct?
Well, that's true. If you write a UserControl (not a class in general) for Windows Forms, then you won't be able to use it with a console. But that's not a problem. Why would you expect a UserControl for Windows Forms to work with any kind of presentation? The UserControl should do one thing and do it well. It's bound to a certain form of presentation by definition. In the end, the user needs something concrete and not an abstraction. This might only be partly true for frameworks, but for end-user applications, it is.
However, the logic behind it should be decoupled, so you can use it again with other presentation technologies. Introduce interfaces where necessary, to maintain orthogonality for your application. The general rule is: The concrete things should be exchangeable with other concrete things.
The reason for the question is that I find myself always getting stuck
and hung up on how to generalize classes so they are most useful. This
is actually working against me and I'm wondering if I'm "trying too
You know, extreme programmers are fond of their YAGNI attitude. Don't try to write everything generically and don't try too hard trying to make everything general purpose. This is called overengineering and will eventually lead to totally convoluted code. Give each component exactly one task and make sure it does it well. Put in abstractions where necessary, where you expect things to change (e.g. interface for drawing context, like stated above).
In general, when writing business applications, you should always try to decouple things. MVC and MVVM are great to decouple the logic from the presentation, so you can reuse it for a web presentation or a console application. Keep in mind that in the end, some things have to be concrete. Your users can't work with an abstraction, they need something concrete. Abstractions are only helpers for you, the programmer, to keep the code extensible and maintainable. You need to hink about where you need your code to be flexible. Eventually all abstractions have to give birth to something concrete.
Edit: If you want to read more about architecture and design techniques which can provide best practices, I suggest you read @Catchops answer and read about SOLID practices on wikipedia.
Also, for starters, I always recommend the following book: Head First Design Patterns. It'll help you understand abstraction techniques/OOP design practices, more so than the GoF book (which is excellent, it just doesn't suit beginners).