7 вересня 2010 р.

"Патерн Вівторка" #5: Специфікація (Specification)

Специфікація

(Перш за все замічу, що Specification не належить до GoF патернів. То на випадок, якщо у вас виникло здивування.)

Загально кажучи Специфікація це предикат, який відповідає на питання чи об'єкт задовольняє, або ж не задовольняє деякий критерій. Використовуючи специфікатори ми можемо переписати нашу складну бізнес логіку словами булевої логіки.


Чи Ви коли небуть задумувалися, що bool TryParse(string s, out int result) може бути якимось дизайн патерном? Так, ми можемо дивитися на цей метод як на специфікацію інтеджера представленого стрінгом - ми валідуємо (Validation) чи воно дійсно так є. Цей патерн може бути використовуваний не тільки для Валідації (Validation), але також і для Запитів (Queuring) та Будування (Building).

Нумо уявімо собі, що нам слід перевірити чи Пацієнт може приймати наркотичні препарати дома під час візиту медичної сестри.

В нашому прикладі знадобляться два специфікатори. Якщо вони два є задоволені ми можемо сказати, що пацієнту можна проводити наркотичні процедури на дому.

Перша специфікація

    public class EligibleForDrugs : ISpecification
    {
        public bool IsSatisfiedBy(Patient patient)
        {
            return patient.IsActive && patient.HasPayer;
        }
    }

Друга специфікація

    public class EligibleForNurseVisit : ISpecification
    {
        public bool IsSatisfiedBy(Patient patient)
        {
            return patient.IsActive && patient.IsAtHome;
        }
    }

Як ви уже здогадалися інтерфейс ISpecification виглядає так:

    internal interface ISpecification
    {
        bool IsSatisfiedBy(Patient patient);
    }

Використання може бути таким:

        public List<Patient> FetchPatientsForVisitWithDrugs(List<Patient> patients)
        {
            var eligiblePatients = new List<Patient>();
            ISpecification drugsSpec = new ElegibleForDrugs();
            ISpecification nurseVisit = new ElegibleForNurseVisit();

            foreach (var patient in patients)
            {
                if(drugsSpec.IsSatisfiedBy(patient) && nurseVisit.IsSatisfiedBy(patient))
                {
                    eligiblePatients.Add(patient);
                }
            }
            return eligiblePatients;
        }

Ви зараз, мабуть, хочете сказати, що ми можемо запхати усі перевірки в один метод FetchPatientsForVisitWithDrugs. Так, але це не зовсім правильно, тому що специфікація може й має бути використана у багатьох місцях. Також якщо ми будемо дивитися на це із точки зору DDD, то ми повинні виносити основні моменти так, щоб їх було видно, що ми й зробили, виділивши декілька перевірок.

Але ще одне питання: чи взагалі вам подобається такий синтаксис?

if(drugsSpec.IsSatisfiedBy(patient) && nurseVisit.IsSatisfiedBy(patient))

Як на мене то він якийсь надлишковий і може бути складним, особливо якщо у вас багато специфікацій і ми захочемо їх комбінувати.

Нумо удосконалимо наш дизайн.

Перш за все додамо пару методів до нашого інтерфейсу:

    public interface ISpecification
    {
        bool IsSatisfiedBy(Patient patient);
        ISpecification And(ISpecification secondSpec);
        ISpecification Or(ISpecification secondSpec);
        ISpecification Not(ISpecification secondSpec);
    }

А також добавимо клас CompositeSpecification, що буде базовим для двох уже існуючих специфікацій.

    public abstract class CompositeSpecification : ISpecification
    {
        public abstract bool IsSatisfiedBy(Patient patient);

        public ISpecification And(ISpecification secondSpec)
        {
            return new AndSpecification(this, secondSpec);
        }

        public ISpecification Or(ISpecification secondSpec)
        {
            return new OrSpecification(this, secondSpec);
        }

        public ISpecification Not(ISpecification secondSpec)
        {
            return new NotSpecification(secondSpec);
        }
    }

Об'єкти класів, що повертаються методами класу CompositeSpecification використовуються для комбінування декількох специфікацій, для того щоб збудувати одну чітку специфікацію. Всі вони подібні. Для прикладу наведемо клас AndSpecification:

    public class AndSpecification : CompositeSpecification
    {
        private ISpecification firstOne;
        private ISpecification secondOne;
        public AndSpecification(ISpecification firstSpec, ISpecification secondSpec)
        {
            firstOne = firstSpec;
            secondOne = secondSpec;
        }

        public override bool IsSatisfiedBy(Patient patient)
        {
            return firstOne.IsSatisfiedBy(patient) && secondOne.IsSatisfiedBy(patient);
        }
    }

Давайте тепер поміняємо уже існуючі специфікації у відповідність із новим дизайном (просто добамимо наслідування від базового класу):


    public class EligibleForDrugs : CompositeSpecification
    {
        public override bool IsSatisfiedBy(Patient patient)
        {
            return patient.IsActive && patient.HasPayer;
        }
    }

І що це нам дає? А дає воно можливість будувати нові специфікації використовуючи існуючі дуже красивим способом.

Новий спосіб використання

            ISpecification drugsAtHomeSpec = new EligibleForDrugs()
                                           .And(new EligibleForNurseVisit());

Тепер ми можемо працювати із цією специфікацією як із однією-єдиною: if(drugsAtHomeSpec.IsSatisfiedBy(patient))
А використовуючи OrSpecification та NotSpecification ми можемо побудувати дуже закручені запити.

Для прикладу поглянемо на один із варіантів використання цього патерну в NHibernate:

        public DetachedCriteria PatietQuery(DetachedCriteria criteria)
        {
            criteria.Add(criteria.EqualTo("patient.IsActive", true));

            criteria.Add(
                    Expression.Not(
                        Expression.Or(
                            Expression.Eq("patient.Type", PatientStatus.Discharge),
                            Expression.Eq("patient.Type", PatientStatus.Death)
                        )
                    )
                );
            return criteria;
        }

Користь із використання Специфікацій:
  • Спеціфікація декларує вимоги до об'єктів, але не розголошує як результати були досягнуті.
  • Правила визначаються явно. Це означає, що програміст чітко певен що очікувати від специфікації, навіть не здогадуючись як воно реалізовано всередині.
  • Отримується гнучкий інтерфейс, який може бути легко доповнений. Також можна побудувати складену специфікацію для запитів.
  • Ще однією із переваг є те, що тестувати все дуже зручно. Ви просто визначаєте fail, або non-fail стани об'єкту для певних ситуацій і перевіряєте використовуючи булівські результати.


Буду вдячний за будь-які коментарі!

    10 коментарів:

    1. мне нравится, тока я вот никогда его не использовал:)

      ВідповістиВидалити
    2. Невже ніяке 3-д парті не мало подібного API ?

      ВідповістиВидалити
    3. а черт его знает. мож и было, но я видать его с точки зрениия паттерна не рассматривал, потому в голове и не отложилось:)

      ВідповістиВидалити
    4. Круто! Ніколи не чув про цей паттерн. Так тримати!

      ВідповістиВидалити
    5. >>Але ще одне питання: чи взагалі вам подобається такий синтаксис?
      Можна закинути всі специфікації у масив і додати тут вкладений цикл. При додаванні нової специфікації просто буде плюс один елемент до масиву. Так можливо нічим не краще, але я б так робив:)

      ВідповістиВидалити
    6. Прикольно, але тобі не завжди треба робити AND, тобі ще може треба буде робити OR, або інакше комбінувати!

      ВідповістиВидалити
    7. Класна стаття, як раз вчасно зустрів! Дякую!

      ВідповістиВидалити
    8. А можно использовать LINQ и вообще обойтись без кастомных експрешенов. Семантика спецификации сохраняется в названии класса спецификации, реализация использует LINQ. Плюс можно реализовать чейнинг спецификаций - с LINQ это тоже просто

      ВідповістиВидалити