Best Practices

This section will largely deal with OOP (Object Oriented programming).

Don't Repeat Yourself (Very Core Principle)

The DRY principle, "Don't Repeat Yourself," guides against duplicating code to make it more maintainable, less error-prone, and easier to update.

Instead of duplicating, extract abstractions.

DRY=Do it ONCE.

img

Though shall not copy paste code.

Benefits of DRY (Don't Repeat Yourself):

  1. Simplified Maintenance: Changes need to be made in just one place, reducing the effort and risk involved in code maintenance.

  2. Reduced Bug Risk: With single-source code, the likelihood of bugs spreading through duplication is minimized. Bugs fixed in one place are fixed system-wide.

  3. Enhanced Readability: DRY code tends to be more organized and modular, making it easier to understand, which is especially beneficial for new team members.

  4. Improved Scalability: Less code means lighter, more agile applications. DRY practices contribute to a codebase that scales more gracefully.

  5. Consistency: A single source of truth for each piece of logic ensures consistent behavior throughout the application.

  6. Optimization Opportunities: It's easier to optimize a codebase where each piece of logic is maintained in one place. Performance improvements benefit the entire application.

  7. Easier Testing: DRY principles often lead to a more testable code structure since you can test single units of logic thoroughly and be confident they work correctly wherever used.

Incorporating DRY principles typically leads to a codebase that's more maintainable, scalable, and robust, providing a solid foundation for growth and development. Conversely, WET code tends to introduce risks and inefficiencies that can significantly hinder a project's long-term viability.

Perils of WET code

Perils of WET code

WET (Write Everything Twice) Perils:

  1. Maintenance Nightmare: With duplicated code, any change needs to be replicated across all instances. This makes maintenance cumbersome and error-prone.

  2. 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.

  3. 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.

  4. Scalability Issues: As the system grows, so does the duplicated code, leading to heavier, less manageable, and slower applications.

  5. Inconsistency Risks: It’s easy to update one instance of the code and miss others, leading to inconsistent behavior across the application.

  6. 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.

Quotes

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. - wiki

Resources

Balancing out with real world:

While DRY is important. It's also important to not be dogmatic. And evaluate tradeoffs of real world.

A little copying is better than a little dependency

A little copying is better than a little dependency. - go-proverbs/video-ref

Take note of Rule of 3

DRY in its purist form can be a bit idealistic.

Hence, rule of 3 comes to balance it out.

The Rule of Three is a pragmatic approach to the Don't Repeat Yourself (Core Principle). While DRY emphasizes reducing duplication to maintain cleaner and more maintainable code, the Rule of Three acknowledges that duplication is sometimes unavoidable or even beneficial in the short term.

Key Points

  1. Duplication Tolerance:

    • First Time: When you write a piece of code for the first time, it's natural and necessary. No duplication exists at this stage.
    • Second Time: When you need to duplicate code a second time, it's at times acceptable. This allows you to explore different contexts and understand how the code is being reused.
    • Third Time: When you find yourself duplicating code for the third time, it's a signal that the code should be refactored. By the third instance, it's clear that the functionality is common enough to warrant abstraction.
  2. Advantages:

    • Clarity: Initially, allowing some duplication can make the code clearer and simpler to understand, especially when the exact needs of abstraction are not yet evident.
    • Flexibility: By the time you need to abstract the duplicated code, you have better insights into how it should be structured to serve multiple use cases effectively.
    • Avoiding Premature Abstraction: Prematurely abstracting code can lead to over-engineering. The Rule of Three helps in avoiding unnecessary complexity.
  3. Application:

    • Monitor for duplication during code reviews.
    • Use the third occurrence as a trigger to refactor and abstract the common code.
    • Ensure that the abstraction is well-named and self-documenting to maintain code readability.

Example

// First time: Code to send an email
public void sendEmail(String recipient, String subject, String body) {
    // logic to send email
}

// Second time: Code to send an SMS
public void sendSms(String phoneNumber, String message) {
    // logic to send SMS
}

// Third time: Similar logic appears again for sending notifications
public void sendNotification(String type, String recipient, String message) {
    if (type.equals("email")) {
        sendEmail(recipient, "Notification", message);
    } else if (type.equals("sms")) {
        sendSms(recipient, message);
    }
}

// Refactor the common sending logic
public void send(String type, String recipient, String subject, String message) {
    if (type.equals("email")) {
        // logic to send email
    } else if (type.equals("sms")) {
        // logic to send SMS
    }
}

By following the Rule of Three, you ensure that your code remains maintainable, readable, and efficient. This approach strikes a balance between avoiding premature optimization and adhering to the DRY principle.

SOLID

img

SOLID Principles

SRP (single-responsibility-principle)

img

Single Responsibility Principle (SRP)

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:

  1. Maintainability: With SRP, classes are smaller and focused on a single task, making them easier to understand, modify, and extend.
  2. Reusability: Well-defined responsibilities promote code reuse, as each class encapsulates a distinct piece of functionality.
  3. 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.

img


SRP in Product

SRP Product Wise: Focus on core Feature

Keep product focus on the core feature.

img

-- image from whats app scaling article

DIP (dependency inversion principle)

img

Dependency Inversion Principle (DIP)

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:

  1. Decoupling: Reduces tight coupling between high-level and low-level modules, making the system more modular.
  2. Flexibility: Easier to swap out implementations without affecting high-level modules.
  3. 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:

  1. Define an Abstraction:

    public interface Database {
        void saveData(String data);
    }
    
  2. 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
        }
    }
    
  3. 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);
        }
    }
    
  4. 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.

too many nested note refs

LSP-liskov-substitution-principle

img

What is LSP?

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:

  1. Subtypes enhance, not alter functionalities: Subclasses should implement their superclass methods in a way that does not weaken superclass behavior.
  2. Do not strengthen preconditions: Subclasses should not impose stricter validation or input requirements than their base classes.
  3. Do not weaken postconditions: The outcome and effects of overridden methods in subclasses should meet or exceed what is specified in the base class.
  4. 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.

OCP-open-closed-principle

img

Open/Closed Principle (OCP)

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:

  1. Maintainability: By avoiding modifications to existing code, the risk of introducing new bugs is minimized.
  2. Scalability: New functionality can be added with minimal impact on the existing system.
  3. 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.

Adhering to OCP (Using Strategy (Design Pattern) design pattern):

// Step 1: Define an abstract base class
public abstract class DiscountStrategy {
    public abstract double calculateDiscount(double amount);
}

// Step 2: Implement specific discount strategies
public 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 DiscountCalculator
public 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.

ISP-interface-segregation-principle

img

Interface Segregation Principle (ISP)

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:

  1. Decoupling: Reduces the dependencies between classes, making the system more modular and easier to maintain.
  2. Cohesion: Ensures that interfaces are more focused and relevant to the clients that use them.
  3. 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.

public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

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.

Note

While SOLID is very important principle it should also be balanced out with YAGNI and at times having a simpler implementation (Keep it super Simple (KISS)) that does not fully adhere to SOLID is the right balance.

KISS

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:

  1. 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.
  2. 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.
  3. 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

img

YAGNI = Do the Needed.

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:

  1. Simplicity: Keeping the codebase simple and manageable by avoiding unnecessary complexity.
  2. Maintainability: Easier to understand, maintain, and refactor code when it only contains necessary features.
  3. Efficiency: Saving time and resources by not implementing features that might never be used.
  4. 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 prematurely
    public 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 parameters
    public 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 case
    public 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:

  1. 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.
  2. 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.

POLS-principle-of-least-surprise

img

Principle of Least Surprise (POLS)

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:

  1. Usability: Users find systems easier to use when they behave as expected, improving user experience.
  2. Learnability: New users can learn and adapt to the system more quickly if it operates in a familiar or predictable manner.
  3. 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 - surprising
int d; // What does 'd' stand for?

// Good naming - not surprising
int daysUntilDeadline;

Method Behavior: Ensure methods perform actions that are consistent with their names and avoid side effects.

// Surprising behavior
public void updateUserProfile(User user) {
    sendWelcomeEmail(user); // Side effect not indicated by the method name
    // Update profile logic
}

// Predictable behavior
public 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.

<!-- Surprising behavior -->
<button onclick="deleteItem()">Save</button> <!-- Button labeled "Save" deletes an item -->

<!-- Predictable behavior -->
<button onclick="saveItem()">Save</button> <!-- Button labeled "Save" saves an item -->

Examples of POLS in Action:

  1. Consistent Command-Line Interfaces:

    • Using familiar commands and options across different tools.
    • Example: -h or --help for displaying help information.
  2. 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.

References


Children
  1. Ref
  2. Tech Guidance
  3. Tech Software Principles
  4. When in Doubt