OO is more like art than science. There is usually more than one way to achieve certain functionality but here are some general suggestions for good code.
- Encapsulate stuff. In other words, make fields private and other functions private, protected or public - make the security as restrictive as you can. The more secure, the less that the function can be called which reduces code use and the chance for problems as well as making it easier to know how to test it. It also prevents people calling functions that seem to be right when they are supposed to be called internally to a class. "Internal" is useful for keeping stuff visible inside a single assembly (dll). Think about the testing required for a dll with 100 public functions rather than one with 10!
- Only pass into functions what you need to use. Do not pass a parent object and then extract some property of it, if it needs the child, pass the child in. This again makes the intent of the function clearer and allows irrelevent changes (such as changing the parent type) to not affect functions that don't care.
- Try and make functions either call a list of other functions OR do something technical and low-level. The functions that call functions are easy to understand and check, the others should be small and easy to unit test. Mixing them makes them hard to understand.
- Try and work out whether it is better to replace structured constructs like switch statements and if/then/else with classes. Putting logic into structure is so much more robust and easier to check. Consider a statemachine in a switch, what is easier, to ensure that all the other case statements change the state variable correctly or in an OO state machine to have a class named MovingState that has specific transitions and guards coded into one function in one class? Again encapsulation is your friend. You can't affect anyone elses variables or functionality because you cannot see them.
- Try and consider a class as either data, control or boundary and don't mix them. Data classes are persistent (usually) and have functions related only to their data. Control classes can be created and destroyed as they are used and manipulate data or send information to the system boundary. Boundary classes translate from one system to another (including the user interface, ports, the network etc) and can be considered pluggable so that, for instance, a web service can be plugged in where a user interface was previously since they can both send data that is conceptually the same even if one has no actual user interface.
- Use the safest types for the data you have. If you are doing money stuff, a decimal is almost always preferred to a double or float because the maths functions work properly. If your data will be integer, use integers for similar reasons. If you have pairs of data that are related, either use something like a Map/Hashtable to link them or otherwise create a type to encapsulate both, don't assume that you can keep any related things up to date with each other without a setter function or constructor as the only means to do so.