Правильный подход, когда кажется, что нарушение SRP передает сложность по цепочке

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

Рассматриваемый код предназначен для очистки данных. В настоящее время здесь есть два отдельных процесса - мы очищаем адресные данные с помощью внешнего приложения, которое вызывается через код. Мы очищаем другие поля данных, используя внутренние алгоритмы C #.

Этот рефакторинг начался, когда мне сказали, что мы, возможно, захотим изменить оба этих процесса в будущем - например, использовать хранимые процедуры базы данных для выполнения этих заданий, а не код C # и внешнее приложение. Итак, моим первым побуждением было взять эти две функции и спрятать их за интерфейсами (FileRow и FileContents - это просто DTO):

public interface IAddressCleaner
{
    string CleanAddress(StringBuilder inputAddress);
    void CleanFile(FileContents fc);
}

public interface IFieldCleaner
{
    string CleanPhoneNumber(string phoneToClean);
    void CleanAllPhoneFields(FileRow row, FileContents fc);
    void MatchObscentities(FileRow row, FileContents fc);
    void CleanEmailFields(FileRow row, FileContents fc);
}

Что нормально. На самом деле, однако, я не могу представить, что класс когда-либо использовал бы одно из них без другого. Таким образом, казалось бы, имеет смысл объединить их (и их реализации) в один класс. Это также имеет смысл, учитывая, что мы можем заменить обе функции одним решением, например базой данных.

С другой стороны, может показаться, что IFieldCleaner уже нарушает SRP, потому что он выполняет три задачи: очищает телефонные номера, электронную почту и ищет грубые слова - все это логически разные процессы. Так что, казалось бы, есть причина разделить его на IPhoneCleaner, IObscenityMatcher и IEmailCleaner.

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

public class ReadFileService : IExecutableObject
{
    private ILogger _log;
    private IRepository _rep;
    private IFileHelper _fileHelp;
    private IFieldCleaner _fieldCleaner;
    private IFileParser _fileParser;
    private IFileWriter _fileWriter;
    private IEmailService _mailService;
    private IAddressCleaner _addressCleaner;

    public ReadFileService(ILogger log, IRepository rep, IFileHelper fileHelp, IFileParser fileParse, IFileWriter fileWrite, IEmailService email, IAddressCleaner addressCleaner)
    {
        // assign to privates
    }

  // functions
}

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

Какой здесь правильный подход? Должен ли я иметь единый интерфейс ICleaner или разделить его на пять?


person Bob Tway    schedule 08.06.2016    source источник
comment
@MatthewWatson Действительно интересно, спасибо. Хотя он предлагает решение, мне все равно было бы интересно узнать, нарушает ли IFieldCleaner в настоящее время SRP, чтобы лучше понять, что это на самом деле означает.   -  person Bob Tway    schedule 08.06.2016
comment
Что ж, я думаю, что, возможно, IFieldCleaner мог бы иметь составной очиститель (скажем, IEnumerable<ISingleFieldCleaner>), у которого был бы список из 3 методов для очистки различных полей, а не 3 отдельных метода, которые у вас есть на данный момент. IFieldCleaner должен будет перебирать этот список, вызывая каждый метод с соответствующими аргументами. Я не уверен, насколько CleanPhoneNumber() вписывается в эту схему ...   -  person Matthew Watson    schedule 08.06.2016
comment
Вы не можете просто проанализировать свой код, основываясь на одних принципах SOLID. Здесь все играют. Есть также другие принципы, которые вступают в игру, такие как «Скажи, не спрашивай», «Высокая сплоченность / низкая взаимосвязь», «Правильный уровень абстракции». Ясно, что MatchObscentities не принадлежит IFieldCleaner, поскольку он имеет низкую связь с другими операциями и с концепцией очистки поля. Также ясно, что FileRow и FileContents, вероятно, не подходящие абстракции для FieldCleaner. Я ожидал, что он будет работать с чем-то более абстрактным, например, Record или List<Record>.   -  person plalx    schedule 09.06.2016
comment
Теперь, то, что IFieldCleaner делает слишком много самостоятельно в соответствии с SRP, зависит от того, насколько взаимосвязан процесс очистки различных полей. Если процесс очистки разных полей может измениться независимо друг от друга, я бы сказал, что IFieldCleaner в настоящее время имеет слишком много ответственности. Вместо этого вы можете реализовать IRecordCleaner, который объединяет столько стратегий очистки, сколько полей. Что касается службы ReadFileService, зачем ей нужна такая зависимость, как IFileWriter? Чтение должно быть идемпотентной операцией.   -  person plalx    schedule 09.06.2016
comment
Есть много других вещей, которые можно было бы рассмотреть только из этого небольшого образца кода и многое другое, если бы у нас было больше деталей, но, в конце концов, вы не можете научить хорошему объектно-ориентированному моделированию в комментарии или ответе ...   -  person plalx    schedule 09.06.2016
comment
@plalx, ​​тогда я задам очевидный вопрос, как научиться хорошему объектно-ориентированному моделированию, если не задавая вопросы?   -  person jaco0646    schedule 09.06.2016
comment
@ jaco0646 Я думаю, что смысл больше в том, что это выходит за рамки формата вопросов и ответов. Лучше всего этому научиться у опытного практикующего, но, к сожалению, я работаю один :(   -  person Bob Tway    schedule 09.06.2016
comment
@plax спасибо за комментарии, очень полезно. ReadFileService, вероятно, плохо назван - в этом случае ему действительно нужно читать файлы, анализировать результаты, записывать вывод и отправлять уведомление по электронной почте.   -  person Bob Tway    schedule 09.06.2016
comment
@MattThrower Что это за анализ? Какие преобразования данных он выполняет? Это поможет вам лучше назвать услугу «Фасад». Кроме того, является ли уведомление по электронной почте частью этого процесса или оно должно быть побочным эффектом этого процесса? Возможно, было бы лучше, если бы возникло событие, подобное CleaningCompleted, и подписчик на это событие отправил бы электронное письмо. Это устранило бы зависимость от почтовой службы. Если вы объясните общую проблему предметной области с помощью правил и поведения, я могу помочь вам с дизайном.   -  person plalx    schedule 09.06.2016
comment
@ jaco0646 Я не говорю, что задавать вопросы - это неправильный способ обучения, но на вопросы дизайна, скорее всего, лучше ответить в ходе обсуждения, если они не имеют очень узкой области. На этот конкретный вопрос очень сложно ответить. Было бы лучше, если бы OP описывал всю проблемную область со всеми необходимыми инвариантами и поведением, не фокусируясь на конкретной реализации кода.   -  person plalx    schedule 09.06.2016
comment
@plax Какими бы ни были недостатки, этот вопрос сейчас набрал несколько голосов - если вы хотите организовать свои полезные комментарии в виде ответа, его, вероятно, стоит принять.   -  person Bob Tway    schedule 10.06.2016
comment
@MattThrower Если бы вы могли отредактировать вопрос, включив в него полное описание проблемы, я отправлю ответ. В противном случае я не считаю, что мои комментарии действительно являются четким ответом на вопрос.   -  person plalx    schedule 10.06.2016


Ответы (1)


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

Удачи!


У вас есть доступ к Pluralsight? Быстрая покупка месяца стоит того, чтобы просто пройти через Encapsulation и SOLID. Один из моментов "а-а-а", которые у меня были, когда я это пережил, был о том, как взглянуть на сигнатуры ваших методов, чтобы помочь идентифицировать интерфейсы, которые вы могли бы извлечь, чтобы упростить код. Игнорируйте имена, просто посмотрите на параметры.

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

На IFieldCleaner у вас есть 3 метода с одинаковой сигнатурой:

void CleanAllPhoneFields(FileRow row, FileContents fc);
void MatchObscentities(FileRow row, FileContents fc);
void CleanEmailFields(FileRow row, FileContents fc);

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

interface IFieldCleaner {
  void Clean(FileRow row, FileContents fc);
}
class PhoneFieldCleaner : IFieldCleaner { }
class ObscentitiesFieldCleaner : IFieldCleaner { }
class EmailFieldCleaner : IFieldCleaner { }

Теперь это красиво разделило обязанности по очистке этих полей на классы небольшого размера.

Теперь у вас есть пара других методов очистки:

string CleanPhoneNumber(string phoneNumber);
string CleanAddress(StringBuilder inputAddress);

Они очень похожи, за исключением того, что берется StringBuilder, предположительно потому, что реализация заботится об отдельных строках? Давайте просто переключим его на string и предположим, что реализация позаботится о разделении / синтаксическом анализе строк, тогда мы получим тот же результат, что и раньше - два метода с одинаковой сигнатурой:

string CleanPhoneNumber(string phoneNumber);
string CleanAddress(string inputAddress);

Итак, следуя нашей предыдущей логике, давайте также создадим интерфейс, связанный с очисткой строк:

interface IStringCleaner {
  string Clean(string s);
}

class PhoneNumberStringCleaner : IStringCleaner { }
class AddressStringCleaner : IStringCleaner { }

Теперь мы разделили эти обязанности на их собственные реализации.

На данный момент у нас остался только один метод:

void CleanFile(FileContents fc);

Я не уверен, что делает этот метод. Почему это часть IAddressCleaner? Итак, пока я оставлю это вне обсуждения - возможно, это способ прочитать файл, найти адрес, а затем очистить его, и в этом случае вы можете сделать это, вызвав наш новый AddressStringCleaner.

Итак, давайте посмотрим, где мы находимся.

interface IFieldCleaner {
  void Clean(FileRow row, FileContents fc);
}

class PhoneFieldCleaner : IFieldCleaner { }
class ObscentitiesFieldCleaner : IFieldCleaner { }
class EmailFieldCleaner : IFieldCleaner { }

interface IStringCleaner {
  string Clean(string s);
}

class PhoneNumberStringCleaner : IStringCleaner { }
class AddressStringCleaner : IStringCleaner { }

Все это мне кажется похожим и чем-то пахнет. Основываясь на ваших исходных именах методов, таких как Очистить Все поля, похоже, что вы можете использовать цикл только для очистки определенных столбцов от FileRow? Но почему также зависит от FileContents? Опять же, я не вижу вашей реализации, поэтому не совсем уверен. Возможно, вы собираетесь передать необработанный файл или ввод базы данных?

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

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

interface IStringCleaner {
  string Clean(string s);
}

class PhoneNumberStringCleaner : IStringCleaner { }
class AddressStringCleaner : IStringCleaner { }
class ObscenitiesStringCleaner : IStringCleaner { }
class EmailStringCleaner : IStringCleaner { }

Обратите внимание, что мы устранили необходимость в IFieldCleaner, потому что эти очистители строк работают только с входной строкой, которую нужно очистить.

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

Итак, судя по предоставленной вами услуге, я вижу несколько вещей, которые могут нам помочь:

IRepository
IFileHelper
IFileWriter
IFileParser

Я предполагаю, что мы намерены сохранить очищенные поля обратно - туда, где я не уверен, учитывая, что я вижу «Репозиторий», а затем также «FileWriter».

Неважно, мы знаем, что нам нужно в конечном итоге получить строки из полей, может быть, IFileParser поможет с этим?

interface IFileParser {
  FileContents ReadContents(File file);
  FileRow[] ReadRows(FileContents fc);
  FileField ReadField(FileRow row, string column);
}

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

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

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

class ExternalAddressStringCleaner : IStringCleaner {
  // depend on whatever you need here

  public string Clean(string s) {
    // call external program
    return cleanString;
  }
}

Теперь вы переключаетесь на хранимую процедуру? Ладно, тоже без проблем:

class DatabaseAddressStringCleaner : IStringCleaner {

  // depend on database
  DatabaseAddressStringCleaner(IRepository repository) {
  }

  string Clean(string s) {
    // call your database sproc
    return cleanString;
  }
}

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

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

public FileCleanerService {
  private IStringCleaner _addressCleaner;
  private IStringCleaner _phoneCleaner;
  private IStringCleaner _obscenityCleaner;
  private IStringCleaner _emailCleaner;

  ctor(IFileParser parser, /* deps */) {
    _parser = parser;

    _addressCleaner = new ExternalAddressStringCleaner(/* deps */);
    _phoneCleaner = new PhoneStringCleaner();
    _obscenityCleaner = new ObscenityStringCleaner();
    _emailCleaner = new EmailStringCleaner();
  }

  public void Clean(FileContents fc) {

    foreach(var row in _parser.ReadRows(fc)) {
      var address = _parser.ReadField(row, "Address");
      var phone   = _parser.ReadField(row, "Phone");
      var post    = _parser.ReadField(row, "PostContent");
      var email   = _parser.ReadField(row, "Email");

      // assumes you want to write back to the field?
      // handle this however you want
      address.Value = _addressCleaner.Clean(address.Value);
      phone.Value = _phoneCleaner.Clean(phone.Value);
      post.Value = _obscenityCleaner.Clean(post.Value);
      email.Value = _emailCleaner.Clean(email.Value);
    }

}

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

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

person kamranicus    schedule 21.07.2016