Пишем гибкий код, используя SOLID



    От переводчика: опубликовали для вас статью Северина Переса об использовании принципов SOLID в программировании. Информация из статьи будет полезна как новичкам, так и программистам с опытом.

    Если вы занимаетесь разработкой, то, скорее всего, слышали о принципах SOLID. Они дают возможность программисту писать чистый, хорошо структурированный и легко обслуживаемый код. Стоит отметить, что в программировании есть несколько подходов к тому, как правильно выполнять ту либо иную работу. У разных специалистов — разные идеи и понимание «правильного пути», все зависит от опыта каждого. Тем не менее, идеи, провозглашенные в SOLID, принимаются практически всеми представителями ИТ-сообщества. Они стали отправной точкой для появления и развития множества хороших методов управления разработкой.

    Давайте разберемся с тем, что такое принципы SOLID и как они помогают нам.

    Skillbox рекомендует: Практический курс «Мобильный разработчик PRO».

    Напоминаем: для всех читателей «Хабра» — скидка 10 000 рублей при записи на любой курс Skillbox по промокоду «Хабр».

    Что такое SOLID?


    Этот термин — аббревиатура, каждая буква термина — начало названия определенного принципа:
    • Single Responsibility Principle (принцип единственной ответственности). Модуль может иметь одну и только одну причину для изменения.
    • The Open/Closed Principle (принцип открытости/закрытости). Классы и другие элементы должны быть открыты для расширения, но закрыты для модификации.
    •  The Liskov Substitution Principle (принцип подстановки Лисков). Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.
    • The Interface Segregation Principle  (принцип разделения интерфейса). Программные сущности не должны зависеть от методов, которые они не используют.
    • The Dependency Inversion Principle (принцип инверсии зависимостей). Модули верхних уровней не должны зависеть от модулей нижних уровней.

    Принцип единственной ответственности


    Принцип единой ответственности (SRP) гласит, что каждый класс или модуль в программе должен нести ответственность только за одну часть функциональности этой программы. Кроме того, элементы этой ответственности должны быть закреплены за своим классом, а не распределены по несвязанным классам. Разработчик и главный евангелист SRP, Роберт С. Мартин, описывает ответственность как причину перемен. Изначально он предложил этот термин в качестве одного из элементов своей работы «Принципы объектно-ориентированного проектирования». В концепцию вошло многое из закономерности связности, которая была определена ранее Томом Демарко.

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

    Кстати, Мартин приводил интересный пример с высшими менеджерами компании (COO, CTO, CFO), каждый из которых применяет специфическое программное обеспечение для бизнеса с разной целью. В итоге любой из них может внедрять изменения в ПО, не затрагивая интересы других менеджеров.

    Божественный объект


    Как обычно, лучший способ изучить SRP — это увидеть все в действии. Давайте посмотрим на участок программы, которая НЕ соответствует принципу единой ответственности. Это Ruby-код, описывающий поведение и атрибуты космической станции.

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

    class SpaceStation
      def initialize
        @supplies = {}
        @fuel = 0
      end
     
      def run_sensors
        puts "----- Sensor Action -----"
        puts "Running sensors!"
      end
     
      def load_supplies(type, quantity)
        puts "----- Supply Action -----"
        puts "Loading #{quantity} units of #{type} in the supply hold."
        
        if @supplies[type]
          @supplies[type] += quantity
        else
          @supplies[type] = quantity
        end
      end
     
      def use_supplies(type, quantity)
        puts "----- Supply Action -----"
        if @supplies[type] != nil && @supplies[type] > quantity
          puts "Using #{quantity} of #{type} from the supply hold."
          @supplies[type] -= quantity
        else
          puts "Supply Error: Insufficient #{type} in the supply hold."
        end
      end
     
      def report_supplies
        puts "----- Supply Report -----"
        if @supplies.keys.length > 0
          @supplies.each do |type, quantity|
            puts "#{type} avalilable: #{quantity} units"
          end
        else
          puts "Supply hold is empty."
        end
      end
     
      def load_fuel(quantity)
        puts "----- Fuel Action -----"
        puts "Loading #{quantity} units of fuel in the tank."
        @fuel += quantity
      end
     
      def report_fuel
        puts "----- Fuel Report -----"
        puts "#{@fuel} units of fuel available."
      end
     
      def activate_thrusters
        puts "----- Thruster Action -----"
        if @fuel >= 10
          puts "Thrusting action successful."
          @fuel -= 10
        else
          puts "Thruster Error: Insufficient fuel available."
        end
      end
    end

    Собственно, наша космическая станция нефункциональна (думаю, что не получу звонок от НАСА в ближайшем обозримом будущем), но здесь есть что проанализироать.

    Так, у класса SpaceStation есть несколько различных ответственностей (или задач). Все они могут быть разбиты по типам:
    • сенсоры;
    • снабжение (расходники);
    • горючее;
    • ускорители.

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

    Можем ли мы сказать, что эта программа не соответствует SRP? Да, конечно. Но класс SpaceStation является типичным «божественным объектом», который знает обо всем и делает все. Это основной антишаблон в объектно-ориентированном программировании. Для новичка такие объекты чрезвычайно сложны в обслуживании. Пока что программа очень простая, да, но представьте, что произойдет, если мы добавим новые функции. Возможно, нашей космической станции понадобится медпункт или переговорная комната. И чем больше будет функций, тем сильнее вырастет SpaceStation. Ну а поскольку этот объект будет связан с другими, то обслуживание всего комплекса станет еще более сложным. В итоге мы можем нарушить работу, к примеру, ускорителей. Если научный сотрудник запросит изменений в работе с сенсорами, то это вполне может повлиять на системы связи станции.

    Нарушение SRP-принципа может дать кратковременную тактическую победу, но в итоге мы «проиграем войну», обслуживать такого монстра в будущем станет весьма непросто. Лучше всего разделить программу на отдельные участки кода, каждый из которых отвечает за выполнение определенной операции. Понимая это, давайте изменим класс SpaceStation.

    Распределим ответственность

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

    class SpaceStation
      attr_reader :sensors, :supply_hold, :fuel_tank, :thrusters
     
      def initialize
        @supply_hold = SupplyHold.new
        @sensors = Sensors.new
        @fuel_tank = FuelTank.new
        @thrusters = Thrusters.new(@fuel_tank)
      end
    end
     
    class Sensors
      def run_sensors
        puts "----- Sensor Action -----"
        puts "Running sensors!"
      end
    end
     
    class SupplyHold
      attr_accessor :supplies
     
      def initialize
        @supplies = {}
      end
     
      def load_supplies(type, quantity)
        puts "----- Supply Action -----"
        puts "Loading #{quantity} units of #{type} in the supply hold."
        
        if @supplies[type]
          @supplies[type] += quantity
        else
          @supplies[type] = quantity
        end
      end
     
      def use_supplies(type, quantity)
        puts "----- Supply Action -----"
        if @supplies[type] != nil && @supplies[type] > quantity
          puts "Using #{quantity} of #{type} from the supply hold."
          @supplies[type] -= quantity
        else
          puts "Supply Error: Insufficient #{type} in the supply hold."
        end
      end
     
      def report_supplies
        puts "----- Supply Report -----"
        if @supplies.keys.length > 0
          @supplies.each do |type, quantity|
            puts "#{type} avalilable: #{quantity} units"
          end
        else
          puts "Supply hold is empty."
        end
      end
    end
     
    class FuelTank
      attr_accessor :fuel
     
      def initialize
        @fuel = 0
      end
     
      def get_fuel_levels
        @fuel
      end
     
      def load_fuel(quantity)
        puts "----- Fuel Action -----"
        puts "Loading #{quantity} units of fuel in the tank."
        @fuel += quantity
      end
     
      def use_fuel(quantity)
        puts "----- Fuel Action -----"
        puts "Using #{quantity} units of fuel from the tank."
        @fuel -= quantity
      end
     
      def report_fuel
        puts "----- Fuel Report -----"
        puts "#{@fuel} units of fuel available."
      end
    end
     
    class Thrusters
      def initialize(fuel_tank)
        @linked_fuel_tank = fuel_tank
      end
     
      def activate_thrusters
        puts "----- Thruster Action -----"
        if @linked_fuel_tank.get_fuel_levels >= 10
          puts "Thrusting action successful."
          @linked_fuel_tank.use_fuel(10)
        else
          puts "Thruster Error: Insufficient fuel available."
        end
      end
    end

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

    Для любой из переменных теперь есть соответствующий класс: Sensors; SupplyHold; FuelTank; Thrusters.

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

    Наши офицеры, работающие на космической станции, вероятно, рады изменениям, поскольку могут запрашивать те, которые необходимы именно им. Обратите внимание, что в коде есть такие методы, как report_supplies и report_fuel, содержащиеся в классах SupplyHold и FuelTank. Что случится, если Земля попросит изменить способ формирования отчетов? Необходимо будет изменить оба класса, SupplyHold и FuelTank. А что, если нужно будет изменить способ доставки топлива и расходников? Вероятно, придется снова изменить все те же классы. А это уже нарушение SRP-принципа. Давайте это исправим.

    class SpaceStation
      attr_reader :sensors, :supply_hold, :supply_reporter,
                  :fuel_tank, :fuel_reporter, :thrusters
     
      def initialize
        @sensors = Sensors.new
        @supply_hold = SupplyHold.new
        @supply_reporter = SupplyReporter.new(@supply_hold)
        @fuel_tank = FuelTank.new
        @fuel_reporter = FuelReporter.new(@fuel_tank)
        @thrusters = Thrusters.new(@fuel_tank)
      end
    end
     
    class Sensors
      def run_sensors
        puts "----- Sensor Action -----"
        puts "Running sensors!"
      end
    end
     
    class SupplyHold
      attr_accessor :supplies
      attr_reader :reporter
     
      def initialize
        @supplies = {}
      end
     
      def get_supplies
        @supplies
      end
     
      def load_supplies(type, quantity)
        puts "----- Supply Action -----"
        puts "Loading #{quantity} units of #{type} in the supply hold."
        
        if @supplies[type]
          @supplies[type] += quantity
        else
          @supplies[type] = quantity
        end
      end
     
      def use_supplies(type, quantity)
        puts "----- Supply Action -----"
        if @supplies[type] != nil && @supplies[type] > quantity
          puts "Using #{quantity} of #{type} from the supply hold."
          @supplies[type] -= quantity
        else
          puts "Supply Error: Insufficient #{type} in the supply hold."
        end
      end
    end
     
    class FuelTank
      attr_accessor :fuel
      attr_reader :reporter
     
      def initialize
        @fuel = 0
      end
     
      def get_fuel_levels
        @fuel
      end
     
      def load_fuel(quantity)
        puts "----- Fuel Action -----"
        puts "Loading #{quantity} units of fuel in the tank."
        @fuel += quantity
      end
     
      def use_fuel(quantity)
        puts "----- Fuel Action -----"
        puts "Using #{quantity} units of fuel from the tank."
        @fuel -= quantity
      end
    end
     
    class Thrusters
      FUEL_PER_THRUST = 10
     
      def initialize(fuel_tank)
        @linked_fuel_tank = fuel_tank
      end
     
      def activate_thrusters
        puts "----- Thruster Action -----"
        
        if @linked_fuel_tank.get_fuel_levels >= FUEL_PER_THRUST
          puts "Thrusting action successful."
          @linked_fuel_tank.use_fuel(FUEL_PER_THRUST)
        else
          puts "Thruster Error: Insufficient fuel available."
        end
      end
    end
     
    class Reporter
      def initialize(item, type)
        @linked_item = item
        @type = type
      end
     
      def report
        puts "----- #{@type.capitalize} Report -----"
      end
    end
     
    class FuelReporter < Reporter
      def initialize(item)
        super(item, "fuel")
      end
     
      def report
        super
        puts "#{@linked_item.get_fuel_levels} units of fuel available."
      end
    end
     
    class SupplyReporter < Reporter
      def initialize(item)
        super(item, "supply")
      end
     
      def report
        super
        if @linked_item.get_supplies.keys.length > 0
          @linked_item.get_supplies.each do |type, quantity|
            puts "#{type} avalilable: #{quantity} units"
          end
        else
          puts "Supply hold is empty."
        end
      end
    end
     
    iss = SpaceStation.new
     
    iss.sensors.run_sensors
      # ----- Sensor Action -----
      # Running sensors!
     
    iss.supply_hold.use_supplies("parts", 2)
      # ----- Supply Action -----
      # Supply Error: Insufficient parts in the supply hold.
    iss.supply_hold.load_supplies("parts", 10)
      # ----- Supply Action -----
      # Loading 10 units of parts in the supply hold.
    iss.supply_hold.use_supplies("parts", 2)
      # ----- Supply Action -----
      # Using 2 of parts from the supply hold.
    iss.supply_reporter.report
      # ----- Supply Report -----
      # parts avalilable: 8 units
     
    iss.thrusters.activate_thrusters
      # ----- Thruster Action -----
      # Thruster Error: Insufficient fuel available.
    iss.fuel_tank.load_fuel(100)
      # ----- Fuel Action -----
      # Loading 100 units of fuel in the tank.
    iss.thrusters.activate_thrusters
      # ----- Thruster Action -----
      # Thrusting action successful.
      # ----- Fuel Action -----
      # Using 10 units of fuel from the tank.
    iss.fuel_reporter.report
      # ----- Fuel Report -----
    # 90 units of fuel available.

    В этой, последней версии программы обязанности были разбиты на два новых класса, FuelReporter и SupplyReporter. Они оба являются дочерними по отношению к классу Reporter. Кроме того, мы добавили экземплярные переменные к классу SpaceStation с тем, чтобы при необходимости инициализировать нужный подкласс. Теперь, если Земля решит поменять еще что-то, то мы внесем правки в подклассы, а не в основной класс.

    Конечно, некоторые классы у нас до сих пор зависят друг от друга. Так, объект SupplyReporter зависит от SupplyHold, а FuelReporter зависит от FuelTank. Само собой, ускорители должны быть связаны с топливным баком. Но здесь уже все выглядит логичным, а внесение изменений не будет особенно сложным — редактирование кода одного объекта не слишком повлияет на другой.

    Таким образом, мы создали модульный код, где обязанности каждого из объектов/классов точно определены. Работать с таким кодом — не проблема, его обслуживание будет простой задачей. Весь «божественный объект» мы преобразовали в SRP.

    Skillbox рекомендует:

    Skillbox
    Онлайн-университет профессий будущего

    Комментарии 20

      +2
      del
        +6
        Дело осталось за малым: объяснить, почему раздутие кода в 2 с лишним раза — это хорошо.
        Обычно этот момент очень слабо отражается в статьях по SOLID-у.
          0
          SOLID, как и паттерны — это рекомендация, а не жесткие требования. Это как совет общего назначения. Использовать вам что то оттдуда или нет — решать только вам. При написании кода всё ещё нужно думать головой. Не раздувайте код — и он не будет раздуваться.
            +1
            Слабо отражается наверное потому, что это не важно. Даже не задумывался никогда об этом.
            Предполагаю, вы видите проблему не в количестве «букв» на экране и диске, а в усложнении кода для поминая. Так вот, мне не кажется, что размер кода вообще связан с простотой понимания, по крайней мере линейно. Например, когда Sensors/FuelTank/etc. представлены отдельными классами — код проще и быстрее прочесть/понять вне зависимости от количества доп. кода и даже доп. сущностей. Разделение и упорядочивание, насколько мне известно — замечательный трюк для представления мозгу человека сложных вещей простыми.

            Я бы упрощенно выразил смысл SOLID в контексте размера кода так:
            Сложность (чтения/понимая/правки) спагетти-кода увеличивается экспоненциально, тогда как количество «букв» растет линейно. Сложность SOLID-based кода увеличивается линейно, хотя количество «букв» растет экспоненциально.

            И выбор как бы очевиден. Когда не предвидится раздутия функциональности компонента — нам пофиг на экспоненциальный рост сложности. Можно не плодить дополнительных сущность.
              +1
              Потому-что пишете код вы один раз, а читаете/редактируете/оптимизируете десятки и сотни раз. И уменьшая связанность кода, вы уменьшаете объём информации которую нужно будет перечитать и учесть при последующих модификациях.
              Меньше вероятность сломать всё приложение при редактировании отдельного участка, меньше зависимостей нужно в итоге держать в голове.
              Чтоб поменять топливный бак, например, вам нужно будет работать только с классом FuelTank а не искать где и как используется топливо по всей космической станции.
              Вроде как этому моменту уделяется довольно много времени в обсуждениях SOLID.
              +1

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


              Зашёл посмотреть на цены, узнал ожидаемую зарплату, что первые пол года бесплатно и что кредит без справок дают.


              Некрасиво поступаете.

                +5
                DIP (принцип инверии зависимостей) описан так, что больше путает, чем объясняет что это и зачем надо.
                Модули верхних уровней не должны зависеть от модулей нижних уровней

                Это в корне неверно. Модуль верхнего уровня всегда зависит от модуля нижнего уровня, который он использует.
                Пример: ваше приложение использует Яндекс.Карты. Теперь ваше приложение зависит от этой библиотеки и её реализации. Яндек.Карты же напротив, понятия не имеют что вы их используете и никак не зависит от кода вашего приложения.

                Теперь о чём действительно говорит DIP:
                • Код, реализующий высокоуровневую политику, не должен зависеть от кода, реализующего низкоуровневые детали

                Звучит похоже, но смысл совершенно другой.
                На примере тех же карт, если вы хотите сделать свой код гибким, то вы должны выделить абстракцию MapImageInterface, содержащей допустим метод drawMap. Тогда реализация на яндекс картах, допустим YaMapImage будет имплементить этот интерфейс (это низкоуровневые детали). YaMapImage зависит от MapImageInterface, но не наоборот. Ваше приложение работает только используя MapImageInterface, не зная конкретных деталей (инстанцировать объект может например абстрактная фабрика). При этом код будет гибким и не будет проблем завтра добавить новую реализацию, например GoogleMapImage.

                И раз уж вы описываете исключительно SRP, то и ограничьтесь его упоминанием в заголовке без кликбейта. SOLID не раскрыт.
                  0
                  Вообще, принцип инверсии зависимостей звучит как то так
                  Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
                  Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

                    0
                    Ок, если претендуем на точность, то нужна ссылка на источник. Могу назвать свой — Clean Architecture, Robert C.Martin, Chapter III — Design Principles:
                    The code that implements high-level policy should not depend on the code that
                    implements low-level details. Rather, details should depend on policies

                    Собственно Роберт Мартин это первоисточник в этом вопросе.
                    А если говорить о сути, то если бы в статье была часть про
                    Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций

                    То я бы не писал развёрнуто смысл, который был потерян.
                    Дело в смысле, а не в формулировке.
                      0
                      Никто не претендует на точность, я только хотел показать, что вот это
                      Модуль верхнего уровня всегда зависит от модуля нижнего уровня, который он использует.
                      не совсем верно, так как они оба должны зависеть от абстракций. По крайней мере я так понимаю принцип.
                        0
                        Мы оба понимаем принцип верно, просто говорим о разном.
                        Слово «модуль» оставляет большое пространство для фантазии и неправильного понимания, т.к. статья никак не раскрывает этот принцип.
                        Я хотел подчеркнуть, что использующий всегда зависит от используемого. На этом принципе строится граф зависимостей, позволяющий понять что вообще происходит с архитектурой в проекте.
                        В моём примере, проект, использующий карты, всегда будет от них зависеть.
                        Вопрос только в том, в каком виде — будет он зависеть от реализации, или от абстракции
                        И да, вы правы, в конце концов и проект и реализация будут завязаны на абстракцию.
                        В этом и инверсия, что вместо старого графа
                        • проект -> реализация карт

                        Появляется инверсия от реализации к абстракции:
                        • проект ->абстракция карт <-реализация карт
                          0
                          Ну, раз мы понимаем принцип одинаково (мое понимание), то вопрос закрыт. А в статье да, тема SOLID не раскрыта.
                  0
                  iss.supply_hold.use_supplies(«parts», 2)
                  iss.supply_hold.load_supplies(«parts», 10)


                  У описанного подхода, наряду с преимуществами, есть огромный такой подводный камень. Если внешние (по отношению к классу) сущности знают про ее составные части (supply_hold, thrusters, etc.), то практически невозможно предотвратить их повсеместное использование. И это через какое-то время приводит к тому, что код опять становится плохо тестируемым (= трудноподдерживаемым).

                  Утрированный пример: пусть наш класс SpaceStation описывает космический корабль, а у нас стоит задача подсчитать суточную потребность межгалактического космопорта в топливе. Подход «в лоб» —
                  * пройтись в цикле по всем терминалам космопорта,
                  * собрать все корабли, находящиеся в терминале,
                  * добавить в коллекцию все корабли, которые планируют прилететь в ближайшие сутки,
                  * отфильтровать только те, которые планируют вылететь в течение суток,
                  * просуммировать вместительность всех их баков и вычесть топливо, в этих баках находящееся.

                  Так вот, если в этом примере код будет спускаться на уровень FuelTank-а каждого корабля, то создать моки для того, чтобы протестировать описанную логику, — будет очень, очень, очень громоздкой задачей. Как результат, на написание тестов часто будут «забивать» по причине горящих сроков, и в конце концов получится забагованный спагетти-код.

                  Нет, я не спорю, разделять сущности в соответствии с SRP — нужно. Но одновременно с этим вложенные сущности придется прятать в приватных полях, наружу выставляя более высокоуровневые методы (условно, GetFuelTanksCapacity(), GetFuelResidue()...). Это, в свою очередь, приведет к тому, что класс SpaceStation снова превратится в god object с множеством методов с совершенно различными обязанностями. Да, большинство этих методов будет тонкими обертками вокруг вложенных сущностей (a.k.a. паттерн фасад), да, этот класс будет сравнительно легко использовать в модульных тестах, но выглядеть он все равно будет страшно. Мало того, со временем разные потребители класса будут стремиться расширить класс еще более.

                  Отчасти это можно компенсировать при помощи принципа разделения итерфейсов: класс SpaceStation может реализовывать множество интерфейсов, в каждом из которых будет 1-2-5… методов, и внешний код будет работать с «толстой» сущностью только по «тонкому» интерфейсу, даже не зная, что работает с таким сложным божественным объектом. Но кода, увы, будет уже не в 2 раза больше, а раз этак в 6…

                  Увы, но у сложных проблем редко бывают простые решения, почти всегда что-то приносится в жертву.
                    0
                    Я думаю, что вот есть SOLID, есть паттерны, есть рекомендации, написано много книжек про всё это, и все эти вещи — они взаимосвязаны. То есть нет смысла искать какие то подводные камни в одном принципе. Есть же и рекомендации про сокрытие реалиации класса, и про DDD и прочее. К тому же, следование принципам не отменяет необходимости думать головой.
                      0
                      GetFuelTanksCapacity() это совершенно то же самое что и .FuelTanks.Capacity — потребитель знает, что у корабля есть топливный бак, у топливного бака есть вместимость.

                      Можно от этого абстрагироваться сделав на уровне корабля GetFuelDemand(), возможно, на уровне терминала тоже. Это позволит например, алгоритму расчета ничего не знать про корабли а на уровне космической станции добавлять свои потребности в топливе — а не только.

                      От god object можно избавляться по разному, см. например конйепцию, bounded context в ddd
                        0

                        То же самое, но не совершенно то же самое. Как минимум, абстрагируемся от способа получения вместимости. Скажем, если появятся корабли с опциональными разгонными модулями, то именно изменения внесём в код станции, но не в код её клиентов.

                          0
                          Как только вы внесете такое изменение, ваши имена перестанут соответствовать ментальной модели.

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

                          Если не придерживаться этого правила, то по идентификатору нельзя будет понять, что собственно код призван делать и команде надо будет держать в голове набор правил типа «Мы не считаем топливный бак разгонного модуля частью топливных баков станции КРОМЕ, случая когда нам надо рассчитать их общую емкость» в этом люди будут путаться.
                            0
                            Разные контексты могут предполагать разные понимания одного термина в ментальной модели. Если контекст станции не интересует разделение вместимости баков на основные баки корабля и баки разгонного модуля, то и не надо их как-то делить или выделять, просто «вместимость баков». А вот в контексте корабля это деление может быть критически важным и там делим.
                              0
                              Не относится ли это к разнице между station.GetFuelTanksCapacity() и spaceship.GetFuelTanksCapacity() а не к разнице между station.FuelTanks.Capacity и station.GetFuelTanksCapacity()?
                        0

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

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

                      Самое читаемое