SW Design Bites: Monolithic Structure

"The only thing worse than starting something and failing ... is not starting something"
- Seth Godin

1.0 Introduction

This DesignBite sequence was inspired by project TextFinder. As that project is designed and implemented, a number of design decisions are made, consciously or unconsciously about structure - how packages and their dependencies are arranged - and implementation details. Each of these pages addresses one answer to questions about structure. To make discussion pragmatic and concrete, we implement a program that evaluates the number of lines in text files. Processing is quite simple so it allows us to see how each structure alternative is implemented. We consider both package structure and logical structure, e.g., the functions and structs used to order design and implementation. In this Basic Structure page, all code is implemented in a single package and a single struct provides all of the organization for processing.

2. Application Structure - Basic

This structure is monolithic, e.g., all code is packaged in a single file and there is only one struct used to implement processing, e.g., counting lines of code in files specified on the command line.
Figure 1. Basic Pkg Structure

Basic Monolithic Structure

Program's operations - read input, process data, output information - are implemented in a single package.

Pros:

  1. Simple, easy to create
  2. Only one piece to track

Cons:

  1. Everything is compiled for any changes
  2. As content grows becomes hard to understand and test
Project Notes: Counting lines:
  • if a file has no content its linecount = 0
  • if it has content but no newlines linecount = 1
  • otherwise its linecount = 1 + the number of newlines
  • the last newline is counted even if there is no following text
File name input
  • For production code, file names would come from command line arguments or path from command line to start directory search.
  • Here, we will specify them statically in Executive code to keep the demonstration and its execution simple.
DesignStructure Repository
Source Code Output
/////////////////////////////////////////////////////////////
// basic_structure::main.rs                                //
//                                                         //
// Jim Fawcett, https://JimFawcett.github.io, 07 Mar 2021  //
/////////////////////////////////////////////////////////////
/*
  BasicStructure
  - Demonstrates simplest form of structure: everything, e.g.,
    input, computation, and output, in one package.
  - It counts the number of lines in a file specified on the
    command line.
  - Simple so we can focus on code structure.
*/
#![allow(dead_code)]
use std::fs::*;
use std::io::{Error, ErrorKind, Read};

/*-- part of input processing --*/
fn open_file_for_read(file_name: &str) -> Result<File, std::io::Error> {
  let rfile = OpenOptions::new().read(true).open(file_name);
  rfile
}
/*-- part of compute processing --*/
fn read_file_to_string(f: &mut File) -> Result<String, std::io::Error> {
  let mut contents = String::new();
  let bytes_rslt = f.read_to_string(&mut contents);
  if bytes_rslt.is_ok() {
    Ok(contents)
  } else {
    Err(Error::new(ErrorKind::Other, "read error"))
  }
}

#[derive(Debug)]
pub struct Basic {
  name: String,
  file: Option<File>,
  lines: usize,
}

impl Default for Basic {
  fn default() -> Self {
    Self::new()
  }
}
impl Basic {
  pub fn new() -> Basic {
    Basic {
      name: String::new(),
      file: None,
      lines: 0,
    }
  }
  /*-----------------------------------------------------
    Input processing
  */
  pub fn parse_cmdln() -> Vec<String> {
    let cl_iter = std::env::args();
    let args: Vec<String> = cl_iter.skip(1).collect();
    args
  }
  pub fn show_cmdln(args: &[String]) {
    if args.is_empty() {
      return;
    }
    print!("\n  {}", args[0]);
    for arg in &args[1..] {
      print!(", {}", arg);
    }
  }
  /*-------------------------------------------------------
    Input processing
  */
  pub fn input(&mut self, name: &str) {
    self.name = name.to_string();
    let rslt = open_file_for_read(name);
    if let Ok(file) = rslt {
      self.file = Option::Some(file);
    } else {
      print!("\n  can't open file {:?}", name);
    }
  }
  /*-----------------------------------------------------
    Compute processing
  */
  pub fn compute(&mut self) {
    if let Some(file) = &mut self.file {
      let rslt = read_file_to_string(file);
      if let Ok(contents) = rslt {
        if contents.len() == 0 {
          self.lines = 0;
        }
        else {
          self.lines = 1;
        }
        for ch in contents.chars() {
          if ch == '\n' {
            self.lines += 1;
          }
        }
      }
    }
  }
  /*-----------------------------------------------------
    Output processing
  */
  pub fn output(&self) {
    print!("\n  {:4} lines in {:?}", self.lines, self.name);
  }
}
/*---------------------------------------------------------
  Executive processing
    cargo run -q <filename1>, <filename2>, ... -q
*/
fn main() {
  print!("\n  -- counting lines in files --\n");

  let mut basic = Basic::new();
  let args = Basic::parse_cmdln();

  for name in args {
    basic.input(&name);
    basic.compute();
    basic.output();
  }

  println!("\n\n  That's all Folks!\n\n");
}
> cargo run -q Cargo.toml ./src/main.rs       

  -- counting lines in files --

    10 lines in "Cargo.toml"
   129 lines in "./src/main.rs"

  That's all Folks!

Cargo.toml

[package]
name = "basic_structure"
version = "0.1.0"
authors = ["James W. Fawcett "]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

Comments:
  • Executive code resides in the left panel. It defines and creates an instance of type Basic.
  • Basic provides public methods:
    1. input(&mut self, name: &str)
      attempts to open named file, and if successul, attaches an Option containing the file handle to private member file.
    2. compute(&mut self)
      counts lines in the file opened with input(&mut self, name: &str) and stores the result in private member lines.
    3. output(&self)
      prints the name and number of lines to stdout.
  • The Executive then simply creates an instance of Basic and calls its parse_cmdln() function to get names of files to process.
  • It finishes by calling input(&name), compute(), and output() to evaluate number of lines and display the result for each file cited in the command line.
  • StructureBasic project has only one source file: src/main.rs. The total line count of the single StructureBasic file is 123 lines (see Program Output, above).
Final Comment:
  • Note from the Cargo.toml file that main.rs has no dependencies other than to the Rust standard library.
  • All of the Structures to follow have multiple files for Executive, Input, Compute, and Output processing. We will show their dependencies by examining each project's Cargo.toml file.

3. Epilogue

The fourh design alternatives considered here:
  1. Monolithic Structure
  2. Factored Structure
  3. DataFlow Structure
  4. TypeErase Structure
  5. PlugIn Structure
are progressively more flexible, eventually resulting in reusable components, but also increasingly complex. Where you settle in these alternatives is determined by design context. Is this a one-of-a-kind project that you want to finish quickly or is it heading for production code that will be maintained by more than one developer?