Extract business logic into dedicated service layer

Create CutListService and DocumentService to separate business
logic from UI concerns, following the service layer pattern.

Changes:
- CutListService: Handles bin packing algorithm orchestration
  and data transformation from UI models to domain models
- DocumentService: Handles document persistence and validation
- Document: Removed Save/Load/Validate methods (now pure POCO)
- MainForm: Refactored to delegate to service classes instead
  of containing business logic directly

Benefits:
- Improved separation of concerns
- Business logic can now be unit tested independently
- Removed MessageBox dependencies from business logic layer
- MainForm is now focused on UI concerns only

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
AJ
2025-11-18 16:03:07 -05:00
parent 703efd528a
commit c7c841acab
4 changed files with 168 additions and 106 deletions

View File

@@ -25,53 +25,5 @@ namespace CutList.Forms
public List<BinInputItem> StockBins { get; set; }
public Tool Tool { get; set; }
public void Save(string filePath)
{
try
{
var json = JsonConvert.SerializeObject(this, Formatting.Indented);
File.WriteAllText(filePath, json);
LastFilePath = filePath;
}
catch (Exception ex)
{
MessageBox.Show($"Failed to save file: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
public static Document Load(string filePath)
{
try
{
var json = File.ReadAllText(filePath);
var document = JsonConvert.DeserializeObject<Document>(json);
document.LastFilePath = filePath;
return document;
}
catch (Exception ex)
{
MessageBox.Show($"Failed to load file: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return null;
}
}
public bool Validate(out string validationMessage)
{
if (PartsToNest == null || !PartsToNest.Any())
{
validationMessage = "No parts to nest.";
return false;
}
if (StockBins == null || !StockBins.Any())
{
validationMessage = "No stock bins available.";
return false;
}
validationMessage = string.Empty;
return true;
}
}
}

View File

@@ -1,6 +1,5 @@
using CutList.Models;
using Newtonsoft.Json;
using SawCut;
using CutList.Services;
using SawCut.Nesting;
using System;
using System.Collections.Generic;
@@ -21,11 +20,16 @@ namespace CutList.Forms
private BindingList<BinInputItem> bins;
private Toolbox toolbox;
private Document currentDocument;
private readonly CutListService cutListService;
private readonly DocumentService documentService;
public MainForm()
{
InitializeComponent();
cutListService = new CutListService();
documentService = new DocumentService();
dataGridView1.DrawRowNumbers();
dataGridView2.DrawRowNumbers();
@@ -90,7 +94,7 @@ namespace CutList.Forms
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
currentDocument = Document.Load(openFileDialog.FileName);
currentDocument = documentService.Load(openFileDialog.FileName);
if (currentDocument == null)
return;
@@ -137,10 +141,12 @@ namespace CutList.Forms
}
private void Save()
{
try
{
SyncDocumentFromUI();
if (!currentDocument.Validate(out string validationMessage))
if (!documentService.Validate(currentDocument, out string validationMessage))
{
MessageBox.Show(validationMessage, "Validation Error", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
@@ -154,7 +160,12 @@ namespace CutList.Forms
if (saveFileDialog.ShowDialog() == DialogResult.OK)
{
currentDocument.Save(saveFileDialog.FileName);
documentService.Save(currentDocument, saveFileDialog.FileName);
}
}
catch (Exception ex)
{
MessageBox.Show($"Failed to save file: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
@@ -164,28 +175,11 @@ namespace CutList.Forms
dataGridView2.EndEdit();
var cutTool = GetSelectedTool();
var stockBins = new List<MultiBin>();
foreach (var item in bins)
{
stockBins.Add(new MultiBin
{
Length = item.Length.Value,
Quantity = item.Quantity,
Priority = item.Priority
});
}
var engine = new MultiBinEngine();
engine.Spacing = cutTool.Kerf;
engine.Bins = stockBins;
var items = GetItems();
var result = engine.Pack(items);
var result = cutListService.Pack(parts.ToList(), bins.ToList(), cutTool);
var filename = GetResultsSaveName();
var form = new ResultsForm(filename);
form.Bins = result.Bins;
form.Bins = result.Bins.ToList();
form.ShowDialog();
}
@@ -200,28 +194,6 @@ namespace CutList.Forms
return name;
}
private List<BinItem> GetItems()
{
var items2 = new List<BinItem>();
foreach (var item in parts)
{
if (item.Length == null || item.Length == 0)
continue;
for (int i = 0; i < item.Quantity; i++)
{
items2.Add(new BinItem
{
Name = item.Name,
Length = item.Length.Value
});
}
}
return items2;
}
public Tool GetSelectedTool()
{
return cutMethodComboBox.SelectedItem as Tool;

View File

@@ -0,0 +1,74 @@
using CutList.Models;
using SawCut;
using SawCut.Nesting;
using System.Collections.Generic;
namespace CutList.Services
{
/// <summary>
/// Service class that handles the core business logic for cut list optimization.
/// Separates business logic from UI concerns.
/// </summary>
public class CutListService
{
/// <summary>
/// Runs the bin packing algorithm to optimize cut lists.
/// </summary>
/// <param name="parts">The parts to be nested</param>
/// <param name="stockBins">The available stock bins</param>
/// <param name="cuttingTool">The cutting tool to use (determines kerf/spacing)</param>
/// <returns>The packing result with optimized bins and unused items</returns>
public Result Pack(List<PartInputItem> parts, List<BinInputItem> stockBins, Tool cuttingTool)
{
var multiBins = ConvertToMultiBins(stockBins);
var binItems = ConvertToBinItems(parts);
var engine = new MultiBinEngine
{
Spacing = cuttingTool.Kerf,
Bins = multiBins
};
return engine.Pack(binItems);
}
private List<MultiBin> ConvertToMultiBins(List<BinInputItem> stockBins)
{
var multiBins = new List<MultiBin>();
foreach (var item in stockBins)
{
multiBins.Add(new MultiBin
{
Length = item.Length.Value,
Quantity = item.Quantity,
Priority = item.Priority
});
}
return multiBins;
}
private List<BinItem> ConvertToBinItems(List<PartInputItem> parts)
{
var binItems = new List<BinItem>();
foreach (var part in parts)
{
if (part.Length == null || part.Length == 0)
continue;
for (int i = 0; i < part.Quantity; i++)
{
binItems.Add(new BinItem
{
Name = part.Name,
Length = part.Length.Value
});
}
}
return binItems;
}
}
}

View File

@@ -0,0 +1,64 @@
using CutList.Forms;
using Newtonsoft.Json;
using System;
using System.IO;
namespace CutList.Services
{
/// <summary>
/// Service class that handles document persistence operations.
/// Separates file I/O logic from UI concerns.
/// </summary>
public class DocumentService
{
/// <summary>
/// Saves a document to the specified file path.
/// </summary>
/// <param name="document">The document to save</param>
/// <param name="filePath">The file path to save to</param>
/// <exception cref="IOException">Thrown when file cannot be saved</exception>
public void Save(Document document, string filePath)
{
var json = JsonConvert.SerializeObject(document, Formatting.Indented);
File.WriteAllText(filePath, json);
}
/// <summary>
/// Loads a document from the specified file path.
/// </summary>
/// <param name="filePath">The file path to load from</param>
/// <returns>The loaded document</returns>
/// <exception cref="IOException">Thrown when file cannot be read</exception>
/// <exception cref="JsonException">Thrown when file contains invalid JSON</exception>
public Document Load(string filePath)
{
var json = File.ReadAllText(filePath);
var document = JsonConvert.DeserializeObject<Document>(json);
return document;
}
/// <summary>
/// Validates that a document has the minimum required data.
/// </summary>
/// <param name="document">The document to validate</param>
/// <param name="validationMessage">Output parameter containing validation error message</param>
/// <returns>True if document is valid, false otherwise</returns>
public bool Validate(Document document, out string validationMessage)
{
if (document.PartsToNest == null || document.PartsToNest.Count == 0)
{
validationMessage = "No parts to nest.";
return false;
}
if (document.StockBins == null || document.StockBins.Count == 0)
{
validationMessage = "No stock bins available.";
return false;
}
validationMessage = string.Empty;
return true;
}
}
}