Regardless of the specific language there are a number of general principles that when followed will result in code that is extensible and maintainable.
Modularity
Well designed software is broken down into smaller, self-contained, and manageable parts, or modules, that adhere to the following criteria. The word module can be substituted for “class”, or “package” and depends on the specific constructs of a given language.
High Cohesion
Ensures that a modules does one thing well and that its tasks are closely associated.
Tight Encapsulation
Limits direct access to internal state of a module protecting its integrity. If access is required is it implemented through a clearly defined contract or API.
Loose Coupling
Ensures modules are as independent as possible, allowing them to be tested and changed in isolation.
Separation of Concerns (SoC)
This is the guiding principle that dictates dividing software into distinct features and acts as the overarching strategy for achieving Modularity. This is the high-level philosophy that drives Cohesion and Loose Coupling.
In essence, it means to cleanly separate related responsibilities and behaviors of a system into distinct modules without spreading them across modules or mixing them together.
See https://www.geeksforgeeks.org/software-engineering/separation-of-concerns-soc/ for more details
SOLID
Five principles of Object-Oriented design (that can be applied to other “classless” languages) that when applied result in maintainable, scalable, and flexible software.
S – Single Responsibility Principle (SRP)
A class should have a single, well-defined job and should only have one reason to change.
O – Open-Closed Principle (OCP)
Classes should be open to extension, but closed to modification. This means that we should be able to add functionality to a class without having to change the existing code and this is typically implemented with interfaces and abstract classes.
L – Liskov Substitution Principle (LSP)
Any subclass must be substitutable for the base class without changing the correctness of the program. This means that you should not violate the intent or semantics of the abstraction that you are extending and or implementing.
I – Interface Segregation Principle (ISP)
Clients should not be forced to implement interfaces that they do not use. More specific interfaces are better than less generic interfaces.
D – Dependency Inversion Principle (DIP)
This is also known as “dependency injection”. Classes should only depend on interfaces or abstract classes and not concrete classes. Further, it is the responsibility of the client code instantiating the class that requires the dependency to also instantiate and “inject” the dependency into the class.
See https://www.freecodecamp.org/news/solid-principles-explained-in-plain-english/ for more details