Dealing with the principles of SOLID some questions arose in understanding. Namely, is it ok to change the class constructor if there is a need to extend the class functionality? Often there is a problem with the fact that it is necessary to inject an additional class into the class in order to add functionality.

It seems to me that in this case the principle of openness / closeness is violated. The question is how to avoid violation of this principle with the following example.

There is an interface

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

And its implementation (I can’t change the interface, because anyone who connected my library to my project can implement this interface).

 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(); } } 

Looks pretty simple. CountableDefaultShopFactory implements ShopFactory , overrides two methods, and accepts two Counter objects in the constructor that will be used to count the number of times the method has been called. As a result, each method returns a result by calling a static method.

Now suppose you need to add functionality to this class and it will return another list of objects of type Coupon . Only he will take them not from the static method of the Coupon class, but because of the database for example. Suppose I have a DAO class that returns this data.

So my class takes the following form

 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(); } } 

As you can see, we had to modify the constructor, namely to add more couponsCounter parameters (which I think is normal) and couponDAO .

For good I think that the class CountableDefaultShopFactory should not know anything about the DAO layer and then the actual question arises as to how best to do this? And how would you do that? Perhaps there are ready-made patterns for such cases, I unfortunately did not find a similar one.

Thank you in advance.

  • you have an error in the code - the CountableDefaultShopFactory class either has no constructor, or you have confused the name with DefaultShopFactory - Mikhail Vaysman
  • Thank you, yes, this is a typo - dmitry182

1 answer 1

  1. OCP says that the entity should be closed to change, but open to extensions. By creating an interface you make an entity. If the essence of the interface does not change, then it is closed for changes. But you can extend the functionality in classes that implement this interface. Here is your example:

     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. There are no problems with the introduction of DAO. This is the right decision. The user object knows only the interface, and the object implementing the DAO interface is responsible for specific details.

  3. I would implement counters as a separate object. Counters are orthogonal functionality and can be implemented in several ways: Aspects (AspectJ) and the Decorator pattern (I prefer and show it)

     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());