about
3/07/2022
Factored Structure

Design Bite - Factored Structure

multi-package line-counter

"The measure of success is not whether you have a tough problem to deal with, but whether it is the same problem you had last year."
- John Foster Dulles

1.0 Introduction

This DesignBite sequence was inspired by BuildOn project TextFinder. As that project is designed and implemented, a number of design decisions are made, consciously or unconsciously. Each of these pages addresses one answer to questions about fundamental decisions of 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., functions and structs used to order design and implementation. In this PlugIn Structure page, code is implemented in a set of packages Executive, Input, Compute, and Output and their structs. That provides all of the organization for processing.

2. Application Structure - Factored

This structure is modular with a classic input-process-output structure. It differs from the previous Basic structure in that:
  • It consists of four packages.
  • Its code size is about 3 times that of the basic structure. That's because each package provides a user-defined type and testing code.
  • For small projects this additional structure may not always be warranted.
  • For bigger projects it is much easier to understand and test.
Figure 3. Factored Pkg Structure

Factored Structure

Program's operations are implemented in separate packages. Some of the factored packages may, themselves, be factored.

Pros:

  1. If only one package is changed, only that one is compiled
  2. Much easier to understand and test

Cons:

  1. Project setup gets more complex
  2. More pieces to track and deploy
  3. Executive has to participate in each stage of the processing

Factored Code Repository
Executive::main.rs ///////////////////////////////////////////////////////////// // FactoredStructure::Executive::main.rs // // - Executive creates and uses all lower level parts // // Jim Fawcett, https://JimFawcett.github.io, 04 Mar 2021 // ///////////////////////////////////////////////////////////// #![allow(dead_code)] #![allow(unused_imports)] use input::*; use compute::*; use output::*; //use crate::exec_proc_mod::{parse_cmdln, show_args}; mod exec_proc_mod; use exec_proc_mod::{parse_cmdln, show_cmdln}; /*-- run using command line args --*/ fn use_args_for_run() -> usize { let args = parse_cmdln(); let mut total_lines:usize = 0; for name in &args { let mut lines = 0; let mut inp = Input::new(); let mut cmp = Compute::new(); let out = Output::new(); let opt = inp.do_input(name); if let Some(file) = opt { cmp.do_compute(name, file); lines = cmp.lines(); out.do_output(name, lines); } else { print!("\n couldn't process {:?}", name); } total_lines += lines; } total_lines } /*-- attempt to open file and count its lines of code --*/ fn test_ops(name: &str) -> usize { let mut inp = Input::new(); let mut cmp = Compute::new(); let out = Output::new(); let mut lines:usize = 0; let opt = inp.do_input(name); if let Some(file) = opt { cmp.do_compute(name, file); lines = cmp.lines(); out.do_output(name, lines); } else { print!("\n couldn't process {:?}", name); } lines } /*-- run test_ops for each package in this project --*/ fn run_test_ops() -> usize { let name = "../Executive/src/main.rs"; let mut lines = 0; lines += test_ops(name); println!(); let name = "../Input/src/lib.rs"; lines += test_ops(name); let name = "../Input/examples/test1.rs"; lines += test_ops(name); println!(); let name = "../Compute/src/lib.rs"; lines += test_ops(name); let name = "../Compute/examples/test1.rs"; lines += test_ops(name); println!(); let name = "../Output/src/lib.rs"; lines += test_ops(name); let name = "../Output/examples/test1.rs"; lines += test_ops(name); println!(); let name = "no-exist"; lines += test_ops(name); println!(); lines } /*-- Executive processing --*/ fn main() { print!("\n -- FactoredStructure::Executive --\n"); let lines = run_test_ops(); // let lines = use_args_for_run(); print!("\n total lines: {}", lines); print!("\n\n That's all Folks!\n\n"); } Executive::exec_proc_mod.rs ///////////////////////////////////////////////////////////// // FactoredStructure::Executive::exec_proc_mod.rs // // - Executive creates and uses all lower level parts // // Jim Fawcett, https://JimFawcett.github.io, 04 Mar 2021 // ///////////////////////////////////////////////////////////// /* Extracted as Module since these could be useful in other programs. */ /*-- extract command line arguments into vector --*/ pub fn parse_cmdln() -> Vec<String> { let cl_iter = std::env::args().into_iter(); let args: Vec<String> = cl_iter.skip(1).collect(); args } /*-- display command line args in comma seperated list --*/ pub fn show_cmdln(args: &Vec<String>) { if args.len() == 0 { return; } print!("\n {}", args[0]); for arg in &args[1..] { print!(", {}", arg); } } Output -- FactoredStructure::Executive -- 103 lines in file "../Executive/src/main.rs" 26 lines in file "../Executive/src/exec_proc_mod.rs" 41 lines in file "../Input/src/lib.rs" 22 lines in file "../Input/examples/test1.rs" 60 lines in file "../Compute/src/lib.rs" 45 lines in file "../Compute/examples/test1.rs" 24 lines in file "../Output/src/lib.rs" 15 lines in file "../Output/examples/test1.rs" can't open file "no-exist" couldn't process "no-exist" total lines: 336 That's all Folks! cargo.toml [package] name = "executive" version = "0.1.0" authors = ["James W. Fawcett"] edition = "2018" # See more keys ... [dependencies] input = { path = "../Input" } compute = { path = "../Compute" } output = { path = "../Output" } Input::lib.rs ///////////////////////////////////////////////////////////// // FactoredStructure::Input::lib.rs // // - Input attempts to open named file and return File // // Jim Fawcett, https://JimFawcett.github.io, 04 Mar 2021 // ///////////////////////////////////////////////////////////// use std::fs::*; mod file_utilities; use file_utilities::open_file_for_read; #[derive(Debug)] pub struct Input { name: String, } impl Input { pub fn new() -> Input { Input { name: String::new(), } } pub fn do_input(&mut self, name: &str) -> Option<File> { let rslt = open_file_for_read(name); if let Ok(file) = rslt { return Some(file); } else { print!("\n can't open file {:?}", name); return None; } } } #[cfg(test)] mod tests { #[test] fn it_works() { assert_eq!(2 + 2, 4); // dummy test } } file_utilities module ///////////////////////////////////////////////////////////// // FactoredStructure::Input::file_utilities.rs // // - Input attempts to open named file and return File // // Jim Fawcett, https://JimFawcett.github.io, 04 Mar 2021 // ///////////////////////////////////////////////////////////// /* This code may be useful for other programs so it is factored into a module here. */ #![allow(dead_code)] use std::fs::*; use std::io::{Read, Error, ErrorKind}; pub fn open_file_for_read(file_name:&str) ->Result<File, std::io::Error> { let rfile = OpenOptions::new() .read(true) .open(file_name); rfile } pub 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")) } } test1.rs ///////////////////////////////////////////////////////////// // FactoredStructure::Input::test1.rs // // - Input attempts to open named file and return File // // Jim Fawcett, https://JimFawcett.github.io, 04 Mar 2021 // ///////////////////////////////////////////////////////////// use input::*; fn main() { print!("\n -- input::test1 --\n"); let mut inp = Input::new(); let name = "./src/lib.rs"; let opt = inp.do_input(name); if let Some(_) = opt { print!("\n opened file {:?}", name); } else { print!("\n couldn't open file {:?}", name); } print!("\n\n That's all Folks!\n\n"); } Test Output -- input::test1 -- opened file "./src/lib.rs" That's all Folks! cargo.toml [package] name = "input" version = "0.1.0" authors = ["James W. Fawcett"] edition = "2018" # See more keys ... [dependencies] Compute::lib.rs ///////////////////////////////////////////////////////////// // FactoredStructure::Compute::lib.rs // // - Input attempts to read File to string & count lines // // Jim Fawcett, https://JimFawcett.github.io, 04 Mar 2021 // ///////////////////////////////////////////////////////////// use std::fs::*; use std::fs::*; mod file_utilities; use file_utilities::read_file_to_string; #[derive(Debug)] pub struct Compute { lines: usize, } impl Compute { pub fn new() -> Compute { Compute { lines: 0, } } /*-- read file, count lines and save count --*/ pub fn do_compute(&mut self, name: &str, mut file:File) { let rslt = read_file_to_string(&mut file); if let Ok(contents) = rslt { self.lines = 1; for ch in contents.chars() { if ch == '\n' { self.lines += 1; } } } else { print!("\n couldn't open {:?}", name); } } /*-- return saved line count --*/ pub fn lines(&self) -> usize { self.lines } } #[cfg(test)] mod tests { #[test] fn it_works() { assert_eq!(2 + 2, 4); } } Module file_utilities Module file copied from input/src folder. This copying can be avoided by using module helper files, but the cure seems worse than the disease. test1.rs ///////////////////////////////////////////////////////////// // FactoredStructure::Compute::test1.rs // // - Compute attempts to read file and count lines // // Jim Fawcett, https://JimFawcett.github.io, 04 Mar 2021 // ///////////////////////////////////////////////////////////// use compute::*; use std::fs::*; use std::io::*; fn open_file_for_read(file_name:&str) -> Result<File> { let rfile = OpenOptions::new() .read(true) .open(file_name); rfile } fn test_compute(name: &str) -> usize { let mut lines = 0usize; let rslt = open_file_for_read(name); if let Ok(file) = rslt { let mut compute = Compute::new(); compute.do_compute(name, file); lines = compute.lines(); } else { print!("\n couldn't open {:?}", name); } lines } fn main() { print!("\n -- compute::test1 --\n"); let name = "./src/lib.rs"; let lines = test_compute(name); print!("\n lines in {:?} = {}", name, lines); let name = "no-exist"; let lines = test_compute(name); print!("\n lines in {:?} = {}", name, lines); print!("\n\n That's all Folks!\n\n"); } Output -- compute::test1 -- lines in "./src/lib.rs" = 48 couldn't open "no-exist" lines in "no-exist" = 0 That's all Folks! cargo.toml [package] name = "compute" version = "0.1.0" authors = ["James W. Fawcett"] edition = "2018" # See more keys ... [dependencies] Output::lib.rs ///////////////////////////////////////////////////////////// // FactoredStructure::Output::lib.rs // // - Output displays line count // // Jim Fawcett, https://JimFawcett.github.io, 04 Mar 2021 // ///////////////////////////////////////////////////////////// #[derive(Debug)] pub struct Output {} impl Output { pub fn new() -> Output { Output {} } pub fn do_output(&self, name: &str, lines: usize) { print!("\n {:4} lines in file {:?}", lines, name); } } #[cfg(test)] mod tests { #[test] fn it_works() { assert_eq!(2 + 2, 4); } } test1.rs ///////////////////////////////////////////////////////////// // FactoredStructure::Output::test1.rs // // - Output displays line count // // Jim Fawcett, https://JimFawcett.github.io, 04 Mar 2021 // ///////////////////////////////////////////////////////////// use output::*; fn main() { print!("\n -- test Output --\n"); let out = Output::new(); out.do_output("SomeFile.rs", 3); print!("\n That's all Folks!\n\n"); } Test Output -- test Output -- 3 lines in file "SomeFile.rs" That's all Folks! cargo.toml [package] name = "output" version = "0.1.0" authors = ["James W. Fawcett"] edition = "2018" # See more keys ... [dependencies]

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?
  Next Prev Pages Sections About Keys