Декоратор Pattern в объектно-ориентированном программировании - PullRequest
0 голосов
/ 31 октября 2018

Я изучал «Шаблон декоратора». Делая некоторые тесты с C #, я не понимаю, почему я не получаю ожидаемый результат. Это код:

public abstract class Drink
{
    public string description = "Generic drink";

    public string GetDescription()
    {
        return description;
    }
}

public abstract class DrinkDecorator : Drink
{
    public abstract string GetDescription();
}


public class SecretIngredient : DrinkDecorator
{
    Drink drink;

    public SecretIngredient (Drink drink)
    {
        this.drink = drink;
    }

    public override string GetDescription()
    {
        return this.drink.GetDescription() + ", SecretIngredient ";
    }
}

public class Espresso : Drink
{
    public Espresso()
    {
        description = "Espresso";
    }
}


[TestFixture]
class TestClass
{
    [Test]
    public void TestMethod()
    {
        Drink drink = new Espresso();
        System.Diagnostics.Debug.WriteLine(drink.GetDescription());

        drink = new SecretIngredient (drink);
        System.Diagnostics.Debug.WriteLine(drink.GetDescription());
    }
}

Выполняя тест, я получаю:

Эспрессо

Универсальный напиток

Пока я бы ожидал:

Эспрессо

эспрессо, секретный ингредиент

Почему? Заранее спасибо.

Ответы [ 2 ]

0 голосов
/ 31 октября 2018

Основная проблема заключается в том, что вы неправильно реализовали шаблон декоратора . Правильная реализация шаблона декоратора будет выглядеть следующим образом (и мы исправим кучу других вещей, пока мы на нем.)

public abstract class Drink
{
  // Fields should be private or protected, but the 
  // description field that was here is useless, and
  // even if it were here, it should be a constant, 
  // not a variable
  // Eliminate it.
  // public string description = "Generic drink";

  // Things that are logically properties should be
  // properties, not GetBlah methods.

  // In new versions of C# you can use compact syntax 
  // for properties.

  // In the decorator pattern the behaviour mutated by the
  // decorator should be virtual.

  public virtual string Description => "generic drink";
}

public abstract class DrinkDecorator : Drink
{
  // The decorator must override the underlying implementation.
  public abstract override string Description { get; }
}

public class SecretIngredient : DrinkDecorator
{
    Drink drink;
    public SecretIngredient (Drink drink)
    {
        this.drink = drink;
    }

    // Use interpolation.
    public override string Description =>
      $"{this.drink.Description}, SecretIngredient ";
}

public class Espresso : Drink
{
    public Espresso()
    {
       // This is just wrong. We have a mechanism for overriding
       // behaviour so **use it**.
       //   description = "Espresso";
    }
    public override string Description => "Espresso";
}


[TestFixture]
class TestClass
{
    [Test]
    public void TestMethod()
    {
        Drink drink = new Espresso();
        System.Diagnostics.Debug.WriteLine(drink.Description);
        drink = new SecretIngredient (drink);
        System.Diagnostics.Debug.WriteLine(drink.Description);
    }
}

И теперь правильная реализация имеет ожидаемый результат.

Причина, по которой ваша неправильная реализация выдает неправильный вывод, была в том, что он был неправильным. У вас было две совершенно отдельные реализации GetDescription, которые не имели ничего общего друг с другом, одна реализована Drink, а другая - декоратором, так что одна вызванная зависела от типа во время компиляции получателя, который был Drink.

Вы должны были получить предупреждение о возможном непреднамеренном сокрытии старого метода новым методом. Обратите внимание на эти предупреждения . Если вы получаете предупреждение о том, что «этот метод, вероятно, неправильный», а затем при вызове этого метода вы получаете неправильный результат, предупреждение было правильным .

0 голосов
/ 31 октября 2018

Это потому, что у вас Drink объявлен тип Drink.

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

System.Diagnostics.Debug.WriteLine(((SecretIngredient)drink).GetDescription());

Когда вы присваиваете ссылку Type, тогда этот Type является запасным вариантом для метаданных. Другими словами, все поля, методы, свойства, которые Type имеет (или унаследовал), используются; ничего выше.

Здесь у нас есть простой Person, который также является основой Employee. Посмотрите на вывод и следуйте объявлениям типа.

using System;
namespace Question_Answer_Console_App
{
    class Program
    {
        static void Main(string[] args)
        {
            Person person = new Person() { Name = "Mathew" };
            Person employeePerson = new Employee() { Name = "Mark" };
            Person castedEmployee = new Employee() { Name = "Luke" };
            Employee employee = new Employee() { Name = "John" };
            //Compile error -> Employee personEmployee = new Person() { Name = "Acts" };    

            Console.WriteLine(person.Name);
            Console.WriteLine(employeePerson.Name); //Referenced Employee but got Person
            Console.WriteLine(((Employee)castedEmployee).Name); //Notice we cast here
            Console.WriteLine(employee.Name);

            Console.ReadKey();
        }
    }

    public class Person
    {
        public string Name { get; set; } = "No Name";
    }

    public class Employee : Person
    {
        new public string Name { get; set; }
        public string Address { get; set; }
    }
    //Output
    //Mathew
    //No Name
    //Luke
    //John
}

Хорошо, так что если вы смогли это понять и понять, как используются метаданные Type, то теперь вам нужно посмотреть на это с помощью interface. Это то же самое, но у нас может быть неловкий поворот.

С помощью interface два интерфейса могут иметь одинаковые свойства или методы или даже имена свойств и методов, но различную логику. Когда Type использует более одного interface, и они совместно используют какой-либо из них, но необходимая логика отличается, нам необходимо явно объявить членов этого интерфейса. Тем не мение; когда мы делаем это, тогда мы ТОЛЬКО используем тех членов, когда Type является ссылками как таковыми. Посмотрите на этот похожий пример:

Сначала обратите внимание, что теперь 'Люк' (ранее это был Марк, но с той же логикой) печатает ... Почему, когда мы ссылаемся на Person, но он создается как Employee. До этого не сработало. Также обратите внимание, что в выводе есть дыра, хотя член был определен; однако в этом случае наша ссылка на IEmployee, когда это произойдет. Поиграйте со всем этим кодом некоторое время, пока он не утонет, потому что это может стать большой проблемой позже.

using System;
namespace Question_Answer_Console_App
{
    class Program
    {
        static void Main(string[] args)
        {
            IPerson iPerson = new Person() { Name = "Mathew" };
            Person person = new Person() { Name = "Mark" };
            Person employeePerson = new Employee() { Name = "Luke" }; //pay attention to this!!
            IPerson iEmployeePerson = new Employee() { Name = "John" };
            IEmployee iEmployee = new Employee() { Name = "Acts" }; //And pay attention to this!!            
            Employee employee = new Employee() { Name = "Romans" };

            Console.WriteLine(iPerson.Name);
            Console.WriteLine(person.Name);
            Console.WriteLine(employeePerson.Name);
            Console.WriteLine(iEmployeePerson.Name);
            Console.WriteLine(iEmployee.Name);
            iEmployee.Name = "Corinthians"; //And pay attention to this!!
            Console.WriteLine(iEmployee.Name);
            Console.WriteLine(employee.Name);

            Console.ReadKey();
        }
    }

    public interface IPerson
    {
        string Name { get; set; }
    }

    public interface IEmployee
    {
        string Name { get; set; }
    }

    public class Person : IPerson
    {
        public string Name { get; set; } = "No Name";
    }

    public class Employee : Person, IEmployee
    {
        public string Address { get; set; }
        string IEmployee.Name { get; set; } //And pay attention to this!! (Explicit interface declaration)
    }
    //Output
    //Mathew
    //Mark
    //Luke
    //John

    //Corinthians
    //Romans
}

Теперь; если вы понимаете это до сих пор, давайте посмотрим, как обойти это. Возьмем первый пример: если вы добавите virtual к свойству Name Person, а затем используете override в свойстве Name Employee, вы увидите, что типы теперь работают как положено. Это потому, что мы не ссылаемся на два разных метода. Мы помечаем одно с возможностью повторной ссылки (виртуальное), а другое - для ссылки (переопределение). Это сильно меняет поведение.

Итак, все, что сказано и понято, давайте сделаем надлежащим декоратором.

Во-первых; нам нужно иметь тип:

public class Person
{
    public virtual string Name { get; set; } = "John Doe";
}

Теперь нам нужны типы с расширенной функциональностью ... (это нужно будет изменить позже)

public class Employee : Person
{
    public override string Name => $"Employee, {base.Name}";
    public string Job { get; set; }
}

public class Customer : Person
{
    public override string Name => $"Customer, {base.Name}";
    public bool IsShopping { get; set; }
}

Теперь сотрудник может быть и клиентом. Основываясь на нашем текущем дизайне, у нас есть проблема ... Мы должны были добавить интерфейсы, но тогда как насчет вычислений? В этом примере нет ничего, кроме Name, которое не является реальным миром, но выполняет свою работу. Поэтому, чтобы позволить динамически обновлять Person, мы должны добавить PersonDecorator. Когда мы добавляем этот декоратор, нам нужно наследовать его и использовать другие типы для создания экземпляров.

Вот наш декоратор:

public abstract class PersonDecorator : Person
{
    protected Person Person { get; }
    public PersonDecorator(Person person) => Person = person;
    public override string Name => Person.Name;
}

Теперь мы можем динамически расширять Person там, где раньше не могли. Обновление Employee и Customer показывает, как это сделать:

public class Employee : PersonDecorator
{
    public Employee(Person person = null) : base(person ?? new Person()) { }
    public override string Name => $"Employee, {base.Name}";
    public string Job { get; set; }
}

public class Customer : PersonDecorator
{
    public Customer(Person person) : base(person ?? new Person()) { }
    public override string Name => $"Customer, {base.Name}";
    public bool IsShopping { get; set; }
}

Теперь мы обновили наши типы, чтобы использовать декоратор (и заметили, что он имеет запасной тип, который может не иметь). Давайте использовать его в небольшом примере:

static void Main(string[] args)
{
    Person person = new Person() { Name = "Mathew" };
    Console.WriteLine(person.Name);

    person = new Employee(person) { Job = "Stocker" };
    Console.WriteLine(person.Name);

    person = new Customer(person) { IsShopping = true };
    Console.WriteLine(person.Name); 

    Console.ReadKey();
}
//OUTPUTS
//Mathew
//Employee, Mathew
//Customer, Employee, Mathew

Обратите внимание, как мы теперь динамически расширяем Person.

Мы также можем сделать человека динамичным:

static void Main(string[] args)
{
    Person person = new Customer(new Employee(new Person(){ Name = "Mathew" }){ Job = "Stocker" }){ IsShopping = true };
    Console.WriteLine(person.Name);
    Console.ReadKey();
}
//OUTPUTS
//Customer, Employee, Mathew

Посмотрите, как это работает без реализованной базы; оно остается динамичным и верным себе.

static void Main(string[] args)
{
    //Person person = new Person() { Name = "Mathew" };
    //Console.WriteLine(person.Name);

    Person person = new Employee() { Job = "Stocker" };
    Console.WriteLine(person.Name);

    person = new Customer(person) { IsShopping = true }
    Console.WriteLine(person.Name); 

    Console.ReadKey();
}
//OUTPUTS
//Employee, John Doe
//Customer, Employee, John Doe

Вот весь код для справки Шаблон декоратора

using System;
namespace Question_Answer_Console_App
{
    class Program
    {
        static void Main(string[] args)
        {
            Person person = new Person() { Name = "Mathew" };
            Console.WriteLine(person.Name);

            person = new Employee(person) { Job = "Stocker" };
            Console.WriteLine(person.Name);

            person = new Customer(person) { IsShopping = true };            
            Console.WriteLine(person.Name);

            Console.ReadKey();
        }
        //OUTPUTS
        //Mathew
        //Employee, Mathew
        //Customer, Employee, Mathew
    }

    public class Person
    {
        public virtual string Name { get; set; } = "John Doe";
    }

    public abstract class PersonDecorator : Person
    {
        protected Person Person { get; }
        public PersonDecorator(Person person) => Person = person;
        public override string Name => Person.Name;
    }

    public class Employee : PersonDecorator
    {
        public Employee(Person person = null) : base(person ?? new Person()) { }
        public override string Name => $"Employee, {base.Name}";
        public string Job { get; set; }
    }

    public class Customer : PersonDecorator
    {
        public Customer(Person person) : base(person ?? new Person()) { }
        public override string Name => $"Customer, {base.Name}";
        public bool IsShopping { get; set; }
    }
}

Вот ваш код обновлен до шаблона декоратора. Обратите внимание, как вы можете динамически обновлять Drink, который был Expresso, добавляя его в декораторы.

using System;
namespace Question_Answer_Console_App
{
    class Program
    {
        static void Main(string[] args)
        {
            Drink drink = new Espresso() { Description = "Expresso" };
            Console.WriteLine(drink.Description);

            drink = new SecretIngredient(drink);
            Console.WriteLine(drink.Description);

            drink = new Ice(drink);
            Console.WriteLine(drink.Description);

            Console.ReadKey();
        }
        //OUTPUTS
        //Expresso
        //Expresso with SecretIngredient
        //Expresso with SecretIngredient with Ice
    }

    public class Drink
    {
        public virtual string Description { get; set; }
    }

    public class Espresso : Drink { }

    public abstract class DrinkDecorator : Drink
    {
        protected Drink drink;
        protected DrinkDecorator(Drink drink) => this.drink = drink;
        public override string Description => drink.Description;
    }

    public class SecretIngredient : DrinkDecorator
    {
        public SecretIngredient(Drink drink) : base(drink) { }
        public override string Description => $"{drink.Description} with {nameof(SecretIngredient)} ";
    }

    public class Ice : DrinkDecorator
    {
        public Ice(Drink drink) : base(drink) { }
        public override string Description => $"{drink.Description} with {nameof(Ice)} ";
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...