7 лютого 2011 р.

S.O.L.I.D

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

S - Single Responsibility Principle

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

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

class SuperClass{
public string GetAccountName()
{
//Some logic of retrieving name
return string.Empty;
}
      public void SendMail()
{
//some logic of sending email here
}
}

Итак, в результате у нас есть объект который умеет как вытаскивать имя пользователя, так и отправлять письма. Если нам потребуется изменить логику отсылки почты, нам прийдется менять этот класс. Или же если имя пользователся вытаскивалось из SQL Server, а нам надо поменять на Oracle - нам опять же прийдется менять этот класс. Логичнее было бы разделить функционал вытаскивания имени и отсылки почты на два отдельных объекта - это бы дало нам больше гибкости, и возможность использовать единожды написанный метод отсылки почты в других компонентах.

O - Open/Closed Principle.

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

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

class SuperBankApp
{
public void ProcessPayment( string paymentType )
{
switch (paymentType)
{
case "tax":
//some logic here
break;
case "fee":
//some logic here
break;
case "loan":
//some logic here
break;
}
}
}

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

Теперь, после того как мы прочитали лекцию нашему начинающему программисту о том что так писать нельзя, покажем ему пример хорошего дизайна:

а. Давайте создадим общий для всех платежей интерфейс:

public interface IAccount
{
void ProcessRequest();
}

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

в. Давайте внесем некоторые изменения в нашу программу:

public List accounts = new List();
public void AddAccount( IAccount acc)
{
accounts.Add(acc);
}
public void ProcessPayment()
{
foreach (IAccount acc in accounts)
{
acc.ProcessRequest();
}
}

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

Собственно этот принцип может быть поддержан разными методами: абстрактные классы, интерфейсы, полиморфизм, и т.д. Просто выберите метод которые наиболее подходит Вашим нуждам.

 

L-Liskov Substitution Principle.

Этот принцип был впервые высказан Барбарой Лисков в 1987 году. Собственно принцип сформулирован следующим образом:

Пусть q(x) является свойством, верным относительно объектов x некоторого типа T. Тогда q(y) также должно быть верным для объектов y типа S, где S является подтипом типа T.

Собственно идея принципа достаточно проста - мы должны иметь возможность заменить базовый класс объектом который наследуется от него, и при этом все должно работать прекрасно и программа ломаться не должна. Этот принцип очень тесно связан с принципом Открытости / Закрытости. Давайте поговрим немного детальнее почему.

Давайте представим ситацию - у насть есть базовый класс Shape, и два класса которые унаследовываются от него: Square, Circle. Снова позовем нашего начинающего программиста, и попросим написать метод который принимает на вход объект типа Shape, и при этом выдает нам площадь фигуры которую мы передали. Наш начинающий программист не глуп, и знает что площадь разных фигур вычитывается разными формулами, потому он нам выдал что-то вроде такого:

public void CountArea(Shape sh)
{
if (sh is Square)
{
//count area for square
}
if (sh is Circle)
{
//count area for circle
}
}

Опять представим ситуацию , появилась у нас новая фигура - прямоугольник. Вот собственно и все - надо менять и добавлять новый функционал. А это нарушает принцип открытости/закрытости.

Чтобы все работало и устраивало оба принципа необходимо произвести такие действия:

а. Базовый клас Shape должен задавать виртуальный метод GetArea.

б. Объекты Circle, Sqare должны перегружать этот метод собственной формулой.

в. Теперь нам не надо определять тип объекта переданного в метод чтобы посчитать площадь:)

 

I - Interface Segregation Principle

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

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

а. Есть у нас два объекта: Cat, Dog.

б. Объекты наследуются от интерфейса IAnimal.

в. В предыдущей версии Вашей программы Вам необходимо было реализовать только 1 метод - Walk.

Логично , что метод задавался в интерфейсе. Теперь, допустим нам надо реализовать методы Meow и Bark. Если включить логику, то станет понятно что собака не сможет мяукать, а кошка не сможет гавкать ( хотя кто их знает, экология ж). Если это задание опять же поручить нашему программисту новчику, то скорее всего он задаст методы в интерфейсе IAnimal , и поставит заглушки в методое Bark класса Cat, и в методу Meow класса Dog. Хорошим же решением было бы добавление двух новых интерфейсов ICommonCat и ICommonDog соответственно для классов Cat и Dog. Общие же методы (например Eat) можна задавть все там же в интерфейсе IAnimal.

D - Dependency Inversion Principle

Данный принцип говорит нам:

Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Собственно ситуация стандартная - у нас есть низкоуровневые классы, которые реализут определенный функционал. Есть высокоуровневые классы, которые пользуюясь низкоуровневыми классами собирают весь функционал в кучу.

Давайте представим ситуацию:) у нас есть объект который считывает данные из базы SQL Server , и передают все в класс Engine, которые эти данные обрабатывает. В один прекрасный день нам надо перейти с базы SQL Server на базу Oracle - нам надо менять в объекте Engine все упоминания про один ридер, и заменять другим.Если бы у нас все было устроено на абстракциях - то таких замен было бы в разы меньше.

Давайте опять позовем нашего начинающего программиста, и посмотрим чего он накодил:

public class Worker
{
public void Work()
{
//do some work
}
}
    public class SeniorWorker
{
public void Work()
{
//do some senior work
}
}
    public class Manager
{
public void Manage( Worker simpleWorker)
{
simpleWorker.Work();
}
        public void Manage( SeniorWorker seniorWorker)
{
seniorWorker.Work();
}
}

Как мы видим есть два объекта Worker и SeniorWorker, а также класс Manager который может заставлять обоих работать. Минус - для каждого из Worker надо отдельный метод, ибо мы зависим от класса а не от асбтракции. Давайте сделаем некоторые модификации в наш код:

 

public interface IWorker
{
void Work();
}
    public class Worker:IWorker
{
public void Work()
{
//do some work
}
}
    public class SeniorWorker:IWorker
{
public void Work()
{
//do some senior work
}
}
    public class Manager
{
public void Manage( IWorker worker)
{
worker.Work();
}
}

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

 

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

2 коментарі:

  1. В системе AutoCAD, есть такая штриховка с названием SOLID - она заливает замкнутый контур цельным цветом, без узоров.

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

    ВідповістиВидалити
  2. Не з усіма принципами особисто згоден, але в принципі рекомендації в цілому вірні, сенкс за статтю :)

    ВідповістиВидалити