the problem of excessive functionality

the problem of excessive functionality

Hello, Habre!

A few years ago, when I was still a novice, I faced a task that seemed simple at first glance, but turned out to be a real nightmare. My blind drive to add as many features as possible resulted in the interface becoming overly cluttered and difficult for the user.

In a small team, we were developing a reporting module that should provide the user with opportunities to analyze project data. We decided to integrate many functions: from basic statistics display to sophisticated predictive analysis tools. As a result, the user interface turned into a maze of buttons, menus and forms, not only that it is not just a maze, but a maze with a minotaur, because after some combination of buttons, users received an unknown error.

After some time, I realized that each function should be justified and necessary, and the interface should be intuitive and easy to use.

Principles of good software design

  1. Modularity

    • Modularity means dividing a large project into smaller, independently functioning parts, or modules. This makes the code easier to test, understand and maintain. Each module focuses on a specific task and has a clearly defined interface for interaction with other parts of the system.

  2. Low connectivity

    • Coherence refers to the degree to which modules depend on each other. Low coupling means that changes in one module have minimal impact on other modules. This increases the flexibility of the system and simplifies its maintenance, as changes or corrections can be made with less expenditure of time and resources.

    • For example, in object-oriented programming, classes should be designed so that they are as self-contained as possible and interact with other classes through well-defined interfaces.

  3. Abstraction

    • Abstraction includes selection of key, essential characteristics of the object and exclusion of secondary details. This allows you to focus on what makes a system or component unique, simplifying the design process and reducing complexity.

    • Abstraction is achieved through the creation of classes and interfaces that represent generalized concepts and behaviors rather than concrete implementations.

  4. Simplicity

    • Simplicity means designing systems in such a way that they are understandable, easy to use and easy to maintain. This includes avoiding unnecessarily complex code, using clear variable and function names, and clear program structure.

  5. Prediction of changes

    • This principle involves designing the system with future changes and expansions in mind. It involves creating a flexible architecture that can adapt to new requirements without major redesign.

Approaches to separation of functionality

Principles of SOLID is a set of five core principles that help create flexible, maintainable, and extensible code:

  1. The principle of single responsibility (Single responsibility principle, SRP): This principle states that a class should have only one reason for change. That is, he must perform only one function or duty. This allows functionality to be broken down into smaller, more manageable parts. If a class has multiple responsibilities, it becomes difficult to maintain and modify.

  2. Open/closed principle (Open/Closed principle, OCP): According to this principle, software entities (classes, modules, functions, etc.) should be open to extension but closed to modification. This means that new functionality can be added without changing existing code. This helps reduce the risk of errors when modifying existing code.

  3. Barbara Liskov’s substitution principle (Liskov substitution principle, LSP): This principle states that objects of subclasses should be able to replace objects of base classes without changing the desired properties of the program. This ensures that the subclasses are compatible with the code for the base classes.

  4. Interface segregation principle (ISP): According to this principle, clients should not depend on interfaces they do not use. Interfaces should be specific to customer needs. This avoids dependencies on deprecated methods and reduces the complexity of interfaces.

  5. Dependency inversion principle (DIP): This principle states that top-level modules must depend on lower-level modules, and both must depend on abstractions. This provides a more flexible architecture, allowing easy replacement of specific implementations and reducing coupling between components.

Let’s consider in more detail the principle of single responsibility (SRP)

The essence of SRP is that classes or modules should be coherent and focused on performing only one task. This makes the code more understandable, easier to maintain and modify, and less likely to cause errors.

For example, for an order accounting system, you might need an Order class to represent and process an order. SRP suggests the following: The Order class should be responsible only for presenting and manipulating order data. For example, it may contain information about the products, the delivery address and the cost of the order. Its main task is to manage order data.

Classes that adhere to SRP are easier to read and understand because their functionality is clear and easily defined. When each class has only one responsibility, it becomes less connected to other parts of the code. This simplifies testing and support.

If the requirements associated with one responsibility change, you can change only the corresponding class, minimizing the impact on other parts of the system.

A couple of examples of refactoring:

A class that performs several functions

Source code:

class Order:
    def __init__(self, products):
        self.products = products

    def calculate_total(self):
        total = 0
        for product in self.products:
            total += product.price
        return total

    def send_notification(self):
        # Отправка уведомления о заказе
        pass

This class has two responsibilities: calculating the total cost of the order and sending the message. Let’s divide these responsibilities into two separate classes.

Refactoring:

class Order:
    def __init__(self, products):
        self.products = products

    def calculate_total(self):
        total = 0
        for product in self.products:
            total += product.price
        return total

class OrderNotifier:
    def send_notification(self, order):
        # Отправка уведомления о заказе
        pass

Now we have two separate classes, each with its own single responsibility: the Order class calculates the total cost of the order, and the OrderNotifier class sends the notification.

A class that works with data and outputs the result

Source code:

class ReportGenerator:
    def __init__(self, data):
        self.data = data

    def generate_report(self):
        # Генерация отчета на основе данных
        pass

    def display_report(self):
        # Вывод отчета на экран
        pass

This class has two responsibilities: generating the report and displaying the report. Let’s divide these responsibilities into two separate classes.

Refactoring:

class ReportGenerator:
    def __init__(self, data):
        self.data = data

    def generate_report(self):
        # Генерация отчета на основе данных
        pass

class ReportPrinter:
    def __init__(self, report):
        self.report = report

    def display_report(self):
        # Вывод отчета на экран
        pass

Now we have two separate classes: ReportGenerator, which is responsible for generating the report, and ReportPrinter, which is responsible for printing the report to the screen.

Applying the Snapshot (Memento) pattern to implement state saving and restoring is a design pattern that allows you to save the current state of an object and restore it in the future without revealing the internal structure of the object. This pattern is particularly useful when you want to save and restore the state of an object without breaking encapsulation.

How does the Snapshot pattern work?

  1. Creating a Snapshot (Memento): A snapshot is an object that stores the current state of another object, but does not provide access to that state directly. Usually, a nested class or a separate object is used for shooting.

  2. Creating a Caretaker: A capturer is an object that is responsible for saving and saving snapshots of an object. It can create snapshots, store them in special storage (for example, the stack), and restore the state of the object from the snapshot.

  3. Creation of the original object (Originator): A native object is an object whose state needs to be saved and restored. It can request the puller to save its state or use the saved state to restore.

Example 1: Using the “Snapshot” pattern to save the state of a text editor in Python

# Снимок (Memento)
class EditorMemento:
    def __init__(self, content):
        self.content = content

# Оригинальный объект (Originator)
class TextEditor:
    def __init__(self):
        self.content = ""

    def create_memento(self):
        return EditorMemento(self.content)

    def restore_from_memento(self, memento):
        self.content = memento.content

# Сниматель (Caretaker)
class History:
    def __init__(self):
        self.snapshots = []

    def save(self, editor):
        self.snapshots.append(editor.create_memento())

    def undo(self, editor):
        if self.snapshots:
            memento = self.snapshots.pop()
            editor.restore_from_memento(memento)

# Пример использования
editor = TextEditor()
history = History()

editor.content = "Первый текст"
history.save(editor)

editor.content = "Второй текст"
history.save(editor)

print("Текущий текст:", editor.content)

history.undo(editor)
print("Отмена: Текущий текст после undo:", editor.content)

history.undo(editor)
print("Отмена: Текущий текст после второго undo:", editor.content)

Example 2: Using the Snapshot pattern to save game state in Python

# Снимок (Memento)
class GameStateMemento:
    def __init__(self, level, score):
        self.level = level
        self.score = score

# Оригинальный объект (Originator)
class Game:
    def __init__(self):
        self.level = 1
        self.score = 0

    def create_memento(self):
        return GameStateMemento(self.level, self.score)

    def restore_from_memento(self, memento):
        self.level = memento.level
        self.score = memento.score

    def play(self):
        self.level += 1
        self.score += 10

# Сниматель (Caretaker)
class GameHistory:
    def __init__(self):
        self.states = []

    def save(self, game):
        self.states.append(game.create_memento())

    def undo(self, game):
        if self.states:
            memento = self.states.pop()
            game.restore_from_memento(memento)

# Пример использования
game = Game()
history = GameHistory()

game.play()
history.save(game)

game.play()
history.save(game)

print("Текущий уровень:", game.level)
print("Текущий счет:", game.score)

history.undo(game)
print("Отмена: Текущий уровень после undo:", game.level)
print("Отмена: Текущий счет после undo:", game.score)

history.undo(game)
print("Отмена: Текущий уровень после второго undo:", game.level)
print("Отмена: Текущий счет после второго undo:", game.score)

In both examples, the “Snapshot” pattern allows you to save and restore the state of the object without revealing its internal structure. This helps manage the state of objects in the application and implement undo and redo functions.

Saving and restoring the state of the object

Saving the state of an object means saving its current data and settings so that the object can be restored in the same state in the future. This is often achieved by serializing (turning the object’s data into a sequence of bytes) and saving that sequence to a file, database, or other storage location.

In many programs, such as text editors or task management programs, the user can perform long operations. In this case, saving state allows you to save the user’s progress so that they can return to work with the program in the future and continue from where they left off.

If an application experiences an error or crash, saving state can prevent data loss. For example, a text editor can automatically save unsaved changes before crashing.

The process of saving and restoring state usually looks like this:

To save the object’s state, the object’s data is serialized, that is, converted into a sequence of bytes. Serialized data is stored in storage such as a file, database, or memory. To restore an object’s state, data is read from the store and deserialized to retrieve the object in its original state.

Python provides a module pickle for object serialization and deserialization. Let’s create an example in which a simple class will be created, its instance will be serialized to a file, and then restored:

import pickle

class Example:
    def __init__(self, data):
        self.data = data

# Создание объекта
example = Example("Сохраненные данные")

# Сериализация объекта в файл
with open('saved_state.pkl', 'wb') as file:
    pickle.dump(example, file)

# Десериализация объекта из файла
with open('saved_state.pkl', 'rb') as file:
    loaded_example = pickle.load(file)
    print(loaded_example.data)

In C# for serialization and deserialization you can use System.Runtime.Serialization. Let’s create a class that is serialized in XML format and saved to a file, and then restored from this file:

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Xml;

[DataContract]
public class Example
{
    [DataMember]
    public string Data { get; set; }

    public Example(string data)
    {
        Data = data;
    }
}

public class Program
{
    public static void Main()
    {
        var example = new Example("Сохраненные данные");

        // Сериализация в XML
        var serializer = new DataContractSerializer(typeof(Example));
        using (var stream = File.Create("saved_state.xml"))
        {
            serializer.WriteObject(stream, example);
        }

        // Десериализация из XML
        Example loadedExample;
        using (var stream = File.OpenRead("saved_state.xml"))
        {
            loadedExample = (Example)serializer.ReadObject(stream);
        }

        Console.WriteLine(loadedExample.Data);
    }
}

The memento pattern is also useful when you want to save and restore the state of an object without breaking encapsulation.

Photo is an object that stores the current state of another object, but does not provide access to that state directly. Usually, a nested class or a separate object is used for shooting. Stripper is an object that is responsible for saving and saving snapshots of the object. It can create snapshots, store them in special storage (for example, the stack), and restore the state of the object from the snapshot. Original object is an object whose state needs to be saved and restored. It can request the puller to save its state or use the saved state to restore.

Applying a pattern to save the state of a text editor in Python:

# Снимок (Memento)
class EditorMemento:
    def __init__(self, content):
        self.content = content

# Оригинальный объект (Originator)
class TextEditor:
    def __init__(self):
        self.content = ""

    def create_memento(self):
        return EditorMemento(self.content)

    def restore_from_memento(self, memento):
        self.content = memento.content

# Сниматель (Caretaker)
class History:
    def __init__(self):
        self.snapshots = []

    def save(self, editor):
        self.snapshots.append(editor.create_memento())

    def undo(self, editor):
        if self.snapshots:
            memento = self.snapshots.pop()
            editor.restore_from_memento(memento)

# Пример использования
editor = TextEditor()
history = History()

editor.content = "Первый текст"
history.save(editor)

editor.content = "Второй текст"
history.save(editor)

print("Текущий текст:", editor.content)

history.undo(editor)
print("Отмена: Текущий текст после undo:", editor.content)

history.undo(editor)
print("Отмена: Текущий текст после второго undo:", editor.content)

Excessive functionality is an inherent part of the process, but it can be managed and controlled by applying

And those who want to learn different models of interaction or data storage and understand how they can be applied in projects, I invite you to a free lesson where different types of storage will be considered, such as relational databases, NoSQL databases, file storage, and many more another Registration is available at the link.

Related posts