Как лучше спроектировать масштабируемый класс? - PullRequest
5 голосов
/ 07 июля 2010

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

class Apple

  float seedCount;
  ...
  ...about 25 variables and properties here.
  void Update() <-- a huge method that checks for each property and updates if so

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

class Apple

  Seed seed;

Многие проблемы из-за этого: мне постоянно приходится проверять каждый отдельный объект и показывать каждый кадр.Если семя не инициализировано, мне не нужно ничего рассчитывать для него.Если это так, я должен.Если я решил добавить более одного свойства / функции в класс Seed, мне нужно проверить каждый из них.Это становится все более и более сложным.Поэтому у меня проблема в том, что мне нужен детальный контроль над всеми функциями, и я не могу разумно разделить их на большие подклассы.Любая форма подкласса будет содержать набор свойств, которые необходимо проверять и обновлять при желании.Я не могу точно создать подклассы Apple из-за необходимости такого высокого гранулярного контроля.Было бы безумием создать столько классов, сколько есть комбинаций свойств.Моя главная цель: я хочу короткий код.

Ответы [ 8 ]

5 голосов
/ 07 июля 2010

Было бы безумием создавать столько классов, сколько есть комбинаций свойств.

Звучит так, как будто вы искали Pattern Decorator. Его цель - упростить управление объектами, которые могут иметь множество различных комбинаций свойств, без экспоненциально растущей иерархии. У вас есть только один маленький подкласс для каждого свойства или поведения (не обязательно одно свойство C #, просто то, что вы можете сгруппировать вместе), а затем вы можете составлять их вместе во время выполнения.

В вашем случае каждый класс декоратора Apple переопределяет ваш метод Update и выполняет вычисления, необходимые для его частей, а затем вызывает base.Update, чтобы передать его следующему в строке.

Ваш окончательный ответ будет в значительной степени зависеть от того, каким именно является ваше «Яблоко».

2 голосов
/ 20 июля 2010

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

Я полностью избавился от декораторов. Абстрактный класс Broodfather остается. У него есть два дополнительных свойства: IBroodfatherMagicAbility и IBroodfatherBloodthirstAbility. Эти два свойства позволят вам получить доступ к различным атрибутам, которые относятся к этим способностям, однако ключом ко всему этому является то, что стратегия реализации способностей может изменяться во время выполнения (см. Шаблон стратегии ).

Существует два класса, каждый из которых реализует «стратегию» как кровожадности, так и магии.

  • IBroodfatherBloodthirstAbility.cs - это интерфейс, который должны реализовывать все "стратегии кровожадности".
  • BroodfatherNonBloodThristy.cs - класс, реализующий атрибуты не кровожадных.
  • BroodfatherBloodThristy.cs - класс, реализующий атрибуты кровожадности.

  • IBroodfatherMagicAbility.cs - это интерфейс, который должны реализовывать все "магические стратегии".

  • BroodfatherNonMagical.cs - класс, реализующий стратегию для немагического.
  • BroodfatherMagical.cs - класс, реализующий магическую стратегию.

  • BasicBroodfather.cs - это похоже на предыдущий пример, за исключением того, что теперь, когда создается экземпляр, свойства магии и кровожадности устанавливаются для новых экземпляров объектов немагической и не кровожадной стратегии.

  • Program.cs - это драйвер, который показывает классы и как различные стратегии могут включаться и выключаться во время выполнения.

Я думаю, вы найдете это более подходящим для того, как вы хотели, чтобы все работало.

1 голос
/ 10 июля 2010

Я немного обдумал этот вопрос и нашел альтернативное решение. Это может быть немного неортодоксальным и анти-объектно-ориентированным, но если вы не унывают, читайте дальше ...

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

  1. Apple
    а. Seed
    a1. GetSeedCount
    a2. ...
    б. Кожа
    b1. GetSkinColor
    Би 2. ...

Я использую объект словаря для хранения всех свойств яблока.

Я написал методы расширения для определения методов доступа к свойствам, используя разные классы для их разделения и организации.

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

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

Вот пример, который я придумал, чтобы проверить, как это будет работать, со стандартным предупреждением о том, что если вы продолжите идти по этому пути, произойдет робустификация (проверка, обработка ошибок и т. Д.).

Apple.cs

namespace ConsoleApplication1
{
    using System.Collections.Generic;
    using System.Text;

    public class Apple
    {
        // Define the set of valid properties for all apple objects.
        private static HashSet<string> AllowedProperties = new HashSet<string>(
            new string [] {
                "Color",
                "SeedCount"
            });

        // The main store for all properties
        private Dictionary<string, string> Properties = new Dictionary<string, string>();

        // Indexer for accessing properties
        // Access via the indexer should be restricted to the extension methods!
        // Unfortunately can't enforce this by making it private because then extension methods wouldn't be able to use it as they are now.
        public string this[string prop]
        {
            get
            {
                if (!AllowedProperties.Contains(prop))
                {
                    // throw exception
                }

                if (Properties.ContainsKey(prop))
                {
                    return this.Properties[prop];
                }
                else
                {
                    // TODO throw 'property unitialized' exeception || lookup & return default value for this property || etc.

                    // this return is here just to make the sample runable
                    return "0"; 
                }
            }

            set
            {
                if (!AllowedProperties.Contains(prop))
                {
                    // TODO throw 'invalid property' exception

                    // these assignments are here just to make the sample runable
                    prop = "INVALID";
                    value = "0";
                }

                this.Properties[prop] = value.ToString();
            }
        }

        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();

            foreach (var kv in this.Properties)
            {
                sb.AppendFormat("{0}={1}\n", kv.Key, kv.Value);
            }

            return sb.ToString();
        }
    }
}

AppleExtensions.cs

namespace AppleExtensionMethods
{
    using System;
    using ConsoleApplication1;

   // Accessors for Seed Properties
    public static class Seed
    {
        public static float GetSeedCount(this Apple apple)
        {
            return Convert.ToSingle(apple["SeedCount"]);
        }

        public static void SetSeedCount(this Apple apple, string count)
        {
            apple["SeedCount"] = count;
        }
    }

   // Accessors for Skin Properties
    public static class Skin
    {
        public static string GetSkinColor(this Apple apple)
        {
            return apple["Color"];
        }

        public static void SetSkinColor(this Apple apple, string color)
        {
            apple["Color"] = ValidSkinColorOrDefault(apple, color);
        }

        private static string ValidSkinColorOrDefault(this Apple apple, string color)
        {
            switch (color.ToLower())
            {
                case "red":
                    return color;

                case "green":
                    return color;

                default:
                    return "rotten brown";
            }
        }
    }
}

Вот тест-драйв:

Program.cs

namespace ConsoleApplication1
{
    using System;
    using AppleExtensionMethods;

    class Program
    {
        static void Main(string[] args)
        {
            Apple apple = new Apple();

            apple.SetSkinColor("Red");
            apple.SetSeedCount("8");

            Console.WriteLine("My apple is {0} and has {1} seed(s)\r\n", apple.GetSkinColor(), apple.GetSeedCount());

            apple.SetSkinColor("green");
            apple.SetSeedCount("4");

            Console.WriteLine("Now my apple is {0} and has {1} seed(s)\r\n", apple.GetSkinColor(), apple.GetSeedCount());

            apple.SetSkinColor("blue");
            apple.SetSeedCount("0");

            Console.WriteLine("Now my apple is {0} and has {1} seed(s)\r\n", apple.GetSkinColor(), apple.GetSeedCount());

            apple.SetSkinColor("yellow");
            apple.SetSeedCount("15");

            Console.WriteLine(apple.ToString());

            // Unfortunatly there is nothing stopping users of the class from doing something like that shown below.
            // This would be bad because it bypasses any behavior that you have defined in the get/set functions defined
            // as extension methods.
            // One thing in your favor here is it is inconvenient for user of the class to find the valid property names as
            // they'd have to go look at the apple class. It's much easier (from a lazy programmer standpoint) to use the
            // extension methods as they show up in intellisense :) However, relying on lazy programming does not a contract make.
            // There would have to be an agreed upon contract at the user of the class level that states, 
            //  "I will never use the indexer and always use the extension methods!"
            apple["Color"] = "don't panic";
            apple["SeedCount"] = "on second thought...";

            Console.WriteLine(apple.ToString());
        }
    }
}

Адресация вашего комментария от 7/11 (дата, а не магазин):)
В приведенном вами примере кода есть комментарий, который гласит:

"Как видите, я не могу позвонить BasicBroodmother методы на "монстра"

Вы понимаете, что можете сделать что-то подобное в этот момент:

BasicBroodmother bm = monster as BasicBroodmother;
if (bm != null)
{
    bm.Eat();
}

В вашем коде не так много мяса (я понимаю, что это всего лишь пример), но когда я смотрю на него, у меня возникает ощущение, что вы сможете улучшить дизайн. Моей непосредственной мыслью было создание абстрактного класса для broodmother, который будет содержать реализации по умолчанию любых атрибутов / действий, которые являются общими для всех broodmothers. Тогда специализированные матери-потомки, такие как магическая мать-мать, будут содержать любые специализированные атрибуты / действия, специфичные для магической семьи-матери, но также наследовать от абстрактного класса и при необходимости переопределять необходимые базовые атрибуты / действия.

Я бы взглянул на шаблон Стратегии для проектирования действий, чтобы такие действия (например, поведение, как "съесть", "порождение", "атака") можно было менять в зависимости от типа монстра.

[редактировать 7/13]
Сейчас у меня нет времени вдаваться в детали (нужен сон), но я собрал пример кода , демонстрирующий другой подход.

Код состоит из:

  • Broodfather.cs - абстрактный класс, заполненный всеми вещами, общими для разных типов "Broodfathers".
  • BasicBroodFather.cs - конкретный класс, наследуемый от Broodfather.
  • BroodfatherDecorator.cs - абстрактный класс, наследуемый всеми декораторами Broodfather.
  • MagicalBroodfather.cs - этот класс украшает / оборачивает Broodfather "магией"
  • BloodthirstyBroodfather.cs - этот класс украшает / оборачивает Отца кровопролитием
  • program.cs - демонстрирует два примера: первый начинается с простого отца, который окутывается магией, а затем кровожадностью. Второй начинается с простого отца и оборачивает его кровью в другом порядке, затем магией.
1 голос
/ 07 июля 2010

Я думаю, что вы на правильном пути: композиция.Вы составляете свой класс с другими необходимыми классами.Но вы также должны делегировать ответственность соответственно.В вашем примере именно класс Seed должен отвечать за проверку его внутреннего состояния, а Apple просто делегирует его.

Что касается проблемы с дополнительными функциями, возможно, вы можете использовать nullобъекты вместо нулевых ссылок.Таким образом, вам не нужно каждый раз проверять null, и код будет более согласованным.

1 голос
/ 07 июля 2010

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

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

1 голос
/ 07 июля 2010

вы можете использовать вложенный класс в классе Apple http://msdn.microsoft.com/en-us/library/ms173120(VS.80).aspx

0 голосов
/ 07 июля 2010

Может быть, ваши методы не были такими, какими они должны быть?

Если вы отделили класс Seed от класса Apple, почему бы вам не переместить методы, которые используют информацию Seed, в класс Seed?

Если этим методам требуется информация о других свойствах Apple, вы можете передать ее в качестве параметра.

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

Это отличная книга о том, как решить эту проблему:

Рефакторинг

0 голосов
/ 07 июля 2010

Моя главная цель: я хочу короткий код.

Параметры:

  1. Переписать все функции как статические и создать класс для каждой.
  2. Перепишите свою кодовую базу на Perl.
  3. Удалить все комментарии.
...