DEV Community

Anmar Hani
Anmar Hani

Posted on • Updated on

Software Development (Implementation and Construction) in The Process

Table of contents

Introduction

In the world of software development, writing clean code and implementing software efficiently are two critical skills. This guide aims to provide a comprehensive overview of these topics, covering everything from programming languages to essential development tools.

Understanding Programming Languages' Types

Low-Level Languages

1. Machine Code

Machine code is the lowest level of programming language, consisting of binary instructions that directly manipulate a computer's hardware.

2. Assembly

Assembly language is a step above machine code, providing a more human-readable format while still offering direct hardware control.

High-Level Languages

1. Procedural

Procedural languages, such as C and Pascal, structure code into procedures or routines.

2. Functional

Functional languages, like Haskell and Lisp, treat computation as the evaluation of mathematical functions and avoid changing-state and mutable data.

3. Object-oriented

Object-oriented languages, such as Java and Python, organize code into objects that contain both data and methods. For example, in Python, you can create a class Car with attributes like year and model, and methods like drive.

class Car:
    def __init__(self, year, model):
        self.year = year
        self.model = model

    def drive(self):
        print("Moving..!")
Enter fullscreen mode Exit fullscreen mode

4. Scripting

Scripting languages, like JavaScript and Ruby, are typically used for automating tasks in web and software applications. Python is also widely used as a scripting language.

5. Logic

Logic programming languages, such as Prolog, are used primarily for AI and mathematical applications.

Software Implementation Process Activites

1. Coding

Coding is the process of translating software requirements into a programming language. It involves writing (quality) code that is functional, maintainable, clean, and scalable.

Clean and Quality code practices envolves:

1. Design Patterns

Design patterns are reusable solutions to common problems in software design. Types are:

  • Creational:

These patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation. Its types are:

  1. Singleton Pattern: Ensures that a class has only one instance and provides a global point of access to it. This pattern is useful when you want to limit the number of instances of a class.
class Singleton:
    _instance = None

    def __new__(cls):
        if not cls._instance:
            cls._instance = super().__new__(cls)
        return cls._instance

Enter fullscreen mode Exit fullscreen mode
  1. Factory Pattern: Provides an interface for creating objects, but allows subclasses to decide which class to instantiate. This pattern is useful when you want to delegate the object creation logic to subclasses.
class Animal:
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return "This is a Dog!"

class Cat(Animal):
    def speak(self):
        return "This is a Cat!"

class AnimalFactory:
    def create_animal(self, animal_type):
        if animal_type == "dog":
            return Dog()
        elif animal_type == "cat":
            return Cat()
        else:
            raise ValueError("Invalid animal type")

factory = AnimalFactory()
animal = factory.create_animal("dog")
animal.speak()  # Output: "This is a Dog!"
Enter fullscreen mode Exit fullscreen mode
  1. Abstract Factory Pattern: Provides an interface for creating families of related or dependent objects without specifying their concrete classes. This pattern is useful when you want to create objects that are related or dependent on each other.
class Button:
    def click(self):
        pass

class WindowsButton(Button):
    def click(self):
        return "Windows button clicked"

class MacButton(Button):
    def click(self):
        return "Mac button clicked"

class GUIFactory:
    def create_button(self):
        pass

class WindowsGUIFactory(GUIFactory):
    def create_button(self):
        return WindowsButton()

class MacGUIFactory(GUIFactory):
    def create_button(self):
        return MacButton()

factory = WindowsGUIFactory()
button = factory.create_button()
button.click()  # Output: "Windows button clicked"

Enter fullscreen mode Exit fullscreen mode
  1. Builder Pattern: Separates the construction of a complex object from its representation, allowing the same construction process to create different representations. This pattern is useful when you want to create complex objects step by step.
class Pizza:
    def __init__(self):
        self.size = None
        self.cheese = False
        self.pepperoni = False
        self.mushrooms = False

    def __str__(self):
        return f"Size: {self.size}, Cheese: {self.cheese}, Pepperoni: {self.pepperoni}, Mushrooms: {self.mushrooms}"

class PizzaBuilder:
    def __init__(self):
        self.pizza = Pizza()

    def set_size(self, size):
        self.pizza.size = size
        return self

    def add_cheese(self):
        self.pizza.cheese = True
        return self

    def add_pepperoni(self):
        self.pizza.pepperoni = True
        return self

    def add_mushrooms(self):
        self.pizza.mushrooms = True
        return self

    def build(self):
        return self.pizza

builder = PizzaBuilder()
pizza = builder.set_size("Large").add_cheese().add_pepperoni().build()
print(pizza)  # Output: "Size: Large, Cheese: True, Pepperoni: True, Mushrooms: False"
Enter fullscreen mode Exit fullscreen mode
  1. Prototype Pattern: Creates new objects by cloning existing ones and modifying them as required. This pattern is useful when you want to create new objects by copying existing ones.
import copy

class Prototype:
    def clone(self):
        pass

class ConcretePrototype(Prototype):
    def __init__(self, name):
        self.name = name

    def clone(self):
        return copy.deepcopy(self)

prototype = ConcretePrototype("Prototype")
clone = prototype.clone()
print(clone.name)  # Output: "Prototype"
Enter fullscreen mode Exit fullscreen mode
  • Structural Patterns:

These patterns concern class and object composition. They use inheritance to compose interfaces and define ways to compose objects to obtain new functionality. Its Types are:

  1. Adapter Pattern: Converts the interface of a class into another interface that clients expect. This pattern is useful when you want to make incompatible classes work together.
class Target:
    def request(self):
        pass

class Adaptee:
    def specific_request(self):
        return "Adaptee's specific request"

class Adapter(Target):
    def __init__(self, adaptee):
        self.adaptee = adaptee

    def request(self):
        return self.adaptee.specific_request()

adaptee = Adaptee()
adapter = Adapter(adaptee)
adapter.request()  # Output: "Adaptee's specific request"
Enter fullscreen mode Exit fullscreen mode
  1. Bridge Pattern: Decouples an abstraction from its implementation, allowing them to vary independently. This pattern is useful when you want to separate the abstraction and implementation hierarchies.
class Abstraction:
    def __init__(self, implementation):
        self.implementation = implementation

    def operation(self):
        return self.implementation.operation_implementation()

class Implementation:
    def operation_implementation(self):
        pass

class ConcreteImplementationA(Implementation):
    def operation_implementation(self):
        return "ConcreteImplementationA operation"

class ConcreteImplementationB(Implementation):
    def operation_implementation(self):
        return "ConcreteImplementationB operation"

implementation_a = ConcreteImplementationA()
abstraction_a = Abstraction(implementation_a)
abstraction_a.operation()  # Output: "ConcreteImplementationA operation"

implementation_b = ConcreteImplementationB()
abstraction_b = Abstraction(implementation_b)
abstraction_b.operation()  # Output: "ConcreteImplementationB operation"
Enter fullscreen mode Exit fullscreen mode
  1. Composite Pattern: Composes objects into tree structures to represent part-whole hierarchies. This pattern is useful when you want to treat individual objects and compositions of objects uniformly.
class Component:
    def operation(self):
        pass

class Leaf(Component):
    def operation(self):
        return "Leaf operation"

class Composite(Component):
    def __init__(self):
        self.children = []

    def add(self, component):
        self.children.append(component)

    def remove(self, component):
        self.children.remove(component)

    def operation(self):
        results = []
        for child in self.children:
            results.append(child.operation())
        return results

leaf1 = Leaf()
leaf2 = Leaf()
composite = Composite()
composite.add(leaf1)
composite.add(leaf2)
composite.operation()  # Output: ["Leaf operation", "Leaf operation"]

Enter fullscreen mode Exit fullscreen mode
  1. Decorator Pattern: Dynamically adds responsibilities to objects by wrapping them in an object of a decorator class. This pattern is useful when you want to add behavior to objects without modifying their code.
class Component:
    def operation(self):
        pass

class ConcreteComponent(Component):
    def operation(self):
        return "ConcreteComponent operation"

class Decorator(Component):
    def __init__(self, component):
        self.component = component

    def operation(self):
        return self.component.operation()

class ConcreteDecorator(Decorator):
    def operation(self):
        return f"ConcreteDecorator operation, {self.component.operation()}"

component = ConcreteComponent()
decorator = ConcreteDecorator(component)
decorator.operation()  # Output: "ConcreteDecorator operation, ConcreteComponent operation"

Enter fullscreen mode Exit fullscreen mode
  1. Facade Pattern: Provides a unified interface to a set of interfaces in a subsystem, simplifying the subsystem's usage. This pattern is useful when you want to provide a simple interface to a complex system.
class SubsystemA:
    def operation_a(self):
        return "SubsystemA operation"

class SubsystemB:
    def operation_b(self):
        return "SubsystemB operation"

class Facade:
    def __init__(self):
        self.subsystem_a = SubsystemA()
        self.subsystem_b = SubsystemB()

    def operation(self):
        results = []
        results.append(self.subsystem_a.operation_a())
        results.append(self.subsystem_b.operation_b())
        return results

facade = Facade()
facade.operation()  # Output: ["SubsystemA operation", "SubsystemB operation"]

Enter fullscreen mode Exit fullscreen mode
  1. Flyweight Pattern: Reduces the memory footprint of objects by sharing common data across multiple objects. This pattern is useful when you want to optimize memory usage by sharing data that is common across objects.
class Flyweight:
    def operation(self, extrinsic_state):
        pass

class ConcreteFlyweight(Flyweight):
    def operation(self, extrinsic_state):
        return f"ConcreteFlyweight operation, Extrinsic State: {extrinsic_state}"

class FlyweightFactory:
    def __init__(self):
        self.flyweights = {}

    def get_flyweight(self, key):
        if key not in self.flyweights:
            self.flyweights[key] = ConcreteFlyweight()
        return self.flyweights[key]

factory = FlyweightFactory()
flyweight1 = factory.get_flyweight("key1")
flyweight2 = factory.get_flyweight("key2")
flyweight1.operation("state1")  # Output: "ConcreteFlyweight operation, Extrinsic State: state1"
flyweight2.operation("state2")  # Output: "ConcreteFlyweight operation, Extrinsic State: state2"

Enter fullscreen mode Exit fullscreen mode
  1. Proxy Pattern: Provides a surrogate or placeholder for another object to control access to it. This pattern is useful when you want to add an extra layer of indirection to control the access to an object.
class Subject:
    def request(self):
        pass

class RealSubject(Subject):
    def request(self):
        return "RealSubject request"

class Proxy(Subject):
    def __init__(self, real_subject):
        self.real_subject = real_subject

    def request(self):
        return self.real_subject.request()

real_subject = RealSubject()
proxy = Proxy(real_subject)
proxy.request()  # Output: "RealSubject request"

Enter fullscreen mode Exit fullscreen mode
  • Behavioral Patterns:

These patterns are about identifying common communication patterns between objects and realize these patterns. Its Types are:

  1. Chain of Responsibility Pattern: Allows an object to pass a request along a chain of potential handlers until the request is handled. This pattern is useful when you want to decouple senders and receivers of a request.
class Handler:
    def set_successor(self, successor):
        pass

    def handle_request(self, request):
        pass

class ConcreteHandlerA(Handler):
    def set_successor(self, successor):
        self.successor = successor

    def handle_request(self, request):
        if request == "A":
            return "ConcreteHandlerA handled the request"
        elif self.successor:
            return self.successor.handle_request(request)
        else:
            return "Request cannot be handled"

class ConcreteHandlerB(Handler):
    def set_successor(self, successor):
        self.successor = successor

    def handle_request(self, request):
        if request == "B":
            return "ConcreteHandlerB handled the request"
        elif self.successor:
            return self.successor.handle_request(request)
        else:
            return "Request cannot be handled"

handler_a = ConcreteHandlerA()
handler_b = ConcreteHandlerB()
handler_a.set_successor(handler_b)
handler_a.handle_request("A")  # Output: "ConcreteHandlerA handled the request"
handler_a.handle_request("B")  # Output: "ConcreteHandlerB handled the request"
handler_a.handle_request("C")  # Output: "Request cannot be handled"
Enter fullscreen mode Exit fullscreen mode
  1. Command Pattern: Encapsulates a request as an object, allowing you to parameterize clients with different requests, queue or log requests, and support undoable operations. This pattern is useful when you want to decouple the sender and receiver of a request.
class Receiver:
    def action(self):
        return "Receiver action"

class Command:
    def __init__(self, receiver):
        self.receiver = receiver

    def execute(self):
        pass

class ConcreteCommand(Command):
    def execute(self):
        return self.receiver.action()

class Invoker:
    def __init__(self):
        self.command = None

    def set_command(self, command):
        self.command = command

    def execute_command(self):
        return self.command.execute()

receiver = Receiver()
command = ConcreteCommand(receiver)
invoker = Invoker()
invoker.set_command(command)
invoker.execute_command()  # Output: "Receiver action"
Enter fullscreen mode Exit fullscreen mode
  1. Interpreter Pattern: Defines a representation for a grammar along with an interpreter to interpret the grammar. This pattern is useful when you want to evaluate sentences in a language.
class Context:
    def __init__(self):
        self.variables = {}

    def get_variable(self, variable_name):
        return self.variables.get(variable_name)

    def set_variable(self, variable_name, value):
        self.variables[variable_name] = value

class AbstractExpression:
    def interpret(self, context):
        pass

class TerminalExpression(AbstractExpression):
    def __init__(self, variable_name):
        self.variable_name = variable_name

    def interpret(self, context):
        return context.get_variable(self.variable_name)

class NonterminalExpression(AbstractExpression):
    def __init__(self, expression1, expression2):
        self.expression1 = expression1
        self.expression2 = expression2

    def interpret(self, context):
        return self.expression1.interpret(context) + self.expression2.interpret(context)

context = Context()
context.set_variable("x", "Hello, ")
context.set_variable("y", "world!")
expression1 = TerminalExpression("x")
expression2 = TerminalExpression("y")
expression = NonterminalExpression(expression1, expression2)
expression.interpret(context)  # Output: "Hello, world!"

Enter fullscreen mode Exit fullscreen mode
  1. Iterator Pattern: Provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation. This pattern is useful when you want to traverse a collection of objects without exposing its internal structure
class Iterator:
    def has_next(self):
        pass

    def next(self):
        pass

class ConcreteIterator(Iterator):
    def __init__(self, collection):
        self.collection = collection
        self.index = 0

    def has_next(self):
        return self.index < len(self.collection)

    def next(self):
        if self.has_next():
            item = self.collection[self.index]
            self.index += 1
            return item
        else:
            raise StopIteration()

class Collection:
    def create_iterator(self):
        pass

class ConcreteCollection(Collection):
    def __init__(self):
        self.items = []

    def add_item(self, item):
        self.items.append(item)

    def create_iterator(self):
        return ConcreteIterator(self.items)

collection = ConcreteCollection()
collection.add_item("Item 1")
collection.add_item("Item 2")
iterator = collection.create_iterator()
while iterator.has_next():
    print(iterator.next())  # Output: "Item 1", "Item 2"

Enter fullscreen mode Exit fullscreen mode
  1. Mediator Pattern: Defines an object that encapsulates how a set of objects interact, promoting loose coupling between the objects. This pattern is useful when you want to reduce direct dependencies between objects.
class Mediator:
    def notify(self, sender, event):
        pass

class ConcreteMediator(Mediator):
    def __init__(self):
        self.component1 = Component1(self)
        self.component2 = Component2(self)

    def notify(self, sender, event):
        if sender == self.component1:
            self.component2.handle_event(event)
        elif sender == self.component2:
            self.component1.handle_event(event)

class BaseComponent:
    def __init__(self, mediator):
        self.mediator = mediator

class Component1(BaseComponent):
    def handle_event(self, event):
        return f"Component1 handled event: {event}"

    def do_something(self):
        self.mediator.notify(self, "Event from Component1")

class Component2(BaseComponent):
    def handle_event(self, event):
        return f"Component2 handled event: {event}"

    def do_something(self):
        self.mediator.notify(self, "Event from Component2")

mediator = ConcreteMediator()
component1 = Component1(mediator)
component2 = Component2(mediator)
component1.do_something()  # Output: "Component2 handled event: Event from Component1"
component2.do_something()  # Output: "Component1 handled event: Event from Component2"
Enter fullscreen mode Exit fullscreen mode
  1. Memento Pattern: Captures and externalizes an object's internal state so that the object can be restored to this state later. This pattern is useful when you want to save and restore the state of an object.
class Memento:
    def __init__(self, state):
        self.state = state

    def get_state(self):
        return self.state

class Originator:
    def __init__(self):
        self.state = None

    def set_state(self, state):
        self.state = state

    def create_memento(self):
        return Memento(self.state)

    def restore_memento(self, memento):
        self.state = memento.get_state()

originator = Originator()
originator.set_state("State 1")
memento = originator.create_memento()
originator.set_state("State 2")
originator.restore_memento(memento)
print(originator.state)  # Output: "State 1"
Enter fullscreen mode Exit fullscreen mode
  1. Observer Pattern: Defines a one-to-many dependency between objects, so that when one object changes state, all its dependents are notified and updated automatically. This pattern is useful when you want to notify multiple objects about changes in another object.
class Observer:
    def update(self, subject):
        pass

class ConcreteObserver(Observer):
    def update(self, subject):
        print(f"ConcreteObserver: Reacted to the event from {subject.__class__.__name__}")

class Subject:
    def __init__(self):
        self.observers = []

    def attach(self, observer):
        self.observers.append(observer)

    def detach(self, observer):
        self.observers.remove(observer)

    def notify(self):
        for observer in self.observers:
            observer.update(self)

class ConcreteSubject(Subject):
    def do_something(self):
        print("ConcreteSubject: I'm doing something.")
        self.notify()

subject = ConcreteSubject()
observer = ConcreteObserver()
subject.attach(observer)
subject.do_something()
Enter fullscreen mode Exit fullscreen mode
  1. State Pattern: Allows an object to alter its behavior when its internal state changes. The object will appear to change its class. This pattern is useful when you want to change the behavior of an object based on its state.
class State:
    def handle(self, context):
        pass

class ConcreteStateA(State):
    def handle(self, context):
        print("ConcreteStateA handling.")
        context.set_state(ConcreteStateB())

class ConcreteStateB(State):
    def handle(self, context):
        print("ConcreteStateB handling.")
        context.set_state(ConcreteStateA())

class Context:
    def __init__(self, state):
        self.state = state

    def set_state(self, state):
        self.state = state

    def request(self):
        self.state.handle(self)

context = Context(ConcreteStateA())
context.request()  # Output: "ConcreteStateA handling."
context.request()  # Output: "ConcreteStateB handling."
Enter fullscreen mode Exit fullscreen mode
  1. Strategy Pattern: Defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern is useful when you want to select an algorithm at runtime.
class Strategy:
    def execute(self):
        pass

class ConcreteStrategyA(Strategy):
    def execute(self):
        return "ConcreteStrategyA execution"

class ConcreteStrategyB(Strategy):
    def execute(self):
        return "ConcreteStrategyB execution"

class Context:
    def __init__(self, strategy):
        self.strategy = strategy

    def set_strategy(self, strategy):
        self.strategy = strategy

    def execute_strategy(self):
        return self.strategy.execute()

context = Context(ConcreteStrategyA())
context.execute_strategy()  # Output: "ConcreteStrategyA execution"
context.set_strategy(ConcreteStrategyB())
context.execute_strategy()  # Output: "ConcreteStrategyB execution"

Enter fullscreen mode Exit fullscreen mode
  1. Template Method Pattern: Defines the skeleton of an algorithm in a superclass but lets subclasses override specific steps of the algorithm without changing its structure. This pattern is useful when you want to let subclasses redefine certain steps of an algorithm without changing the algorithm's structure.
class AbstractClass:
    def template_method(self):
        self.primitive_operation1()
        self.primitive_operation2()

    def primitive_operation1(self):
        pass

    def primitive_operation2(self):
        pass

class ConcreteClass(AbstractClass):
    def primitive_operation1(self):
        print("ConcreteClass primitive operation1")

    def primitive_operation2(self):
        print("ConcreteClass primitive operation2")

concrete_class = ConcreteClass()
concrete_class.template_method()  # Output: "ConcreteClass primitive operation1", "ConcreteClass primitive operation2"
Enter fullscreen mode Exit fullscreen mode
  1. Visitor Pattern: Represents an operation to be performed on the elements of an object structure. This pattern is useful when you want to perform operations on objects without changing their classes.
class Element:
    def accept(self, visitor):
        pass

class ConcreteElementA(Element):
    def accept(self, visitor):
        visitor.visit_concrete_element_a(self)

class ConcreteElementB(Element):
    def accept(self, visitor):
        visitor.visit_concrete_element_b(self)

class Visitor:
    def visit_concrete_element_a(self, element):
        pass

    def visit_concrete_element_b(self, element):
        pass

class ConcreteVisitor(Visitor):
    def visit_concrete_element_a(self, element):
        print("ConcreteVisitor visited ConcreteElementA")

    def visit_concrete_element_b(self, element):
        print("ConcreteVisitor visited ConcreteElementB")

element_a = ConcreteElementA()
element_b = ConcreteElementB()
visitor = ConcreteVisitor()
element_a.accept(visitor)  # Output: "ConcreteVisitor visited ConcreteElementA"
element_b.accept(visitor)  # Output: "ConcreteVisitor visited ConcreteElementB"

Enter fullscreen mode Exit fullscreen mode
  • Concurrency Patterns:

These patterns deal with multi-threaded programming paradigms. Its Types Are:

  1. Active Object Pattern: Encapsulates each method call as an object and introduces a scheduler for handling these method calls. This pattern is useful when you want to decouple method execution from method invocation.
  2. Balking Pattern: Only executes an action on an object when the object is in a particular state. This pattern is useful when you want to avoid unnecessary actions when an object is not in a suitable state.
  3. Binding Properties Pattern: Binds properties of disparate objects together, so that when one property changes, the other automatically updates. This pattern is useful when you want to keep two properties in sync.
  4. Compute Kernel Pattern: Encapsulates a computation inside a kernel, which can be executed concurrently on a GPU. This pattern is useful when you want to perform computations on a GPU.
  5. Double-checked locking Pattern: Reduces the overhead of acquiring a lock by testing the locking criterion before acquiring the lock. This pattern is useful when you want to improve performance in a multithreaded application.
  6. Event-based asynchronous Pattern: Passes a callback method into a method call, which is invoked when the called method has completed. This pattern is useful when you want to perform asynchronous operations.
  7. Guarded suspension Pattern: Suspends the execution of a method until a certain condition is met. This pattern is useful when you want to prevent a method from executing when certain conditions are not met.
  8. Thread pool Pattern: Creates a number of threads during startup, which are placed in a pool and used to execute tasks. This pattern is useful when you want to limit the number of threads running at the same time.

Please note that due to the complexity and the nature of these concurrency patterns, providing Python examples for them would be quite extensive and beyond the scope of this article.

2. Coding Principles

Coding principles provide guidelines for writing clean, maintainable code. These are:

  • SOLID Principles:

SOLID is an acronym for five principles that help in creating effective, scalable, and maintainable software architectures. The principles of it when combined together, make it easier to maintain and extend the software over the long term. They also help in reducing the risk of bugs when making changes in the codebase. They are:

  1. Single Responsibility Principle (SRP): This principle states that a class should have only one reason to change. In other words, a class should only have one job or responsibility. If a class has more than one responsibility, it becomes coupled. A change to one responsibility results in modification of the other responsibility.
  2. Open-Closed Principle (OCP): According to this principle, "software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification". This means that a class should be easily extendable without modifying the class itself.
  3. Liskov Substitution Principle (LSP): This principle, named after Barbara Liskov, states that "objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program". In other words, each derived class should be substitutable for their base class without causing any issues.
  4. Interface Segregation Principle (ISP): This principle states that "clients should not be forced to depend upon interfaces that they do not use". This means that a class should not have to implement methods it doesn't use. Instead of one fat interface, numerous small interfaces are preferred based on groups of methods, each one serving one submodule.
  5. Dependency Inversion Principle (DIP): This principle states that "high-level modules should not depend on low-level modules. Both should depend on abstractions". Additionally, "abstractions should not depend on details. Details should depend on abstractions". This principle allows for decoupling.
  • KIS (Keep It Simple):

This principle states that most systems work best if they are kept simple rather than made complicated. Simplicity should be a key goal in design, and unnecessary complexity should be avoided.

  • DRY (Don't Repeat Yourself):

This principle is aimed at reducing repetition of software patterns, replacing it with abstractions or using data normalization to avoid redundancy.

  • YAGNI (You Aren't Gonna Need It):

This principle states that a programmer should not add functionality until deemed necessary. It's a principle to avoid additional complexity and over-engineering.

3. Clean and Quality Code Practices and Guidelines

1. General Rules

Some rules and guidelines to follow generally in writing clean code:

  • Consistency:

Consistency in coding style, naming conventions, and project structure can make the code easier to read and understand. It also makes the code easier to maintain.

  • Readability:

Code should be written in a way that is easy to read and understand. This includes proper indentation, use of whitespace, and clear variable and function names.

  • Modifiability:

Code should be written in a way that it can be easily modified. This includes writing modular code, using clear and concise comments, and following good coding practices.

  • Scalability:

Code should be written in a way that it can handle increased load. This includes writing efficient code, using appropriate data structures and algorithms, and considering the performance implications of your code.

  • Extensibility


2. Effective Use of Variables

Variables should be named clearly and consistently, and prefixes or postfixes should be used when appropriate. Here are some principles to follow:

  • Post or Pre Fixes (For better search):

Using prefixes or postfixes can make it easier to search for variables in your code. For example, you might prefix all global variables with g_ to make them easy to find.

  • Naming Conventions (For better meaning and consistency):

Consistent naming conventions can make your code easier to read and understand. For example, in Python, variable names should be lowercase with words separated by underscores (snake_case).

  • Magic Numbers to Constants:

Magic numbers are numbers that appear in the code without any explanation of what they represent. These should be replaced with named constants to improve readability.

  • Use Types:

Using types can help catch errors at compile time and make the code easier to understand.

  • Reduce Globals:

Global variables can make the code harder to understand and maintain. Where possible, use local variables instead.

3. Commenting

Commenting in code is an essential part if it is used in place, else it would be redundant and worse than not writing it. Here is some principles in commenting for effective commenting:

  • Function and Class Docstring:

Each function should have a comment that explains what the function does, what parameters it takes, what it returns, and what exceptions it might throw.

Also, each class should have a comment that explains what the class does and how it should be used.

  • Explaining Statement Comments:

Comments should be used to explain complex or non-obvious parts of the code.

  • Warning of consequences:

Comments should be used to warn of potential consequences of changing the code.

4. Functions

Functions should be designed with a few key principles in mind:

  • Single Responsibility:

Each function should do one thing and do it well. This makes the function easier to understand, test, and reuse.

  • Reusability:

Functions should be written in a way that they can be reused in different parts of the code. This often involves making the function more general, taking parameters instead of using hard-coded values.

  • No side effects:

Functions should not have side effects. That is, they should not change anything in the program state other than what is being returned by the function.

5. Classes (OOP)

When designing classes, a few key principles should be kept in mind:

  • Dependency Injection (Increased cohesion and reduced coupling):

Dependency Injection is a technique where an object receives other objects that it depends on. This can help to reduce the coupling between classes and increase the cohesion of the class.

  • Law of Demeter:

The Law of Demeter is a design guideline for developing software, particularly object-oriented programs. It promotes loose coupling between software components.

  • Principles:

Image of OOP Principles

source: khalilstemmler.com

- **Encapsulation**
- **Abstraction**
- **Polymorphism**
- **Inheritance**
Enter fullscreen mode Exit fullscreen mode
6. Code Formatting

Code should be formatted consistently to improve readability. Python's PEP 8 provides a set of guidelines for code formatting. Tools like pylint or flake8 can be used to check your code for PEP 8 compliance. For more complex projects, you might consider using a tool like Black to automatically format your code. Also, whitespace and spaces between functions, classes, or code blocks are necessary since it seperates each block than the other visually.

2. Debugging and Error Handling

Debugging

involves identifying and fixing errors while programming and writing code. It's the first step after a run command that does not work.

Debugging can be made more efficient by following a systematic approach:

  1. Understanding the problem
  2. Reproducing the error
  3. Diagnosing the problem
  4. Planning a solution
  5. Implementing the solution
  6. Testing the solution.

Tools like Python's built-in debugger (pdb) can be used to step through the code, inspect variables, and understand what's going wrong.

Error handling

refers to the process of responding to errors during program execution. Python provides several tools for this, including the try/except block:

try:
    print(5/0)
except ZeroDivisionError:
    print("You can't divide by zero!")
Enter fullscreen mode Exit fullscreen mode

In addition to Python's built-in exceptions, you can define your own custom exceptions.

Custom exceptions can make your code more reliable and easier to debug by providing more specific error messages. For example:

class CustomError(Exception):
    pass

try:
    raise CustomError("This is a custom error")
except CustomError as e:
    print(e)
Enter fullscreen mode Exit fullscreen mode

3. Writing Tests

Writing tests is a crucial part of software development. It ensures that your code works as expected and helps prevent future bugs. Python's unittest module is a powerful tool for writing and running tests. Other popular testing libraries in Python include pytest and nose.

Unit Tests

Here's an example of a unit test for a simple sum function:

def sum(a, b):
    return a + b

def test_sum():
    assert sum(1, 2) == 3
Enter fullscreen mode Exit fullscreen mode

Integration Tests

Integration tests, on the other hand, check that different parts of your code work together correctly. For example, if you have a function that reads data from a database and another function that processes that data, an integration test might check that these functions work correctly together.

We will cover testing as a topic in detail in the next article.

4. Documentation

Documentation is the written text that accompanies your code, explaining how it works and how to use it.

Documentations may be websites, comments, or external documents. In this section, we will focus on web and external document documentation instead of "Docstrings as comments".

Tools like Sphinx can be used to generate HTML documentation for Python code.

You can also use platforms like Read the Docs to host your documentation online.

Good documentation makes your code easier to understand and use, and it's especially important if your code is going to be used by other people. Also, it gives a good overview of the codebase, functions, classes, variables, and codeblocks.

5. Refactoring

Refactoring is the process of restructuring existing code without changing its external behavior, with the goal of improving code readability and reducing complexity.

Also, may improve non-functional attributes, such as performance, reliability, etc.

Refactoring is usually the last step when you finally ensure that your code is working as intended.

A good pattern to follow when refactoring is the Red-Green-Refactor cycle:

  1. Write a failing test (red)
  2. Write code to make the test pass (green)
  3. Refactor the code to improve its structure and readability.

Here's an example of a code block that could be refactored:

def calculate(a, b, operation):
    if operation == 'add':
        return a + b
    elif operation == 'subtract':
        return a - b
    elif operation == 'multiply':
        return a * b
    elif operation == 'divide':
        return a / b
Enter fullscreen mode Exit fullscreen mode

Refactored code:

def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

def multiply(a, b):
    return a * b

def divide(a, b):
    return a / b

OPERATIONS = {
    'add': add,
    'subtract': subtract,
    'multiply': multiply,
    'divide': divide,
}

def calculate(a, b, operation):
    func = OPERATIONS.get(operation)
    if func is None:
        raise ValueError(f'Invalid operation: {operation}')
    return func(a, b)
Enter fullscreen mode Exit fullscreen mode

In the refactored code, each operation is a separate function, which makes the code more modular and easier to read and test. The calculate function uses a dictionary to map operation names to functions, which makes it easier to add new operations in the future.

Code Smells (Bad Code Practices)

Code smells are bad code practices that atleast even if your code is not clean you try to avoid, this is a list of bad code practices:

  • Rigidity - code is difficult to change
  • Fragility - code breaks easily with changes
  • Immobility - cannot reuse code
  • Needless Complexity - overcomplicated code
  • Needless Repetition - duplicated code
  • Opacity - hard to understand

Good Codebase Folder Structre

Folder structre is very important in finding files and searching for functions and bugs, so here are some best practices on structuring the codebase of your project:

  • Organize by feature rather than file type
  • Locate files based on dependencies
  • Group related files together
  • Use descriptive folder and file names
  • Limit nesting to avoid deep hierarchies
  • Be consistent across project

You may see software projects that include folders such as src, public, build, utils, core, test, config, controllers, views, middlewares, models, services, assets, .env, .gitignore, ts.config, package.json, etc.. You can make your own folder structure since it is well-known and easy to navigate.

Software Development Types

In software engineering, there are several types of applications that is being developed. Most of it falls of one of these or more:

Desktop Applications

Desktop applications were widely used. However, with the expansion of web and mobile applications its development is decreasing, but there are several applications in the real-world until now, such as companies' management systems, and apps that need to write/read files in the computer. Desktop applications are seperated into two types:

1. Native Development

This type of development is OS-Based development, so you develop to Windows or MacOS only. Some examples are; Objective-C.

2. Cross-Platform Development

This type is not OS-Based, so when you develop a codebase, it is deployable and built for different OS and platforms. Some examples are; Electron, and Java Swing.

Web Applications

Web applications is widely used due to the wide spread of internet, you can see websites everywhere. Web applications can contain several types:

1. Static Website

This type is without using an external server other than the webserver that serves frontend files. So, there is no backend nor database. Using only HTML, CSS, and JS.

2. Backend (API) and Frontend Applications

This type of development is so common nowadays, you develop a seperate backend server that is called "API Server" that returns usually JSON data, and develop the frontend externally, then you can call you API server endpoints from the frontend application to use its services. Some examples are; Django Rest Framework (API), FastAPI (API), and React (Frontend).

3. Full-Stack Web Applications

This type of development combines the both of two types, It is a development of a webserver, where you develop the backend and frontend in the same codebase, so the backend server usually serves Frontend server when its endpoints is hit (instead of JSON in the API type). Some examples are; Django, NextJS, and Laravel.

Mobile Applications

Mobile applications are also widely used. It has two types similar to Desktop applications:

1. Native Development

This type focuses on a single OS such as Android or IOS, it has several benifits such as performance, OS-Based styles, and more. Some examples are; Kotlin, and Swift.

2. Cross-Platform Development

This type doesnt focus on a single OS, it develops for both platforms (Android and IOS). Some examples are; Flutter, and React Native.

CLI Applications

This type doesnt care about graphical interfaces, it is usually developed for developers. Also, bash scripts may fall to this type, where you develop bash scripts that run automatically desktop functions. You can develop CLI applications with any programming language that interacts with the console/terminal. Some examples are; Powershell, bash script, and Python.

Embedded Systems Softwares

This type is neglected by alot of software engineers, where you develop systems specific for some hardwares, such as car, flight, and IoT systems. These systems are common to use low-level languages such as Assembly, or C. Some systems are now developing a higher level such as Raspberrypi (Python) and Arduino (C++) where you can develop embedded systems with these two microcontrollers.

Some Useful Tools for Software Implementation

There are many tools available to help with software development.

Some are:

  • source code management tools
  • integrated development environments (IDEs)
  • autocompletion and Intellisense tools.

References:


Real-World Skills and Diving Deeper


Jobs:

  1. Programmer
  2. Software Developer
  3. Software Engineer
  4. Database Engineer
  5. AI and ML Engineer
  6. Data Scientist or Analyst

What's Next?

Next activity is the Verification and Validation, where we start making tests and verifying and validating that the software meets the requirements and make better quality code.


TODO:

  • Add more tools

Top comments (0)