Слайд 1Принципы объектно-ориентированного дизайна
SOLID:
Single responsibility
Open-closed
Liskov substitution
Interface segregation
Dependency inversion
Слайд 2Что такое SOLID
SOLID - это аббревиатура пяти основных принципов дизайна
классов в объектно-ориентированном проектировании.
Аббревиатура была введена Робертом Мартином в начале
2000-х.
Рекомендую почитать:
Чистый код. Роберт Мартин
Слайд 3Основные принципы
Single responsibility - Принцип единственной обязанности
Open-closed - Принцип открытости/закрытости
Liskov
substitution - Принцип подстановки Барбары Лисков
Interface segregation - Принцип разделения
интерфейса
Dependency inversion - Принцип инверсии зависимостей
Слайд 4Single responsibility
Принцип единственной обязанности
Класс или модуль должны иметь одну и
только одну причину измениться.
Слайд 5Пример нарушения принципа SRP
class Order
{
public void calculate(){ ...
}
public void addItem(Product product){ ... }
public
List
getItems(){ ... }
...
public void load(){ ... }
public void save(){ ... }
public void print(){ ... }
}
Слайд 6Как исправить
class Order
{
public void calculate();
public void
addItem(Product product){ ... }
public List getItems(){ ... }
}
class OrderRepository
{
public Order load(int orderId){ ... }
public void save(Order order){ ... }
}
class OrderPrintManager
{
public void print(Order order){ ... }
}
Слайд 7Но...
Существует, например, паттерн Active Record, который нарушает принцип SRP
Active Record
может быть успешно использован в небольших проектах с простой бизнес-логикой.
ActiveRecord
post = Post.newRecord();
post.setData("title", "Happy Java Programming");
post.setData("body", "Java programming is fun.");
post.create();
Слайд 8Open-closed
Принцип открытости/закрытости
Объекты проектирования (классы, функции, модули и т.д.) должны
быть открыты для расширения, но закрыты для модификации.
Это означает, что
новое поведение должно добавляться только добавлением новых сущностей, а не изменением старых.
Слайд 9Пример нарушения OCP
class MessageSender {
…
public void
send(String message, MessageType type){
if(type
== MessageType.SMS) sendSMS(msg);
else
if(type == MessageType.EMAIL) sendEmail(msg);
}
}
Слайд 10Как исправить
Воспользуемся паттерном “Стратегия”
interface SendingStrategy {
void send(String message);
}
class
MessageSender {
private SendingStrategy strategy;
public MessageSender(SendingStrategy strategy)
{
this.strategy = strategy;
}
public void send(String message) {
this.strategy.send(message);
}
}
Слайд 11Как исправить(продолжение)
Конкретные стратегии отправки
class EmailSendingStrategy implements SendingStrategy {
@Override
public void send(String message) {
System.out.println("Sending
Email: " + message);
}
}
class SMSSendingStrategy implements SendingStrategy {
@Override
public void send(String message) {
System.out.println("Sending SMS: " + message);
}
}
Слайд 12Liskov substitution
Принцип подстановки Барбары Лисков
Роберт С. Мартин определил этот
принцип так:
Функции, которые используют базовый тип, должны иметь
возможность использовать подтипы базового типа не зная об этом.
Слайд 13Замещение
T
S
Объекты типа T могут быть замещены объектами типа S без
каких-либо изменений желательных свойств этой программы
Слайд 14Нарушение принципа LSP
Circle-ellipse problem
Square-rectangle problem
Слайд 15Square-rectangle problem
Является ли класс Квадрат подклассом класса Прямоугольник?
Rectangle
Square
?
Слайд 16Класс Rectangle
class Rectangle {
private double width;
private
double height;
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
public String toString() {
return this.width + "x" + this.height;
}
}
Слайд 17Класс Square
class Square extends Rectangle {
public void setWidth(double width)
{
this.setSide(width);
}
public void setHeight(double height) {
this.setSide(height);
}
public void setSide(double side) {
super.setWidth(side);
super.setHeight(side);
}
}
Слайд 18В чем же проблема?
public class LiskovViolation {
public static
void main(String[] args) {
Rectangle rectangle = new
Square();
rectangle.setWidth(10);
System.out.println(rectangle); // 10.0x10.0
rectangle.setHeight(20);
System.out.println(rectangle); // 20.0x20.0 !!!! Should be 10.0x20.0
}
}
Слайд 19Как исправить
Если использовать концепцию неизменяемого объекта (immutable object), то принцип
не будет нарушаться.
Необходимо убрать возможность изменения объекта после его создания.
Слайд 20Interface segregation
Принцип разделения интерфейса
Слишком «толстые» интерфейсы необходимо разделять на
более маленькие и специфические, чтобы клиенты маленьких интерфейсов знали только
о методах, которые необходимы им в работе.
Слайд 21“Толстый” интерфейс
Если среди методов интерфейса можно выделить группы методов, которые
нужны определенным пользователям интерфейса, то скорее всего интерфейс “толстый”.
Такой интерфейс
нужно разбить на более мелкие, которые будут выражать потребности конкретной группы пользователей интерфейса.
Слайд 22Пример нарушения ISP
interface Person {
void goToWork();
void
withdrawSalary();
void eat();
}
Если мы захотим сделать реализацию, в которой
единственным необходимым методом будет eat() придется реализовывать и все остальные методы
Слайд 23Как исправить
public interface Person {
void eat();
}
public interface Worker
{
void goToWork();
void withdrawSalary();
}
Слайд 24Dependency inversion
Принцип инверсии зависимостей
Все взаимосвязи в программе должны поддерживаться
с помощью абстракных классом или интерфейсов.
Слайд 25Нарушение принципа DIP
public class Crawler {
public void saveHtmlDocument()
{
DomBasedHtmlParser parser = new DomBasedHtmlParser();
HtmlDocument document = parser.parseUrl("http://example.com/");
save(document, "index.html");
}
public void save(HtmlDocument htmlDocument, String pageName) {
// сохранение документа в файл
}
}
Экземпляр класса парсер создается внутри метода saveHtmlDocument() и не использует интерфейс, что делает невозможным использования другой реализации парсера и затрудняет тестирование.
Слайд 26Как исправить. Вариант 1
public class Crawler {
private HtmlParser
parser;
public Crawler(HtmlParser parser) {
this.parser
= parser;
}
public void saveHtmlDocument() {
HtmlDocument document = parser.parseUrl("http://example.com/");
save(document, "index.html");
}
public void save(HtmlDocument htmlDocument, String pageName) {
// сохранение документа в файл
}
}
Слайд 27Как исправить. Вариант 2
public class Crawler{
private HtmlParser parser
= ParserFactory.getHtmlParser();
public void saveHtmlDocument() {
HtmlDocument
document = parser.parseUrl("http://example.com/");
save(document, "index.html");
}
public void save(HtmlDocument htmlDocument, String pageName) {
// сохранение документа в файл
}
}