Как предупредить других программистов о реализации классов

96

Я пишу классы, которые «должны использоваться особым образом» (я думаю, все классы должны ...).

Например, я создаю fooManagerкласс, который требует вызова, скажем, Initialize(string,string). И, чтобы продвинуть пример немного дальше, класс был бы бесполезен, если бы мы не слушали его ThisHappenedдействие.

Я хочу сказать, что класс, который я пишу, требует вызовов методов. Но он будет хорошо скомпилирован, если вы не вызовете эти методы, и в результате вы получите пустой новый FooManager. В какой-то момент он либо не будет работать, либо может зависнуть, в зависимости от класса и того, что он делает. Программист, который реализует мой класс, очевидно, заглянет в него и поймет: «О, я не вызывал Initialize!», И это было бы хорошо.

Но мне это не нравится. В идеале я хотел бы, чтобы код НЕ компилировался, если метод не вызывался; Я предполагаю, что это просто невозможно. Или что-то, что было бы сразу видно и ясно.

Меня беспокоит текущий подход, который я здесь использую, а именно:

Добавьте приватное логическое значение в класс и везде проверяйте, инициализирован ли класс; если нет, я сгенерирую исключение, говорящее «класс не был инициализирован, вы уверены, что звоните .Initialize(string,string)?».

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

Кроме того, иногда даже больше кода, когда есть больше методов, чем просто Initiliazeвызов. Я стараюсь держать в своих занятиях не слишком много открытых методов / действий, но это не решает проблему, а просто делает ее разумной.

То, что я ищу здесь:

  • Мой подход правильный?
  • Есть ли лучший?
  • Что вы, ребята, делаете / советуете?
  • Я пытаюсь решить не проблему? Мне сказали коллеги, что программист должен проверить класс, прежде чем пытаться его использовать. Я с уважением не согласен, но это другое дело, я верю.

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

РАЗЪЯСНЕНИЯ:

Чтобы уточнить многие вопросы здесь:

  • Я определенно говорю не только об части инициализации класса, а скорее о целой жизни. Не позволяйте коллегам дважды вызывать метод, следя за тем, чтобы они вызывали X перед Y и т. Д. Все, что в конечном итоге было бы обязательным и в документации, но мне бы хотелось, чтобы в коде он был как можно более простым и небольшим. Мне очень понравилась идея Asserts, хотя я совершенно уверен, что мне нужно смешать некоторые другие идеи, поскольку Asserts не всегда будут возможны.

  • Я использую язык C #! Как я не упомянул это ?! Я работаю в среде Xamarin и создаю мобильные приложения, в которых обычно используется от 6 до 9 проектов, включая проекты PCL, iOS, Android и Windows. Я был разработчиком около полутора лет (школа и работа вместе), поэтому мои иногда смешные заявления \ вопросы. Все это, вероятно, здесь неактуально, но слишком много информации не всегда плохо.

  • Я не всегда могу поместить все, что является обязательным, в конструктор из-за ограничений платформы и использования Dependency Injection, когда параметры, отличные от Interfaces, не обсуждаются. Или, может быть, моих знаний недостаточно, что весьма возможно. Большую часть времени это не проблема инициализации, но больше

как я могу убедиться, что он зарегистрирован на это событие?

как я могу убедиться, что он не забыл «остановить процесс в какой-то момент»

Здесь я помню класс извлечения рекламы. До тех пор, пока видимость объявления видима, класс будет получать новое объявление каждую минуту. Этот класс нуждается в представлении, когда он создан, где он может отображать объявление, которое может явно входить в параметр. Но как только представление исчезнет, ​​необходимо вызвать StopFetching (). В противном случае класс продолжит получать рекламу для просмотра, которого даже нет, и это плохо.

Кроме того, в этом классе есть события, которые необходимо прослушивать, например, AdClicked. Все работает нормально, если не слушать, но мы теряем отслеживание аналитики там, если отводы не зарегистрированы. Реклама по-прежнему работает, поэтому пользователь и разработчик не увидят разницы, а аналитика просто будет иметь неверные данные. Этого следует избегать, но я не уверен, как разработчик может знать, что он должен зарегистрироваться на событие Дао. Хотя это упрощенный пример, но идея есть: «убедитесь, что он использует публичное действие, которое доступно» и в нужное время, конечно!

Гил Сэнд
источник
13
Гарантировать, что вызовы методов класса происходят в определенном порядке, вообще невозможно . Это одна из многих, многих проблем, которые эквивалентны проблеме остановки. Хотя в некоторых случаях было бы возможно сделать несоответствие ошибкой времени компиляции, общего решения не существует. Вероятно, именно поэтому немногие языки поддерживают конструкции, которые позволили бы вам диагностировать хотя бы очевидные случаи, даже если анализ потоков данных стал довольно сложным.
Килиан Фот
5
Ответ на другой вопрос не имеет значения, вопрос не тот же, поэтому это разные вопросы. Если кто-то ищет эту дискуссию, он не наберет название другого вопроса.
Гил Санд
6
Что мешает объединить содержимое инициализации в ctor? Должен ли звонок initializeбыть сделан поздно после создания объекта? Будет ли ctor слишком «рискованным» в том смысле, что он может генерировать исключения и разрывать цепочку создания?
Пятнадцатое
7
Эта проблема называется временной связью . Если вы можете, постарайтесь не переводить объект в недопустимое состояние, генерируя исключения в конструкторе при обнаружении недопустимых входных данных. Таким образом, вы никогда не пройдете вызов, newесли объект не готов к использованию. Полагаться на метод инициализации непременно придется преследовать вас, и его следует избегать, если в этом нет крайней необходимости, по крайней мере, таков мой опыт.
Seralize

Ответы:

161

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

Но вызывающие абоненты должны создать a, FooManagerпрежде чем они смогут его инициализировать, например, потому что FooManagerон передается как зависимость.

Не создавайте, FooManagerесли у вас его нет. Вместо этого вы можете передать объект, который позволяет вам получить полностью построенный объект FooManager, но только с информацией об инициализации. (Говоря о функциональном программировании, я предлагаю вам использовать частичное приложение для конструктора.) Например:

ctorArgs = ...;
getFooManager = (initInfo) -> new FooManager(ctorArgs, initInfo);
...
getFooManager(myInitInfo).fooMethod();

Проблема в том, что вы должны предоставлять информацию об инициализации каждый раз, когда вы получаете доступ к FooManager.

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

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

Можно найти компромисс. Мы создаем класс-обертку, MaybeInitializedFooManagerкоторый имеет get()метод, который возвращает FooManager, но выдает, если FooManagerне был полностью инициализирован. Это работает, только если инициализация выполняется через обертку или если есть FooManager#isInitialized()метод.

class MaybeInitializedFooManager {
  private final FooManager fooManager;

  public MaybeInitializedFooManager(CtorArgs ctorArgs) {
    fooManager = new FooManager(ctorArgs);
  }

  public FooManager initialize(InitArgs initArgs) {
    fooManager.initialize(initArgs);
    return fooManager;
  }

  public FooManager get() {
    if (fooManager.isInitialized()) return fooManager;
    throw ...;
  }
}

Я не хочу менять API своего класса.

В этом случае вы захотите избежать if (!initialized) throw;условных выражений в каждом методе. К счастью, есть простой способ решить эту проблему.

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

class FooManager {
  private CtorArgs ctorArgs;
  private Impl impl;

  public FooManager(CtorArgs ctorArgs) {
    this.ctorArgs = ctorArgs;
    this.impl = new UninitializedImpl();
  }

  public void initialize(InitArgs initArgs) {
    impl = new MainImpl(ctorArgs, initArgs);
  }

  public X foo() { return impl.foo(); }
  public Y bar() { return impl.bar(); }
}

interface Impl {
  X foo();
  Y bar();
}

class UninitializedImpl implements Impl {
  public X foo() { throw ...; }
  public Y bar() { throw ...; }
}

class MainImpl implements Impl {
  public MainImpl(CtorArgs c, InitArgs i);
  public X foo() { ... }
  public Y bar() { ... }
}

Это извлекает основное поведение класса в MainImpl.

Амон
источник
9
Мне нравятся первые два решения (+1), но я не думаю, что ваш последний фрагмент с повторением методов in FooManagerи in UninitialisedImplлучше, чем повторение if (!initialized) throw;.
Берги
3
@ Берги Некоторые люди слишком любят занятия. Хотя, если FooManagerесть много методов, это может быть проще, чем потенциально забыть некоторые if (!initialized)проверки. Но в этом случае вы, вероятно, предпочтете разбить класс.
user253751
1
MaybeInitializedFoo кажется мне не лучше инициализированного Foo, но +1 за некоторые варианты / идеи.
Мэтью Джеймс Бриггс
7
+1 Заводской вариант правильный. Вы не можете улучшить дизайн, не изменив его, так что ИМХО последний бит ответа о том, как наполовину это не поможет ОП.
Натан Купер
6
@ Bergi Я считаю, что техника, представленная в последнем разделе, чрезвычайно полезна (см. Также технику рефакторинга «Замена условных выражений через полиморфизм» и Шаблон состояния). Легко забыть проверку в одном методе, если мы используем if / else, гораздо труднее забыть реализацию метода, требуемого интерфейсом. Наиболее важно, что MainImpl точно соответствует объекту, где метод инициализации сливается с конструктором. Это означает, что реализация MainImpl может пользоваться более строгими гарантиями, которые это дает, что упрощает основной код. ...
Амон
79

Самый эффективный и полезный способ предотвратить "неправильное" использование объекта клиентами - сделать его невозможным.

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

Если вам нужно иметь доступ к неинициализированному объекту до инициализации, вы можете реализовать два состояния как отдельные классы, поэтому вы начнете с UnitializedFooManagerэкземпляра, у которого есть Initialize(...)метод, который возвращает InitializedFooManager. Методы, которые можно вызывать только в инициализированном состоянии, существуют только в InitializedFooManager. Этот подход может быть распространен на несколько состояний, если вам нужно.

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

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

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

Простой пример: у вас есть File-объект, который позволяет вам читать из файла. Тем не менее, клиент должен вызвать Openдо того, как ReadLineметод может быть вызван, и должен помнить, чтобы всегда вызывать Close(даже если возникает исключение), после чего ReadLineметод больше не должен вызываться. Это временная связь. Этого можно избежать, используя единственный метод, который принимает в качестве аргумента обратный вызов или делегат. Метод управляет открытием файла, вызовом обратного вызова и последующим закрытием файла. Он может передавать отличный интерфейс с Readметодом к обратному вызову. Таким образом, клиент не может забыть вызывать методы в правильном порядке.

Интерфейс с временной связью:

class File {
     /// Must be called on a closed file.
     /// Remember to always call Close() when you are finished
     public void Open();

     /// must be called on on open file
     public string ReadLine();

     /// must be called on an open file
     public void Close();
}

Без временной связи:

class File {
    /// Opens the file, executes the callback, and closes the file again.
    public void Consume(Action<IOpenFile> callback);
}

interface IOpenFile {
    string ReadLine();
}

Более тяжелым решением будет определение Fileв качестве абстрактного класса, который требует от вас реализации (защищенного) метода, который будет выполняться в открытом файле. Это называется шаблоном метода шаблона.

abstract class File {
    /// Opens the file, executes ConsumeOpenFile(), and closes the file again.
    public void Consume();

    /// override this
    abstract protected ConsumeOpenFile();

    /// call this from your ConsumeOpenFile() implementation
    protected string ReadLine();
}

Преимущество то же самое: клиент не должен помнить, чтобы вызывать методы в определенном порядке. Просто невозможно вызвать методы в неправильном порядке.

JacquesB
источник
2
Также я помню случаи, когда было просто невозможно перейти от конструктора, но у меня нет примера, чтобы показать прямо сейчас
Гил Сэнд
2
Пример теперь описывает фабричный шаблон, но первое предложение фактически описывает парадигму RAII . Так какой же ответ должен рекомендовать этот ответ?
Ext3h
11
@ Ext3h Я не думаю, что фабричный шаблон и RAII являются взаимоисключающими.
Питер Б
@PieterB Нет, это не так, фабрика может обеспечить RAII. Но только с дополнительным подтекстом, что аргументы шаблона / конструктора (вызываемые UnitializedFooManager) не должны совместно использовать состояние с instances ( InitializedFooManager), как на Initialize(...)самом деле имеет Instantiate(...)смысл. В противном случае этот пример приводит к новым проблемам, если один и тот же шаблон используется разработчиком дважды. И это невозможно предотвратить / проверить статически, если язык явно не поддерживает moveсемантику, чтобы надежно использовать шаблон.
Ext3h
2
@ Ext3h: я рекомендую первое решение, так как оно самое простое. Но если клиент должен иметь возможность доступа к объекту перед вызовом Initialize, тогда вы не можете его использовать, но вы можете использовать второе решение.
JacquesB
39

Обычно я просто проверяю инициализацию и выкидываю (скажем), IllegalStateExceptionесли вы пытаетесь использовать его, пока он не инициализирован.

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

ComponentBuilder builder = new ComponentBuilder();
Component forClients = builder.initialise(); // the 'Component' is what you give your clients

и поэтому вы контролируете создание объектов и жизненный цикл, и ваши клиенты получают в Componentрезультате вашей инициализации. Это фактически ленивый экземпляр.

Брайан Агнью
источник
1
Хороший ответ - 2 хороших варианта в хорошем кратком ответе. Другие ответы выше настоящей системы типов гимнастики, которые (теоретически) являются действительными; но для большинства случаев эти 2 более простых варианта являются идеальным практическим выбором.
Томас В.
1
Примечание. В .NET Microsoft создает InvalidOperationException как то, что вы назвали IllegalStateException.
miroxlav
1
Я не голосую против этого ответа, но это не очень хороший ответ. Просто выбрасывая IllegalStateExceptionили InvalidOperationExceptionнеожиданно, пользователь никогда не поймет, что он сделал не так. Эти исключения не должны стать препятствием для исправления ошибки дизайна.
displayName
Вы заметите, что исключение - это один из возможных вариантов, и я перейду к своему предпочтительному варианту - обеспечению безопасности во время компиляции. Я отредактировал свой ответ, чтобы подчеркнуть это
Брайан Агнью
14

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

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

С другой стороны, если вы пишете C, ваши коллеги были бы полностью в своих правах отобрать вас и отстрелить вас за определение отдельных типов struct FooManagerи struct UninitializedFooManagerтипов, которые поддерживают различные операции, потому что это привело бы к излишне сложному коду с очень небольшой выгодой. ,

Если вы пишете на Python, в языке нет механизма, позволяющего вам это делать.

Вы, вероятно, не пишете на Haskell, Python или C, но они являются иллюстративными примерами с точки зрения того, сколько / как мало работы система типов ожидает и может выполнить.

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

Патрик Коллинз
источник
Я немного озадачен тем, что «если вы пишете на Python, в языке нет механизма, позволяющего вам это делать». В Python есть конструкторы, и создавать фабричные методы довольно просто. Так что, может быть, я не понимаю, что вы подразумеваете под «этим»?
А.Л. Фланаган
3
Под «этим» я имел в виду ошибку времени компиляции, а не ошибку времени выполнения.
Патрик Коллинз
Просто прыгаем мимо упоминания Python 3.5, который является текущей версией, имеет типы через подключаемую систему типов. Определенно возможно вывести Python на ошибку перед запуском кода только потому, что он был импортирован. До 3.5 это было совершенно правильно, хотя.
Бенджамин Грюнбаум
О хорошо Запоздалый +1. Даже в замешательстве это было очень проницательно.
А.Л. Фланаган
Мне нравится твой стиль, Патрик.
Гил Сэнд
4

Поскольку вы, кажется, не хотите отправлять проверку кода клиенту (но, кажется, это хорошо для программиста), вы можете использовать функции assert , если они доступны на вашем языке программирования.

Таким образом, у вас есть проверки в среде разработки (и любые тесты, которые разработчик назвал бы, БЕЗ предсказуемо провалится), но вы не отправите код заказчику, поскольку утверждения (по крайней мере, в Java) компилируются только выборочно.

Итак, класс Java, использующий это, будет выглядеть так:

/** For some reason, you have to call init and connect before the manager works. */
public class FooManager {
   private int state = 0;

   public FooManager () {
   }

   public synchronized void init() {
      assert state==0 : "you called init twice.";
      // do something
      state = 1;
   }

   public synchronized void connect() {
      assert state==1 : "you called connect before init, or connect twice.";
      // do something
      state = 2;
   }

   public void someWork() {
      assert state==2 : "You did not properly init FooManager. You need to call init and connect first.";
      // do the actual work.
   }
}

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

Кроме того, они довольно тонкие и не занимают больших частей синтаксиса if () throw ..., их не нужно перехватывать и т. Д.

Анджело Фукс
источник
3

Глядя на эту проблему более широко, чем текущие ответы, которые в основном сосредоточены на инициализации. Рассмотрим объект, который будет иметь два метода, a()и b(). Требование заключается в том, что a()всегда вызывается раньше b(). Вы можете создать проверку во время компиляции, чтобы это происходило, возвращая новый объект a()и перемещаясь b()к новому объекту, а не к исходному. Пример реализации:

class SomeClass
{
   private int valueRequiredByMethodB;

   public IReadyForB a (int v) { valueRequiredByMethodB = v; return new ReadyForB(this); }

   public interface IReadyForB { public void b (); }

   private class ReadyForB : IReadyForB
   {
      SomeClass owner;
      private ReadyForB (SomeClass owner) { this.owner = owner; }
      public void b () { Console.WriteLine (owner.valueRequiredByMethodB); }
   }
}

Теперь невозможно вызвать b () без предварительного вызова a (), так как оно реализовано в интерфейсе, который скрыт до вызова a (). Следует признать, что это довольно много усилий, так что я бы не обычно использую этот подход, но бывают ситуации , когда это может быть полезным (особенно , если ваш класс будет повторно использоваться во многих ситуациях программистов , которые не могут быть знакомы с его реализацией или в коде, где надежность имеет решающее значение). Также обратите внимание, что это обобщение шаблона построителя, как это предлагается во многих существующих ответах. Он работает почти так же, единственное реальное различие заключается в том, где хранятся данные (в исходном объекте, а не в возвращенном) и когда он предназначен для использования (в любое время и только во время инициализации).

Жюль
источник
2

Когда я реализую базовый класс, который требует дополнительной информации об инициализации или ссылок на другие объекты, прежде чем он станет полезным, я стремлюсь сделать этот базовый класс абстрактным, а затем определить несколько абстрактных методов в этом классе, которые используются в потоке базового класса (например, abstract Collection<Information> get_extra_information(args);и abstract Collection<OtherObjects> get_other_objects(args);который по договору наследования должен быть реализован конкретным классом, заставляя пользователя этого базового класса предоставлять все вещи, требуемые базовым классом.

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

РЕДАКТИРОВАТЬ: чтобы уточнить, это почти то же самое, что и предоставление аргументов конструктору базового класса, но реализации абстрактного метода позволяют реализации обрабатывать аргументы, передаваемые вызовам абстрактного метода. Или даже в случае отсутствия аргументов, это может быть полезно, если возвращаемое значение абстрактного метода зависит от состояния, которое вы можете определить в теле метода, что невозможно при передаче переменной в качестве аргумента конструктор Конечно, вы все равно можете передать аргумент, который будет иметь динамическое поведение, основанное на том же принципе, если вы предпочитаете использовать композицию вместо наследования.

Клар
источник
2

Ответ: да, нет, а иногда. :-)

Некоторые из описанных вами проблем легко решаются, по крайней мере, во многих случаях.

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

RE время выполнения:

Концептуально просто заставить функции вызываться в определенном порядке и т. Д. С помощью проверок во время выполнения. Просто вставьте флаги, которые говорят, какая функция была запущена, и пусть каждая функция начинается с чего-то вроде «если не pre-condition-1 или pre-condition-2, то выведите исключение». Если проверки становятся сложными, вы можете поместить их в приватную функцию.

Вы можете сделать некоторые вещи автоматически. Чтобы взять простой пример, программисты часто говорят о «ленивом населении» объекта. Когда экземпляр создается, для флага is-заполнено значение false или для некоторой ссылки на ключевой объект - значение null. Затем, когда вызывается функция, требующая соответствующих данных, она проверяет флаг. Если это false, он заполняет данные и устанавливает флаг true. Если это правда, это просто продолжает предполагать, что данные есть. Вы можете сделать то же самое с другими предпосылками. Установите флаг в конструкторе или в качестве значения по умолчанию. Затем, когда вы достигнете точки, в которой должна была быть вызвана некоторая функция предварительного условия, если она еще не была вызвана, вызовите ее. Конечно, это работает, только если у вас есть данные, чтобы вызвать его в этот момент, но во многих случаях вы можете включить любые необходимые данные в параметры функции "

Время компиляции RE:

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

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

Если две функции всегда должны выполняться вместе, простое решение - сделать их одной функцией. Например, если каждый раз, когда вы добавляете рекламу на страницу, вы также должны добавить для нее обработчик метрик, а затем поместить код для добавления обработчика метрик в функцию «добавить рекламу».

Некоторые вещи было бы почти невозможно проверить во время компиляции. Как требование, чтобы определенная функция не вызывалась более одного раза. (Я написал много подобных функций. Я только недавно написал функцию, которая ищет файлы в волшебном каталоге, обрабатывает все, что находит, а затем удаляет их. Конечно, после удаления файла перезапустить невозможно. И т. Д.) Я не знаю ни одного языка, в котором есть функция, позволяющая предотвратить повторный вызов функции на одном и том же экземпляре во время компиляции. Я не знаю, как компилятор мог окончательно выяснить, что это происходит. Все, что вы можете сделать, это поставить проверки во время выполнения. Эта проверка времени выполнения очевидна, не так ли? msgstr "если уже было здесь = истина, тогда выдается исключение, иначе установлено уже было здесь = истина".

RE невозможно обеспечить

Для класса нередко требуется некоторая очистка, когда вы закончите: закрытие файлов или соединений, освобождение памяти, запись окончательных результатов в БД и т. Д. Нет простого способа заставить это произойти во время компиляции. или время выполнения. Я думаю, что большинство языков ООП имеют своего рода функцию для функции «финализатора», которая вызывается, когда экземпляр собирается мусором, но я думаю, что большинство также говорят, что они не могут гарантировать, что это когда-либо будет выполнено. Языки ООП часто включают функцию «dispose» с какими-то условиями для установки области действия при использовании экземпляра, предложения «using» или «with», и когда программа выходит из этой области действия, выполняется dispose. Но для этого программист должен использовать соответствующий спецификатор области видимости. Это не заставляет программиста делать это правильно,

В случаях, когда невозможно заставить программиста правильно использовать класс, я стараюсь сделать так, чтобы программа взорвалась, если он сделал это неправильно, вместо того, чтобы давать неверные результаты. Простой пример: я видел много программ, которые инициализируют кучу данных фиктивными значениями, так что пользователь никогда не получит исключение нулевого указателя, даже если ему не удастся вызвать функции для правильного заполнения данных. И мне всегда интересно, почему. Вы изо всех сил стараетесь найти ошибки. Если, когда программисту не удалось вызвать функцию «загрузить данные», а затем беззаботно попытался использовать данные, он получил исключение нулевого указателя, исключение быстро показало бы ему, в чем проблема. Но если вы скрываете это, делая данные пустыми или равными нулю в таких случаях, программа может работать до конца, но давать неточные результаты. В некоторых случаях программист может даже не осознавать, что возникла проблема. Если он заметит, ему, возможно, придется пройти длинный путь, чтобы выяснить, в чем дело. Лучше потерпеть неудачу рано, чем попытаться смело продолжать.

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

Но есть также момент, когда вы должны сказать: программист должен прочитать документацию по функции.

сойка
источник
1
Что касается принудительной очистки, существует шаблон, который можно использовать, когда ресурс может быть выделен только путем вызова определенного метода (например, в типичном языке OO он может иметь частный конструктор). Этот метод выделяет ресурс, вызывает функцию (или метод интерфейса), которая передается ему со ссылкой на ресурс, а затем уничтожает ресурс после возврата этой функции. Помимо внезапного завершения потока, этот шаблон гарантирует, что ресурс всегда уничтожается. Это распространенный подход в функциональных языках, но я также видел, что он хорошо используется в ОО-системах.
Жюль
@Jules aka "disposers"
Бенджамин Грюнбаум
2

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

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

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

Проектирование компонента, чтобы избежать того, что даже проблема в первую очередь, намного тоньше, и мне нравится, что Buffer подобен примеру Element . Часть 4 (опубликованная двадцать лет назад! Вау!) Содержит пример с обсуждением, очень похожим на ваш случай: этот класс должен использоваться осторожно, так как buffer () может не вызываться после того, как вы начнете использовать read (). Скорее, он должен использоваться только один раз, сразу после строительства. Если бы класс управлял буфером автоматически и незаметно для вызывающей стороны, это концептуально позволило бы избежать проблемы.

Вы спрашиваете, если тесты могут быть выполнены во время выполнения для обеспечения надлежащего использования, вы должны это сделать?

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

Возможно, проверка выполняется только в отладочной сборке.

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

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

Ваши инстинкты хороши. Так держать!

JDługosz
источник
0

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

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

  • Либо явно запросите эту необходимую информацию в конструкторе;
  • Или оставьте конструкторы нетронутыми и измените методы так, чтобы для вызова методов вы предоставили недостающие данные.

Мудрые слова:

* В любом случае вам придется перепроектировать класс и / или конструктор и / или методы.

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

* Если вы плохо написали исключения и сообщения об ошибках, ваш класс будет выдавать еще больше о себе.

* Наконец, если посторонний должен знать, что он должен инициализировать a, b и c, а затем вызвать d (), e (), f (int), то у вас есть утечка в вашей абстракции.

отображаемое имя
источник
0

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

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

public abstract class Foo
{
   [TemporallyCoupled(1)]
   public abstract void Init();

   [TemporallyCoupled(2)]
   public abstract void DoBar();

   [TemporallyCoupled(3)]
   public abstract void Close();
}

1: я никогда не писал Roslyn Code Analyzer, так что это может быть не самая лучшая реализация. Идея использовать Roslyn Code Analyzer для проверки правильности использования вашего API, однако, на 100% обоснована.

Erik
источник
-1

Я решил эту проблему раньше, используя приватный конструктор и статический MakeFooManager()метод в классе. Пример:

public class FooManager
{
     public string Foo { get; private set; }
     public string Bar { get; private set; }

     private FooManager(string foo, string bar)
     {
         Foo = foo;
         Bar = bar;
     }

     public static FooManager MakeFooManager(string foo, string bar)
     {
         // Can do other checks here too.
         if(foo == null || bar == null)
         {
             return null;
         }
         else
         {
             return new FooManager(foo, bar);
         }
     }
}

Так как конструктор реализован, никто не сможет создать экземпляр FooManagerбез прохождения MakeFooManager().

WillB3
источник
1
это, кажется, просто повторяет высказанное и объясненное в предыдущем ответе, который был опубликован за несколько часов до этого
коммент
1
это имеет какое-либо преимущество перед простой проверкой на ноль в ctor? кажется, что вы только что создали метод, который ничего не делает, кроме как оборачивает другой метод. Почему?
Сара
Кроме того, почему переменные-члены foo и bar?
Патрик М
-1

Есть несколько подходов:

  1. Сделайте класс легким в использовании правильным и трудным в неправильном. Некоторые другие ответы сосредоточены исключительно на этом вопросе, поэтому я просто упомяну RAII и шаблон сборки .

  2. Будьте внимательны к тому, как использовать комментарии. Если класс говорит сам за себя, не пишите комментарии. * Таким образом, люди с большей вероятностью будут читать ваши комментарии, когда они действительно нужны классу. И если у вас есть один из этих редких случаев, когда класс трудно использовать правильно и требует комментариев, включите примеры.

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

  4. Бросайте исключения, если использование класса недопустимо.

* Это комментарии, которые вы не должны писать:

// gets Foo
GetFoo();
Питер
источник