Software Design

Overview

  • Software design connects the requirements of a system to the actual coding and debugging phases.

  • It is essential for both small-scale and large-scale projects.

  • The design process plays a critical role in managing project complexity, ensuring structural integrity, and balancing various necessary trade-offs.

Inherent Challenges

  • Wicked Problems: Design issues are often not fully understood until solutions start emerging, exemplified by the Tacoma Narrows Bridge disaster.

  • Sloppy Process: Software design typically involves trial and error, where mistakes are recognized and rectified to yield a clean and functional result.

  • Nondeterministic Nature: There can exist multiple valid solutions for any single design problem, which complicates decision-making.

Trade-offs in Software Design

  • Designers must juggle various competing objectives, including performance, simplicity, and extensibility.

  • Different design alternatives will cater to varying priorities, which are strongly dependent on the specific system requirements.

  • Limitations on design possibilities can simplify the process and prevent over-complexity.

Emergent Practice

  • Good design evolves over time, rather than being achieved instantaneously.

  • Elements like reviews, discussions, and real-world testing are instrumental in refining the design.

  • Flexibility in adapting and improving designs is a hallmark of effective emergent design.

Managing Complexity

  • The central technical imperative is to manage complexity effectively.

  • There exists a distinction between essential complexity (stemming from real-world scenarios) and accidental complexity (arising from poor initial design).

  • Segmenting larger problems into smaller, more manageable components is a foundational technique to mitigate complexity.

Design Heuristics

  • Minimal Complexity: Embrace simplicity to decrease cognitive load on users and developers.

  • Ease of Maintenance: Anticipate future code maintenance, particularly considering someone else may need to manage your code.

  • Loose Coupling: Maintain minimal interdependencies among components to enhance modularity and flexibility.

Typical Software Application Architecture

  • Presentation Layer: Represents the user interface.

  • Business Logic Layer: Encompasses the core logic of the application.

  • Data Access Layer: Interfaces with databases to retrieve or store data.

  • Database Layer: Persistently stores data across sessions.

  • (With an optional emphasis on flexibility based on platform or application specifics.)

Types of Architecture

  • Monolithic Architecture: The application is structured as a cohesive single unit where all layers (presentation, logic, data access) are intertwined in a single codebase, facilitating simpler initial development but causing increased scalability and maintenance challenges over time.

  • Microservices Architecture: The application is decomposed into a collection of loosely coupled services, each serving a single business goal. This distributes the complexity and improves scalability and maintainability.

Experience in Architecture

  • Students may find themselves utilizing microservices architecture in a professional environment, or employing 3-Tier Architecture in academically structured projects, where typical setups might include UI (HTML/React), Node.JS for business logic, and SQL databases for data management.

Characteristics of Good Software

  • Reusability: Code should be written such that it can be reused across other systems effectively.

  • Extensibility: Modifications in one segment of the application should not adversely affect other parts of the system.

  • High Fan-In: Aim to maximize the number of classes that utilize utility classes for efficiency.

  • Portability: The design should ensure that the application can be easily adapted or transferred to different platforms.

Architectural Organization Levels

  • System Level: Structure the entire system into subsystems or distinct components.

  • Class Level: Breakdown subsystems into relevant classes, allowing for clear functionality delegation.

  • Routine Level: Focus on the design of individual methods or routines within classes to enhance modularity.

Core Tenets of Design

  • Coupling: Indicates the degree of interdependency between modules. Low coupling is favorable as it promotes flexibility and ease of maintenance.

  • Cohesion: Represents how closely related and focused the responsibilities of a single module are. A higher degree of cohesion signifies a module that effectively performs a single task or a cohesive set of tasks.

Good Design Principles

  • Achieving a successful software design necessitates balancing various trade-offs while controlling complexity.

  • Design should evolve autonomously over time based on new insights or information gleaned.

  • Effectively managing complexity is a critical element in developing maintainable and efficient software solutions.

Challenges in Code Design

  • Example Analyses reveal problematic code structures, especially high coupling and low cohesion issues with the following function:

   function fetchDataAndProcess(url) {
       fetch(url)
       .then(response => response.json())
       .then(data => {
           // Process data inside the same function
           data.forEach(item => console.log(item.value * 2));
       })
       .catch(error => console.error('Error:', error));
   }
  • This design is challenging to maintain and properly reuse; it performs dual responsibilities: fetching and processing data.

Implications of Poor Function Design

  • The mention of the function's duality results in the adversities of reusability and testability; should the need arise to fetch data without processing (or vice versa), there will be a requirement to rewrite or refactor.

  • Testing both tasks simultaneously complicates unit testing due to the intertwining of responsibilities.

  • Effective isolation and testing can become significantly difficult because testing one part necessitates invoking the other.

Better Function Structure

  • Constructing a function that adheres to high cohesion and low coupling can be demonstrated as follows:

   function fetchData(url) {
       return fetch(url)
           .then(response => response.json())
           .catch(error => console.error('Error:', error));
   }

   function processData(data) {
       return data.map(item => item.value * 2);
   }

   function main(url) {
       fetchData(url).then(data => {
           const result = processData(data);
           console.log(result);
       });
   }
  • This approach allows for good separation of concerns and promotes a clearer structure for each function's responsibilities.

Coupling vs. Dependency Inversion

  • Illustrating a case of high coupling:

   function fetchUserData() {
       return { id: 1, name: "John" };
   }

   function getUserDetails() {
       const user = fetchUserData();
       // Directly dependent on fetchUserData. 
       console.log(user.name);
   }
  • A better approach would involve dependency injection:

   function getUserDetails(fetchFn) {
       const user = fetchFn();
       console.log(user.name);
   }
   getUserDetails(fetchUserData);

Cohesion Practices

  • Showing the flaws in a function that has low cohesion by performing multiple tasks:

   function fetchAndProcessUserData(userId) {
       const user = { id: userId, name: "John" };
       console.log(user.name + " (" + user.id + ")");
       console.log("Logging extra info: Operation complete.");
   }
  • Instead, via a high cohesion layout:

   function fetchUserData(userId) {
       return { id: userId, name: "John" };
   }

   function processUserData(user) {
       console.log(user.name + " (" + user.id + ")");
   }

   function logCompletion() {
       console.log("Logging extra info: Operation complete.");
   }

   const user = fetchUserData(1);
   processUserData(user);

Dependency Management Lessons

  • Hardcoding dependencies leads to problematic design. Passing parameters between functions can improve information flow and allows for the effortless swapping of implementations without necessitating code rewrites.

SOLID Principles Explained

  • Single Responsibility Principle (SRP): Each class or function should only have a single reason to change, thus having one responsibility which enhances maintainability and comprehendibility.

  • Example: Prefer a single function for data fetching or processing over combining both tasks.

  • Open/Closed Principle (OCP): Classes should be open for extension but closed for modification, allowing for the addition of new features without altering existing code, and reducing bug risks.

  • Example: One should add functionality by utilizing new modules rather than modifying existing ones.

  • Liskov Substitution Principle (LSP): Subclasses must be workable replacements for their base classes without affecting system integrity, ensuring derived classes extend functionalities accordingly.

  • Example: A subclass of a base Bird class should function properly and not disrupt existing behavior.

  • Interface Segregation Principle (ISP): Clients should not be coerced to rely on interfaces they do not utilize. Instead, large interfaces should be subdivided into smaller, specific ones.

  • This enhances cohesion by restricting clients to only know necessary methods and minimizing overhead.

  • Dependency Inversion Principle (DIP): High-level modules should not depend directly on low-level modules but rather on abstractions, and details should defer to abstractions.

  • This principle dramatically reduces coupling between system components and supports more robust designs.

DRY (Don't Repeat Yourself) Principle

  • Definition: Asserts there should be only one unequivocal representation for every segment of knowledge or logic in the system; the objective is to eliminate redundancy across code, data, and logic to facilitate easier maintenance.

  • Why It Matters:

    • Promotes easier maintenance since altering one instance reflects on all instances, hence fewer bugs.

    • Enhances the readability of code, presenting more concise logic free from repetitive clutter.

    • Reduces the overall complexity of codebases significantly.

DRY in Practice

  • Refactor Repeated Logic: Identify repetitive patterns and consolidate them into functions, classes, or modules.

    • Example: Create a unified validateUser() function to handle user validation instead of redundant code across various places.

  • Use Variables and Constants: Prefer variables/constants over hard-coded multiple instances.

    • Example: Define constants like const TAXRATE = 0.20; to manage rates easily and efficiently.

Refactoring Examples

  • Before Refactoring (Repeated logic for area calculation)
    javascript const area1 = 5 * 10; // Rectangle 1 (width * height) const area2 = 7 * 3; // Rectangle 2 (width * height)

  • After Refactoring

    function calculateArea(width, height) {
        return width * height;
    }
    const area1 = calculateArea(5, 10);
    const area2 = calculateArea(7, 3);
    
  • Before Refactoring (Hardcoded taxation logic)
    javascript const priceWithTax1 = 100 * 1.2; const priceWithTax2 = 200 * 1.2;

  • After Refactoring

    const TAX_RATE = 0.20;
    const priceWithTax1 = 100 * (1 + TAX_RATE);
    const priceWithTax2 = 200 * (1 + TAX_RATE);
    
  • Before Refactoring (Repeated formatting logic)
    javascript const price1 = '$' + 100; const price2 = '$' + 200;

  • After Refactoring
    javascript function formatPrice(amount) { return '$' + amount; } const price1 = formatPrice(100); const price2 = formatPrice(200);

Good Coding Practices

  • Good coding necessitates considering solutions seriously, utilizing tools like whiteboarding, flowcharts, and UML for structured thinking.

  • It is important to resist the impulse to dive directly into coding, even when dealing with seemingly straightforward problems, as elegant solutions may emerge when managed thoughtfully.

Ethical Considerations in Software Development

  • A significant realization is the potential for one's code to impact others, highlighted by future applications in critical areas like self-driving cars.

  • This acknowledges the joint responsibility placed on developers to ensure quality and safety in their coding practices so that their work can confidently support vital systems in modern contexts.