Объектно ориентированный код

Пришло время поговорить об ООП и об удовольствии получаемом нами на своей работе. Одними из наших любимых вопросов на собеседовании который задается кандидатам есть следующие: «Что Вы понимаете под словом «хороший» код?», «Какой код лично Вы, код пишите и каким критериями руководствуетесь при написании вашего кода?» Я слышал много вариантов (очень много) ответов на эти вопросы. Сегодня хочу поделиться собственной позицией по этому вопросу. Так уж сложилось, что за годы участия в разработке программного обеспечения я столкнулся с огромным числом интересных людей, а также с различными подходами к разработке программного обеспечения. Как человек от природы любознательный я всегда стремился почерпнуть, что то новое не в зависимости от моего окружения и типов проектов на которых довелось поработать, но признаться по секрету никогда ранее я не испытывал такого чувства удовлетворения от работы коллектива, как то, что я испытываю сейчас. Я попытался понять, что же изменилось за прошедшие годы. Конечно, можно смело утверждать, что багаж накопленного опыта дает свое и многие вещи, которые приходилось осваивать ранее, сейчас заняли достойное место, на полке моего инструментария делая мою жизнь и жизнь нашей команды легче и приятней. С некоторых пор появилась возможность брать на работу только сильных и состоявшихся программистов, в большинстве своем тимлидов на своих бывших местах работы. Это не могло не сказаться на уровне дискуссий в коллективе, на оперативности принимаемых нами решений, а также на скорости и качестве разработки. Но поразмыслив глубже, я пришел к выводу, что это не главное, хотя имеет большой удельный вес в общем успехе. Что – то за прошедшие годы изменилось в моем сознании и в сознании окружающих меня людей и это «что-то» позволило смотреть на нашу работу в несколько ином разрезе, чем просто набор изученных технологий, удачно примененных практик и прогрессивных архитектурных решений. Несколько лет назад я нашел в лице одного из тогда еще новых коллег хорошего архитектора, а впоследствии и друга. Многочасовые споры и возможность общаться себе подобными стоит дорогого, что еще более важно в ходе этих споров я заинтересовался пропагандируемыми им идеями. Стоит оговориться, что он не был моим учителем в прямом смысле этого слова, но его увлечение объектно-ориентированным дизайном, гибкими методологиями, unit – тестированием, в конечно итоге оказали влияние и на мои взгляды, на суть того, что мы делаем, а главное на, то как мы это делаем. Что-то бесповоротно изменилось в самом восприятии мной кода, в том числе и чужого. Это привело к незаметным сиюминутно, но качественным трансформациям базы кода в дальнейшем. Идеи, еще недавно казавшиеся реформаторскими и недостижимо далекими стали применяться на практике и давать свои результаты. Естественно, что над многим нам придется еще работать, но самое главное, что пришло осознание того, что такое хороший код, а также знание того как его писать и мы стараемся делать именно так.

Что же такое хороший код ? Большинство собеседуемых считают, что хороший код обладает следующими характеристиками: это документированный, ясный (по их собственному определению обычно ясность заключается в выборе удачных имен классов, переменных и имен методов) код и обязательно написанный с по утвержденному в их компаниях code style стандарту. Я не готов с этим спорить т.к. полностью согласен со всеми этими утверждениями, все эти характеристики чрезвычайно важны, но … такой код можно было писать и 20 лет назад. Согласны? Определенно этого не достаточно, что бы считать написанный код хорошим. С моей точки зрения к этим характеристикам, стоит добавить, что хороший код это объектно-ориентированный код и код качественный. Объектно-ориентированность очень комплексный критерий оценки, какой код считать объектно-ориентированым, а какой нет? Первое, что приходит на ум это то, что код должен являть собой максимально точно абстракцию той предметной области, которую мы моделируем, но это очень глобальный взгляд на вещи, мне кажется, что должны быть дополнительные критерии оценки того, чей код более объектно-ориентирован. Мы часто спрашиваем на собеседованиях, что дает Вам как программисту, объектно-ориентированный подход к написанию приложений? Диапазон ответов чрезвычайно широк, но я смею утверждать, что многие из аргументов могли быть использованы адептами процедурного или функционального подхода. Что бы разобраться в том, что дает нам ООП, нужно вспомнить о том, что оно базируется на трех основных постулатах: инкапсуляции, наследовании и полиморфизме. Именно этих трех составляющих не было в языках процедурных или они имеют ограниченную поддержку в языках стремящихся поддержать объектно-ориентированную парадигму. Именно осознание того, какую выгоду дают нам эти три основополагающие концепции позволяют понять все достоинства ООП и соответственно код базирующийся на этих трех столпах и будет объектно-ориентированным в лучшем смысле. Я не наивен, что бы утверждать, что если мы будем применять наследование, инкапсуляцию и полиморфизм в своих проектах мы достигнем лучшего результата, чем если бы мы использовали процедурный подход. Более того я осмелюсь утверждать, что многие из нас спустя десятилетия пропаганды ООП, так и не научились им пользоваться себе во благо, а потому человек вооруженный ножом, на практике, обычно стократ опасней обезьяны с базукой. Я надеюсь, что Вы поняли мою мысль.

Что же это за принципы которые стоит знать руководствоваться при проектировании и написании кода использующего концепции ООП? Прежде чем познакомится с ними я хочу сделать обсуждение более предметным. Предположим, Вы подарили своей любимой модный цифровой фотоаппарат. Она без нашей помощи сделала свои фотографии, которые без даже вашей помощи смогла перенести в компьютер и горит желанием продемонстрировать их всему миру в «Одноклассниках», но ваш GPRS модем благодаря качеству покрытия с огромным трудом передает мегабайтные фотографии. Вы как истинный джедай в течении 10 минут пишете программу, суть которой полезть в папку D:PhotosFirstPhotos вытащить все фотографии из нее и ее подпапок, сконвертиртировать в изображения с меньшим разрешением и сохранить из в созданный подкаталог temp все потому же пути D:PhotosFirstPhotos.

Тривиально не правда ли? Уверен большинство читателей без труда решат эту задачу. Давайте пофантазируем на тему самого худшего решения, это нам понадобиться, что бы вывести одну очень любопытную зависимость. Предположим, что мы создадим класс ImageConvertor с единственным методом Process не принимающим никаких аргументов. Класс просто в этом методе выполняет всю необходимую по трансформации работу. Какая полезность данного класса? Насколько легко сделать так, что класс стал полезным и так, что бы мы могли его повторно использовать в своей программе (программах)? Что именно нужно сделать для того, что бы класс стал полезным? Первое, что приходит на ум это избавиться от жестких путей к каталогам, добавить возможность установки извне форматов выходных файлов, конечного разрешения файлов и.т.д. На самом деле суть происходящего сводиться к тому, что бы передать управление нашим классом коду извне. Пока неважно даже как мы это делаем, важно следствие, что полезность класса прямо пропорционально тому насколько клиент класса имеет возможность управлять им извне. И наоборот чем меньше сам класс принимает решений о том, что ему нужно делать тем он становиться полезней с точки зрения повторного использования. Я уверен, что многие из Вас задумывались об удачности используемых абстракций. Некоторые абстракции настолько мощны, что позволяют использовать базирующиеся на них классы без всяких изменений в различных частях приложения. Чего стоит только затяганый книжный пример с очередью. Выведенное нами следствие наталкивает меня на мысли о том насколько полезными в действительности являются классы которые мы с вами пишем. Одним из лейтмотивом применения ООП является повторное использование кода и мысль о копировании кода должна вызывать отвращение в здоровых умах. И сейчас мы с Вами сделали первые пол-шага на пути понятия того, что такое действительно полезный класс. Нужно использовать абстракцию и инкапсуляцию для выделения максимально правильного контракта класса. Передавая управление клиенту класса и делая его удобным мы создаем компоненты которыми удобно пользоваться во многих местах нашего приложения. Я упомянул инкапсуляцию в этом контексте именно потому, что верю, что инкапсуляция это не столько книжное – «сокрытие» данных, сколько выделение общедоступного контракта класса. Мы еще вернемся к контрактам, а пока я хочу рассказать об одном интересном принципе, который называется принципом единой ответственности класса (Single responsibility principle). Вернемся к нашему примеру с изображениями и классу ImageConverter. Все ли в нем хорошо после того как мы позволили клиенту класса управлять им? Я уверен, что нет. Для того, что бы понимать, что с нашим классом не так давайте подумаем над тем, какие обязанности возложены нами на него. Первая рекурсивно перебирать файлы, вторая создавать уменьшенные копии изображений. На лицо факт того, что класс перегружен обязанностями. Принцип единой ответственности как раз и говорит нам, что нельзя на класс возлагать больше одной обязанности одновременно. Какие следствия влечет руководствование этим принципом при разработке и проектировании классов для нас? Применив этот принцип к нашему классу мы будем вынуждены разделить его на два класса. Первый класс который будет отвечать за рекурсивный обход каталогов и извлечение файлов (возможно по неким критериям, т.к. дата, расширение и.т.д.), второй непосредственно класс предназначенный для производства уменьшенных копий изображений. Использование принципа единой ответственности позволяет нам делать наши классы более полезными и более приспособленными к повторному использованию. До использования этого принципа мы имели единственный класс который умел искать фотографии и уметь преобразовывать их. Мы могли использовать такую абстракцию исключительно в такой ситуации, но разделив класс, мы получаем возможность использовать первый класс во всех ситуациях когда нам понадобиться обходить каталоги и выполнять некое действие над файлами находящимися в них, а второй вполне вероятно будет использовать для генерации preview файлов в абсолютно другом месте приложения или вообще в другом приложении! Более того использование такого принципа позволяет нам зачастую решать дилемму о том, когда нам следует выделять новую сущность в нашем приложении. Общеизвестна практика, когда программисты, основываясь на задании, выделяют сущности используя существительные как имена классов, а глаголы (сказуемые) как имена методов этих классов. Очевидно, подход сложно назвать универсальным, зачастую задания изобилуют неточностями или элементарно не полны. Принцип единой ответственности должен помочь Вам на этапе проектирования, а так же помочь с принятием решений уже на этапе кодирования. Вернемся к оставленным нами на время контрактам классов. Правильное использование инкапсуляции позволяет определять удобные и лаконичные контракты классов. Если рассмотреть это с точки зрения сделанных нами изменений в рассматриваемом нами примере (разделение класса на два), мы легко заметим, что кроме создания нового класса, мы еще изменили контракт. Чем это плохо? Вероятно многие слышали, что это не хорошо и их не обманывали. Изменение контракта (интерфейса) класса приводит к тому, что вероятно придется изменять и код который пользовался этими самими классами. Логично, что этого нужно стараться избегать. Легко сказать, но сложно сделать. Мы живем в постоянно изменяющемся мире, когда технологии меняются скоростью мысли, а пожелания заказчика с легкостью дуновения ветра. Мы часто сами, пытаясь улучшить код его реорганизуем, и как следствие, мы иногда меняем контракты наших классов. Что мы можем сделать, что бы ослабить сложность таких изменений и облегчить самим себе жизнь? Прежде всего, нужно отдать должное уже тому, что мы знаем. Часто выделение удачной абстракции и правильное использование инкапсуляции позволяет нам предоставить «идеальный» интерфейс не требующий изменений, но проза жизни …

Принцип изоляции интерфейсов (Interface segregation principle), вот то что позволит нам частично ослабить зависимость клиентского кода. Принцип проповедует следующую идею, что если интерфейс слишком обширен, то следует его разделить на несколько более простых и узких интерфейсов. Для начала разберемся чем узкий интерфейс лучше широкого. Предположим что один класс предоставляет 12 общедоступных методов и свойств, а другой 24. От какого интерфейса больше зависят клиенты? К гадалке не ходи от второго. Каждый лишний метод в общедоступном интерфейсе это как команда фас-«чужой» для собаки. Часто наши контракты предоставляют методы, которые никто не использует по нашему мнению, но реальность оказывается прозаична. Кто-то где то «заюзал». Клиентский код контрактно зависимый и чем шире предоставляемый нами контракт тем более сложные и болезные изменения нас ждут в будущем. Тем сложнее производить реорганизацию нашего собственного кода. Для примера зачастую оказывается невозможным разделить без изменения клиентского кода наш собственный. Все это не добавляет радости. Возвращаясь к совету разделять интерфейсы сужая их я приведу на мой взгляд показательный пример. Предположим мы с вами разрабатываем класс Cell для игры Minesweeper. Пусть изначально контракт класса выглядит так:

   public interface ICell
    {
        int Row { get; }
        int Column { get; }
 
        bool IsMarked { get; }
        bool IsMined { get; }
        bool IsFaceup { get; }
        bool IsQuestion { get; }
        CellWeight Weight { get; }
 
        void Mark();
        void Mine();
        void Faceup();
        void Reset();
    }

Впоследствии мы решили использовать биндинг ячеек игрового поля для отображения в WPF приложении. Предположим, что класс Board имеет метод, возвращающий такой массив ячеек. Насколько полезными для представления являются методы:

        void Mark();
        void Mine();
        void Faceup();
        void Reset();

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

    public interface IPosition
    {
        int Row { get; }
        int Column { get; }
    }
    public interface ICellState : IPosition
    {
        bool IsMarked { get; }
        bool IsMined { get; }
        bool IsFaceup { get; }
        bool IsQuestion { get; }
        CellWeight Weight { get; set; }
    }
    public interface ICell : ICellState
    {
        void Mark();
        void Mine();
        void Faceup();
        void Reset();
    }

Метод класса Board будет возвращать всегда и только всегда массив ICellState. Это страхует клиента от ошибок и позволяет сузить интерфейс взаимодействия, а соответственно и зависимость клиентского кода от контракта нашего класса. Другим показательным примером на мой взгляд являются распределенные системы на базе .NET Remoting, где в угоду инфраструктуре и инструментальному коду часто создают общедоступные фасады. Зачастую такие фасады бывают гигантскими, … Что же нам мешает использовать Interface Segregation принцип, предоставляя удаленным клиентам лаконичный и удобный интерфейс взаимодействия с сервером? Хороший объектно-ориентированный код заботится о своих клиентах и уменьшает их зависимость от себя.

Задумываясь о зависимостях, часто ловишь себя на мысли, что клиентами нашего кода является наш собственный библиотечный код. Вообще тема зависимостей в коде чрезвычайно обширна. Еще одним принципом которым я руководствуюсь при оценке того, хороший ли код мы производим является принцип инверсии зависимостей - Dependency inversion principle. Сейчас на слуху паттерн Inversion of Control, который часто специфицируют и разделяют на два паттерна Service Locator и Dependency Injection. Прежде чем обсудить использование этих паттернов я хочу рассказать о том, как я дошел к тому, что эти паттерны полезны для написания хорошего объектно-ориентированного кода. Мой путь к осознанию важности этих паттернов был не прямым, изначально я понял это только тогда когда стал применять Unit тестирование. То есть я начал использовать эти паттерны облегчая себе жизнь в одном аспекте и только в последствии понял, что это только частный случай того, что хороший код всегда легко тестировать, следовательно, использование этих паттернов приводит к написанию хорошего кода. Для начала рассмотрим, какие типы зависимостей бывают.

1) A has B 2) B is A 3) A has B && B has C

Первый тип зависимости означает агрегацию. Второй наследование. Третий транзитивные зависимости, когда A не прямо, но косвенно зависит от C. Если класс A зависит от класса B, то вспоминая наш самый первый пример с вынесением управляющих параметров для класса ImageConverter мы легко придем к выводу, что неплохо было бы позволить клиенту класса самому управлять классом B. Тем самым делая наш код более гибким для модификации, расширения и давая более точный контроль клиенту над поведением нашего класса A. В действительности это делает класс A более универсальным и адаптированным к повторному использованию. Возьмем за основу следующий пример:

    public class ReportBuilder
    {
        private MailService serive = new MailService();
 
        public void PrepareReport()
        {
            // подготовливаем отчет
 
 
            // шлем отчет по почте
            serive.Send(emailReciepient.Email, ..., report);
        }
    }

Налицо зависимость класса ReportBuilder от типа MailService. Часто ли вы пишите такой код? Я думаю, что это код не очень хороший и причин очень много. С точки зрения ООП класс ReportBuilder очень сложно использовать в программе, которой потребуется выкладывать отчеты на ftp или сохранять их в базу данных. При поступлении нового требования класс ReportBuilder придется изменять. Изменяя этот класс и делая его впоследствии универсальным, - вероятно всего придется изменить и код клиента. Я уже не говорю о том, что такой код довольно сложно подвергать блочному тестированию. Давайте подумаем класс MailService для отправки отчета по почте наверняка потребует возможности установки соединения с почтовым сервером. Все портит зависимость класса ReportBuilder от класса MailService. Почему же нам изначально, когда мы пишем код не избавляться от таких неприятных зависимостей?! Ведь наш код станет только лучше! Есть несколько способов разрыва зависимостей. Наиболее известные это уже упоминавшиеся мной паттерны Serivce Locator и Dependency Injection. Я не стану нагружать вас UML диаграммами, лучше посмотрим на конкретно взятом примере как и что мы можем с Вами сделать, что бы ослабить зависимость нашего класса, а попутно мы сами доберемся к пониманию этих паттернов. Первое, что приходит в голову это извлечь интерфейс из класса MailSerivce, причем сделать это нужно аккуратно, что бы наш интерфейс был, не зависим от того, куда именно мы заходим отправить наш отчет. Я предлагаю воспользоваться следующим вариантом:

    public interface IReportDeliverySerive
    {
        void Send(IReciepient recipient, IReport report);
    }

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

    public class ReportBuilder
    {
        private IReportDeliverySerive _serive = new MailService();
 
        public void PrepareReport(Company company)
        {
            // подготовливаем отчет
 
 
            // шлем отчет по почте
            _serive.Send(recipient, report);
        }
    }

Теперь внутри класса ReportBuilder мы будем пользоваться только им, обеспечивая себе возможность использовать унифицированное API отправки отчетов. Мы только слегка ослабили зависимость от класса MailSerivce. Следующий шаг это вынесение зависимости. Я решил воспользоваться для этого конструктором класса:

    public class ReportBuilder
    {
        private IReportDeliverySerive _serive;
 
        public ReportBuilder(IReportDeliverySerive service)
        {
            _serive = service;
        }
 
        public void PrepareReport(Company company)
        {
            // подготовливаем отчет
 
 
            // шлем отчет по почте
            _serive.Send(recipient, report);
        }
    }

Заметим, что наш класс больше не имеет зависимости от класса MailSerivce, теперь он зависит от абстракции - интерфейса IReportDeliverySerivce, более того клиент класса получил возможность управлять поведением нашего класса, путем использования различных реализаций этого самого интерфейса, сделав наш класс максимально полезным и обеспечив таким образом возможность его повторного использования. Все гениальное просто. Этот паттерн называется Dependency Injection т.к. зависимость классу впрыскивается посредством конструктора, метода или общедоступного свойства клиентом. К сожалению, мир не идеален. Где то нам с Вами придется переходить от абстракции к вполне конкретному инстанцированию объекта типа MailSerivice. Можно представить ситуацию, когда для разрешения транзитивных зависимостей клиенту придется инстанцировать огромное число связанных между собой объектов, иногда иерархии могут достигать десятка вложений. В данном случае на помощь могут прийти фабрики, которые смогут создавать транзитивные цепочки. Часто код в методах таких фабричных классов, чрезвычайно запутан и не очевиден. Это можно было рассматривать как недостаток, если бы не одно, но. Сейчас нам доступно огромное число готовых библиотек, которые позволяют разрешать зависимости автоматически предлагая обширный (иногда безполезный) функционал в довесок. Лично я предпочитаю Unity от Microsoft и MEF от той же Microsoft. К сожалению, в данной статье нет места для того, что бы осветить их функциональность, но поверьте мне Dependency Injection контейнера это очень простой и отличный и простой способ управления зависимостями в Вашем приложении. Более того контейнера автоматически реализуют и паттерн Serivce Locator, который мы с Вами рассмотрим чуть ниже как альтернативное решение для избавления от зависимостей. Вернемся к нашему коду:

public class ReportBuilder
    {
        private IReportDeliverySerive _serive = new MailService();
 
        public void PrepareReport(Company company)
        {
            // подготовливаем отчет
 
 
            // шлем отчет по почте
            _serive.Send(recipient, report);
        }
    }

Если мы по неким соображениям не хотим выносить зависимости в методы или конструкторы или свойства, мы можем попытаться переложить разрешение зависимостей на некий сторонний объект. Мы можем создать фабрику, которая будет поставлять нам объекты типа IReportDeliverySerivce. Тогда наш код может выглядеть так:

    public class ReportBuilder
    {
 
        private IReportDeliverySerive _serive = ServiceLocator.Create<IReportDeliverySerive>();
 
        public void PrepareReport(Company company)
        {
            // подготовливаем отчет
 
 
            // шлем отчет по почте
            _serive.Send(recipient, report);
        }
    }

Безусловно, мы имеем зависимость от локатора, но это согласитесь меньшее зло, теперь с его помощью мы сможем управлять поведением нашего класса извне путем конфигурации фабрики-локатора. Еще одним возможным недостатком такого подхода является то, что сам локатор теперь должен знать о всех реализациях интерфейса IDeliverySerivce. Он теперь имеет зависимость от конкретной реализации. На это легко смотреть следующим образом если ответственность этого класса в том, что бы избавить остальные классы от зависимостей, то это неизбежное меньшее зло. Повторюсь, что Dependency Injection контейнера, предоставляют Вам реализацию Serivce Locator задарма. Достоинством использования Serivce Locator является лаконичность и более легкое восприятие кода в следствии его очевидности. Нам не нужно настраивать зависимости руками, и мы четко видим, что и где создается, не полагаясь на алгоритмы разрешения зависимостей контейнерами, в случае их использования в связке с Dependency Injection. Есть еще один аспект, - так при написании тестов Dependency Injection паттерн, дает вам большую свободу и гибкость, чем использование Service Locator. Хотя и последний в большинстве своем делает код гораздо более приспособленным к тестированию, чем классы, которые имеют прямые зависимости от других классов. В любом случае ослабляя зависимости и разрывая зависимости, Вы добиваетесь, универсализации своих классов, делая их максимально полезными в различных задачах, а не только в конкретном специфическом месте своей программы. Вы делаете свой код адаптированным к модификациям. Все это достигается передачей управления классом клиенту (вспоминаем первый пример с фотографиями) при этом, добиваясь того, что не их код, не код клиента класса в последствии не придется модифицировать. Большинство изменений будет приходиться на конфигурирование самого контейнера. Более того, подавляющие число контейнеров, позволяет выносить конфигурации из кода в настроечные файлы, зачастую это позволяет обойтись и без перекомпиляции вашего кода.

Мы плавно подходим к еще одному принципу, используя который мы можем делать наш код лучше. Это принцип Open-Close, который формулируется просто: наши классы должны быть открыты для модификации и закрыты для изменения. Есть две трактовки этого принципа, но с практической точки зрения оба полагаются на наследование. Я постараюсь объяснить обе. Первая трактовка настаивает на том, что единожды написанный класс должен модифицироваться только для исправления ошибок. Дальнейшее расширение его функционала или изменение существующего должно проводиться путем создания нового класса и полагаться на наследование. При этом не обязательно использование общего интерфейса. В дальнейшем этот принцип был переформулирован, с учетом того, что мы можем определять интерфейсы, в таком случае их реализация допускает изменения и полиморфные замещения методов в наследниках. В данном случае мы требуем, что бы наследники как минимум реализовывали общий интерфейс, при этом сам интерфейс закрыт для модификации. Мне кажется второй вариант более современен. Он диктует неизменность наших контрактов, оставляя нам свободу в их имплементации. Это потрясающие правило способствует тому, что наши клиенты используют сильные контракты, при этом их реализация может меняться как угодно. Учитывая это требование по сути навязывающие нам интерфейс ориентированное программирование, а также руководствуясь остальными, уже рассмотренными, основополагающими принципами, можно писать действительно хороший код, когда клиент не зависит от того, как наш код реализован и застрахован от изменений в нем.

Еще мне хочется сказать немного о unit тестировании. Я упомянул его как вариант достижения действительно качественного кода. На самом деле я категоричен еще больше, код, которые не покрыт тестами я не готов назвать качественным и хорошим. Может быть, много причин, по которым в проекте отсутствуют тесты, но это не должно снижать моих ожиданий от качества кода. Если не рассматривать организационные вопросы, то часто препятствием на пути написания теста к классу, стоят как раз зависимости. Очень сложно протестировать класс с зависимостями т.к. для его инстанцирования часто требуются классы, которые невозможно создать в среде тестирования. Примером может служить известный класс System.Web.HttpContext. Предположим, что мы решили сохранить в контексте данные о пользователе и наш класс оперирует данными из контекста. Такой класс практически невозможно протестировать без разрыва зависимости. Вооружившись полученными знаниями, мы можем определить интерфейс IApplicationContext с методом CurrentUser. Мы можем реализовать этот интерфейс в классе и использовать HttpContext, а можем использовать фейковую реализацию в которой вернем заготовленного пользователя специально для тестов, решая таким образом проблему тестирования класса. На самом деле есть хорошее правило, при котором чужое API всегда стоит обвязывать собственными интерфейсами, мы не только сокращаем площади зависимости от него, мы и не только обеспечиваем возможность безболезненной его смены в будущим, но и развязываем себе руки в тестировании классов базирующихся на этом API.

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

До новых встреч.