Главная | Обратная связь | Поможем написать вашу работу!
МегаЛекции

Каждый класс должен иметь одну ответственность. Все методы этого класса должны быть направлены исключительно на обеспечение этой ответственности.




Ответственность – причина изменения класса.

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

1. Если требуется изменить печать на принтер

2. Если потребуется, например, печатать штрихкод не только товара, но и штрихкод сотрудника.

То есть на класс наложено слишком много обязанностей. Его необходимо разделить.

Однако слепое следование этому принципу может привести к избыточной сложности приложения.

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

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

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

Пример.

class Report

{

public string Text { get; set; }

public void GoToFirstPage()

{

Console.WriteLine("Переход к первой странице");

}

 

public void GoToLastPage()

{

Console.WriteLine("Переход к последней странице");

}

 

public void GoToPage(int pageNumber)

{

Console.WriteLine("Переход к странице {0}", pageNumber);

}

 

public void Print()

{

Console.WriteLine("Печать отчета");

Console.WriteLine(Text);

}

}

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

В этом случае мы могли бы вынести функционал печати в отдельный класс, а потом применить агрегацию

interface IPrinter

{

void Print(string text);

}

 

class ConsolePrinter: IPrinter

{

public void Print(string text)

{

Console.WriteLine(text);

}

}

 

class Report

{

public string Text { get; set; }

 

public void GoToFirstPage()

{

Console.WriteLine("Переход к первой странице");

}

 

public void GoToLastPage()

{

Console.WriteLine("Переход к последней странице");

}

 

public void GoToPage(int pageNumber)

{

Console.WriteLine("Переход к странице {0}", pageNumber);

}

public void Print(IPrinter printer)

{

printer.Print(this.Text);

}

}

 

И применение

IPrinter printer = new ConsolePrinter();

Report report = new Report();

report.Text = "Hello Wolrd";

report.Print(printer);


 

 

2. O – Принцип открытости/закрытости. The Open Closed Principle

Сущности (классы) программы должны быть открыты для расширения, но закрыты для изменения.

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

Пример. Класс повар.

class Cook

{

public string Name { get; set; }

public Cook(string name)

{

this.Name = name;

}

 

public void MakeDinner()

{

Console.WriteLine("Чистим картошку");

Console.WriteLine("Ставим почищенную картошку на огонь");

Console.WriteLine("Сливаем остатки воды, разминаем варенный картофель в пюре");

Console.WriteLine("Посыпаем пюре специями и зеленью");

Console.WriteLine("Картофельное пюре готово");

}

}

Применение класса:

Cook bob = new Cook("Bob");

bob.MakeDinner();

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

Изменим класс Cook следующим образом:

class Cook

{

public string Name { get; set; }

 

public Cook(string name)

{

this.Name = name;

}

 

public void MakeDinner(IMeal meal)

{

meal.Make();

}

}

 

interface IMeal

{

void Make();

}

class PotatoMeal: IMeal

{

public void Make()

{

Console.WriteLine("Чистим картошку");

Console.WriteLine("Ставим почищенную картошку на огонь");

Console.WriteLine("Сливаем остатки воды, разминаем варенный картофель в пюре");

Console.WriteLine("Посыпаем пюре специями и зеленью");

Console.WriteLine("Картофельное пюре готово");

}

}

class SaladMeal: IMeal

{

public void Make()

{

Console.WriteLine("Нарезаем помидоры и огурцы");

Console.WriteLine("Посыпаем зеленью, солью и специями");

Console.WriteLine("Поливаем подсолнечным маслом");

Console.WriteLine("Салат готов");

}

}

Использование класса:

Cook bob = new Cook("Bob"); bob.MakeDinner(new PotatoMeal()); Console.WriteLine(); bob.MakeDinner(new SaladMeal());

 

 

3. L – Принцип замещения Лисков. The Liskov Substitution Principle

Если у нас есть класс A (не виртуальный, а вполне реально используемый в коде) и отнаследованный от него класс B, то если мы заменим все использования класса A на B, ничего не должно измениться в работе программы. Ведь класс B всего лишь расширяет функционал класса A. Если эта проверка работает, то поздравляю: ваша программа соответствует принципу подстановки Лисков! Если нет, стоит задуматься: «а правильно ли спроектированы классы?».

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

Пример. Допустим, есть два класса: Account (общий счет) и MicroAccount (мини-счет с ограничениями). И второй класс переопределяет метод SetCapital:

class Account

{

public int Capital { get; protected set; }

 

public virtual void SetCapital(int money)

{

if (money < 0)

throw new Exception("Нельзя положить на счет меньше 0");

this.Capital = money;

}

}

 

class MicroAccount: Account

{

public override void SetCapital(int money)

{

if (money < 0)

throw new Exception("Нельзя положить на счет меньше 0");

 

if (money > 100)

throw new Exception("Нельзя положить на счет больше 100");

 

this.Capital = money;

}

}

Сама программа:

class Program

{

static void Main(string[] args)

{

Account acc = new MicroAccount(); //замена класса, используем класс Account, получаем результат класса MicroAccount

InitializeAccount(acc);

 

Console.Read();

}

 

public static void InitializeAccount(Account account)

{

account.SetCapital(200);

Console.WriteLine(account.Capital);

}

}

Идет несоответствие принципу!!!! Если бы при такой замене программа отработала бы верно, то можно было бы говорить о соблюдении принципа и верной его архитектуре.

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

 

4. I – Принцип разделения интерфейсов. The Interface Segregation Principle

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

Принцип разделения интерфейсов можно сформулировать так:

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

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

Рассмотрим на примере. Допустим у нас есть интерфейс отправки сообщения:

interface IMessage { void Send(); string Text { get; set;} string Subject { get; set;} string ToAddress { get; set; } string FromAddress { get; set; } }

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

class EmailMessage: IMessage { public string Subject { get; set; } public string Text { get; set; } public string FromAddress { get; set; } public string ToAddress { get; set; }   public void Send() { Console.WriteLine("Отправляем по Email сообщение: {0}", Text); } }

Надо отметить, что класс EmailMessage выглядит целостно, вполне удовлетворяя принципу единственной ответственности. То есть с точки зрения связанности (cohesion) здесь проблем нет.

Теперь определим класс, который бы отправлял данные по смс:

class SmsMessage: IMessage { public string Text { get; set; } public string FromAddress { get; set; } public string ToAddress { get; set; }   public string Subject { get { throw new NotImplementedException(); }   set { throw new NotImplementedException(); } }   public void Send() { Console.WriteLine("Отправляем по Sms сообщение: {0}", Text); } }

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

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

Класс голосовой почты также имеет отправителя и получателя, только само сообщение передается в виде звука, что на уровне C# можно выразить в виде массива байтов. И в этом случае было бы неплохо, если бы интерфейс IMessage включал бы в себя дополнительные свойства и методы для этого, например:

interface IMessage { void Send(); string Text { get; set;} string ToAddress { get; set; } string Subject { get; set; } string FromAddress { get; set; }   byte[] Voice { get; set; } }

Тогда класс голосовой почты VoiceMessage мог бы выглядеть следующим образом:

    class VoiceMessage: IMessage { public string ToAddress { get; set; } public string FromAddress { get; set; } public byte[] Voice { get; set; }   public string Text { get { throw new NotImplementedException(); }   set { throw new NotImplementedException(); } }   public string Subject { get { throw new NotImplementedException(); }   set { throw new NotImplementedException(); } }   public void Send() { Console.WriteLine("Передача голосовой почты"); } }

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

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

interface IMessage { void Send(); string ToAddress { get; set; } string FromAddress { get; set; } } interface IVoiceMessage: IMessage { byte[] Voice { get; set; } } interface ITextMessage: IMessage { string Text { get; set; } }   interface IEmailMessage: ITextMessage { string Subject { get; set; } }   class VoiceMessage: IVoiceMessage { public string ToAddress { get; set; } public string FromAddress { get; set; }   public byte[] Voice { get; set; } public void Send() { Console.WriteLine("Передача голосовой почты"); } } class EmailMessage: IEmailMessage { public string Text { get; set; } public string Subject { get; set; } public string FromAddress { get; set; } public string ToAddress { get; set; }   public void Send() { Console.WriteLine("Отправляем по Email сообщение: {0}", Text); } }   class SmsMessage: ITextMessage { public string Text { get; set; } public string FromAddress { get; set; } public string ToAddress { get; set; } public void Send() { Console.WriteLine("Отправляем по Sms сообщение: {0}", Text); } }

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

 

5. D – Dependency Inversion Principle Принцип инверсии зависимостей.

Формулировка:

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

- Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

class Book

{

public string Text { get; set; }

public ConsolePrinter Printer { get; set; }

 

public void Print()

{

Printer.Print(Text);

}

}

 

class ConsolePrinter

{

public void Print(string text)

{

Console.WriteLine(text);

}

}

Класс Book, представляющий книгу, использует для печати класс ConsolePrinter. При подобном определении класс Book зависит от класса ConsolePrinter. Более того мы жестко определили, что печать книгу можно только на консоли с помощью класса ConsolePrinter. Другие же варианты, например, вывод на принтер, вывод в файл или с использованием каких-то элементов графического интерфейса - все это в данном случае исключено. Абстракция печати книги не отделена от деталей класса ConsolePrinter. Все это является нарушением принципа инверсии зависимостей.

Приведем наши классы в соответствие с принципом инверсии зависимостей

interface IPrinter

{

void Print(string text);

}

 

class Book

{

public string Text { get; set; }

public IPrinter Printer { get; set; }

 

public Book(IPrinter printer)

{

this.Printer = printer;

}

 

public void Print()

{

Printer.Print(Text);

}

}

 

class ConsolePrinter: IPrinter

{

public void Print(string text)

{

Console.WriteLine("Печать на консоли");

}

}

 

class HtmlPrinter: IPrinter

{

public void Print(string text)

{

Console.WriteLine("Печать в html");

}

}

Реализация:

Book book = new Book(new ConsolePrinter());

book.Print();

book.Printer = new HtmlPrinter();

book.Print();

 

 

Закон Деметры.

Закон Деметры говорит нам о том же, о чем в детстве говорили родители: «Не разговаривай с незнакомцами». А разговаривать можно вот с кем:

 

— С методами самого объекта.

— С методами объектов, от которых объект зависит напрямую.

— С созданными объектами.

— С объектами, которые приходят в метод в качестве параметра.

— С глобальными переменными (что лично мне не кажется верным, так как глобальные переменные во многом увеличивают общую сложность)

Таким образом, a.b.Method() нарушает Закон Деметры, а код a.Method() является корректным.

 

Еще раз обсудим эти принципы проектирования в конце курса.

 

Решение задачи о печи.

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

 


 

UML.

Унифицированный язык моделирования UML (Unified Modeling Language) – это язык графического описания программных сущностей в виде диаграмм.

Модели строятся для того, чтобы понять, будет ли некая конструкция работать.

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

Диаграммы классов мы уже рассмотрели.

Диаграмма переходов состояний.

Скругленными прямоугольниками представлены состояния. Имя состояния записывается в верхнем отделении. В нижнем отделении записываются специальные действия, говорящие о том, что нужно сделать при входе в данное состояние и при выходе из него. Например, при входе в состояние Приглашение к входу вызывается действие showLoginScreen, и при выходе из этого состояния – действие hideLoginScreen.

Стрелки, соединяющие состояния, называются переходами. Каждый переход помечен именем вызвавшего его события, а некоторые – также действием, которое выполняется при срабатывании перехода. Например, если, находясь в состоянии Приглашение к входу, мы получаем событие login, то переходим в состояние Проверка пользователя и вызываем действие validateUser.

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

В нижнем отделении прямоугольника состояния находятся пары событие/действие. События entry (вход) и exit (выход) стандартные, но можно включать и свои собственные события.

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

 

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

Читать ТПС нужно следующим образом: «Если мы находимся в состоянии Locked и получаем событие coin, то переходим в состояние Unlocked и вызываем функцию Unlock».

 

Возьмем процесс «Снятие денег с банкомата» - одна операция и построим диаграмму состояний и таблицу переходов состояний.


 

 

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

 

Поделиться:





Воспользуйтесь поиском по сайту:



©2015 - 2024 megalektsii.ru Все авторские права принадлежат авторам лекционных материалов. Обратная связь с нами...