Skip to content
IRC-CodingIRC-Coding
Software Designпринципы проектированиямодульностьClean CodeSoftware Architectureлучшие практикиалгоритмыосновы

Основы Software Design

Введение в Software Design: принципы, паттерны и лучшие практики для надежного кода.

S

schutzgeist

9 min read
Основы Software Design

Основы проектирования программного обеспечения 2024

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

Что такое проектирование ПО?

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

Три уровня проектирования ПО:

  1. Архитектурное проектирование: общая структура всего приложения
  2. Детальное проектирование: конкретная реализация компонентов
  3. Проектирование интерфейсов: определение APIs и пользовательских интерфейсов

Значение качественного дизайна

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

Преимущества качественного дизайна:

  • Поддерживаемость: проще понимать и изменять код
  • Расширяемость: новые функции добавляются легко
  • Тестируемость: компоненты можно тестировать в изоляции
  • Переиспользуемость: модули можно применять в других проектах
  • Производительность: эффективное использование ресурсов

SOLID-принципы

SOLID это пять фундаментальных принципов проектирования для объектно-ориентированной разработки:

1. Single Responsibility Principle (SRP)

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

// ❌ Плохой пример: класс имеет несколько обязанностей
public class UserService {
    public void saveUser(User user) { /* сохраняет пользователя */ }
    public void sendEmail(User user) { /* отправляет письмо */ }
    public void generateReport(User user) { /* создает отчет */ }
}

// ✅ Хороший пример: каждый класс имеет одну обязанность
public class UserService {
    public void saveUser(User user) { /* сохраняет пользователя */ }
}

public class EmailService {
    public void sendEmail(User user) { /* отправляет письмо */ }
}

public class ReportService {
    public void generateReport(User user) { /* создает отчет */ }
}

2. Open/Closed Principle (OCP)

Компоненты должны быть открыты для расширения, но закрыты для модификации.

// ❌ Плохой пример: код нужно менять при добавлении типа скидки
public class DiscountCalculator {
    public double calculateDiscount(String type, double amount) {
        if (type.equals("STUDENT")) return amount * 0.1;
        if (type.equals("SENIOR")) return amount * 0.2;
        // новый тип требует изменения кода
        return 0;
    }
}

// ✅ Хороший пример: открыт для расширения
public interface Discount {
    double calculate(double amount);
}

public class StudentDiscount implements Discount {
    public double calculate(double amount) { return amount * 0.1; }
}

public class SeniorDiscount implements Discount {
    public double calculate(double amount) { return amount * 0.2; }
}

3. Liskov Substitution Principle (LSP)

Подклассы должны быть заменяемы на своих базовых классов.

// ❌ Плохой пример: нарушает LSP
public class Rectangle {
    protected int width, height;
    public void setWidth(int width) { this.width = width; }
    public void setHeight(int height) { this.height = height; }
    public int getArea() { return width * height; }
}

public class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width; // нарушает поведение Rectangle
    }
}

// ✅ Хороший пример: соответствует LSP
public abstract class Shape {
    public abstract int getArea();
}

public class Rectangle extends Shape {
    private int width, height;
    public int getArea() { return width * height; }
}

public class Square extends Shape {
    private int side;
    public int getArea() { return side * side; }
}

4. Interface Segregation Principle (ISP)

Лучше иметь много специализированных интерфейсов, чем один универсальный.

// ❌ Плохой пример: большой интерфейс
public interface Worker {
    void work();
    void eat();
    void sleep();
}

public class Robot implements Worker {
    public void work() { /* ... */ }
    public void eat() { /* робот не ест */ }
    public void sleep() { /* робот не спит */ }
}

// ✅ Хороший пример: специализированные интерфейсы
public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public class Robot implements Workable {
    public void work() { /* ... */ }
}

5. Dependency Inversion Principle (DIP)

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

// ❌ Плохой пример: прямая зависимость
public class LightSwitch {
    private LightBulb bulb;
    
    public LightSwitch() {
        this.bulb = new LightBulb(); // прямая зависимость
    }
    
    public void flip() {
        bulb.turnOn();
    }
}

// ✅ Хороший пример: зависимость от интерфейса
public interface Switchable {
    void turnOn();
    void turnOff();
}

public class LightBulb implements Switchable {
    public void turnOn() { /* ... */ }
    public void turnOff() { /* ... */ }
}

public class LightSwitch {
    private Switchable device;
    
    public LightSwitch(Switchable device) {
        this.device = device; // зависимость от абстракции
    }
    
    public void flip() {
        device.turnOn();
    }
}

Другие важные принципы проектирования

KISS (Keep It Simple, Stupid)

Сохраняй код простым и понятным. Простые решения часто оказываются лучшими.

DRY (Don’t Repeat Yourself)

Избегай дублирования кода. Не повторяйся в своих решениях.

YAGNI (You Ain’t Gonna Need It)

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

Separation of Concerns

Разделяй различные аспекты приложения на отдельные модули.

Архитектурные паттерны

Layered Architecture

Слоистая архитектура организует код в логические уровни:

  • Presentation Layer: пользовательский интерфейс
  • Business Layer: бизнес-логика
  • Data Access Layer: доступ к данным
  • Database Layer: база данных

MVC (Model-View-Controller)

Разделяет данные, представление и управление:

  • Model: данные и бизнес-логика
  • View: пользовательский интерфейс
  • Controller: связь между Model и View

Microservices Architecture

Разделение на небольшие независимые сервисы:

  • каждый сервис имеет собственную базу данных
  • взаимодействие через APIs
  • независимое развертывание

Event-Driven Architecture

Системы взаимодействуют через события:

  • слабая связанность между компонентами
  • асинхронное взаимодействие
  • хорошая масштабируемость

Design Patterns (паттерны GoF)

Creational Patterns (порождающие паттерны)

Factory Method

Создает объекты без необходимости знать их конкретный класс.

public interface Vehicle {
    void drive();
}

public class Car implements Vehicle {
    public void drive() { System.out.println("Car drives"); }
}

public class Motorcycle implements Vehicle {
    public void drive() { System.out.println("Motorcycle drives"); }
}

public abstract class VehicleFactory {
    public abstract Vehicle createVehicle();
}

public class CarFactory extends VehicleFactory {
    public Vehicle createVehicle() { return new Car(); }
}

Singleton

Гарантирует, что класс имеет только один экземпляр.

public class DatabaseConnection {
    private static DatabaseConnection instance;
    
    private DatabaseConnection() { /* приватный конструктор */ }
    
    public static DatabaseConnection getInstance() {
        if (instance == null) {
            instance = new DatabaseConnection();
        }
        return instance;
    }
}

Структурные паттерны

Adapter

Позволяет несовместимым интерфейсам работать вместе.

public interface MediaPlayer {
    void play(String audioType, String fileName);
}

public interface AdvancedMediaPlayer {
    void playVlc(String fileName);
    void playMp4(String fileName);
}

public class MediaAdapter implements MediaPlayer {
    private AdvancedMediaPlayer advancedMusicPlayer;
    
    public MediaAdapter(String audioType) {
        if (audioType.equalsIgnoreCase("vlc")) {
            advancedMusicPlayer = new VlcPlayer();
        }
    }
    
    public void play(String audioType, String fileName) {
        if (audioType.equalsIgnoreCase("vlc")) {
            advancedMusicPlayer.playVlc(fileName);
        }
    }
}

Поведенческие паттерны

Observer

Оповещает подписчиков об изменении состояния.

import java.util.ArrayList;
import java.util.List;

public interface Observer {
    void update(String message);
}

public interface Subject {
    void registerObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers();
}

public class NewsAgency implements Subject {
    private List<Observer> observers = new ArrayList<>();
    private String news;
    
    public void registerObserver(Observer observer) {
        observers.add(observer);
    }
    
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(news);
        }
    }
    
    public void setNews(String news) {
        this.news = news;
        notifyObservers();
    }
}

Strategy

Определяет семейство алгоритмов и делает их взаимозаменяемыми.

public interface PaymentStrategy {
    void pay(int amount);
}

public class CreditCardPayment implements PaymentStrategy {
    public void pay(int amount) {
        System.out.println("Paid " + amount + " using Credit Card");
    }
}

public class PayPalPayment implements PaymentStrategy {
    public void pay(int amount) {
        System.out.println("Paid " + amount + " using PayPal");
    }
}

public class ShoppingCart {
    private PaymentStrategy paymentStrategy;
    
    public void setPaymentStrategy(PaymentStrategy strategy) {
        this.paymentStrategy = strategy;
    }
    
    public void checkout(int amount) {
        paymentStrategy.pay(amount);
    }
}

Техники рефакторинга

Extract Method

Извлеките код в отдельный метод для повышения читаемости.

// Раньше
public void processOrder(Order order) {
    // Валидация
    if (order == null) throw new IllegalArgumentException();
    if (order.getItems().isEmpty()) throw new IllegalArgumentException();
    
    // Расчет
    double total = 0;
    for (Item item : order.getItems()) {
        total += item.getPrice() * item.getQuantity();
    }
    
    // Сохранение
    order.setTotal(total);
    orderRepository.save(order);
}

// Теперь
public void processOrder(Order order) {
    validateOrder(order);
    double total = calculateTotal(order);
    saveOrder(order, total);
}

private void validateOrder(Order order) {
    if (order == null) throw new IllegalArgumentException();
    if (order.getItems().isEmpty()) throw new IllegalArgumentException();
}

private double calculateTotal(Order order) {
    double total = 0;
    for (Item item : order.getItems()) {
        total += item.getPrice() * item.getQuantity();
    }
    return total;
}

private void saveOrder(Order order, double total) {
    order.setTotal(total);
    orderRepository.save(order);
}

Replace Conditional with Polymorphism

Замените условия полиморфизмом.

// Раньше
public class Bird {
    public void fly(String type) {
        if (type.equals("Eagle")) {
            System.out.println("Eagle flies high");
        } else if (type.equals("Penguin")) {
            System.out.println("Penguin cannot fly");
        }
    }
}

// Теперь
public abstract class Bird {
    public abstract void fly();
}

public class Eagle extends Bird {
    public void fly() {
        System.out.println("Eagle flies high");
    }
}

public class Penguin extends Bird {
    public void fly() {
        System.out.println("Penguin cannot fly");
    }
}

Метрики качества кода

Cyclomatic Complexity

Измеряет сложность кода через количество точек принятия решений.

Maintainability Index

Оценивает, насколько легко поддерживать код.

Test Coverage

Процент кода, покрытого тестами.

Важность документации

Хорошая документация критична для удобства поддержки кода.

Types of Documentation

  • API Documentation: Описание интерфейсов
  • Architecture Documentation: Обзор архитектуры системы
  • Code Comments: Объяснение сложных участков кода
  • User Documentation: Руководства пользователя

Documentation Best Practices

  • Документируйте “почему”, а не только “что”
  • Поддерживайте документацию в актуальном состоянии
  • Используйте ясный и понятный язык
  • Применяйте диаграммы для визуализации

Практический пример: E-Commerce система

Вот практический пример, применяющий обсуждённые принципы:

// Интерфейсы для слабой связанности
public interface OrderService {
    Order createOrder(List<Item> items);
    void processPayment(Order order, PaymentStrategy strategy);
}

public interface InventoryService {
    boolean checkAvailability(Item item);
    void reserveItem(Item item);
}

// Реализация с использованием SOLID-принципов
@Service
public class OrderServiceImpl implements OrderService {
    private final InventoryService inventoryService;
    private final NotificationService notificationService;
    
    public OrderServiceImpl(InventoryService inventoryService, 
                           NotificationService notificationService) {
        this.inventoryService = inventoryService;
        this.notificationService = notificationService;
    }
    
    @Override
    public Order createOrder(List<Item> items) {
        validateItems(items);
        reserveItems(items);
        
        Order order = new Order(items);
        order.calculateTotal();
        
        return order;
    }
    
    @Override
    public void processPayment(Order order, PaymentStrategy strategy) {
        strategy.pay(order.getTotal());
        notificationService.sendOrderConfirmation(order);
    }
    
    private void validateItems(List<Item> items) {
        for (Item item : items) {
            if (!inventoryService.checkAvailability(item)) {
                throw new ItemNotAvailableException(item);
            }
        }
    }
    
    private void reserveItems(List<Item> items) {
        for (Item item : items) {
            inventoryService.reserveItem(item);
        }
    }
}

Вопросы и ответы для экзамена

1. Что такое SOLID-принципы и почему они важны?

Ответ: SOLID-принципы это пять принципов проектирования: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion. Они важны потому что способствуют написанию более поддерживаемого, расширяемого и тестируемого кода. Они помогают избежать технического долга и улучшить качество кода.

2. Объясните принцип единственной ответственности на примере.

Ответ: SRP гласит, что класс должен иметь одну ответственность. Пример: класс UserService должен отвечать только за управление пользователями, а не за отправку писем или составление отчётов. Это повышает поддерживаемость, поскольку изменения в одной ответственности не влияют на другую.

3. В чём отличие между связанностью и сплочённостью?

Ответ: Сплочённость описывает, насколько тесно элементы в модуле связаны между собой (высокая сплочённость хороша). Связанность описывает, насколько модули зависят друг от друга (низкая связанность хороша). Цель: высокая сплочённость и низкая связанность для лучшей поддерживаемости.

4. Когда используют паттерн Singleton?

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

5. Объясните принцип Open/Closed.

Ответ: OCP гласит, что компоненты должны быть открыты для расширения, но закрыты для модификации. Это означает, что новую функциональность можно добавлять через расширение (например, наследование), без изменения существующего кода.

Рекомендуемые книги по проектированию ПО

Это внешние партнёрские ссылки. Мы получаем выгоду от твоей покупки:

Langlebige Software-Architekturen

Langlebige Software-Architekturen

Clean Code

Clean Code

Design Patterns

Design Patterns

Назад к блогу
Share:

Похожие статьи