Ron and Ella Wiki Page

Extremely Serious

Page 10 of 33

DRY Principle

Introduction

In the realm of software development, writing maintainable, efficient, and scalable code is of utmost importance. One of the fundamental principles that guide developers in achieving these goals is the DRY principle, which stands for "Don't Repeat Yourself." This principle emphasizes the significance of avoiding code duplication and promoting code reusability, leading to cleaner, more manageable, and more robust software systems.

Understanding the DRY Principle

The DRY principle can be summed up in a single phrase: Every piece of knowledge or logic in a software system should have a single, unambiguous representation within that system. In other words, duplicating code, data, or logic should be avoided whenever possible. This includes similar code with slight variations. By adhering to the DRY principle, developers can enhance the maintainability and overall quality of their codebase.

Benefits of the DRY Principle

  1. Code Maintenance: Duplicated code creates a maintenance nightmare. When a bug needs fixing or a feature requires updating, developers must remember to apply changes consistently across all instances of the duplicated code. This not only increases the likelihood of introducing errors but also consumes valuable time and effort. Adhering to the DRY principle ensures that changes need only be made in a single location, simplifying maintenance tasks.
  2. Consistency: Reusing code promotes consistency throughout a project. If a particular piece of functionality is implemented in one place, it can be reused throughout the application, guaranteeing a uniform user experience and reducing the chances of discrepancies.
  3. Reduced Development Time: Writing code from scratch for each occurrence of a particular logic or functionality is time-consuming. The DRY principle encourages developers to create reusable components and functions that can be leveraged across the codebase, ultimately accelerating development cycles.
  4. Bug Reduction: Duplication often leads to bugs. If a bug is discovered and fixed in one instance of duplicated code, other instances may remain unaffected, potentially causing unexpected behavior. By centralizing logic, the DRY principle helps in reducing the number of bugs and making it easier to identify and address issues.

Applying the DRY Principle

  1. Modularization: Divide your code into small, modular components that encapsulate specific functionalities. These components can then be reused across different parts of the application.
  2. Functions and Methods: Instead of repeating the same code in multiple places, encapsulate it within functions or methods. This not only promotes reusability but also enhances readability and maintainability.
  3. Data Abstraction: Abstract data structures and variables that are used in multiple places. By centralizing data definitions, you can ensure consistency and simplify future modifications.
  4. Template Engines and Inheritance: In web development, template engines and inheritance mechanisms allow you to create reusable layouts and components for consistent UI rendering.
  5. Version Control and Package Management: Leverage version control systems (e.g., Git) and package management tools (e.g., npm, pip) to manage and share reusable code across projects.

Conclusion

The DRY principle is a cornerstone of software development, advocating for efficient and maintainable code by avoiding redundancy and promoting reusability. By adhering to this principle, developers can create cleaner, more reliable software systems that are easier to maintain, enhance, and scale. As software projects become increasingly intricate, the DRY principle remains a guiding beacon, helping developers navigate the complexities of code while striving for excellence.

SOLID Principles

Introduction

In the ever-evolving world of software development, creating maintainable and scalable code is crucial. The SOLID principles offer a set of guidelines to achieve just that. First introduced by Robert C. Martin, these five principles provide a foundation for writing clean, flexible, and efficient code. In this article, we will delve into each SOLID principle, understand its significance, and explore how they contribute to building robust and maintainable software.

Single Responsibility Principle (SRP)

The Single Responsibility Principle advocates that a class should have only one reason to change. In other words, it should have a single responsibility and encapsulate a single functionality. By adhering to SRP, we can avoid coupling and improve maintainability. This principle encourages us to decompose complex functionalities into smaller, independent classes, making our code easier to understand, test, and modify.

Example

// Bad example: A class with multiple responsibilities
class Order {
    public void calculateTotalPrice() {
        // Calculation logic here
    }

    public void saveToDatabase() {
        // Database insertion logic here
    }

    public void sendConfirmationEmail() {
        // Email sending logic here
    }
}

// Good example: Separating responsibilities into different classes
class OrderCalculator {
    public void calculateTotalPrice() {
        // Calculation logic here
    }
}

class OrderRepository {
    public void saveToDatabase() {
        // Database insertion logic here
    }
}

class EmailService {
    public void sendConfirmationEmail() {
        // Email sending logic here
    }
}

Open/Closed Principle (OCP)

The Open/Closed Principle suggests that software entities (classes, modules, functions) should be open for extension but closed for modification. This means that we should design our code in a way that new functionalities can be added without altering existing code. This promotes code reuse and allows us to adapt to changing requirements without affecting the stability of the existing system.

// Bad example: A class that needs to be modified to add new shapes
class Shape {
    public void draw() {
        // Drawing logic for the shape
    }
}

// Good example: Using an abstract class or interface to support new shapes
interface Shape {
    void draw();
}

class Circle implements Shape {
    public void draw() {
        // Drawing logic for a circle
    }
}

class Rectangle implements Shape {
    public void draw() {
        // Drawing logic for a rectangle
    }
}

The Factory Pattern is a design pattern that aligns well with the Open/Closed Principle (OCP) by allowing you to create new objects without modifying existing code.

Liskov Substitution Principle (LSP)

The Liskov Substitution Principle emphasizes that objects of a superclass should be replaceable with objects of its subclasses without altering the correctness of the program. In simpler terms, derived classes should adhere to the contract established by their base class. This principle ensures that polymorphism works as expected, promoting code flexibility and reliability.

// Bad example: Square is a subclass of Rectangle but violates LSP
class Rectangle {
    protected int width;
    protected int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getArea() {
        return width * height;
    }
}

class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width);
    }

    @Override
    public void setHeight(int height) {
        super.setWidth(height);
        super.setHeight(height);
    }
}

// Good example: Avoiding LSP violation by not using inheritance
class Shape {
    public int getArea() {
        return 0;
    }
}

class Rectangle extends Shape {
    protected int width;
    protected int height;

    // constructor, getters, and setters
}

class Square extends Shape {
    protected int side;

    // constructor, getters, and setters
}

Interface Segregation Principle (ISP)

The Interface Segregation Principle suggests that a class should not be forced to implement interfaces it does not use. Instead of having a single large interface, we should create multiple smaller interfaces, each representing a specific set of related methods. This allows clients to depend on the minimal set of methods they require, reducing the risk of coupling and providing a more coherent system.

// Bad example: A large interface containing unrelated methods
interface Worker {
    void work();

    void eat();

    void sleep();
}

// Good example: Splitting the interface into smaller, cohesive ones
interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

interface Sleepable {
    void sleep();
}

class Robot implements Workable {
    public void work() {
        // Robot working logic
    }
}

class Human implements Workable, Eatable, Sleepable {
    public void work() {
        // Human working logic
    }

    public void eat() {
        // Human eating logic
    }

    public void sleep() {
        // Human sleeping logic
    }
}

Dependency Inversion Principle (DIP)

The Dependency Inversion Principle focuses on decoupling high-level modules from low-level modules by introducing abstractions and relying on these abstractions. High-level modules should not depend on low-level modules directly; instead, they should depend on interfaces or abstract classes. This promotes flexibility, ease of testing, and modularity, as changes in low-level modules won't affect the higher-level ones.

// Bad example: High-level module depends on low-level module directly
class OrderService {
    private DatabaseRepository repository;

    public OrderService() {
        this.repository = new DatabaseRepository();
    }

    // OrderService logic using DatabaseRepository
}

// Good example: Using abstractions to invert the dependency
interface Repository {
    void saveData();
}

class DatabaseRepository implements Repository {
    public void saveData() {
        // Database saving logic
    }
}

class OrderService {
    private Repository repository;

    public OrderService(Repository repository) {
        this.repository = repository;
    }

    // OrderService logic using Repository
}

High-Level Module

A high-level module is a component or module that deals with broader, more abstract, and higher-level functionality of a software system. It often represents a larger part of the application and is responsible for orchestrating the interactions between various low-level modules. High-level modules tend to focus on business logic, overall system behavior, and user interactions.

Low-Level Module

A low-level module is a more specialized and granular component that handles specific, detailed, and focused functionality within a software system. These modules are typically closer to the underlying hardware or foundational operations of the system. They encapsulate specific operations or algorithms and are designed to perform a specific task or handle a specific aspect of the application.

Conclusion

The SOLID principles serve as a compass to guide software developers towards writing cleaner, more maintainable, and robust code. By adhering to these principles, developers can create flexible and scalable software systems that are easier to understand, modify, and extend. Embracing SOLID principles fosters good coding practices, promotes teamwork, and contributes to the long-term success of software projects. As you embark on your software development journey, keep these principles in mind, and witness the positive impact they bring to your projects. Happy coding!

Java Cleaner

Java Cleaner is a new feature introduced in Java 9 that allows you to define cleanup actions for groups of objects. Cleaners are implemented with the Cleanable interface, which descends from Runnable. Each Cleanable represents an object and a cleaning action registered in a Cleaner. Each Cleanable runs in a dedicated thread. All exceptions thrown by the cleaning action are ignored.

Java 18, marked the finalize method for removal. It is better to avoid the usage of finalize method.

The most efficient use of Cleaner is to explicitly invoke the clean() method when the object is closed or no longer needed. Note that the cleaning action must not refer to the object being registered.

Cleaners provide a more reliable way to perform cleanup tasks than relying on finalization. Finalization is often unreliable because it is not guaranteed to be called before the object is garbage collected. Cleaners, on the other hand, are guaranteed to be called before the object is garbage collected, as long as the clean() method is invoked.

See the sample usage of a Java Cleaner below:

import java.lang.ref.Cleaner;

public class SampleResourceUsage implements AutoCloseable {

    /**
     * A cleaner, preferably one shared within a library
     */
    private static final Cleaner cleaner = Cleaner.create();

    /**
     * An instance of Cleaner.Cleanable that the register returned.
     */
    final private Cleaner.Cleanable cleanable;

    /**
     * The resource that should be cleaned after use.
     */
    final private CloseableResource resource;

    public SampleResourceUsage() {
        //Instantiate a resource that must be cleaned after use.
        resource = new CloseableResource();

        //Register with the cleaner.
        cleanable = cleaner.register(this, resource::close);
    }

    public void use() {
        resource.use();
    }

    @Override
    public void close() {
        //Use clean method from the Cleanable instance to close the resource.
        cleanable.clean();
    }

    public static void main(String ... args) throws InterruptedException {
        //Create an instance with cleanable resource.
        var resource = new SampleResourceUsage();
        resource.use();

        //Remove the instance reference.
        resource = null;

        //Request for a garbage collection.
        System.gc();

        //Try to wait for a garbage collection to complete.
        //If garbage collection was not triggered. Increase the timeout.
        Thread.sleep(1000);
    }

    /**
     * The class that should have clean up operation.
     */
    private static class CloseableResource {
        public void use() {
            System.out.println("Using the resource that must be cleaned.");
        }

        /**
         * The cleaner that must be called for garbage collection.
         */
        public void close() {
            System.out.println("Cleaning up.");
        }
    }
}

Expect to see the following output:

Using the resource.
Cleaning up.

If you didn't see preceding output, you might need to check the timeout value.

Here are some additional things to keep in mind when using Java Cleaner:

  • The Cleaner object is a heavyweight object, so it should be shared whenever possible.
  • The Cleaner thread is a daemon thread, so it will not prevent your program from exiting.
  • The clean() method can only be called once. If you need to perform multiple cleanup actions, you should use a Runnable object to wrap the cleanup actions.

Checksum Using SHA-256 MessageDigest

//The data to be checksumed.
final var data = "Hello";
final MessageDigest md;
try {
    md = MessageDigest.getInstance("SHA-256");
    try (DigestInputStream dis = new DigestInputStream(
            //This can be replaced with FileInputStream.
            //The data just needs to be a File path.
            new ByteArrayInputStream(data.getBytes())
            , md)) {
        final var buffer = new byte[4096];
        int bytesRead;
        while ((bytesRead = dis.read(buffer)) != -1) {
            // Read the data into the buffer to update the digest
            md.update(buffer, 0, bytesRead);
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
    final var digest = md.digest();
    final var sb = new StringBuilder();
    for (final byte b : digest) {
        sb.append(String.format("%02x", b & 0xff));
    }
    System.out.println("Checksum: " + sb.toString());
} catch (NoSuchAlgorithmException e) {
    throw new RuntimeException(e);
}

Retrieving the X509 Certificate Information in Java

//This will hold the target leaf certificate.
Optional<Certificate> cert;

try {
    //Must hold the target URL.
    final var targetUrl = "https://www.google.com";
    final var httpsURL = new URL(targetUrl);
    final var connection = (HttpsURLConnection) httpsURL.openConnection();
    connection.connect();

    //Retrieve the first certificate. This is normally the leaf certificate.
    cert = Arrays.stream(connection.getServerCertificates()).findFirst();
} catch (IOException e) {
    throw new RuntimeException(e);
}

cert.ifPresent(cer -> {
    //Check if the instance of certificate is of type of X509Certificate.
    if(cer instanceof final X509Certificate x509) {
        System.out.println("Subject: " + x509.getSubjectX500Principal().getName());
        System.out.println("From: " + x509.getNotBefore());
        System.out.println("To: " + x509.getNotAfter());
        System.out.println("Duration: " + ChronoUnit.DAYS.between(LocalDate.now(), LocalDate.ofInstant(x509.getNotAfter().toInstant(),ZoneId.systemDefault())));
        System.out.println("\n");
    }
});

Showing JVM Flags and Values

Flags

Flag Description
UnlockDiagnosticVMOptions Unlocks the options intended for diagnosing the JVM. By default, this option is disabled and diagnostic options are not available.
PrintFlagsFinal Shows the final flag values that JVM uses.

Usage

java -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal -version

The usage of version argument is used to show the java version information instead of the help information.

Basic Docker Container Management

Run Command

Run a command in a new container

Display the help information using the following command:

docker container run --help

Example of creating a container for the swaggerapi/swagger-editor image with the name swagger-editor.

docker container run -it -p 80:8080 --name swagger-editor swaggerapi/swagger-editor

Since the -it options were used, you can stop the container using CTRL+C.

Access the swagger-editor using the following address:

http://localhost

This command can only be used for creating a container based on an image. Once the container was created you can start it using the start command. If you are not sure about the name of the container find it using the list command.

It is better to have a name for the container so that you know what to start (or stop or delete).

List Command

List containers.

Display the help information using the following command:

docker container ls --help

Example of displaying all the containers.

docker container ls --all

Start Command

Start one or more stopped containers

Display the help information using the following command:

docker container start --help

Example of starting the container created from the run command section.

docker container start swagger-editor

Stop Command

Stop one or more running containers

Display the help information using the following command:

docker container stop --help

Example of stopping the container created from the run command section.

docker container stop swagger-editor

Delete Command

Remove one or more containers

Display the help information using the following command:

docker container rm --help

Example of deleting the container created from the run command section.

docker container rm swagger-editor

Container Aware JVM Parameters for Heap Memory

JVM Parameters

Modify the java heap memory based on container memory using one of the following JVM parameters:

Parameter Description
-XX:InitialRAMPercentage The initial size of the heap based on the total container memory.
-XX:MinRAMPercentage The maximum heap size based on the size of the JVM running on small heap. The small is heap is of approximately 125MB.
-XX:MaxRAMPercentage The maximum heap size based on the size of the JVM running on greater than small heap.

Displaying the Current Value

Use the following command the display the current value of the container aware JVM Parameters using the following:

docker container run -it --rm openjdk:17.0.2-slim java -XX:+PrintFlagsFinal -version | grep -E ".*RAMPercentage"

Expect to see something like the following:

   double InitialRAMPercentage                     = 1.562500
                   {product} {default}
   double MaxRAMPercentage                         = 25.000000
                   {product} {default}
   double MinRAMPercentage                         = 50.000000
                   {product} {default}

Checking the Current Memory and CPU using JShell

  1. Open a jshell in the container using the following:

    docker container run -it --rm openjdk:17.0.2-slim jshell
  2. Paste the following command in jshell:

    var rt = Runtime.getRuntime();
    System.out.printf("Heap size: %dMB%nMaximum size of heap: %dMB%nAvailable processors: %d%n", 
     rt.totalMemory()/1024/1024, rt.maxMemory()/1024/1024, rt.availableProcessors());
  3. Press enter.

    Expect to see something similar to the following:

    rt ==> java.lang.Runtime@1d81eb93
    Heap size: 252MB
    Maximum size of heap: 3966MB
    Available processors: 16
    $2 ==> java.io.PrintStream@34c45dca
  4. Type the following in jshell and press enter.

    /exit

Allocating a Memory and CPU to a Container

  1. Execute the following command to allocate 100mb of memory and 1 CPU to the container:

    docker container run -it --rm -m 100m --cpus=1 openjdk:17.0.2-slim jshell
  2. Paste the following command in jshell:

    var rt = Runtime.getRuntime();
    System.out.printf("Heap size: %dMB%nMaximum size of heap: %dMB%nAvailable processors: %d%n", 
     rt.totalMemory()/1024/1024, rt.maxMemory()/1024/1024, rt.availableProcessors());
  3. Press enter.

    Expect to see something similar to the following:

    rt ==> java.lang.Runtime@6659c656
    Heap size: 7MB
    Maximum size of heap: 48MB
    Available processors: 1
    $2 ==> java.io.PrintStream@2d8e6db6

    Notice the following:

    Value
    Heap size 7MB
    Maximum size of heap 48MB
    Available processors 1
  4. Type the following in jshell and press enter.

    /exit

Modifying the Allocated Maximum Size of Heap Memory

  1. Execute the following command to allocate 100mb of memory and 1 CPU to the container:

    docker container run -it --rm -m 100m --cpus=1 openjdk:17.0.2-slim jshell -R-XX:MinRAMPercentage=80
  2. Paste the following command in jshell:

    var rt = Runtime.getRuntime();
    System.out.printf("Heap size: %dMB%nMaximum size of heap: %dMB%nAvailable processors: %d%n", 
     rt.totalMemory()/1024/1024, rt.maxMemory()/1024/1024, rt.availableProcessors());
  3. Press enter.

    Expect to see something similar to the following:

    rt ==> java.lang.Runtime@6659c656
    Heap size: 7MB
    Maximum size of heap: 77MB
    Available processors: 1
    $2 ==> java.io.PrintStream@2d8e6db6

    Notice the Maximum size of heap become 77MB. This is because of the JVM argument -XX:MinRAMPercentage=80 passed in jshell as:

    -R-XX:MinRAMPercentage=80

    We use the --XX:MinRAMPercentage=80 because the memory allocated is a small heap.

  4. Type the following in jshell and press enter.

    /exit
« Older posts Newer posts »