Как применить ограничения между отделенными объектами? - PullRequest
4 голосов
/ 08 ноября 2010

Примечание - Я переместил исходный пост в конец, потому что я думаю, что он по-прежнему имеет ценность для новичков в этой теме. Ниже следует попытка переписать вопрос на основе обратной связи.

Полностью отредактированный пост

Хорошо, я постараюсь подробнее рассказать о моей конкретной проблеме. Я понимаю, что немного смешиваю доменную логику с логикой взаимодействия / представления, но, если честно, я не уверен, где ее отделить. Пожалуйста, потерпите меня :)

Я пишу приложение, которое (среди прочего) выполняет моделирование логистики для перемещения вещей. Основная идея заключается в том, что пользователь видит проект, аналогичный Visual Studio, где он может добавлять, удалять, присваивать имена, организовывать, комментировать и т. Д. Различные объекты, которые я собираюсь обрисовать в общих чертах:

  • Элементы и Местоположения являются базовыми элементами данных без поведения.

    class Item { ... }
    
    class Location { ... }
    
  • A WorldState - это коллекция пар предмет-местоположение. WorldState является изменчивым: пользователь может добавлять и удалять элементы или менять их местоположение.

    class WorldState : ICollection<Tuple<Item,Location>> { }
    
  • A План представляет перемещение предметов в разные места в нужное время. Они могут быть импортированы в проект или сгенерированы в программе. Он ссылается на WorldState, чтобы получить начальное местоположение различных объектов. План также изменчив.

    class Plan : IList<Tuple<Item,Location,DateTime>>
    {
       WorldState StartState { get; }
    }
    
  • A Симуляция затем выполняет план. Он инкапсулирует много довольно сложного поведения и других объектов, но конечным результатом является SimulationResult , который представляет собой набор метрик, которые в основном описывают, насколько эта стоимость и насколько хорошо был выполнен План (представьте, что Проект треугольник)

    class Simulation 
    {
       public SimulationResult Execute(Plan plan);
    }
    
    class SimulationResult
    {
       public Plan Plan { get; }
    }
    

Основная идея заключается в том, что пользователи могут создавать эти объекты, связывать их вместе и потенциально использовать повторно. WorldState может использоваться несколькими объектами Plan. Затем симуляция может быть запущена на нескольких планах.

Риск быть ужасно многословным, пример

var bicycle = new Item();
var surfboard = new Item();
var football = new Item();
var hat = new Item();

var myHouse = new Location();
var theBeach = new Location();
var thePark = new Location();

var stuffAtMyHouse = new WorldState( new Dictionary<Item, Location>() {
    { hat, myHouse },
    { bicycle, myHouse },
    { surfboard, myHouse },
    { football, myHouse },
};

var gotoTheBeach = new Plan(StartState: stuffAtMyHouse , Plan : new [] { 
    new [] { surfboard, theBeach, 1/1/2010 10AM }, // go surfing
    new [] { surfboard, myHouse, 1/1/2010 5PM }, // come home
});

var gotoThePark = new Plan(StartState: stuffAtMyHouse , Plan : new [] { 
    new [] { football, thePark, 1/1/2010 10AM }, // play footy in the park
    new [] { football, myHouse, 1/1/2010 5PM }, // come home
});

var bigDayOut = new Plan(StartState: stuffAtMyHouse , Plan : new [] { 
    new [] { bicycle, theBeach, 1/1/2010 10AM },  // cycle to the beach to go surfing
    new [] { surfboard, theBeach, 1/1/2010 10AM },  
    new [] { bicycle, thePark, 1/1/2010 1PM },  // stop by park on way home
    new [] { surfboard, thePark, 1/1/2010 1PM },
    new [] { bicycle, myHouse, 1/1/2010 1PM },  // head home
    new [] { surfboard, myHouse, 1/1/2010 1PM },

});

var s1 = new Simulation(...);
var s2 = new Simulation(...);
var s3 = new Simulation(...);

IEnumerable<SimulationResult> results = 
    from simulation in new[] {s1, s2}
    from plan in new[] {gotoTheBeach, gotoThePark, bigDayOut}
    select simulation.Execute(plan);

Проблема в том, когда выполняется нечто подобное:

stuffAtMyHouse.RemoveItem(hat); // this is fine
stuffAtMyHouse.RemoveItem(bicycle); // BAD! bicycle is used in bigDayOut, 

Таким образом, в основном, когда пользователь пытается удалить элемент из WorldState (и, возможно, весь проект) с помощью вызова world.RemoveItem(item), я хочу убедиться, что этот элемент не упоминается ни в каких объектах Plan, которые используют этот WorldState. Если это так, я хочу сказать пользователю: «Эй! Следующий План X использует этот Предмет! Пойди и разберись с этим, прежде чем пытаться удалить его!». Тип поведения, который я не хочу получить от world.RemoveItem(item) вызова:

  • Удаление элемента, но план все еще имеет ссылку на него.
  • Удаляя элемент, но с помощью Плана без уведомления удалите все элементы в своем списке, которые ссылаются на этот элемент. (на самом деле это, вероятно, желательно, но только как вторичный вариант).

Так что мой вопрос в основном состоит в том, как можно реализовать такое желаемое поведение в чистом виде. Я подумал о том, чтобы сделать это областью пользовательского интерфейса (поэтому, когда пользователь нажимает «del» на элементе, он запускает сканирование объектов Plan и выполняет проверку перед вызовом world.RemoveItem (item)) - но (a) I Я также позволяю пользователю писать и выполнять собственные сценарии, чтобы они могли сами вызывать world.RemoveItem(item), и (б) я не уверен, что это поведение является чисто «пользовательским интерфейсом».

Уф. Ну, я надеюсь, что кто-то все еще читает ...

Исходное сообщение

Предположим, у меня есть следующие классы:

public class Starport
{
    public string Name { get; set; }
    public double MaximumShipSize { get; set; }
}

public class Spaceship
{
    public readonly double Size;
    public Starport Home;
}

Итак, предположим, что существует ограничение, согласно которому размер космического корабля должен быть меньше или равен MaximumShipSize его дома.

Так как нам с этим справиться?

Традиционно я делал что-то похожее на это:

partial class Starport
{
    public HashSet<Spaceship> ShipsCallingMeHome; // assume this gets maintained properly
    private double _maximumShipSize;
    public double MaximumShipSize
    {
        get { return _maximumShipSize; } 
        set
        {
            if (value == _maximumShipSize) return;
            foreach (var ship in ShipsCallingMeHome)
                if (value > ship)
                    throw new ArgumentException();
            _maximumShipSize = value
        }
    }
}

Это легко сделать для простого примера, подобного этому (так что, вероятно, это плохой пример), но я нахожу, что ограничения становятся больше и сложнее, и я хочу больше связанных функций (например, реализовать метод bool CanChangeMaximumShipSizeTo(double) илидополнительные методы, которые будут собирать корабли, которые слишком велики) Я заканчиваю тем, что пишу больше ненужных двунаправленных отношений (в этом случае SpaceBase-Spaceship, возможно, уместно) и сложный код, который в значительной степени не имеет отношения к уравнению владельцев.

Так как же с такими вещами обычно справляются?Вещи, которые я рассмотрел:

  1. Я рассмотрел возможность использования событий, аналогичных шаблону ComponentModel INotifyPropertyChanging / PropertyChanging, за исключением того, что EventArgs будет иметь какую-то возможность Veto () или Error () (так же, как winforms позволяет вам использовать ключ или подавить выход из формы).Но я не уверен, является ли это злоупотреблением событиями или нет.

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

Мне нужна эта строка, иначе форматирование не будет работать

interface IStarportInterceptor
{
    bool RequestChangeMaximumShipSize(double newValue);
    void NotifyChangeMaximumShipSize(double newValue);
}

partial class Starport
{
    public HashSet<ISpacebaseInterceptor> interceptors; // assume this gets maintained properly
    private double _maximumShipSize;
    public double MaximumShipSize
    {
        get { return _maximumShipSize; } 
        set
        {
            if (value == _maximumShipSize) return;
            foreach (var interceptor in interceptors)
                if (!RequestChangeMaximumShipSize(value))
                    throw new ArgumentException();
            _maximumShipSize = value;
            foreach (var interceptor in interceptors)
                NotifyChangeMaximumShipSize(value);
        }
    }
}

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

Третий вариант - это, может быть, какой-то очень странный пример использования PostSharp или контейнера IoC / Dependency Injection.Я еще не вполне готов идти по этому пути.

Объект Бога, который управляет всеми проверками и т. Д. - просто ищу стек-поток для богаобъект создает у меня впечатление, что это плохо и неправильно

Моя главная проблема в том, что это кажется довольно очевидной проблемой, и то, что я думал, было бы разумнообщий, но я не видел никаких обсуждений по этому поводу (например, System.ComponentModel не предоставляет никаких средств для вето событий PropertyChanging - не так ли?);это заставляет меня бояться, что я (еще раз) не смог понять некоторые фундаментальные понятия в соединении или (что еще хуже) объектно-ориентированного проектирования в целом.

Комментарии?}

Ответы [ 5 ]

1 голос
/ 09 ноября 2010

Вы хотите применить ограничения к действиям, но применить их к данным.

Во-первых, почему допускается изменение Starport.MaximumShipSize? Когда мы "изменяем размеры" Starport, не должны ли взлететь все корабли?

Это такие вопросы, чтобы лучше понять, что нужно делать (и нет «правильного и неправильного» ответа, есть «мой и ваш»).

Посмотрите на проблему под другим углом:

public class Starport
{
    public string Name { get; protected set; }
    public double MaximumShipSize { get; protected set; }

    public AircarfDispatcher GetDispatcherOnDuty() {
        return new AircarfDispatcher(this); // It can be decoupled further, just example
    }
}

public class Spaceship
{
    public double Size { get; private set; };
    public Starport Home {get; protected set;};
}

public class AircarfDispatcher
{
    Startport readonly airBase;
    public AircarfDispatcher(Starport airBase) { this.airBase = airBase; }

    public bool CanLand(Spaceship ship) {
        if (ship.Size > airBase.MaximumShipSize)
            return false;
        return true;
    }

    public bool CanTakeOff(Spaceship ship) {
        return true;
    }

    public bool Land(Spaceship ship) {
        var canLand = CanLand(ship);
        if (!canLand)
            throw new ShipLandingException(airBase, this, ship, "Not allowed to land");
        // Do something with the capacity of Starport
    }

}


// Try to land my ship to the first available port
var ports = GetPorts();
var onDuty = ports.Select(p => p.GetDispatcherOnDuty())
    .Where(d => d.CanLand(myShip)).First();
onDuty.Land(myShip);

// try to resize! But NO we cannot do that (setter is protected)
// because it is not the responsibility of the Port, but a building company :)
ports.First().MaximumShipSize = ports.First().MaximumShipSize / 2.0
1 голос
/ 09 ноября 2010

На основании пересмотренного вопроса:

Я думаю, классу WorldState нужен делегат ... И Plan установит метод, который должен вызываться для проверки, используется ли элемент. Сортоф, как:

delegate bool IsUsedDelegate(Item Item);

public class WorldState {

    public IsUsedDelegate CheckIsUsed;

    public bool RemoveItem(Item item) {

        if (CheckIsUsed != null) {
            foreach (IsUsedDelegate checkDelegate in CheckIsUsed.GetInvocationList()) {
                if (checkDelegate(item)) {
                    return false;  // or throw exception
                }
            }
        }

        //  Remove the item

        return true;
    }

}

Затем в конструкторе плана установите делегат с именем

public class plan {

    public plan(WorldState state) {
        state.IsUsedDelegate += CheckForItemUse;
    }

    public bool CheckForItemUse(Item item) {
         // Am I using it?
    }

}

Это очень грубо, конечно, я постараюсь добавить еще после обеда :) Но вы поняли основную идею.

(после обеда :) Недостатком является то, что вы должны полагаться на Plan, чтобы установить делегата ... но просто невозможно избежать этого. У Item нет способа узнать, сколько на него ссылок, или контролировать его использование.

Лучшее, что вы можете получить - это понятный договор ... WorldState соглашается не удалять предмет, если Plan использует его, и Plan соглашается сообщить WorldState, что он использует предмет. Если Plan не задерживает конец контракта, он может оказаться в недопустимом состоянии. Не повезло, Plan, вот что ты получаешь за несоблюдение правил.

Причина, по которой вы не используете события, заключается в том, что вам нужно возвращаемое значение. Альтернативой будет WorldState предоставить метод для добавления «слушателей» типа IPlan, где IPlan определяет CheckItemForUse(Item item). Но вам все равно придется полагаться на то, что Plan уведомляет WorldState, чтобы спросить перед удалением элемента.

Один огромный пробел, который я вижу: в вашем примере Plan, который вы создаете, не привязан к WorldState stuffAtMyHouse. Например, вы можете создать Plan, чтобы вывести свою собаку на пляж, и Plan будет совершенно счастлив (вам, конечно, придется создать собаку Item). Редактировать: вы хотите передать stuffAtMyHouse конструктору Plan вместо myHouse?

Поскольку они не привязаны, вы в настоящее время не заботитесь, убираете ли вы велосипед из stuffAtMyHouse ... потому что вы сейчас говорите: "Мне все равно, где начинается велосипед, и мне все равно, где это, просто возьми это на пляж ". Но что ты имеешь в виду (я считаю): «Возьми мой велосипед из моего дома и иди на пляж». У Plan должен быть начальный контекст WorldState.

TLDR: Лучшая развязка, на которую вы можете надеяться, это позволить Plan выбрать, какой метод WorldState следует запросить перед удалением элемента.

HTH,
Джеймс



Оригинальный ответ
Мне не на 100% ясно, какова твоя цель, и, возможно, это просто вынужденный пример. Некоторые возможности:


I. Обеспечение максимального размера корабля с помощью таких методов, как SpaceBase.Dock(myShip)

Довольно просто ... SpaceBase отслеживает размер при вызове и бросает TooBigToDockException на корабль, пытающийся состыковаться, если он слишком большой. В этом случае на самом деле нет никакой связи ... вы бы не уведомили судно о новом максимальном размере корабля, потому что управление максимальным размером корабля не является обязанностью корабля.

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

Ваши подозрения верны ... Объекты Бога обычно плохие; четко очерченные обязанности заставляют их исчезать из дизайна в клубах дыма.


II. Запрашиваемое свойство SpaceBase

Если вы хотите, чтобы судно спрашивало вас, слишком ли оно велико для швартовки, вы можете выставить это свойство. Опять же, вы на самом деле не связаны ... вы просто позволяете кораблю принимать решение о стыковке или нет, исходя из этого свойства. Но база не доверяет кораблю, чтобы он не пришвартовался, если он слишком большой ... база все равно проверит вызов Dock() и выдаст исключение.

Ответственность за проверку связанных с док-станцией ограничений лежит на базе.


III. Как истинная связь, когда информация нужна обеим сторонам

Для того, чтобы состыковаться, базе может понадобиться управлять кораблем. Здесь уместен интерфейс ISpaceShip, который может иметь такие методы, как Rotate(), MoveLeft() и MoveRight().

Здесь вы избегаете связи благодаря самому интерфейсу ... Каждый корабль будет реализовывать Rotate() по-разному ... базе все равно, если она может вызвать Rotate() и заставить корабль развернуться место. NoSuchManeuverException может быть брошено судном, если оно не знает, как вращаться, и в этом случае база принимает решение попробовать что-то другое или отказывается от дока. Объекты взаимодействуют, но они не связаны за пределами Интерфейса (контракта), а база по-прежнему отвечает за стыковку.


IV. Проверка на установщик MaxShipSize

Вы говорите о создании исключения для вызывающей стороны, если она пытается установить значение MaxShipSize меньше, чем пристыкованные корабли. Я должен спросить, однако, кто пытается установить MaxShipSize и почему? Либо MaxShipSize должен быть установлен в конструкторе и быть неизменным, либо установка размера должна следовать естественным правилам, например, Вы не можете установить размер корабля меньше его текущего размера, потому что в реальном мире вы бы расширили SpaceBase, но никогда не уменьшали его.

Предотвращая нелогичные изменения, вы визуализируете принудительную расстыковку и связь, которая сопровождает это.


Суть, которую я пытаюсь подчеркнуть, заключается в том, что когда вы чувствуете, что ваш код становится излишне сложным, вы почти всегда правы, и ваше первое внимание должно быть основополагающим. И что в коде, меньше всегда больше. Когда вы говорите о написании Veto () и Error (), а также о дополнительных методах «сбора слишком больших кораблей», я начинаю беспокоиться о том, что код превратится в машину Rube Goldberg. И я думаю, что разделенные обязанности и инкапсуляция сведут на нет многие ненужные осложнения, которые вы испытываете.

Это похоже на раковину с проблемами с сантехникой ... Вы можете устанавливать всевозможные изгибы и трубы, но правильное решение обычно простое, прямолинейное и элегантное.

HTH,
Джеймс

1 голос
/ 08 ноября 2010

Вы можете сделать что-то вроде классов CL STL - реализовать универсальный SpaceBase<Ship, Traits>, который имеет два параметра Type s - один, который определяет член SpaceShip, и другой, который ограничивает SpaceBase и его * 1005. * с использованием класса SpaceBaseTraits для инкапсуляции характеристик базы, таких как ограничения для кораблей, которые она может содержать.

1 голос
/ 08 ноября 2010

Интерфейс INotifyPropertyChanging был разработан для привязки данных, что объясняет, почему у него нет возможностей, которые вы ищете. Я мог бы попробовать что-то вроде этого:

interface ISpacebaseInterceptor<T>
{ 
    bool RequestChange(T newValue); 
    void NotifyChange(T newValue); 
} 
1 голос
/ 08 ноября 2010

Вы знаете, что космический корабль должен иметь размер;поместите Size в базовый класс и реализуйте проверки валидации в аксессоре.

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

...