How to enforce constraint such as any field (or specific field) must not change if entity is in some state?

前端 未结 3 1842
名媛妹妹
名媛妹妹 2021-01-03 09:03

I\'m trying to use DDD in my current project (c#, mvc, nhibernate, castle) and I\'m thinking of best way to check a constraint that says any field (or specific field) must n

相关标签:
3条回答
  • 2021-01-03 09:18

    Although I can't find a good reference (I could swear I heard it from Martin Fowler several years ago, but a search of his site came up dry), I'm used to hearing this concept referred to as "freezing" or "freezable". It's normally used in combination with two-legged accounting transactions.

    Specifically, an accounting transaction is not created until the corresponding item is frozen, at which point no actions are allowed to be taken on the item which could change the balance. In many cases, no further actions may be taken at all, except possibly for cancellation, which actually doesn't change the frozen item but simply results in a retroactive event being added.

    Oddly, Microsoft implemented this in a completely different context with WPF. They use "freezable" primarily to indicate that change notifications are no longer necessary. If you are, in fact, using WPF, you might consider looking at the Freezable class.

    Otherwise, if you want a truly generic pattern, I highly suggest you read through Kozmic's Dynamic Proxy Tutorial. Although it's mainly an excuse to show off the features of Castle Proxy, the "freezable" concept is exactly what he chooses to implement, and he shows a way to do this using a generic reusable library without having to write much additional code after the fact.

    Although there is quite a lot of code to work out all the kinks, the very basic idea is to just write an interceptor and then create a proxy with it:

    internal class FreezableInterceptor : IInterceptor, IFreezable
    {
        private bool _isFrozen;
    
        public void Freeze()
        {
            _isFrozen = true;
        }
    
        public bool IsFrozen
        {
            get { return _isFrozen; }
        }
    
        public void Intercept(IInvocation invocation)
        {
            if (_isFrozen && invocation.Method.Name.StartsWith("set_",
                StringComparison.OrdinalIgnoreCase))
            {
                throw new ObjectFrozenException();
            }
    
            invocation.Proceed();
        }
    }
    
    public static TFreezable MakeFreezable<TFreezable>() 
      where TFreezable : class, new()
    {
        return _generator.CreateClassProxy<TFreezable>(new FreezableInterceptor());
    }
    

    Note that the above is not production-quality code, it's just the intro. You should read more of the linked site for more information.

    As far as I know, class/interface proxying is really the only way you can do this in a domain-agnostic way. Otherwise, you're going to have to re-implement the freezable logic for every freezable class - that is to say, putting a lot of if-then statements in your property setters and throwing an FrozenException if the status is set.

    0 讨论(0)
  • 2021-01-03 09:19

    Domain objects in DDD are 'self validating'. In other words, it is not possible for client code to break domain rules because objects enforce their internal invariants. For example:

    public class Invoice {
        private Money _amount;
        private InvoiceState _state;
    
        public void ChangeAmount(Money newAmount) {
            if(_state == State.Booked) {
                throw new InvalidOperationException(
                          "Unable to change amount for booked invoice.");
            }
            _amount = newAmount;
        }
    
        // Methods like this can be used by external code (UI) to check business
        // rules upfront, to avoid InvalidOperationException.
        public Boolean CanChangeAmount() {
            if(_state == State.Booked) {
                return false;
            }
            return true;
        }
    }
    

    Another example from DDD sample:

      public HandlingEvent(final Cargo cargo,
                           final Date completionTime,
                           final Date registrationTime,
                           final Type type,
                           final Location location,
                           final Voyage voyage) {
    
        ...
    
        if (type.prohibitsVoyage()) {
          throw new IllegalArgumentException(
                           "Voyage is not allowed with event type " + type);
        }
    

    Never allow your UI framework treat domain object as dumb data container. Unfortunately this is encouraged by a lot examples on the internet and C#'s emphasis on getters and setters. If you change object state without enforcing business rules you will eventually end up with 'corrupted' objects. This is especially true for NHibernate because its Session 'remembers' all objects and will happily dump them into database on next commit or flush. But this is just a technicality, the main reason is that you need to be able to reason about Invoice related business rules just by looking at Invoice class. Also note that the code should be based on Ubiquitous Language. You should see words like 'Invoice', 'Booked', 'Amount' instead of generic 'Field', 'Property', 'Validator'.

    UPDATE: empi, thank you for restating your problem. You might want to open a new question. This is the quote with my emphasis

    As I said in one of my comments - this question is a part of a bigger problem I'm having. I'm looking for a standard way to define domain logic and constraints only in the domain and then translate it to gui and other layers. I don't see any common pattern to the following very common demand: the domain states that field cannot be edited and this rule is automatically converted to gui logic that disables the field and makes it readonly. I'm looking for sample solution in mvc stack. I feel like I'm reinventing the wheel and most developers just give up and duplicate the logic in gui

    I think that you looking for a way to state everything in domain and then 'generate' UI. Something like Naked Objects for MVC? I never used this approach but I doubt that generated UI would ever win beauty or usability contest. In my opinion there will always be some 'restating' of business logic in UI. Some domain invariants are too complex, involve multiple fields, require repository and maybe even external services. I'm not sure that it is possible to generate high quality user interface automatically. I think that attempts to do this may start bending your model to conform to the UI infrastructure.

    0 讨论(0)
  • 2021-01-03 09:22

    When designing my domain objects, I try not to think of them just as a collection of data, but as an object that can be acted upon. Instead of providing direct access to the data (even through getter and setter methods), provide methods that correspond to the actions one might take with the object. Sometimes an action will change multiple data fields. Sometimes it might only change one and functionally be no different from a setter, but it is named such that it represents the action and not just a data modification.

    With this approach you can easily enforce what actions would be allowed based on the state of the entity. For example with an Invoice, you might Add or Remove items. This would change the total, but access isn't provided to modify the total directly. When the invoice is in a certain state (e.g. booked) when you no longer allow changes, then enforce that by throwing an exception from the Add or Remove methods indicating the methods aren't valid in the current state. However other methods may still be valid such as those related to shipping or payment of the invoice.

    Along with this approach, I've also used different entities to represent the same data at different points in the lifecycle. While the invoice is active, it needs to be an object that can be acted upon. However, once it reaches a final state, it is only used for viewing and reporting and none of the data changes. By using different entities (e.g. ActiveInvoice and CompletedInvoice), it becomes clear in the application where it is used as part of the process and where it is used just for viewing. It also makes it easier when dealing with archiving data that may be coming from a different table or read-only view.

    If the object only has two states representing a mutable and non-mutable state without much logic for allowing different methods for various states, you can use Eric Lippert's 'Popsicle Immutability' pattern. It allows more direct modification of the object than, but then enforces its immutability once it is frozen.

    0 讨论(0)
提交回复
热议问题