Несколько конфигураций для одного класса

В последние дни я пытаюсь понять, как я могу изменить свой код, чтобы следовать SOLID принципы.

Но с этим примером я не могу найти правильный способ справиться.

Продукт имеет Name и BarCode:

public partial class Product
{
    public string Name { get; set; }
    public string BarCode { get; set; }
}

Кроме того, некоторые продукты должны иметь Batch Number и / или Sale Deadline Date:

public partial class Product
{
    public bool IsBatchNumberManaged { get; set; }
    public bool IsSaleDeadlineDateManaged { get; set; }
}

у меня есть немного Stock которые включают эти Products плюс некоторые данные:Quantity, Batch Number если нужно, Sale Deadline Date если нужно.

public class Stock
{
    public Product Product { get; set; }
    public int Quantity { get; set; }
    // should be null if Product is not managed by BN,
    // else it has not to be null
    public string BatchNumber { get; set; }
    // should be null if Product is not managed by SDD,
    // else it has not to be null
    public DateTime? SaleDeadlineDate { get; set; }
}

Я предполагаю, что мой Stock записи должны быть построены так:

public interface IStock
{
    Product Product { get; set; }
    int Quantity { get; set; }
}

public interface IBatchNumberManagedStock : IStock
{
    string BatchNumber { get; set; }
}

public interface ISaleDeadlineDateManagedStock : IStock
{
    DateTime SaleDeadlineDate { get; set; }
}

public class Stock : IStock
{
    public Product Product { get; set; }
    public int Quantity { get; set; }
}

А вот и классы, которые мне не нравятся:

public class BatchNumberManagedStock : IStock, IBatchNumberManagedStock
{ ... }

public class SaleDeadlineDateManagedStock : IStock, ISaleDeadlineManagedStock
{ ... }

public class BatchNumberAndSaleDeadlineDateManagedStock : IStock, IBatchNumberManagedStock, ISaleDeadlineDateManagedStock
{ ... }

Нужно ли создавать другой класс для каждой возможной конфигурации?
Что если теперь у меня есть это?

public partial class Product
{
    public bool IsSerialNumberManaged { get; set; }
}

Мне нужно создать четыре новых класса, просто чтобы реализовать это очень новое свойство?

public interface ISerialNumberManagedStock : IStock
{
    string SerialNumber { get; set; }
}

public class SerialNumberManagedStock : IStock, ISerialNumberManagedStock { ... }
public class BatchNumberAndSerialNumberManagedStock : ... { ... }
public class SaleDeadlineDateAndSerialNumberManagedStock : ... { ... }
public class BatchNumberAndSaleDeadlineDateAndSerialNumberManagedStock : ... { ... }

Это правильный способ реализации этой проблемы? Я действительно чувствую, что создал кракен, который потребует много работы по написанию кода для каждой новой конфигурации Product учебный класс...


Немного в будущем:< Эй, смотри, для такого рода Product нам нужно, чтобы BatchNumber был SaleDeadlineDate в формате "yyyyMMdd". >
Должен ли я снова строить новые классы?


Ох, и я забыл тебе сказать! Запас продуктов, управляемых SerialNumber, составляет 1!

3 ответа

Это может помочь так, как ваша проблема в настоящее время заявлена:

Вы заметили, что все ваши примеры, касающиеся "дополнительных" свойств, касаются управления? А как насчет того, чтобы разбить это на собственную концепцию и затем применить шаблон посетителей?

public interface IProductManagement
{
  void Accept(IProductManagementVisitor visitor);
}

public interface IManagedByBatchNumber
  : IProductManagement
{
  public int BatchNo { get; set; }      
}

public interface IManagedBySerialNumber
  : IProductManagement
{
  public int SerialNo { get; set; }   
}

... etc ...

public interface IProductManagementVisitor
{
  void Visit(IManagedByBatchNumber management);
  void Visit(IManagedBySerialNumber management);
  ...etc...
}

И обновить сток:

public class Stock
{
  public Product Product { get; set; }
  public int Quantity { get; set; }
  public IProductManagement Management { get; set; }
}

Добавить посетителей:

public class BatchNumberPrintingVisitor
  : IProductManagementVisitor
{
  public void Visit(IManagedByBatchNumber management)
  {
    Console.WriteLine($"Batch: {management.BatchNo}");
  }
  public void Visit(IManagedBySerialNumber management)
  { /* ignore */ }
}

Добавить менеджеров:

public class BatchNumberManagement
  : IManagedByBatchNumber
{
  public int BatchNo { get; set; }

  public void Accept(IProductManagementVisitor visitor)
  {
    visitor.Visit(this);
  }
}

public class SerialNumberManagement
  : IManagedBySerialNumber
{
  public int SerialNo { get; set; }

  public void Accept(IProductManagementVisitor visitor)
  {
    visitor.Visit(this);
  }
}

public class CompositeProductManagement
  : IProductManagement
{
  private readonly IEnumerable<IProductManagement> parts_; 

  public CompositeProductManagement(params IProductManagement[] parts)
  {
    parts_ = parts.ToArray();
  }

  public void Accept(IProductManagementVisitor visitor)
  {
    foreach (var part in parts_)
    {
      part.Accept(visitor);
    }
  }
}

И использовать:

var stockManagedByBatch = new Stock
{
  Product =  "A",
  Quantity = 1,
  Management = new BatchNumberManagement
  {
    BatchNo = 123456
  }
};

var stockManagedByBatchAndSerialNo = new Stock
{
  Product = "B",
  Quantity = 1,
  Management = new CompositeProductManagement(
    new BatchNumberManagement { BatchNo = 123456 },
    new SerialNumberManagement { SerialNo = 9870 }
  }
};

var stocks = new [] { stockManagedByBatch, stockManagedByBatchAndSerialNo };

// print batch# of all stocks managed by batch to console
var printingVisitor = new BatchNumberPrintingVisitor();

foreach (var stock in stocks)
{
  stock.Management.Accept(printingVisitor);
}

Обратите внимание, что IProductManagementVisitor Интерфейс также может рассматриваться как нарушение принципов SOLID, так как добавление новой концепции управления требует обновления всех посетителей. Если это действительно начинает болеть, можно переключиться на динамический шаблон посетителя:

public interface IProductManagement
{
  void Accept(IProductManagementVisitor visitor);
}

public interface IManagedByBatchNumber
  : IProductManagement
{
  public int BatchNo { get; set; }      
}

public interface IProductManagementVisitor
{
  void Visit(IProductManagement management);
}

public class BatchNumberPrintingVisitor
  : IProductManagementVisitor
{
  void Visit(IProductManagement management)
  {
    var batchManagement = management as IManagedByBatchNumber;
    if (Object.ReferenceEquals(null, batchManagement))
      return;

    Console.WriteLine($"Batch number: {batchManagement.BatchNo}");
  }
}

Я думаю, что вы перестарались. Если нет веской причины для создания отдельного интерфейса для всех расширений свойств, почему бы не просто

public interface IStock
{
    Product Product { get; set; }
    int Quantity { get; set; }
    bool IsBatchNumberManaged { get; }
    string BatchNumber { get; set; }
    bool IsSaleDeadlineDateManaged { get; }
    DateTime? SaleDeadlineDate { get; set; }
}

public class Stock : IStock
{
    public Product Product { get; set; }
    public int Quantity { get; set; }

    public IsBatchNumberManaged { get { return BatchNumber != null;} }
    string BatchNumber { get; set; }

    IsSaleDeadlineDateManaged { get { return SaleDeadlineDate != null;} }
    DateTime? SaleDeadlineDate { get; set; }    

}

После комментария @plalx и Zoran Horvat - Пользовательская реализация Option/Maybe Type в C#, я предложил следующее решение:

public abstract class Option<T>
{
    public abstract bool IsValueRequired { get; }
    public abstract bool HasValue { get; }

    public static implicit operator Option<T>(T value) =>
        new Some<T>(value);

    public static implicit operator Option<T>(None none) =>
        new None<T>();
}

public sealed class Some<T> : Option<T>, IEquatable<Some<T>>
{
    public T Value { get; }

    public override bool IsValueRequired { get { return true; } }
    public override bool HasValue { get { return Value != null; } }

    public Some()
    { this.Value = default(T); }

    public Some(T value)
    { this.Value = value; }

    /* ... */
}

public sealed class None<T> : Option<T>, IEquatable<None<T>>, IEquatable<None>
{
    public override bool IsValueRequired { get { return false; } }
    public override bool HasValue { get { return false; } }

    /* ... */
}

С этим классом у меня теперь есть три возможных состояния для каждого свойства:

  • Мне не нужно устанавливать значение None<T>
  • Мне нужно установить значение, и это значение не установлено Some<T>.Content = null
  • Мне нужно установить значение, и это значение установлено Some<T>.Content = someValue

мой Box класс теперь представлен с использованием этих Option свойства:

public class Stock
{
    public Product Product { get; }
    public int Quantity { get; set; }

    private Option<string> _batchNumber;
    public Option<string> BatchNumber
    {
        get { return _batchNumber; } 
        set { if (!_batchNumber.IsValueRequired) return; _batchNumber = value; }
    }
    /* ... */

    public Stock(Product product)
    {
        Product = product;

        if (product.IsBatchNumberManaged)
            _batchNumber = new Some<string>();
        else
            _batchNumber = None.Value;

        /* ... */
    }

    public bool CheckIntegrity()
    {
        if (BatchNumber.IsValueRequired && !BatchNumber.HasValue)
            return false;

        /* ... */

        return true;
    }
}

Наконец, я использую свой Stock Класс таким образом:

Product product = new Product
{
    Name = "Abc", BarCode = "123",
    IsBatchNumberManaged = true,
    IsSerialNumberManaged = true
};

Stock stock = new Stock(product);
stock.Quantity = 10;

// produces nothing
// it has no required value
// it already contains None<DateTime?>
// it does not change its value
stock.SaleDeadlineDate = DateTime.Now;

// it is Some<string> { Content = null }
stock.SerialNumber = "987654321";
// it now is Some<string> { Content = "987654321" }

Console.WriteLine($"Product Name:  {stock.Product.Name}");        // Abc
Console.WriteLine($"Quantity:      {stock.Quantity}");            // 10
Console.WriteLine($"Batch Number:  {stock.BatchNumber}");         // 
Console.WriteLine($"Sale Deadline: {stock.SaleDeadlineDate}");    // None
Console.WriteLine($"Serial Number: {stock.SerialNumber}");        // 987654321
Console.WriteLine();

if (stock.CheckIntegrity())   // return false because BatchNumber requires value and is Some<string> { Content = null }
    Console.WriteLine("This stock IS in a correct state");
else
    Console.WriteLine("This stock is NOT in a correct state");
Другие вопросы по тегам