Принцип open/closed при работе с контруктором

474
02 января 2017, 22:14

Разбираясь с принципами SOLID возникли некоторые вопросы в понимании. А именно, нормально ли изменяеть конструктор класса, если появилась необходимость расширить функциональность класса? Часто возникает проблема с тем, что надо заинжектить в класс дополнительный класс, чтобы добавить функционал.

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

Есть интерфейс

public interface ShopFactory {
    List<Discount> getDiscounts();
    List<Sale> getSales();
}

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

public class CountableDefaultShopFactory implements ShopFactory {
    Counter discountsCounter;
    Counter salesCounter;
    public CountableDefaultShopFactory(Counter discountsCounter, Counter salesCounter) {
        this.discountsCounter = discountsCounter;
        this.salesCounter = salesCounter;
    }
    @Override
    List<Discount> getDiscounts() {
        discountsCounter.count();
        return Discount.defaultDiscounts();
    }
    @Override
    List<Sale> getSales() {
        salesCounter.count();
        return Sale.defaultSales();
    }
}

Выглядит довольно просто. CountableDefaultShopFactory реализует ShopFactory, переопределяет два метода и принимает в конструкторе два объекта типа Counter, которые будут использоваться для подсчета кол-ва раз вызванного метода. В результате каждый метод возвращает результат вызовом статического метода.

Теперь предположим, что необходимо добавить функционала в этот класс и он будет возвращать еще список объектов типа Coupon. Только брать он их уже будет не из статического метода у класса Coupon, а из-за базы данных например. Предположим, что у меня есть DAO класс, который возвращает эти данные.

Таким образом, мой класс принимает следующий вид

public class CountableDefaultShopFactory implements ShopFactory {
    Counter discountsCounter;
    Counter salesCounter;
    Counter couponsCounter;
    CouponDAO couponDAO;
    public DefaultShopFactory(Counter discountsCounter, Counter salesCounter, Counter couponsCounter, CouponDAO couponDAO) {
        this.discountsCounter = discountsCounter;
        this.salesCounter = salesCounter;
        this.couponsCounter = couponsCounter;
        this.couponDAO = couponDAO;
    }
    @Override
    List<Discount> getDiscounts() {
        discountsCounter.count();
        return Discount.defaultDiscounts();
    }
    @Override
    List<Sale> getSales() {
        salesCounter.count();
        return Sale.defaultSales();
    }
    @Override
    List<Coupon> getCoupons() {
        couponsCounter.count();
        return couponDAO.getDefaultCoupons();
    }
}

Как видно, то пришлось модифицировать конструктор, а именно добавить еще параметры couponsCounter (что я считаю нормально) и couponDAO.

По хорошему я считаю, что класс CountableDefaultShopFactory не должен знать ничего о DAO слое и тут собственно возникает вопрос в том, как лучше это сделать? И как бы вы это сделали? Возможно есть готовые паттерны для таких случаев, я к сожалению не нашел похожего.

Заранее спасибо.

Answer 1
  1. OCP говорит, что сущность должна быть закрыта от изменений, но открыта для расширений. Создав интерфейс вы делаете сущность. Если суть интерфейса не меняется, то значит он закрыт для изменений. Но вы можете расширять функциональность в классах, которые реализуют этот интерфейс. Вот ваш пример:

    interface ShopFactory {
        // описание я опустил
    }
    public class CountableDefaultShopFactory implements ShopFactory {
        // полностью реализует интерфейс (closed), но добавляет счетчики (open)
        // OCP для ShopFactory выполняется
        // CountableDefaultShopFactory использует статические методы других классов 
        // и классы поменять нельзя! OCP не выполняется и SRP нарушается.
    }
    public class CouponDefaultShopFactory implements ShopFactory {
        // полностью реализует интерфейс (closed), но добавляет счетчики и купоны (open)
        // OCP для ShopFactory выполняется
        // купоны реализуются другим объектом и он внедряется в конструкторе - отличное 
        // решение. OCP и SRP - соблюдены.
        // Нужны другие купоны? Делаете новый объект и внедряете его. Оригинальный класс 
        // менять не надо (closed), функциональность расширить можно (open)
        // А вот с Discount и Sale такая же проблема как и в CountableDefaultShopFactory
    }
    
  2. С внедрением DAO нет никаких проблем. Это правильное решение. Объект пользователь знает только интерфейс, а объект реализующий интерфейс DAO отвечает за конкретные детали.

  3. Счетчики я бы реализовал отдельным объектом. Счетчики это ортогональная функциональность и ее можно реализовать несколькими способами: Аспекты (AspectJ) и паттерн Декоратор (я предпочитаю и покажу его)

    interface Sales {
        List<Sale> defaultSales();
    }
    interface Countable {
        int getCounter();
    }
    class Sale implements Sales {
        List<Sale> defaultSales() { /* магия */ }
    }
    class CountableSale implements Sales, Countable {
        private Sales origin;
        private int counter;
        public CountableSale(Sales origin) {
            this.origin = origin;
            counter = 0;
        }
        @Override
        public List<Sale> defaultSales() {
            counter++;
            return origin.defaultSales();
        }
        @Override
        public int getCounter() { return counter; }
    }
    // вариант использования
    Sales sale = new CountableSale(
                     new Sale()
                 );
    ShopShopFactory shopFactory = new SomeShopShopFactory(sales);
    // магия 
    System.out.println(((Countable)sale).getCounter());
    
READ ALSO
Рефактор метода onCreateView

Рефактор метода onCreateView

Есть фрагмент, в котором метод onCreateVew:

457
Помощь в написании запроса Facebook Graph API

Помощь в написании запроса Facebook Graph API

Пытаюсь разобраться в запросах Graph api FacebookНашел запрос (в одной из тем):

497
(android)Как внести изменения в библиотеку (ресурсы) которая подключена к проекту?

(android)Как внести изменения в библиотеку (ресурсы) которая подключена к проекту?

Такой вопрос,в сети нашел библиотеку,но данная библиотека не поддерживает рускязык (нет перевода файла string для ру региона) я залез в корень...

405
Android Выбор Activity при старте

Android Выбор Activity при старте

Есть MainActivity и при его старте вызывается AlertDialog в котором спрашивается какую активность запустить первую или вторуюПри выборе запускается...

499