Add validation and business logic to domain models

Enhance Tool, BinItem, and MultiBin classes with:
- Input validation in constructors and property setters
- Guard clauses for invalid states (negative values, empty names)
- Business logic methods (CanFitItem, CalculateWaste, etc.)
- Proper encapsulation with backing fields
- Comprehensive XML documentation
- Override Equals/GetHashCode for value semantics
- Better ToString() implementations

These changes enforce business rules at the domain level and prevent
invalid states from being created.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
AJ
2025-11-22 23:02:37 -05:00
parent f55092d877
commit d1a5dd279c
3 changed files with 303 additions and 11 deletions

View File

@@ -1,16 +1,109 @@
namespace CutList using System;
namespace CutList
{ {
/// <summary>
/// Represents a cutting tool with its kerf (blade width).
/// Enforces business rules for valid tools.
/// </summary>
public class Tool public class Tool
{ {
public string Name { get; set; } private string _name;
private double _kerf;
public double Kerf { get; set; } public Tool(string name, double kerf, bool allowUserToChange = false)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Tool name cannot be empty", nameof(name));
if (kerf < 0)
throw new ArgumentException("Kerf cannot be negative", nameof(kerf));
_name = name;
_kerf = kerf;
AllowUserToChange = allowUserToChange;
}
/// <summary>
/// Parameterless constructor for serialization only.
/// Use the parameterized constructor for creating valid instances.
/// </summary>
public Tool()
{
}
public string Name
{
get => _name;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Tool name cannot be empty", nameof(value));
_name = value;
}
}
public double Kerf
{
get => _kerf;
set
{
if (value < 0)
throw new ArgumentException("Kerf cannot be negative", nameof(value));
_kerf = value;
}
}
public bool AllowUserToChange { get; set; } public bool AllowUserToChange { get; set; }
/// <summary>
/// Calculates the total material wasted for a given number of cuts.
/// </summary>
public double CalculateTotalWaste(int numberOfCuts)
{
if (numberOfCuts < 0)
throw new ArgumentException("Number of cuts cannot be negative", nameof(numberOfCuts));
return Kerf * numberOfCuts;
}
/// <summary>
/// Checks if this tool is compatible with the given material thickness.
/// Some tools may have minimum/maximum thickness requirements.
/// </summary>
public bool IsCompatibleWithThickness(double thickness)
{
// For now, all tools are compatible with any thickness
// This can be extended based on specific tool requirements
return thickness > 0;
}
public override string ToString() public override string ToString()
{ {
return Name; return Name;
} }
public override bool Equals(object obj)
{
if (obj is Tool other)
{
return Name == other.Name &&
Math.Abs(Kerf - other.Kerf) < 0.0001 &&
AllowUserToChange == other.AllowUserToChange;
}
return false;
}
public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = hash * 23 + (Name?.GetHashCode() ?? 0);
hash = hash * 23 + Kerf.GetHashCode();
hash = hash * 23 + AllowUserToChange.GetHashCode();
return hash;
}
}
} }
} }

View File

@@ -1,9 +1,97 @@
namespace SawCut using System;
namespace SawCut
{ {
/// <summary>
/// Represents an item to be placed in a bin.
/// Enforces business rules for valid items.
/// </summary>
public class BinItem public class BinItem
{ {
public string Name { get; set; } private string _name;
private double _length;
public double Length { get; set; } public BinItem(string name, double length)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Item name cannot be empty", nameof(name));
if (length <= 0)
throw new ArgumentException("Item length must be greater than zero", nameof(length));
_name = name;
_length = length;
}
/// <summary>
/// Parameterless constructor for serialization only.
/// Use the parameterized constructor for creating valid instances.
/// </summary>
public BinItem()
{
}
public string Name
{
get => _name;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Item name cannot be empty", nameof(value));
_name = value;
}
}
public double Length
{
get => _length;
set
{
if (value <= 0)
throw new ArgumentException("Item length must be greater than zero", nameof(value));
_length = value;
}
}
/// <summary>
/// Checks if this item can fit in the given available length.
/// </summary>
public bool CanFitIn(double availableLength)
{
return Length <= availableLength;
}
/// <summary>
/// Checks if this item can fit in the given available length with spacing.
/// </summary>
public bool CanFitInWithSpacing(double availableLength, double spacing)
{
return Length + spacing <= availableLength;
}
public override string ToString()
{
return $"{Name} ({Length}\")";
}
public override bool Equals(object? obj)
{
if (obj is BinItem other)
{
return Name == other.Name && Length.IsEqualTo(other.Length);
}
return false;
}
public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = hash * 23 + (Name?.GetHashCode() ?? 0);
hash = hash * 23 + Length.GetHashCode();
return hash;
}
}
} }
} }

View File

@@ -1,14 +1,125 @@
namespace SawCut using System;
namespace SawCut
{ {
/// <summary>
/// Represents a type of bin with quantity and priority.
/// Enforces business rules for valid bin configurations.
/// </summary>
public class MultiBin public class MultiBin
{ {
public int Quantity { get; set; } = 1; private int _quantity;
private double _length;
private int _priority;
public double Length { get; set; } public MultiBin(double length, int quantity = 1, int priority = 25)
{
if (length <= 0)
throw new ArgumentException("Bin length must be greater than zero", nameof(length));
if (quantity < -1 || quantity == 0)
throw new ArgumentException("Quantity must be positive or -1 for unlimited", nameof(quantity));
_length = length;
_quantity = quantity;
_priority = priority;
}
/// <summary> /// <summary>
/// Lower value priority with be used first. Default to 25 /// Parameterless constructor for serialization only.
/// Use the parameterized constructor for creating valid instances.
/// </summary> /// </summary>
public int Priority { get; set; } = 25; public MultiBin()
{
_quantity = 1;
_priority = 25;
}
/// <summary>
/// Quantity of bins available. Use -1 for unlimited bins.
/// </summary>
public int Quantity
{
get => _quantity;
set
{
if (value < -1 || value == 0)
throw new ArgumentException("Quantity must be positive or -1 for unlimited", nameof(value));
_quantity = value;
}
}
public double Length
{
get => _length;
set
{
if (value <= 0)
throw new ArgumentException("Bin length must be greater than zero", nameof(value));
_length = value;
}
}
/// <summary>
/// Lower value priority will be used first. Default is 25.
/// </summary>
public int Priority
{
get => _priority;
set => _priority = value;
}
/// <summary>
/// Checks if this bin type has unlimited quantity.
/// </summary>
public bool IsUnlimited => Quantity == -1;
/// <summary>
/// Checks if an item of the given length can fit in this bin.
/// </summary>
public bool CanFitItem(double itemLength)
{
return itemLength <= Length && itemLength > 0;
}
/// <summary>
/// Calculates the waste for a single bin if the given length is used.
/// </summary>
public double CalculateWaste(double usedLength)
{
if (usedLength < 0 || usedLength > Length)
throw new ArgumentException("Used length must be between 0 and bin length", nameof(usedLength));
return Length - usedLength;
}
public override string ToString()
{
var quantityStr = IsUnlimited ? "Unlimited" : Quantity.ToString();
return $"Bin {Length}\" (Qty: {quantityStr}, Priority: {Priority})";
}
public override bool Equals(object? obj)
{
if (obj is MultiBin other)
{
return Quantity == other.Quantity &&
Length.IsEqualTo(other.Length) &&
Priority == other.Priority;
}
return false;
}
public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = hash * 23 + Quantity.GetHashCode();
hash = hash * 23 + Length.GetHashCode();
hash = hash * 23 + Priority.GetHashCode();
return hash;
}
}
} }
} }