Every developer has experienced the joy of working on an application where things just work like a breeze; and also the pain of an application that collapses like a pack of cards on the slightest code change in the most unrelated part. (Of course there are people who enjoy this challenge). Most often the difference between these two is the Architecture and the Design of the application. If the foundation is not firm enough, any amount of effort on coding will never help you in any way.
The exact architecture and design does depend upon the problem at hand. But, there are some principles that can guide you in the process - help you choose between available alternatives. These are called Design Principles. Experts have identified 5 major principles for Object Oriented programming. These are referred as the SOLID principles. They provide a bunch of best practices to be used while laying out the design of an object oriented module. They help you make a SOLID design that should be able to sustain the changes in requirements.
The first and foremost in the list is the Single Responsibility Principle. This suggests that the responsibilities should be granulated and separated into smaller units across the components of the application to the extent that each component or class should have just one responsibility. This responsibility should be encapsulated and contained within the class so that any change to that part of the functionality should require changes in just that class - and that class should not be changed for any other requirement change.
Of course this may be too idealistic for a real life application. But, anyone who has enhanced an existing application can appreciate the pain of a minor change in requirements leading to a series of single line changes across several classes and modules in the code. The Single Responsibility Design Principle, can help you reduce this to a great extent.
One might want to design a module to be as generalized as possible - so that it can absorb any kind of change. But this is not possible in real life. The most general code is the one that does nothing. That is not what we want. The Open Close Principle gives you a direction to what should be open and what should be closed. Specifically, it suggests that your application should be open for extension and closed for modification.
The core functionality that you develop should be complete in itself - not dependent on the specific requirement. It should be developed in a way that it would do its job precisely and completely - independent of the application requirements. This should be extended to implement the specific functional requirement. Thus, any change in the requirements should not lead to modification of the core functionality - you just need to extend it to include the new functionality. This helps you split the core functionality of the application from the requirements.
The LSP says that a subclass should extend the parent's functionality in implementation and not in principle. The child class should be what the parent expects to be. This sounds trivial. But this is one important point that designers often forget in a hurry to reduce implementation time. People tend to misuse the inheritance and get any class inherit an existing class - just to reuse some code implemented there - overriding or just ignoring the rest. Such practices soon lead to chaos.
For example, a class responsible for getting the list of students from the database has code to instantiate the DB connection object and fire a query using a prepared statement object. It would have a method getStudentList that would return the list of students fetched by the database query. A subclass of this class should get the list of students - perhaps adding some flavor to it. You should not subclass this just to reuse the code that instantiates the DB connection to fire a query - and get the distance between two cities.
This is another important principle that enables the others. Consider a class that acts as a facade for several clients that talk to a module. It offers an interface for the clients to refer. In such a case, it is better to provide a separate interface for each client and its functionality - instead of creating a global interface that each client refers. Thus, you have the facade class that implements different interfaces - each for the particular client. If there is a change in one particular client, only that particular interface would be updated and the rest of the application can continue without recompiling.
This is an important principle in object oriented design - separation of the interface and functionality. Two modules should share an interface rather than a class. That keeps the implementations isolated from each other. If one module changes its implementation for some change - the other modules that access it need not be touched so long at the interface remains constant.
These are only guiding principles. They suggest what is the right direction to take while designing. It certainly does not mean that any design that follows these principles is a perfect design. Sometimes, overdoing these principles can create mess. For example, You cannot define an interface for each and every class in the application. That itself will cause chaos. You have to understand them and follow principles rather than the words.