лучшая практика, когда важен порядок вызовов в классе?

#c# #optimization

#c# #оптимизация

Вопрос:

У меня есть один класс с двумя важными функциями:

 public class Foo {
    //plenty of properties here
    void DoSomeThing(){/*code to calculate results*/}
    void SaveSomething(){/* code to save the results in DB*/}

}
  

SaveSomething() использует результаты, вычисленные в DoSomeThing() .

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

есть ли какой-нибудь способ справиться с этим?

Я думаю о 3 методах, как показано ниже

  1. выбрасывание исключения SaveSomething() , если оно вызывалось ранее DoSomeThing()
  2. наличие a, bool которые установлены в DoSomeThing() и SaveSomething() изменяют код на:

     bool resultsAreCalculated = false;
    void SaveSomething(){
        if (!resultsAreCalculated) {
            DoSomeThing();
            // the resultsAreCalculated = true; is set in DoSomeThing();
            // can we throw some exception?
        }
        /* code to save the results in DB*/
    }
      
  3. реализация этого свободно, как :

     Foo x = new Foo();
    x.DoSomeThing().SaveSomething();
      

    в этом случае важно гарантировать, что этого не произойдет:

     x.SaveSomething().DoSomeThing();
      

прямо сейчас я использую второй метод. есть ли какой-нибудь лучший способ или этого достаточно?

Комментарии:

1. На самом деле это не проверка кода, поскольку она не завершена. Возможно, вы захотите опубликовать это в Stack Overflow, поскольку это вопрос дизайна.

2. @S.Лотт да, это так, но я думаю, что это не полный код здесь. Я публикую полный код в виде нового вопроса, упоминая здесь 2 или 3 ответа, и тогда кажется, что это еще один обзор кода. Кстати, этот вопрос относится к link . после 1,5 лет первого запуска, когда все члены команды новички (в этом проекте, не в программировании!).

3. Проверка кода предназначена для полного кода. После всех проектных решений. Вот что означает «проверка» — это означает, что после того, как вся работа по его созданию выполнена. Вопрос дизайна касается переполнения стека.

4. Это не код, это псевдокод для дизайна. Это не относится к проверке кода. Если вы прочитаете FAQ по обзору кода, вы увидите, что в нем четко указано, что подобные вопросы о наилучшей практике не по теме.

Ответ №1:

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

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

Чтобы использовать подход шаблонного метода, ваш класс Foo будет иметь абстрактную базу, которая определяет порядок выполнения Do() и Save() , что-то вроде:

 public abstract class FooBase
{
    protected abstract void DoSomeThing(); 
    protected abstract void SaveSomething();
    public void DoAndSave()
    {
        //Enforce Execution order
        DoSomeThing();
        SaveSomething();
    }

}

public class Foo : FooBase
{
    protected override void DoSomeThing()
    {
        /*code to calculate results*/
    }

    protected override void SaveSomething()
    {
        /* code to save the results in DB*/
    }
}
  

Таким образом, пользователи вашего класса будут иметь доступ только к DoAndSave() , и они не будут нарушать порядок выполнения, который вы намеревались.

Существуют другие шаблоны, которые имеют дело с ситуациями типа workflow / state transition. Вы можете сослаться на цепочку команд и шаблоны состояний.

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

 public abstract class FooBase
{
    protected abstract void DoSomeThing();
    protected abstract void SaveSomething();
    protected abstract bool AreValidResults();
    public void DoAndSave()
    {
        //Enforce Execution order
        DoSomeThing();

        if (AreValidResults())
            SaveSomething();
    }

}
  

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

Комментарии:

1. что, если промежуточные результаты нужны другой части? например, если DoSomeThing() результаты должны быть показаны пользователю, а затем, если они согласны (щелкните где-нибудь), то SaveSomething() произойдет?

2. Пожалуйста, посмотрите мой отредактированный ответ.. в разделе комментариев не удалось разместить длинный ответ.

3. Просто обратите внимание, что это было бы столь же расплывчато для разработчиков метода DoAndSave(). В этом случае обратите внимание на важность комментария, добавленного @Anas Karkoukli. Комментарий становится тем, что гарантирует, что сопровождающие не нарушат код.

Ответ №2:

Один из вариантов, помогающих избежать ошибки пользователя, — прояснить это, передав переменную. Делая это, он поднимает флаг для пользователя, который им необходим для получения результатов (т. Е. doSomething()) перед вызовом SaveSomething(…).

 results = DoSomething(); // returns the results to be saved
SaveSomething(results);
  

Комментарии:

1. Немного смущен этим ответом, поскольку нет способа проголосовать только за вторую его часть. 1 в любом случае

2. Редактировать: Удалена 1-я часть. Вы правы — это скорее касательная, чем прямой ответ на вопрос. Хороший вызов.

Ответ №3:

Как насчет этого?

 interface Result {
     void Save();
     SomeData GetData();
}
class Foo {
     Result DoSomething() { /* ... */ }
}
  

Использование:

 myFoo.DoSomething().Save();
//or something like:
var result = myFoo.DoSomething();
if (result.GetData().Importance > threshold) result.Save();
  

С внешней точки зрения, это имеет большой смысл. Создается Result и при желании предоставляет средства сохранения, в то время как реализация полностью непрозрачна. Мне не нужно беспокоиться о передаче этого обратно в нужный Foo экземпляр. Фактически я могу передать результат объектам, которые даже не знают Foo экземпляр, который его создал (фактически создатель должен передать всю необходимую информацию для сохранения в результате при создании). У результата может быть способ сообщить мне, был ли он уже сохранен, если это необходимо. И так далее.

По сути, это просто применение SRP, хотя в первую очередь к интерфейсу, а не к реализации. Foo интерфейс предоставляет средства для получения результатов, Result абстрагирует средства для манипулирования результатами.

Ответ №4:

Развивая ответ Левинариса ( 1, если бы у меня был представитель), вы могли бы в качестве альтернативы использовать Save() метод для объекта результатов, возвращаемого из DoSomthing() метода. Таким образом, вы получили бы что-то вроде этого:

 var obj = new Foo();

// Get results
var results = obj.DoSomething();

// Check validity, and user acceptance
if(this.AreValidResults(results) amp;amp; this.UserAcceptsResults(results))
{
    // Save the results
    results.Save();
}
else
{
    // Ditch the results
    results.Dispose();
}
  

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

Ответ №5:

Мне нравится ответ Анаса Каркукли, но другой альтернативой является конечный автомат.

 public class Foo {

    private enum State {
        AwaitingDo,
        AwaitingValidate,
        AwaitingSave,
        Saved
    }

    private State mState = State.AwaitingDo;

    private void Do() {

        // Do something
        mState = State.AwaitingValidate;
    }

    private void Validate() {

        // Do something
        mState = State.AwaitingSave;
    }

    private void Save() {

        // Do something
        mState = State.Saved;
    }

    public void MoveToNextState() {
        switch (mState) {
            case State.AwaitingDo:
                Do();
                break;

            case State.AwaitingValidation:
                Validate();
                break;

            case State.AwaitingSave:
                Save();
                break;

            case State.Saved:
                throw new Exception("Nothing more to do.");
                break;
        }
    }
}
  

Это немного небрежно, но идею вы уловили.

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

Ответ №6:

В превосходной книге Стива Макконнела «Code Complete» этому вопросу посвящена целая глава. Это глава 14 во втором издании.

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

 calculateResults();
saveResults();
  

(сохранение результатов в переменных экземпляра) запись

 Results r = calculateResults();
saveResults(r);
  

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

Комментарии:

1. да, и тогда Results можно каким-то образом использовать, например : UpdateUI(r);

Ответ №7:

Do и Save методы мне не кажутся упорядоченной парой. Вам нужно упорядочивать их только потому, что вы не возвращаете состояние вычисления из Do метода. Если вы пишете Do метод как метод, который возвращает результаты в клиентский код, то вы можете переписать Save , чтобы он получал результаты в качестве параметра.

Преимущества:

  1. Вам больше не нужно упорядочивать методы, потому что Save методу все равно, как клиент получил параметр. Он просто получает это, и все.
  2. Вы можете более легко проводить модульное тестирование Do метода, потому что методы становятся менее связанными.
  3. Вы можете переместить свой Save метод в другой класс, если вам когда-нибудь понадобится написать сложную логику сохранения или реализовать шаблон репозитория.