This section will largely deal with OOP (Object Oriented programming).
Tech Software Principles
Don't Repeat Yourself (Very Core Principle)
DRY (Don't Repeat Yourself) In Terms of Code
DRY forbids duplicating knowledge, not code. Identical code representing different concepts that change independently isn't a violation—it's appropriate separation.
DRY = One source of truth per concept.
Benefits
Single-point maintenance — fix or update once, applied everywhere
Fewer bugs — no inconsistent copies drifting out of sync
Better readability — modular, organized code
Easier testing — test one unit, trust it everywhere
Consistent behavior — one source of truth per concept
The Key Distinction: Knowledge vs. Code Duplication
DRY targets knowledge duplication, not code duplication.
Identical-looking code isn’t a violation if it represents different concepts that should evolve independently. Two functions with the same implementation today might change for completely different reasons tomorrow—coupling them prematurely creates the wrong abstraction.
Not All Code Duplication Is Knowledge Duplication
As part of your online wine ordering application you’re capturing and validating your user’s age, along with the quantity they’re ordering. According to the site owner, they should both be numbers, and both greater than zero. So you code up the validations:
During code review, the resident know-it-all bounces this code, claiming it’s a DRY violation: both function bodies are the same.
They are wrong. The code is the same, but the knowledge they represent is different. The two functions validate two separate things that just happen to have the same rules. That’s a coincidence, not a duplication. - pragmatic-programmer
“A little copying is better than a little dependency” — Go Proverbs
Rule of Three: Wait until you see duplication three times before abstracting. Premature abstraction often creates worse problems than duplication. - rule-of-3
Perils of WET (Write Everything Twice) code
vs-WET-write-everything-twice-perils
WET (Write Everything Twice) Perils:
Maintenance Nightmare: With duplicated code, any change needs to be replicated across all instances. This makes maintenance cumbersome and error-prone.
Bug Propagation: If a bug exists in a duplicated code segment, it’s likely present everywhere the code is replicated. This can lead to widespread system vulnerabilities.
Increased Complexity: WET code often leads to bloated, complex systems that are hard to understand and navigate, making it challenging for new developers to onboard.
Scalability Issues: As the system grows, so does the duplicated code, leading to heavier, less manageable, and slower applications.
Inconsistency Risks: It’s easy to update one instance of the code and miss others, leading to inconsistent behavior across the application.
Poor Optimization: Duplicate code makes it harder to implement optimizations. Changes meant to improve performance must be applied in multiple places, increasing the risk of missing some.
When the DRY principle is applied successfully, a modification of any single element of a system does not require a change in other logically unrelated elements. — Wikipedia
The Single Responsibility Principle (SRP) is one of the five SOLID principles of object-oriented design. It states that a class should have only one reason to change, meaning it should have only one job or responsibility. This principle helps in creating more maintainable and understandable code by ensuring that each class or module focuses on a single functionality.
Key Benefits:
Maintainability: With SRP, classes are smaller and focused on a single task, making them easier to understand, modify, and extend.
Reusability: Well-defined responsibilities promote code reuse, as each class encapsulates a distinct piece of functionality.
Testability: Testing is simplified because classes have a single focus, reducing the complexity of unit tests.
Example:
Consider a class Invoice that handles both invoice calculations and printing.
Violation of SRP:
public class Invoice { public void calculateTotal() { // Code to calculate total } public void printInvoice() { // Code to print invoice }}
Adhering to SRP:
public class Invoice { public void calculateTotal() { // Code to calculate total }}public class InvoicePrinter { public void printInvoice(Invoice invoice) { // Code to print invoice }}
In the second example, the Invoice class is only responsible for calculations, while the InvoicePrinter class handles printing. This separation of concerns makes each class more focused and adheres to the SRP.
The Dependency Inversion Principle (DIP) is one of the five SOLID principles of object-oriented design. It states that high-level modules should not depend on low-level modules. Both should depend on abstractions (e.g., interfaces). Additionally, abstractions should not depend on details. Details (concrete implementations) should depend on abstractions (eg. interfaces).
Key Benefits:
Decoupling: Reduces tight coupling between high-level and low-level modules, making the system more modular.
Flexibility: Easier to swap out implementations without affecting high-level modules.
Maintainability: Simplifies changes and enhancements, as changes in low-level modules do not ripple through the entire system.
Example:
Consider a scenario where a CustomerService class depends on a MySQLDatabase class for data storage.
Violation of DIP:
public class MySQLDatabase { public void saveData(String data) { // Code to save data to MySQL database }}public class CustomerService { private MySQLDatabase database = new MySQLDatabase(); public void addCustomer(String customerData) { database.saveData(customerData); }}
In this case, CustomerService is tightly coupled to MySQLDatabase. If you want to switch to a different database, you must modify CustomerService.
Adhering to DIP:
Define an Abstraction:
public interface Database { void saveData(String data);}
Implement Concrete Classes:
public class MySQLDatabase implements Database { @Override public void saveData(String data) { // Code to save data to MySQL database }}public class MongoDBDatabase implements Database { @Override public void saveData(String data) { // Code to save data to MongoDB }}
Refactor High-Level Module:
public class CustomerService { private Database database; public CustomerService(Database database) { this.database = database; } public void addCustomer(String customerData) { database.saveData(customerData); }}
Usage:
public class Main { public static void main(String[] args) { Database mySQLDatabase = new MySQLDatabase(); CustomerService customerService = new CustomerService(mySQLDatabase); customerService.addCustomer("John Doe"); // Switch to MongoDB without changing CustomerService Database mongoDBDatabase = new MongoDBDatabase(); customerService = new CustomerService(mongoDBDatabase); customerService.addCustomer("Jane Doe"); }}
Summary:
The Dependency Inversion Principle promotes the use of abstractions to decouple high-level modules from low-level implementations. By relying on interfaces or abstract classes, you can swap out concrete implementations with minimal impact on the system, enhancing flexibility, maintainability, and modularity.
Program to an Interface Not to Implementation
To follow this well you should be using interfaces.
You should start by creating an interface and writing out how you want the interface to look like without being bogged down by implementation details yet.
Even when it seems like the interface is not needed yet and there will be just one implementation, creating an interface switches our mind into a different mindset.
Creating an interface forces us to scope down what the object is allowed to do.
Creating an interfaces makes it easy to see what is the object allowed to do.
Creating an interface makes it more likely that some javadoc documentation is written.
Further notes
Also note: Sometimes POJO is all you need.
There is some cost to creation of the interface. Not just in a manner of creating initially (hint: lower this cost by using intellij extract interface functionality to speed up creation of interfaces from classes that already exist). But more so later to hop from interface down to implementation.
Hence, at times it makes sense just to create a data object holding data without making an interface to it for the sake of KISS.
The Liskov Substitution Principle is one of the five object-oriented design principles collectively known as SOLID. It was introduced by Barbara Liskov in 1987 during her conference keynote. Essentially, the LSP states:
“Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.”
In simpler terms, if class B is a subtype of class A, then we should be able to replace A with B without disrupting the behavior of our program.
Why is LSP Important?
Highlights challenges with inheritance: Inheritance is taught as a core and relatively simple concept. While in reality inheritance is much harder to get right. LSP highlights this fact and points us towards: favor-composition-over-inheritance.
Promotes robustness: Adhering to the LSP increases the robustness of the system by ensuring that extending the system via inheritance does not introduce new bugs.
Enhances maintainability: Systems that follow LSP are more modular and easier to understand, making maintenance simpler.
Improves reusability: By ensuring that subclasses remain substitutable for their base classes, LSP aids in reusing components, which is a hallmark of effective object-oriented design.
Implementing LSP
To comply with the Liskov Substitution Principle, ensure that:
Subtypes enhance, not alter functionalities: Subclasses should implement their superclass methods in a way that does not weaken superclass behavior.
Do not strengthen preconditions: Subclasses should not impose stricter validation or input requirements than their base classes.
Do not weaken postconditions: The outcome and effects of overridden methods in subclasses should meet or exceed what is specified in the base class.
Exceptions are consistent: Subclasses should not introduce new exceptions that can be thrown by methods of the base class, unless those exceptions are equivalents of the base class exceptions.
Example Violation
Consider a class Bird with a method fly(). A subclass Penguin inherits from Bird but cannot fly. If the program attempts to make a Penguin fly, this would violate LSP because Penguin cannot be used as a substitute for Bird in this context.
Remember…
Following the Liskov Substitution Principle helps in creating truly modular systems, where individual components can be replaced and updated without affecting the integrity of the overall system. It’s all about keeping your inheritance tree clean and logical.
The Open/Closed Principle (OCP) is one of the five SOLID principles of object-oriented design. It states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means that the behavior of a module can be extended without modifying its source code, enhancing flexibility and reducing the risk of introducing bugs.
Key Benefits:
Maintainability: By avoiding modifications to existing code, the risk of introducing new bugs is minimized.
Scalability: New functionality can be added with minimal impact on the existing system.
Flexibility: Encourages the use of abstractions, making the codebase more adaptable to changing requirements.
Example:
Consider a class DiscountCalculator that calculates discounts for different customer types.
Violation of OCP:
public class DiscountCalculator { public double calculateDiscount(String customerType, double amount) { switch (customerType) { case "Regular": return amount * 0.05; case "Premium": return amount * 0.10; default: return 0; } }}
To add a new discount type, you must modify the calculateDiscount method, violating OCP.
// Step 1: Define an abstract base classpublic abstract class DiscountStrategy { public abstract double calculateDiscount(double amount);}// Step 2: Implement specific discount strategiespublic class RegularDiscount extends DiscountStrategy { @Override public double calculateDiscount(double amount) { return amount * 0.05; }}public class PremiumDiscount extends DiscountStrategy { @Override public double calculateDiscount(double amount) { return amount * 0.10; }}// Step 3: Use the strategy in DiscountCalculatorpublic class DiscountCalculator { public double calculateDiscount(DiscountStrategy strategy, double amount) { return strategy.calculateDiscount(amount); }}
In the second example, the DiscountCalculator class does not need to be modified to add a new discount type. Instead, new discount strategies can be created by extending the DiscountStrategy class. This adheres to the OCP, making the system more maintainable and scalable.
The Interface Segregation Principle (ISP) is one of the five SOLID principles of object-oriented design. It states that no client should be forced to depend on methods it does not use. Instead of having a single large interface, ISP recommends breaking down the interface into smaller, more specific ones, so that clients only need to know about the methods that are of interest to them.
Key Benefits:
Decoupling: Reduces the dependencies between classes, making the system more modular and easier to maintain.
Cohesion: Ensures that interfaces are more focused and relevant to the clients that use them.
Flexibility: Simplifies the implementation of classes by avoiding unnecessary methods.
Example:
Consider an interface Worker that combines multiple responsibilities.
Violation of ISP:
public interface Worker { void work(); void eat();}
A Robot class that implements Worker would have to implement eat method, which is not relevant.
public class Robot implements Worker { @Override public void work() { // Robot working code } @Override public void eat() { // Irrelevant for Robot }}
Adhering to ISP:
Separate the interface into more specific ones.
Now, Robot implements only the relevant interface.
public class Robot implements Workable { @Override public void work() { // Robot working code }}
And a HumanWorker can implement both.
public class HumanWorker implements Workable, Eatable { @Override public void work() { // Human working code } @Override public void eat() { // Human eating code }}
Summary:
The Interface Segregation Principle promotes the creation of fine-grained, specific interfaces that align with the actual needs of clients. By following ISP, developers can create more modular, maintainable, and understandable code, avoiding the pitfalls of large, monolithic interfaces that try to do too much.
While SOLID is very important principle it should also be balanced out with YAGNI and at times having a simpler implementation (KISS) that does not fully adhere to SOLID is the right balance.
KISS Principle: Keep it Super Simple, OR Keep It Simple, Stupid!
What is KISS? = Do It Simple.
The KISS principle stands for “Keep It Simple, Stupid”. It’s a reminder to avoid unnecessary complexity. Here’s how you can apply it:
Simplicity over complexity: If you’re writing code that your future self will need a treasure map and a Rosetta Stone to understand, you’re doing it wrong.
Reduce steps: If your code looks like it’s trying to win an award for “Most Steps Taken to Accomplish Something Simple,” you might want to reconsider your approach.
Clarify: Your code should be as easy to read as a children’s book, not like a cryptic crossword puzzle.
Why Embrace KISS?
Easier maintenance: Simple code is easier to fix when things go wrong. Think of it like LEGO blocks; if you stick to the basic shapes, it’s easier to replace one when it’s missing.
Enhanced readability: Other developers (or ‘future you’) will thank you. Simple code is like a well-organized bookshelf, making it easy to find what you need.
Efficient problem solving: Often, the simplest solution is also the most effective. Don’t build a robotic arm to flip a switch!
Examples of KISS in Action
Good: Using a loop to process items in a list.
Bad: Writing a separate function for each item in the list because you thought it might be “neat”.
Remember…
When you think you’ve made things simple enough, take a step back and see if you can make it even simpler. The goal is to make your code so straightforward that even your grandma could follow along. Well, maybe not… but close enough!
YAGNI stands for “You Aren’t Gonna Need It”, a principle in software development that advises against adding functionality until it is necessary. This principle is a key tenet of agile development methodologies, emphasizing the importance of simplicity and focusing on the current requirements rather than speculative future needs.
Key Benefits:
Simplicity: Keeping the codebase simple and manageable by avoiding unnecessary complexity.
Maintainability: Easier to understand, maintain, and refactor code when it only contains necessary features.
Efficiency: Saving time and resources by not implementing features that might never be used.
Focus: Concentrating on delivering immediate value to the users or stakeholders.
Application in Software Development:
Avoid Premature Optimization:
Optimize code only when there is a proven performance issue.
// Don't add complex caching logic prematurelypublic int calculateSum(int a, int b) { return a + b;}
Add Features When They Are Needed:
Implement features based on actual requirements, not assumptions.
// Avoid adding unnecessary methods or parameterspublic class User { private String name; public User(String name) { this.name = name; } // Add other methods only when needed}
Focus on Current Requirements:
Design and build for the present needs of the project.
// Only implement the features required for the current use casepublic class ShoppingCart { private List<Item> items = new ArrayList<>(); public void addItem(Item item) { items.add(item); } // Add other functionalities when there's a requirement}
Examples of YAGNI in Action:
Code Development:
Write code to solve the problem at hand without speculating on potential future needs.
Example: Avoid creating abstract base classes or extensive inheritance hierarchies unless there’s a clear requirement for them.
Project Management:
Prioritize features and tasks that deliver immediate value.
Example: In an agile backlog, only include and work on features that are necessary for the next release.
Summary:
The YAGNI principle encourages developers to keep the codebase lean and focused by implementing only what is needed at the moment. By adhering to YAGNI, you can avoid over-engineering, reduce complexity, and ensure that development efforts are aligned with actual requirements, resulting in more efficient and maintainable software.
The Principle of Least Surprise (POLS) is a design philosophy applied in software development, user interface design, and system architecture. It states that a system should behave in a way that least surprises the user or developer. This principle aims to make systems intuitive and predictable, reducing the cognitive load and potential for errors.
Key Benefits:
Usability: Users find systems easier to use when they behave as expected, improving user experience.
Learnability: New users can learn and adapt to the system more quickly if it operates in a familiar or predictable manner.
Maintainability: Developers find it easier to maintain and extend systems that follow established conventions and predictable patterns.
Application in Software Development:
Naming Conventions: Use meaningful and consistent names for variables, methods, and classes that clearly indicate their purpose.
// Bad naming - surprisingint d; // What does 'd' stand for?// Good naming - not surprisingint daysUntilDeadline;
Method Behavior: Ensure methods perform actions that are consistent with their names and avoid side effects.
// Surprising behaviorpublic void updateUserProfile(User user) { sendWelcomeEmail(user); // Side effect not indicated by the method name // Update profile logic}// Predictable behaviorpublic void updateUserProfile(User user) { // Update profile logic}public void sendWelcomeEmail(User user) { // Logic to send email}
User Interface Design: Design UI elements to behave in ways users expect based on common conventions and prior experiences.
Using familiar commands and options across different tools.
Example: -h or --help for displaying help information.
APIs:
Adhering to common RESTful conventions in web APIs.
Example: Using GET for retrieving data, POST for creating resources, PUT for updating resources, and DELETE for deleting resources.
Summary:
The Principle of Least Surprise emphasizes designing systems that meet users’ and developers’ expectations, making them intuitive and predictable. By following this principle, you can enhance usability, learnability, and maintainability, ultimately leading to a better overall experience and more robust software.