ООП. Описание процесса переливания чего-то из одного сосуда в другой

В рамках изучения попробовал реализовать программу в которой можно перелить что-то (скорее всего жидкость) из одного сосуда в другой, получилась следующая архитектура.

В мейне понял что есть метод с неоднозначным вызовом. ((IStatable<IVesselState>)vesselA).getState().Volum понимаю что в нормальной ситуации это не решение. Подскажите, пожалуйста, как сделать нормально, но не просто переименовывая методы. Может я с построением архитектуры ошибся и вообще неправильно мыслю.

Код:

interface IState
{
        
}
interface IStatable<T> where T: IState
{
    T getState();
}
interface ITransfuser<T> where T: IState
{
    void Transfuse(ITransfuseable<T> a, ITransfuseable<T> b);
}
interface ITransfuseable<T> where T: IState
{
    T getState();
}
interface IVesselState : IState
{
    double Volum { get; set; }
    double getMaxVolume();
}
interface IVessel : IStatable<IVesselState>, ITransfuseable<IVesselState>
{
    void pour(double v);
}
public void Transfuse(ITransfuseable<IVesselState> a, ITransfuseable<IVesselState> b)
{
    double canAccept = a.getState().getMaxVolume() - a.getState().Volum;
    if(canAccept > 0)
    {
        double leftB = b.getState().Volum - canAccept;
        if (leftB < 0)
        {
             canAccept -= Math.Abs(leftB);
        }
        a.getState().Volum += canAccept;
        b.getState().Volum -= canAccept;
        }
    }
}
class VesselState : IVesselState
{
    private double _maxVolume;
    public double Volum { get; set; }

    public VesselState(double max)
    {
       _maxVolume = max;
    }
    public double getMaxVolume()
    {
        return _maxVolume;
    }
}
class Vessel : IVessel
{
   IVesselState _state;
   public Vessel(double max)
   {
    _state = new VesselState(max);
   }
   public void pour(double v)
   {
       _state.Volum += v;
   }
   public IVesselState getState()
   {
       return _state;
   }
}
class Program
{ 
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");

        IVessel vesselA = new Vessel(20);
        vesselA.pour(10);
        IVessel vesselB = new Vessel(30);
        vesselB.pour(5);

        ITransfuser<IVesselState> transfuser = new VesselTransfuser();
        Console.WriteLine(((IStatable<IVesselState>)vesselA).getState().Volum);
        Console.WriteLine(((IStatable<IVesselState>)vesselA).getState().Volum);
        //Код ниже не работает: неоднозначный вызов getState()
        //Console.WriteLine(vesselA.getState().Volum);
        //Console.WriteLine(vesselB.getState().Volum);
        transfuser.Transfuse(vesselA, vesselB);
        Console.WriteLine(((IStatable<IVesselState>)vesselA).getState().Volum);
        Console.WriteLine(((IStatable<IVesselState>)vesselB).getState().Volum);
    }
}

Ответы (1 шт):

Автор решения: aepot

При проектировании объектной модели приложения надо учитывать несколько вещей:

  • SRP (из SOLID) - интерфейс должен предоствалять какую-то одну роль для класса.
  • Дубликаты - методы или свойства, которые дублируют друг друга. Например ваша ошибка в том, что у вас есть метод "налить", и в то же время можно налить через изменение Volume.
  • Слабые связи. Абстракции друг о друге должны знать как можно меньше, но ровно столько, чтобы обеспечить друг другу нормальное поведение.
  • Конкретная реализация класса может быть конкретнее, чем интерфейс, даже в публичной части. Этот фокус мне и помог закрыть Volume на запись извне.

Именно исправление ITransfuseable как раз и решило проблему проектирования. Остальное всё блестяшки. Переливаемое означает, что оттуда можно слить и можно налить - 2 очевидных метода.

То есть слив и залив теперь возможен только через специально предназначенные для этого методы. Например вы решите, что в бочке плохое горлышко и 10% заливаемой в нее воды будет проливаться мимо. Метод залива поможет вам это сделать не задевая реализацию переливальщика.

Интерфейсы

interface IState
{

}

interface IStatable<T> where T : IState
{
    T State { get; }
}
   
interface ITransfuseable<T> : IStatable<T> where T : IState
{
    void Pour(double value);
    void Drain(double value);
}

interface ITransfuser<T, TState> 
    where T : ITransfuseable<TState> 
    where TState : IState
{
    void Transfuse(T dst, T src);
}

interface IVesselState : IState
{
    double Volume { get; }
    double MaxVolume { get; }
}

interface IVessel : ITransfuseable<IVesselState>
{

}

interface IVesselTransfuser : ITransfuser<IVessel, IVesselState>
{

}

Реализации

class VesselState : IVesselState
{
    public double Volume { get; set; }
    public double MaxVolume { get; }

    public VesselState(double max)
    {
        MaxVolume = max;
    }
}

class Vessel : IVessel
{
    private VesselState _state;

    public IVesselState State => _state;

    public Vessel(double max)
    {
        _state = new VesselState(max);
    }

    public void Pour(double value)
    {
        _state.Volume += value;
    }
        
    public void Drain(double value)
    {
        _state.Volume -= value;
    }
}

class VesselTransfuser : IVesselTransfuser
{
    public void Transfuse(IVessel dst, IVessel src)
    {
        double amount = dst.State.MaxVolume - dst.State.Volume;
        if (amount > 0)
        {
            double leftB = src.State.Volume - amount;
            if (leftB < 0)
            {
                amount -= Math.Abs(leftB);
            }
            dst.Pour(amount);
            src.Drain(amount);
        }
    }
}

Кстати, старайтесь давать имена хотя-бы аргументам понятные, а то у вас получилось Transfuse(a, b) - это откуда куда? "Из А в Б" - подумал я, а вот и нет!

Тестирование

class Program
{
    static void Main(string[] args)
    {
        IVessel vesselA = new Vessel(20);
        vesselA.Pour(10);
        IVessel vesselB = new Vessel(30);
        vesselB.Pour(5);

        IVesselTransfuser transfuser = new VesselTransfuser();
        Console.WriteLine(vesselA.State.Volume);
        Console.WriteLine(vesselB.State.Volume);
        transfuser.Transfuse(vesselA, vesselB);
        Console.WriteLine(vesselA.State.Volume);
        Console.WriteLine(vesselB.State.Volume);
    }
}

Вывод в консоль

10
5
15
0

P.S. Имена методов в C# пишутся в CamelCase, то есть с большой буквы.

→ Ссылка