Go to content
The Codest
  • About Us
  • Services
  • Our Team
  • Case studies
    • Blog
    • Meetups
    • Webinars
    • Resources
Careers Get in touch
  • About Us
  • Services
  • Our Team
  • Case studies
    • Blog
    • Meetups
    • Webinars
    • Resources
Careers Get in touch
2019-07-02
Software Development

Open-closed principle. Do I ever have to use it?

Mateusz Lesniak

Open-closed principle. Do I ever have to use it? - Image

Most developers have heard about open - closed principle - one of Uncle Bob’s SOLID principles. It sounds reasonable, but it can still be a little bit blurry until the first usage on ‘live’ code. Full state of principle is: software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

So what does it really mean?

We came across a development problem that has shown us what an open-closed principle really is about. In one of our web applications we had a form with two sections (among others):

  • demand channels
  • dynamic filters

Users can add as many filters as they wish, but there are some rules - filter availability depends on chosen channels.

Demand channels: AD_EXCHANGE, HEADER_BIDDING, RESERVATION, OTHER Dynamic filters(dimensions): website, ad_unit, geo, creative_size, device

This article is mostly about code refactor, so there will be a lot of code snippets below. I tried to reduce it, but some amount of code is necessary to show code refactoring. You don't need to understand every small part of code to get the main idea.

First implementation of the problem was simple:

class ResearchFormStateUpdater {
  update () {
    (...)
    this._updateDynamicFilters();
  }

  _updateDynamicFilters () {
    $('.dynamic-filter').each((_, filter) => {
      $(filter).trigger('dynamicFilter:disableWebsites', this._shouldDisableWebsitesFields());
    });
  }

  _shouldDisableWebsitesFields () {
    return this._shouldDisableFields(ResearchFormStateUpdater.WEBSITE_DISABLING_DEMAND_CHANNELS);
  }

  _shouldDisableFields (disablingDemandChannels) {
    // is any of disablingDemandChannels checked ?
  }
}

ResearchFormStateUpdater.WEBSITE_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other'];

class ResearchDynamicFilter {
  _setDynamicFilterDisableWebsitesEvent () {
    $(this._getBody()).on('dynamicFilter:disableWebsites', (event, shouldDisableWebsites) => {
      // disable website filters
    });
  }
}

As you can see, the website filter is supposed to be unavailable for HEADER_BIDDING, RESERVATION and OTHER channels, so it is available only for AD_EXCHANGE channel.

The last thing you can say about code is that it is permanent or static. So we have more requests from our client making these classes bigger and more complex.

Feature development

  • Add another channel - EBDA (Website filter should be unavailable while EBDA is chosen):

    • expand DISABLING_DEMAND_CHANNELS by EBDA demand channel

    • a lot of name changing - in the first implementation, we specified website in methods and constants names. For example:

      • isWebsitesDimensionDisabled to _areFormStateDimensionsDisabled
      • WEBSITE_DISABLING_DEMAND_CHANNELS to DISABLING_DEMAND_CHANNELS

Spoiler alert -> when a component is open for changes, there will be a lot of name changing in the future. We won't pay attention to this in the next steps.

  • Add another filter for 'Product' (Product filter availability scheme is same as Website)

    • ResearchDynamicFilter class has to check for one more dimension while disabling/enabling fields
  • Let’s go bigger and add some switcher above channels -> ‘Source’. All demand channels we were having until now are in Ad Manager source. New source - SSP - has no demand channels and the only available filter is website.

    • Rules:

      • There are two states of source: Ad Manager, SSP.
      • All of our demand channels are available only for Ad Manager source.
      • There are no demand channels for SSP source
      • ‘Website’ is the only filter available for SSP source.
    • Implementation:

      • When 'SSP' is chosen:

        • Disable demand channels.
        • trigger 'dynamicFilter:disableWebsitesAndProducts' <- enable both
        • trigger 'dynamicFilter:disableNonSspOptions'
      • When Ad Manager checked:

        • trigger 'dynamicFilter:disableWebsitesAndProducts' <- check weather enabled or disabled
  • Add another filter for 'Platform'

    • Rules:

      • Platform is available only when the source is SSP
    • Difficulty:

      • Now we have 'Website', which is available for AD_EXCHANGE channel from Ad Manager and for Ssp and we have 'Platform' which is available for Ssp but not for Ad Manager
      • Toggling state of the form can get really tricky and confusing

Implementation with new functionality:

I present you next snippet mainly to show code complexity. Feel free to leave it hidden.

class ResearchFormStateUpdater {
  update () {
    (...)
    this._triggerCallbacks();
  }

  _triggerCallbacks () {
    // choose callbacks depending on source
  }

  _adManagerSourceCallbacks () {
    (...)
    this._enableDemandChannels(ResearchFormStateUpdater.AD_MANAGER_DEMAND_CHANNELS);
    this._updateDefaultStateOfDynamicFilters();
    this._updateAdManagerDynamicFilters();
  }

  _sspSourceCallbacks () {
    (...)
    this._removeDemandChannelsActiveClassAndDisable(ResearchFormStateUpdater.AD_MANAGER_DEMAND_CHANNELS);
    this._updateDefaultStateOfDynamicFilters();
  }

  _updateDefaultStateOfDynamicFilters () {
    $('.dynamic-filter').each((_, filter) => {
      $(filter).trigger('dynamicFilter:enableSspFilters', this.isSourceSsp);
    });
  }

  _updateAdManagerDynamicFilters () {
    $('.dynamic-filter').each((_, filter) => {
      $(filter).trigger('dynamicFilter:disableWebsitesAndProducts', this._areFormStateDimensionsDisabled() && !this.isSourceSsp);
    });
  }

  _shouldDisableFields (disablingDemandChannels) {
    // is any of disablingDemandChannels is checked
  }
}

ResearchFormStateUpdater.AD_MANAGER_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other', 'ebda'];

class ResearchDynamicFilter {
  // I didn't simplify those two methods body to show current implementation complexity

  _setDefaultDynamicFiltersToggleEvent () {
    $(this._getBody()).on('dynamicFilter:enableSspFilters', (event, shouldEnableSspOptions) => {
      this._setDefaultFiltersOptionDisabledState(shouldEnableSspOptions);

      const selectedFilterDimension = this._getFiltersDimension().find('option:selected').val();
      if (selectedFilterDimension === 'website') {
        this._toggleChosenFilterDisabledState(false);
      } else if (selectedFilterDimension === 'platform') {
        this._toggleChosenFilterDisabledState(!shouldEnableSspOptions);
      } else {
        this._toggleChosenFilterDisabledState(shouldEnableSspOptions);
      }
    });
  }

  _setDynamicFilterDisableWebsitesAndProductsEvent () {
    $(this._getBody()).on('dynamicFilter:disableWebsitesAndProducts', (event, shouldDisableWebsitesAndProducts) => {
      const selectedFilterDimension = this._getFiltersDimension().find('option:selected').val();
      if ($.inArray(selectedFilterDimension, ['website', 'product']) >= 0) {
        this._toggleChosenFilterDisabledState(shouldDisableWebsitesAndProducts);
      }
      this._setMethodSelectWebsiteAndProductOptionDisabledState(shouldDisableWebsitesAndProducts);
    });
  }

  _toggleNonSspFilters (dimensionSelect, shouldDisable) {
    $.each(ResearchDynamicFilter.NON_SSP_FILTERS_OPTIONS, (_, option) => {
      // toggle filter state depending on 'shouldDisable'
    });
  }
}

ResearchDynamicFilter.NON_SSP_FILTERS_OPTIONS = ['ad_unit', 'creative_size', 'geo', 'device', 'product'];

We still use some 'toggle' mechanism. It is really hard to switch 4 levers and get to expected state and now DynamicFilter has to know, which dimensions are not for ssp source.

We do have ResearchFormStateUpdater, why shouldn’t it be in charge?

Final request

Add another filter for 'Yield partner'

That is the exact moment when we decided to refactor those classes. Channels and filters being analyzed are just a small part of the problem. There are multiple form sections here and all of them have the same problem. Our refactor should neutralize the need for changing inside methods of those classes *to* add some new channels or dimensions.

In the next snippet, I left the main classes almost as they are in our production code to show you how easy to understand they are now.

class ResearchFormStateUpdater {
  update () {
    (...)
    this._updateDynamicFilters();
  }

  _updateDynamicFilters () {
    this._toggleAllDynamicFiltersState(this._dynamicFiltersDimensionsToBeDisabled());
  }

  _dynamicFiltersDimensionsToBeDisabled () {
    if (this.isSourceSsp) { return ResearchFormStateUpdater.NO_SSP_FILTERS; }

    var disabledFilters = ResearchFormStateUpdater.ONLY_SSP_FILTERS;
    if (this.areDemandChannelsExceptAdxSelected) {
      disabledFilters = disabledFilters.concat(ResearchFormStateUpdater.ONLY_ADX_FILTERS);
    }
    return disabledFilters;
  }

  _toggleAllDynamicFiltersState (disabledFilters) {
    $('.dynamic-filter').each((_, filter) => {
      this._toggleDynamicFilterState(filter, disabledFilters);
    });
  }

  _toggleDynamicFilterState (dynamicFilter, disabledFilters) {
    $(dynamicFilter).trigger('dynamicFilter:toggleDynamicFilters', disabledFilters);
  }
}

ResearchFormStateUpdater.NO_SSP_FILTERS = ['ad_unit', 'creative_size', 'geo', 'device', 'product'];

ResearchFormStateUpdater.ONLY_SSP_FILTERS = ['platform'];

ResearchFormStateUpdater.ONLY_ADX_FILTERS = ['website', 'product'];

class ResearchDynamicFilter {
  _setDynamicFiltersToggleEvent () {
    $(this._getBody()).on('dynamicFilter:toggleDynamicFilters', (event, disabledFilters) => {
      this._disableFilters(disabledFilters.split(','));
      this._enableFilters(disabledFilters.split(',')));
    });
  }

  _disableFilters (filtersToDisable) {
    // disable filtersToDisable
  }

  _enableFilters (filtersToDisable) {
    const filtersToEnable = $(ResearchDynamicFilter.ALL_FILTERS).not(filtersToDisable).get();
    // enable filtersToEnable
  }
}

ResearchDynamicFilter.ALL_FILTERS = ['website', 'ad_unit', 'creative_size', 'geo', 'device', 'product', 'platform'];

We did it! Did we?

Now the only thing 'ResearchDynamicFilter' has to know is a list of all filters - seems fair. The rest of logic and control comes from above - some higher methods and constants.

So let's try out our new structure with adding a filter for 'Yield_partner':

class ResearchFormStateUpdater {
  _dynamicFiltersDimensionsToBeDisabled () {
    (...)
    if (this.areDemandChannelsExceptEbdaSelected) {
      disabledFilters = disabledFilters.concat(ResearchFormStateUpdater.ONLY_EBDA_FILTERS);
    }
    return disabledFilters;
  }
}

ResearchFormStateUpdater.NO_SSP_FILTERS = [(...), 'yield_partner'];

ResearchFormStateUpdater.ONLY_EBDA_FILTERS = [(...), 'yield_partner'];

ResearchDynamicFilter.ALL_FILTERS = [(...), 'yield_partner'];

As you can see, it is all about adding some values to constants and some additional conditions.

Thanks to ‘open-closed principle’ we are able to change business logic of form with only adding some values and conditions on a higher level of abstraction. We do not need to go inside the component and change anything. This refactor affected the whole form and there were more sections and they all obey the open-closed principle now.

We didn't reduce the amount of code - as a matter of fact, we even increased it (before/after):

  • ResearchFormStateUpdater - 211/282 lines
  • ResearchDynamicFilter - 267/256 lines

It's all about the collection in constants -> it's our public interface now, our console to control process without tens of switchers.

Read also:

  • How to write a good and quality code?
  • Vuelendar. A new Codest’s project based on Vue.js
  • What is Ruby on Jets and how to build an app using it?

Related articles

Software Development

3 Useful HTML Tags You Might Not Know Even Existed

Nowadays, accessibility (A11y) is crucial on all stages of building custom software products. Starting from the UX/UI design part, it trespasses into advanced levels of building features in code. It provides tons of benefits for...

Jacek Ludzik
Software Development

5 examples of Ruby’s best usage

Have you ever wondered what we can do with Ruby? Well, the sky is probably the limit, but we are happy to talk about some more or less known cases where we can use this powerful language. Let me give you some examples.

Pawel Muszynski
Software Development

Maintaining a Project in PHP: 5 Mistakes to Avoid

More than one article has been written about the mistakes made during the process of running a project, but rarely does one look at the project requirements and manage the risks given the technology chosen.

Sebastian Luczak
Software Development

5 reasons why you will find qualified Ruby developers in Poland

Real Ruby professionals are rare birds on the market. Ruby is not the most popular technology, so companies often struggle with the problem of finding developers who have both high-level skills and deep experience; oh, and by the...

Jakub
Software Development

9 Mistakes to Avoid While Programming in Java

What mistakes should be avoided while programming in Java? In the following piece we answers this question.

Rafal Sawicki
Software Development

A quick dive into Ruby 2.6. What is new?

Released quite recently, Ruby 2.6 brings a bunch of conveniences that may be worth taking a glimpse of.  What is new? Let’s give it a shot!

Patrycja Slabosz

Subscribe to our knowledge base and stay up to date on the expertise from industry.

About us

We are an agile software development company dedicated to empowering our clients' digital transformation projects and ensuring successful IT project delivery.

    United Kingdom - Headquarters

  • Office 303B, 182-184 High Street North E6 2JA London, England

    Poland - Local Tech Hubs

  • Business Link High5ive, Pawia 9, 31-154 Kraków, Poland
  • Brain Embassy, Konstruktorska 11, 02-673 Warsaw, Poland
  • Aleja Grunwaldzka 472B, 80-309 Gdańsk, Poland

    The Codest

  • Home
  • About us
  • Services
  • Case studies
  • Know how
  • Careers

    Services

  • PHP development
  • Java development
  • Python development
  • Ruby on Rails development
  • React Developers
  • Vue Developers
  • TypeScript Developers
  • DevOps
  • QA Engineers

    Resources

  • What are top CTOs and CIOs Challenges? [2022 updated]
  • Facts and Myths about Cooperating with External Software Development Partner
  • From the USA to Europe: Why do American startups decide to relocate to Europe
  • Privacy policy
  • Website terms of use

Copyright © 2022 by The Codest. All rights reserved.

We use cookies on the site for marketing, analytical and statistical purposes. By continuing to use, without changing your privacy settings, our site, you consent to the storage of cookies in your browser. You can always change the cookie settings in your browser. You can find more information in our Privacy Policy.