Понимание принципа единой ответственности с помощью этого реального фрагмента кода

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

class Dog {
    String bark() {
         return "Woof";
    }
}

Но мне очень сложно применить этот принцип в разработке корпоративных приложений.

У нас есть приложение, в котором есть «Элементы проекта», «Действия» и «Сотрудники». У сотрудника будет много действий, а у действия может быть один элемент проекта.

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

У одного из наших клиентов было требование, они хотели видеть список любимых элементов проекта при добавлении новых действий. Неважно, как сотрудник выбирает элемент проекта, это другой взгляд.

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

Так что моей обязанностью было реализовать это, и вот реализация, которая у меня есть:

public List<ProjectElement> getFavoriteProjectElementsSortedDescendingByTotalWorkHoursInLast60Days(Employee owner) {
    // All Favorite Project Elements of Owner
    final List<ProjectElement> favoriteProjectElementsOfOwner
            = (List<ProjectElement>) (List<?>) favoriteBusinessObjectHelper.getAllFavoriteObjectsForBusinessObjectType(BusinessObjectType.PROJECT_ELEMENT.toInt());

    // Activities by Favorite Project Elements
    final List<Activity> favoriteProjectElementActivitiesForLast60Days
            = activityFinder.findByInProjectElementsForLastGivenDates(owner, favoriteProjectElementsOfOwner, 60);

    // Create a Map with Favorite Project Element - Calculated Work
    final Map<ProjectElement, Long> projectElementTotalWorkMap = new HashMap<ProjectElement, Long>();
    for (Activity activity : favoriteProjectElementActivitiesForLast60Days) {
        final ProjectElement activityProjectElement = activity.getProjectElement();
        if (!projectElementTotalWorkMap.containsKey(activityProjectElement)) {
            projectElementTotalWorkMap.put(activityProjectElement, 0L);
        }
        final long calculatedWork = activity.getCalculatedWork();
        final long totalCalculatedWork = projectElementTotalWorkMap.get(activityProjectElement) + calculatedWork;
        projectElementTotalWorkMap.put(activityProjectElement, totalCalculatedWork);
    }

    // Sort the Map by value descending
    final Map<ProjectElement, Long> sortedProjectElementTotalWorkMap
            = InnboundSortTool.sortByValueDescending(projectElementTotalWorkMap);

    // We do not want to show owners Favorite Project Element in the sidebar, if the Project Element is not available
    // for Employee anymore.. See the comments at the end of the file.
    final Set<ProjectElement> allowedProjectElementsForOwner = owner.getExplicitlyAssignedOnlyActiveProjectElements();

    final ArrayList<ProjectElement> projectElementsSorted = new ArrayList<ProjectElement>();
    for (ProjectElement projectElement : sortedProjectElementTotalWorkMap.keySet()) {
        if (allowedProjectElementsForOwner.size() == 0) { // This means Employee does not have any restrictions, all Project Elements are available to him.
            projectElementsSorted.add(projectElement);
        } else { // If Employee has assigned Project Elements, we must check if the Favorite Project Element is assigned to him..
            if (allowedProjectElementsForOwner.contains(projectElement)) {
                projectElementsSorted.add(projectElement); // If yes, add it to list, if no simply continue the loop without adding.
            }
        }
        if (projectElementsSorted.size() == 20) {
            break; // 20 is an arbitrary value, we do not want to show too many Favorite Project Elements in the UI.. Limit by 20.
        }
    }

    return projectElementsSorted;
}

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

Могу ли я представить вспомогательный класс и делегировать все этому классу и начать звонить:

final Map<ProjectElement, Long> projectElementTotalWorkMap = helper.CreateprojectElementTotalWorkMap(); 

helper.removeUnassignedFavoriteProjectElementsFromEmployee();

и т.д? Но тогда я могу представить Помощника самому Помощнику? Где это вообще заканчивается? Когда я начинаю подобный рефакторинг, у меня появляются крайне бесполезные методы, которые просто вызывают другие методы, и этот класс будет выглядеть так:

List<ProjectElement> favoritesList;
favoritesList = helper.doThis();
favoritesList = helper.doThat();
favoritesList = helper.sort();
return favoritesList;

Я вообще не понимаю этого принципа? Думаю, нет, поэтому возникает вопрос, как мне исправить этот метод, чтобы он соответствовал «SRP»?


person Koray Tugay    schedule 08.06.2017    source источник


Ответы (1)


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

В качестве примера приведем первую часть вашей функции после некоторого рефакторинга:

public List<ProjectElement> getFavoriteProjectElementsSortedDescendingByTotalWorkHoursInLast60Days(Employee owner) {
    // All Favorite Project Elements of Owner
    final List<ProjectElement> favoriteProjectElementsOfOwner
            = (List<ProjectElement>) (List<?>) favoriteBusinessObjectHelper.getAllFavoriteObjectsForBusinessObjectType(BusinessObjectType.PROJECT_ELEMENT.toInt());

    // Activities by Favorite Project Elements
    final List<Activity> favoriteProjectElementActivitiesForLast60Days
            = activityFinder.findByInProjectElementsForLastGivenDates(owner, favoriteProjectElementsOfOwner, 60);

    // Create a Map with Favorite Project Element - Calculated Work
    final Map<ProjectElement, Long> projectElementTotalWorkMap = this.makeFacoriteProjectMap(favoriteProjectElementActivitiesForLast60Days)

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

private Map<ProjectElement, Long> projectElementTotalWorkMap makeFacoriteProjectMap(List<Activity> favoriteProjectElementActivitiesForLast60Days)
{
    Map<ProjectElement, Long> projectElementTotalWorkMap= new HashMap<ProjectElement, Long>();
    for (Activity activity : favoriteProjectElementActivitiesForLast60Days) {
        final ProjectElement activityProjectElement = activity.getProjectElement();
        if (!projectElementTotalWorkMap.containsKey(activityProjectElement)) {
            projectElementTotalWorkMap.put(activityProjectElement, 0L);
        }
        final long calculatedWork = activity.getCalculatedWork();
        final long totalCalculatedWork = projectElementTotalWorkMap.get(activityProjectElement) + calculatedWork;
        projectElementTotalWorkMap.put(activityProjectElement, totalCalculatedWork);
    }

    return projectElementTotalWorkMap;
}

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

Оркестровка теперь является единственной ответственностью основных функций.

Этот же рефакторинг может быть применен для создания вашего projectElementsSorted ArrayList, после чего функция getFavoriteProjectElementsSortedDescendingByTotalWorkHoursInLast60Days будет иметь 1 обязанность - организовать шаги, чтобы предоставить вам отсортированный список рабочих часов за последние 60 дней.

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

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

class Dog
{
    public void PerformRoutine()
    {
        this.bark();
        this.sit();
        this.walkInCircle();
        this.rollOver();
    }
    //...
}

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

В контексте корпоративных приложений у вас часто есть уровень службы / API, управляющий сохранением, загрузкой и вызовом объектов домена. Эти функции кажутся нарушающими СР, но все они по-прежнему являются частью одной и той же ответственности. Сохранение и загрузка - это ответственность ваших репозиториев. Фактическая логика - это ответственность ваших доменных объектов. Эти API / сервисы на самом деле просто делают одно - организуют и делегируют другим сервисам.

Я надеюсь это тебе поможет.

РЕДАКТИРОВАТЬ: Чтобы ответить на ваш вопрос о том, какова ценность всего этого:

Рассмотрим предыдущий пример собаки без SR:

class Dog
{
    public IEnumerator PerformRoutine()
    {
        this.audioController.PlaySound("c:/bar.mp3");
        this.animationControl.Play("Sit.anim");

        yield return new WaitForSeconds(2);

        this.animationController.Play("stand.anim");         

        yield return new WaitForSeconds(1);

        foreach(Vector3 pos in this.WalkPositions)
        {
            this.animationController.Play("walk.anim");
            this.position.lerpTo(pos, 5.0f); // move to position over 5 seconds.

        }

        this.animationController.Play("roll.anim");
    }
    //...
}

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

Если вы спросите нового члена команды (или даже если вы вернетесь к своему старому коду), в идеале вам захочется просмотреть его и понять, что он делает. Имя функции «PerformRoutine» не подготавливает читателя к анимации или логике движения.

Если вас вернули к этому коду из-за того, что собака ходит по квадрату, а не по кругу, будет сложнее настроить этот код.

Теперь примените те же рассуждения в корпоративном решении с сотнями тысяч строк кода и тысячами функций. Это становится настолько неуправляемым, что в конце концов хочется бросить курить.

Вот еще один пример интеграции REAL LIFE, которую я сделал для инвестиционной компании. Вот примерно сторонний код, который занял несколько дней в моем графике разработки:

public List<Event> getEventData()
{
    var toReturn = new List<Event>(this.eventData);
    this.eventData.Clear();
    return toReturn;
}

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

С минимальными усилиями это могло быть две функции - getEventDate() и clearEventData(). Я бы даже остановился на одной функции с другим именем - getAndClearEventData(), хотя последнее нарушает SR.

person Reasurria    schedule 09.06.2017
comment
Да, определенно полезно, спасибо. Но все же, просто переместить 5 строк кода в частный метод? Вероятно, я тот, кто не может понять концепцию (или преимущества), которая связана с этим .. - person Koray Tugay; 09.06.2017
comment
@KorayTugay Вы правы, что это кажется бессмысленным и иногда требует дополнительной работы. Однако сила этого для меня приходит, когда я получаю новых членов в моей команде, которым необходимо понимать код. Если им нужно понять общую картину, они просто смогут взглянуть на эти функции оркестровки. Только когда что-то пойдет не так, им нужно будет пойти глубже, и тогда, надеюсь, они смогут выяснить, где искать. Также очень пугает когнитивная перегрузка - смотреть на большую функцию. Хорошим чтением будет «Чистый код» Роберта Мартина. - person Reasurria; 09.06.2017