Capsule 1: Object-Oriented Design

Delve into the fundamental principles of Object-Oriented Design and learn how to apply them in software development.

Author: Sami Belhadj

Connect on LinkedIn

Hear the audio version :

1/2 :

2/2 :

Capsule Overview

Capsule 1 introduces the key principles of Object-Oriented Design (OOD), which is fundamental to building robust and scalable software systems. This capsule covers:

Session 1: Principles of Object-Oriented Design (2 hours)

Introduction

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "objects", which are instances of classes. OOP principles are essential for creating robust, reusable, and maintainable software.

Core Principles

1. Encapsulation

Encapsulation is the principle of bundling the data (variables) and the methods (functions) that operate on the data into a single unit, known as a class. It helps to protect the data from outside interference and misuse.

Example of Encapsulation
      class Employee:
        def __init__(self, name, salary):
            self.__name = name
            self.__salary = salary

        def get_name(self):
            return self.__name

        def set_salary(self, salary):
            self.__salary = salary

        def get_salary(self):
            return self.__salary
            

In this example, the Employee class encapsulates the properties name and salary, and provides methods to access and modify these properties.

2. Inheritance

Inheritance is a mechanism where a new class inherits properties and behaviors (methods) from an existing class. This promotes code reuse and establishes a natural hierarchy between classes.

Example of Inheritance
      class Person:
        def __init__(self, name):
            self.name = name

        def greet(self):
            print(f"Hello, my name is {self.name}")

      class Employee(Person):
        def __init__(self, name, salary):
            super().__init__(name)
            self.salary = salary

        def show_salary(self):
            print(f"My salary is {self.salary}")
            

In this example, the Employee class inherits from the Person class and adds additional properties and methods.

3. Polymorphism

Polymorphism allows objects of different classes to be treated as objects of a common superclass. It is often implemented through method overriding, where a subclass provides a specific implementation of a method already defined in its superclass.

Example of Polymorphism
      class Animal:
        def speak(self):
            pass

      class Dog(Animal):
        def speak(self):
            return "Bark"

      class Cat(Animal):
        def speak(self):
            return "Meow"

      animals = [Dog(), Cat()]

      for animal in animals:
        print(animal.speak())
            

In this example, the Dog and Cat classes override the speak method of the Animal class. The speak method is called on each object in the animals list, demonstrating polymorphism.

4. Abstraction

Abstraction involves hiding the complex implementation details and showing only the necessary features of an object. It helps in reducing programming complexity and effort.

Example of Abstraction
      from abc import ABC, abstractmethod

      class Vehicle(ABC):
        @abstractmethod
        def start_engine(self):
            pass

      class Car(Vehicle):
        def start_engine(self):
            print("Car engine started")

      class Motorcycle(Vehicle):
        def start_engine(self):
            print("Motorcycle engine started")

      vehicles = [Car(), Motorcycle()]

      for vehicle in vehicles:
        vehicle.start_engine()
            

In this example, the Vehicle class is an abstract class with an abstract method start_engine. The Car and Motorcycle classes provide concrete implementations of this method.

Session 2: SOLID Principles (2 hours)

Introduction

SOLID is a set of design principles that help developers create more understandable, flexible, and maintainable software. The principles are:

1. Single Responsibility Principle (SRP)

A class should have one, and only one, reason to change. This means that a class should only have one job or responsibility.

Example of SRP
      class Invoice:
      def __init__(self, items):
        self.items = items

      def calculate_total(self):
        total = sum(item.price for item in self.items)
        return total

      def print_invoice(self):
        for item in self.items:
            print(f"{item.name}: {item.price}")
        print(f"Total: {self.calculate_total()}")

      # Refactored version adhering to SRP
      class Invoice:
      def __init__(self, items):
        self.items = items

      def calculate_total(self):
        return sum(item.price for item in self.items)

      class InvoicePrinter:
      def __init__(self, invoice):
        self.invoice = invoice

      def print_invoice(self):
        for item in self.invoice.items:
            print(f"{item.name}: {item.price}")
        print(f"Total: {self.invoice.calculate_total()}")
        

In this example, the Invoice class is responsible for calculating the total, while the InvoicePrinter class is responsible for printing the invoice.

2. Open/Closed Principle (OCP)

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. This means you should be able to add new functionality without changing existing code.

Example of OCP
      class Discount:
      def apply(self, price):
        return price

      class PercentageDiscount(Discount):
      def __init__(self, percent):
        self.percent = percent

      def apply(self, price):
        return price * (1 - self.percent / 100)

      class FixedAmountDiscount(Discount):
      def __init__(self, amount):
        self.amount = amount

      def apply(self, price):
        return price - self.amount
        

In this example, we can add new types of discounts by creating new classes that inherit from Discount without modifying the existing discount classes.

3. Liskov Substitution Principle (LSP)

Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. This means that subclasses should extend the behavior of the superclass without changing its behavior.

Example of LSP
      class Bird:
      def fly(self):
        pass

      class Sparrow(Bird):
      def fly(self):
        print("Sparrow flying")

      class Ostrich(Bird):
      def fly(self):
        raise Exception("Ostrich can't fly")

      # Refactored version adhering to LSP
      class Bird:
      def move(self):
        pass

      class Sparrow(Bird):
      def move(self):
        print("Sparrow flying")

      class Ostrich(Bird):
      def move(self):
        print("Ostrich running")
        

In this example, the Ostrich class violates LSP because it cannot fly. The refactored version replaces fly with move, which allows different birds to implement appropriate movement behaviors.

4. Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they do not use. This means that a class should not be required to implement interfaces it does not use.

Example of ISP
      class Worker:
      def work(self):
        pass

      def eat(self):
        pass

      class Robot(Worker):
      def work(self):
        print("Robot working")

      def eat(self):
        raise Exception("Robot doesn't eat")

      # Refactored version adhering to ISP
      class Workable:
      def work(self):
        pass

      class Eatable:
      def eat(self):
        pass

      class Human(Workable, Eatable):
      def work(self):
        print("Human working")

      def eat(self):
        print("Human eating")

      class Robot(Workable):
      def work(self):
        print("Robot working")
        

In this example, the Robot class violates ISP because it is forced to implement the eat method. The refactored version splits the interfaces into Workable and Eatable, so that Robot only implements Workable.

5. Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.

Example of DIP
      class LightBulb:
      def turn_on(self):
        print("LightBulb: turned on")

      def turn_off(self):
        print("LightBulb: turned off")

      class Switch:
      def __init__(self, bulb):
        self.bulb = bulb

      def operate(self, action):
        if action == "on":
            self.bulb.turn_on()
        elif action == "off":
            self.bulb.turn_off()

      # Refactored version adhering to DIP
      class Switchable:
      def turn_on(self):
        pass

      def turn_off(self):
        pass

      class LightBulb(Switchable):
      def turn_on(self):
        print("LightBulb: turned on")

      def turn_off(self):
        print("LightBulb: turned off")

      class Fan(Switchable):
      def turn_on(self):
        print("Fan: turned on")

      def turn_off(self):
        print("Fan: turned off")

      class Switch:
      def __init__(self, device):
        self.device = device

      def operate(self, action):
        if action == "on":
            self.device.turn_on()
        elif action == "off":
            self.device.turn_off()
        

In this example, the Switch class depends on the LightBulb class, which violates DIP. The refactored version introduces the Switchable interface, which both LightBulb and Fan implement. The Switch class now depends on the abstraction Switchable.

Practical Exercises

  1. Refactor a given class to adhere to the Single Responsibility Principle.
  2. Create a new feature by extending a class following the Open/Closed Principle.
  3. Identify and refactor a class hierarchy that violates the Liskov Substitution Principle.
  4. Split a large interface into smaller interfaces to adhere to the Interface Segregation Principle.
  5. Refactor a dependency in your code to follow the Dependency Inversion Principle.

Session 3: Clean Code Practices (2 hours)

Introduction

Writing clean code is essential for creating maintainable, readable, and efficient software. Clean code practices ensure that the codebase is understandable, reduces technical debt, and facilitates easier debugging and enhancement.

1. Meaningful Names

Choosing meaningful names for variables, functions, classes, and other identifiers makes the code more readable and self-explanatory.

Example of Meaningful Names
      # Bad naming
      def calc(x):
      return x * 2

      # Good naming
      def calculate_area(radius):
      return radius * 2 * 3.14159
        

In this example, the function name calculate_area clearly indicates its purpose, unlike the ambiguous name calc.

2. Functions Should Do One Thing

Functions should be small and focused on a single task. This makes them easier to understand, test, and maintain.

Example of Single Responsibility in Functions
      # Bad example
      def process_order(order):
      validate_order(order)
      save_order(order)
      notify_user(order)

      # Good example
      def validate_order(order):
      # validation logic

      def save_order(order):
      # save logic

      def notify_user(order):
      # notification logic
        

In this example, the single function process_order is split into three smaller functions, each handling a specific task.

3. Use Comments Wisely

Comments should be used to explain why something is done, not what is done. Clean code should be self-explanatory as much as possible.

Example of Using Comments Wisely
      # Bad example
      i = 0  # Initialize i to 0

      # Good example
      # Resetting the user login attempt counter
      login_attempts = 0
        

In this example, the good comment explains the reason behind the action, while the bad comment is redundant and explains what is already clear from the code.

4. Consistent Coding Style

Adhering to a consistent coding style, such as following naming conventions, indentation, and formatting, makes the code more predictable and readable.

Example of Consistent Coding Style
      # Bad example
      def myFunction ( ):
      print("Hello World")

      # Good example
      def my_function():
      print("Hello, World!")
        

In this example, the good code follows a consistent naming convention, proper indentation, and spacing.

5. Avoid Magic Numbers

Magic numbers are unnamed numerical constants that appear in the code without explanation. Use named constants instead.

Example of Avoiding Magic Numbers
      # Bad example
      if score > 9000:
      print("It's over 9000!")

      # Good example
      MAX_POWER_LEVEL = 9000

      if score > MAX_POWER_LEVEL:
      print("It's over 9000!")
        

In this example, the use of the named constant MAX_POWER_LEVEL makes the code more readable and easier to maintain.

6. Handle Errors Gracefully

Proper error handling ensures that the software can handle unexpected situations without crashing or producing incorrect results.

Example of Graceful Error Handling
      # Bad example
      def read_file(file_path):
      f = open(file_path)
      data = f.read()
      return data

      # Good example
      def read_file(file_path):
      try:
        with open(file_path, 'r') as f:
            return f.read()
      except FileNotFoundError:
        return "File not found"
      except IOError:
        return "Error reading file"
        

In this example, the good code includes error handling to manage file reading errors gracefully.

7. Avoid Duplication

Duplicated code can lead to inconsistencies and make maintenance harder. DRY (Don't Repeat Yourself) principle should be followed.

Example of Avoiding Duplication
      # Bad example
      def calculate_discount(price):
      return price * 0.1

      def calculate_tax(price):
      return price * 0.2

      # Good example
      def calculate_percentage(price, percentage):
      return price * percentage

      def calculate_discount(price):
      return calculate_percentage(price, 0.1)

      def calculate_tax(price):
      return calculate_percentage(price, 0.2)
        

In this example, the good code refactors the common logic into a separate function to avoid duplication.

Practical Exercises

  1. Refactor a piece of code by renaming variables, functions, and classes to more meaningful names.
  2. Break down a large function into smaller, single-responsibility functions.
  3. Review a codebase and add comments to explain why certain decisions were made, while removing redundant comments.
  4. Adopt a consistent coding style in a given code snippet and format it accordingly.
  5. Identify magic numbers in a codebase and replace them with named constants.
  6. Implement proper error handling in a function that currently lacks it.
  7. Refactor duplicated code into a single reusable function or module.

Session 4: Refactoring Techniques (2 hours)

Introduction

Refactoring is the process of restructuring existing computer code without changing its external behavior. Its purpose is to improve the nonfunctional attributes of the software, such as readability, maintainability, and scalability.

1. Extract Method

Extract Method involves moving a fragment of code from an existing method into a new method with a name that explains the purpose of the method.

Example of Extract Method
      # Before refactoring
      def print_owing(invoice):
      outstanding = 0.0
      print("*************************")
      print("****** Customer Owes *****")
      print("*************************")

      for item in invoice:
        outstanding += item.amount

      print(f"Amount owed is {outstanding}")

      # After refactoring
      def print_banner():
      print("*************************")
      print("****** Customer Owes *****")
      print("*************************")

      def calculate_outstanding(invoice):
      outstanding = 0.0
      for item in invoice:
        outstanding += item.amount
      return outstanding

      def print_owing(invoice):
      print_banner()
      outstanding = calculate_outstanding(invoice)
      print(f"Amount owed is {outstanding}")
        

In this example, the banner printing code and the calculation of the outstanding amount are extracted into separate methods, improving readability and maintainability.

2. Inline Method

Inline Method involves replacing a method call with the method’s content. This technique is useful when a method body is as clear as the method name.

Example of Inline Method
      # Before refactoring
      def get_rating(driver):
      return more_than_five_late_deliveries(driver)

      def more_than_five_late_deliveries(driver):
      return driver.number_of_late_deliveries > 5

      # After refactoring
      def get_rating(driver):
      return driver.number_of_late_deliveries > 5
        

In this example, the content of the more_than_five_late_deliveries method is inlined into the get_rating method.

3. Extract Class

Extract Class involves moving part of the code from an existing class into a new class to make the code easier to understand and manage.

Example of Extract Class
      # Before refactoring
      class Person:
      def __init__(self, name, office_area_code, office_number):
        self.name = name
        self.office_area_code = office_area_code
        self.office_number = office_number

      def get_telephone_number(self):
        return f"({self.office_area_code}) {self.office_number}"

      # After refactoring
      class TelephoneNumber:
      def __init__(self, area_code, number):
        self.area_code = area_code
        self.number = number

      def get_telephone_number(self):
        return f"({self.area_code}) {self.number}"

      class Person:
      def __init__(self, name, office_telephone):
        self.name = name
        self.office_telephone = office_telephone

      def get_telephone_number(self):
        return self.office_telephone.get_telephone_number()
        

In this example, the telephone number-related code is extracted into a new TelephoneNumber class, simplifying the Person class.

4. Move Method

Move Method involves moving a method from one class to another. This is useful when a method uses more features of another class than the class it is currently in.

Example of Move Method
      # Before refactoring
      class Account:
      def __init__(self, bank, amount):
        self.bank = bank
        self.amount = amount

      def calculate_interest(self):
        return self.bank.interest_rate * self.amount

      class Bank:
      def __init__(self, interest_rate):
        self.interest_rate = interest_rate

      # After refactoring
      class Account:
      def __init__(self, bank, amount):
        self.bank = bank
        self.amount = amount

      class Bank:
      def __init__(self, interest_rate):
        self.interest_rate = interest_rate

      def calculate_interest(self, amount):
        return self.interest_rate * amount
        

In this example, the calculate_interest method is moved from the Account class to the Bank class where it belongs.

5. Replace Temp with Query

Replace Temp with Query involves replacing temporary variables with method calls to improve readability and reduce duplication.

Example of Replace Temp with Query
      # Before refactoring
      def get_price(order):
      base_price = order.quantity * order.item_price
      discount_factor = 0.98 if base_price > 1000 else 0.99
      return base_price * discount_factor

      # After refactoring
      def get_base_price(order):
      return order.quantity * order.item_price

      def get_price(order):
      return get_base_price(order) * get_discount_factor(order)

      def get_discount_factor(order):
      return 0.98 if get_base_price(order) > 1000 else 0.99
        

In this example, the temporary variables base_price and discount_factor are replaced with queries, improving readability.

Practical Exercises

  1. Identify a long method in a given codebase and refactor it using the Extract Method technique.
  2. Find a method that can be inlined and refactor it using the Inline Method technique.
  3. Refactor a class that handles multiple responsibilities by extracting some of its code into a new class using the Extract Class technique.
  4. Identify a method that uses more features of another class and refactor it using the Move Method technique.
  5. Refactor a function with temporary variables by replacing them with method calls using the Replace Temp with Query technique.

Session 5: Design by Contract (2 hours)

Introduction

Design by Contract (DbC) is a programming methodology where software designers define precise and verifiable interface specifications for software components. These specifications include assertions such as preconditions, postconditions, and invariants, which must hold true during the execution of the software.

1. Preconditions

Preconditions are conditions that must be true before a method or function is executed. They specify the requirements that the caller must meet before invoking the method.

Example of Preconditions
      class BankAccount:
      def __init__(self, balance):
        self.balance = balance

      def withdraw(self, amount):
        assert amount > 0, "Withdraw amount must be positive"
        assert self.balance >= amount, "Insufficient funds"
        self.balance -= amount
        return self.balance
        

In this example, the preconditions ensure that the withdrawal amount is positive and that the account has sufficient funds before proceeding with the withdrawal.

2. Postconditions

Postconditions are conditions that must be true after a method or function has executed. They specify the guarantees that the method provides to the caller.

Example of Postconditions
      class BankAccount:
      def __init__(self, balance):
        self.balance = balance

      def deposit(self, amount):
        assert amount > 0, "Deposit amount must be positive"
        initial_balance = self.balance
        self.balance += amount
        assert self.balance == initial_balance + amount, "Postcondition failed: incorrect balance after deposit"
        return self.balance
        

In this example, the postconditions ensure that the deposit amount is positive and that the account balance is correctly updated after the deposit.

3. Invariants

Invariants are conditions that must always be true for a class instance, both before and after any method call. They ensure the object's consistency throughout its lifecycle.

Example of Invariants
      class BankAccount:
      def __init__(self, balance):
        self.balance = balance
        self._check_invariants()

      def _check_invariants(self):
        assert self.balance >= 0, "Invariant failed: balance must be non-negative"

      def withdraw(self, amount):
        assert amount > 0, "Withdraw amount must be positive"
        assert self.balance >= amount, "Insufficient funds"
        self.balance -= amount
        self._check_invariants()
        return self.balance

      def deposit(self, amount):
        assert amount > 0, "Deposit amount must be positive"
        self.balance += amount
        self._check_invariants()
        return self.balance
        

In this example, the invariant ensures that the account balance is always non-negative. The _check_invariants method is called after each operation that modifies the balance to ensure this condition holds.

4. Benefits of Design by Contract

Practical Exercises

  1. Add preconditions to a method in a given codebase to ensure the input parameters meet certain criteria.
  2. Add postconditions to a method to verify the method's output and state changes.
  3. Identify and enforce invariants in a class to maintain its consistent state throughout its lifecycle.
  4. Refactor an existing codebase to apply Design by Contract principles and verify their correctness.

Capsule 1: Practical Lab Exercise (2 hours)

Introduction

This lab exercise is designed to help you apply the concepts covered in Capsule 1, including Object-Oriented Design, SOLID principles, Clean Code practices, and Design by Contract. You will be refactoring a small application and ensuring that it adheres to these principles.

Exercise Overview

You are given a simple banking application that currently has several design flaws. Your task is to refactor the codebase to make it more maintainable, readable, and robust by applying the principles you have learned.

Initial Code (Before Refactoring)

      // BankAccount class with multiple responsibilities and design issues
      class BankAccount {
      constructor(owner, balance) {
      this.owner = owner;
      this.balance = balance;
      }

      // Method to withdraw money
      withdraw(amount) {
      if (amount <= 0) {
      console.log("Invalid amount");
      return;
      }
      if (amount > this.balance) {
      console.log("Insufficient funds");
      return;
      }
      this.balance -= amount;
      console.log(`Withdrawn: ${amount}, New Balance: ${this.balance}`);
      }

      // Method to deposit money
      deposit(amount) {
      if (amount <= 0) {
      console.log("Invalid amount");
      return;
      }
      this.balance += amount;
      console.log(`Deposited: ${amount}, New Balance: ${this.balance}`);
      }

      // Method to print account balance
      print_balance() {
      console.log(`Account Balance: ${this.balance}`);
      }
      }

      // Sample usage
      const account = new BankAccount("John Doe", 1000);
      account.deposit(500);
      account.withdraw(200);
      account.print_balance();
      account.withdraw(1500);
      

Step 1: Apply Object-Oriented Design Principles

Refactor the BankAccount class to adhere to Object-Oriented Design principles:

Expected Refactoring Outcome:

      class BankAccount {
      constructor(owner, initialBalance) {
        this.owner = owner;
        this._balance = initialBalance;
      }

      // Method to withdraw money
      withdraw(amount) {
        if (amount <= 0) throw new Error("Invalid amount");
        if (amount > this._balance) throw new Error("Insufficient funds");
        this._balance -= amount;
      }

      // Method to deposit money
      deposit(amount) {
        if (amount <= 0) throw new Error("Invalid amount");
        this._balance += amount;
      }

      // Method to get the current balance
      getBalance() {
        return this._balance;
      }

      // Method to print account balance
      printBalance() {
        console.log(`Account Balance: ${this.getBalance()}`);
      }
      }
        

Step 2: Apply SOLID Principles

Refactor the code to adhere to the SOLID principles:

Expected Refactoring Outcome:

      class Transaction {
      constructor(account, amount) {
        this.account = account;
        this.amount = amount;
      }

      execute() {
        throw new Error("Method 'execute' must be implemented.");
      }
      }

      class Withdrawal extends Transaction {
      execute() {
        this.account.withdraw(this.amount);
      }
      }

      class Deposit extends Transaction {
      execute() {
        this.account.deposit(this.amount);
      }
      }

      class BankAccount {
      constructor(owner, initialBalance) {
        this.owner = owner;
        this._balance = initialBalance;
      }

      withdraw(amount) {
        if (amount <= 0) throw new Error("Invalid amount");
        if (amount > this._balance) throw new Error("Insufficient funds");
        this._balance -= amount;
      }

      deposit(amount) {
        if (amount <= 0) throw new Error("Invalid amount");
        this._balance += amount;
      }

      getBalance() {
        return this._balance;
      }

      printBalance() {
        console.log(`Account Balance: ${this.getBalance()}`);
      }
      }

      // Sample usage
      const account = new BankAccount("John Doe", 1000);
      const deposit = new Deposit(account, 500);
      const withdrawal = new Withdrawal(account, 200);

      deposit.execute();
      withdrawal.execute();
      account.printBalance();
        

Step 3: Apply Clean Code Practices

Refactor the code to make it clean and readable:

Expected Refactoring Outcome:

      class BankAccount {
      constructor(owner, initialBalance) {
        this.owner = owner;
        this._balance = initialBalance;
      }

      withdraw(amount) {
        this._validateAmount(amount);
        if (amount > this._balance) throw new Error("Insufficient funds");
        this._balance -= amount;
      }

      deposit(amount) {
        this._validateAmount(amount);
        this._balance += amount;
      }

      getBalance() {
        return this._balance;
      }

      printBalance() {
        console.log(`Account Balance: ${this.getBalance()}`);
      }

      _validateAmount(amount) {
        if (amount <= 0) throw new Error("Amount must be greater than zero");
      }
      }
        

Step 4: Apply Design by Contract

Refactor the code to enforce Design by Contract principles:

Expected Refactoring Outcome:

      class BankAccount {
      constructor(owner, initialBalance) {
        this.owner = owner;
        this._balance = initialBalance;
        this._checkInvariants();
      }

      withdraw(amount) {
        this._validateAmount(amount);
        if (amount > this._balance) throw new Error("Insufficient funds");
        this._balance -= amount;
        this._checkInvariants();
      }

      deposit(amount) {
        this._validateAmount(amount);
        this._balance += amount;
        this._checkInvariants();
      }

      getBalance() {
        return this._balance;
      }

      printBalance() {
        console.log(`Account Balance: ${this.getBalance()}`);
      }

      _validateAmount(amount) {
        if (amount <= 0) throw new Error("Amount must be greater than zero");
      }

      _checkInvariants() {
        if (this._balance < 0) throw new Error("Invariant violation: balance must be non-negative");
      }
      }
        

Submission

Submit your refactored code along with a short document explaining the changes you made and how they adhere to the principles of Object-Oriented Design, SOLID, Clean Code, and Design by Contract.

Facebook Facebook Twitter Twitter LinkedIn LinkedIn Email Email Reddit Reddit Pinterest Pinterest WhatsApp WhatsApp Telegram Telegram VK VK



Consent Preferences

Capsule 1: Object-Oriented Design
Capsule 2: Design Patterns
Capsule 3: Software Architecture
Capsule 4: Service-Oriented Architecture (SOA)
Capsule 5: Advanced Topics in Software Architecture
Capsule 6: Clean Code and Testing Strategies
Capsule 7: Modern Development Practices
Capsule 8: Capstone Project and Assessment
Tooling: Best of the breed tooling for training
Tutorial: Setting up Azure DevOps Infrastructure and Pipelines for a Full-Stack .NET 8 Application
Accelerating Software Delivery with DevOps and CI/CD
Further Readings
Welcome to DevHub
Tools
Tutorials
DeFi Central 3
DeFi Central 2
DeFi Central 1