Так ли хорош DRY или все же он может нарушать O из SOLID

в 8:26, , рубрики: copy-paste, DRY, open-closed, solid, Анализ и проектирование систем, Программирование, Промышленное программирование, Совершенный код

Принцип DRY (Do not Repeat Yourself) давно всем вполне очевиден и любим многими программистами. И многие согласны, что Copy/Paste это совсем не круто. В этой статье я хочу привести пример того, в каких случаях в промышленном программировании использование Copy/Paste более уместно и помогает красиво реализовать Open-Closed принцип из SOLID.

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

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

Имеем класс Parcel, который описывает работу обычной посылки:

public interface IParcel {
      string Barcode {get; set;}
}
public class Parcel: IParcel  { 
      public string Barcode {get; set;}
}

Очень соблазнительно просто добавить поле и в старый класс Parcel, и в интерфейс IParcel:

public interface IParcel {
      string Barcode  {get; set;}
      bool   IsUrgent {get; set;}
}
public class Parcel: IParcel  { 
      public string Barcode  {get; set;}
      public bool   IsUrgent {get; set;}
}

Однако, такой код не должен проходить CodeReview! Строгий и опытный «проверяльщик» кода должен вернуть его с замечанием: «такая реализация нарушает Open-closed принцип.»

Гораздо лучше создать новый класс UrgentParcel, и не нужно будет менять ни интерфейс, ни класс Parcel. Файлы класса и интерфейса останутся нетронутыми:

public class UrgentParcel: IParcel { 
      public string Barcode {get; set;}
}

Это будет соблюдением Open-closed принципа, и такой код не получит замечания при CodeReview.

Теперь давайте вернемся к DRY и к тому, каким образом он мешает реализовать Open-closed принцип.

Представим, что в классе Parcel у нас есть поле «статус посылки» и некая логика изменения этого статуса:

public class Parcel: IParcel  { 
      public string Barcode {get; set;}
      // Статус посылки (в пути, или уже доставлена и т.п.)
      public ParcelStatuses Status {get; }
      // метод, который меняет статус посылки на "доставлена"
      public void ArrivedToRecipient(){
            this.Status = ParcelStatuses.Arrived;
      }
}

Нужно ли эту логику копировать в класс UrgentParcel? Принцип DRY говорит, что ни в коем случае. Гораздо лучше, чтобы класс UrgentParcel просто наследовался от класса Parcel, что решит задачу и не придется Copy/Paste'ть тело метода ArrivedToRecipient в класс UrgentParcel.

Однако, если не копировать код, а наследовать его, то изменения метода ArrivedToRecipient в классе Parcel сразу же приведут к изменению поведения класса UrgentParcel, что и будет являться нарушением Open-closed принципа. Это действительно нарушение, потому что задача со срочными посылками (реализация класса UrgentParcel) уже была сдана, протестирована и, как следствие, работает «в бою». Значит, эта логика, реализованная в методе UrgentParcel.ArrivedToRecipient и примененная к срочным посылкам — всех устраивает и она НЕ должна меняться при изменении работы других видов посылок. Так вот Open-closed принцип как раз и предназначен защищать систему от подобных действий неопытных junior-программистов, которые, решая задачу, как обычно «в лоб», не осознают еще пока всех зависимостей, и их изменения в одном месте затрагивают многие другие функциональные области.

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

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

Вот пример такого комментария:

public class Parcel: IParcel{ 
    ... 
    /// <summary>
    /// Проставляет посылке статус "доставлена" 
    /// </summary>    
    /// <remarks>NOTE: код метода копировался в классы: <see cref="UrgentParcel"/></remarks>
    public void ArrivedToRecipient(){ ... }
}

public class UrgentParcel: IParcel{ 
    ... 
    /// <summary>
    /// Проставляет посылке статус "доставлена"
    /// </summary>    
    /// <remarks>NOTE: код метода был скопирован из класса: <see cref="Parcel"/></remarks>
    public void ArrivedToRecipient(){ ... }
}

Очень интересно мнение сообщества по этому вопросу. Заранее благодарен за ваши комментарии.

Автор: Basim108

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js