Οι περισσότεροι προγραμματιστές έχουν ακούσει για την αρχή ανοιχτό - κλειστό - μία από τις αρχές SOLID του θείου Bob. Ακούγεται λογικό, αλλά μπορεί να είναι ακόμα λίγο θολό μέχρι την πρώτη χρήση σε "ζωντανό" κώδικα. Η πλήρης κατάσταση της αρχής είναι: οι οντότητες λογισμικού (κλάσεις, ενότητες, συναρτήσεις κ.λπ.) πρέπει να είναι ανοικτές για επέκταση, αλλά κλειστές για τροποποίηση.
Τι σημαίνει λοιπόν πραγματικά;
Αντιμετωπίσαμε ένα πρόβλημα ανάπτυξης που μας έδειξε τι πραγματικά σημαίνει η αρχή "ανοικτό-κλειστό". Σε μια από τις διαδικτυακές μας εφαρμογές είχαμε μια φόρμα με δύο τμήματα (μεταξύ άλλων):
- κανάλια ζήτησης
- δυναμικά φίλτρα
Οι χρήστες μπορούν να προσθέσουν όσα φίλτρα επιθυμούν, αλλά υπάρχουν ορισμένοι κανόνες - η διαθεσιμότητα των φίλτρων εξαρτάται από τα επιλεγμένα κανάλια.
Κανάλια ζήτησης: ADΑΝΤΑΛΛΑΓΉ, ΕΠΙΚΕΦΑΛΊΔΑΠΡΟΣΦΟΡΑ, ΚΡΑΤΗΣΗ, ΑΛΛΑ Δυναμικά φίλτρα (διαστάσεις): ιστοσελίδα, διαφήμισημονάδα, geo, δημιουργικήμέγεθος, συσκευή
Αυτό το άρθρο αφορά κυρίως την αναδιαμόρφωση κώδικα, οπότε θα υπάρχουν πολλά αποσπάσματα κώδικα παρακάτω. Προσπάθησα να τα μειώσω, αλλά κάποια ποσότητα κώδικα είναι απαραίτητη για να δείξω αναδιοργάνωση κώδικα. Δεν χρειάζεται να καταλαβαίνετε κάθε μικρό μέρος του κώδικα για να κατανοήσετε την κύρια ιδέα.
Η πρώτη εφαρμογή του προβλήματος ήταν απλή:
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) {
// έχει τσεκαριστεί κάποιο από τα disablingDemandChannels ?
}
}
ResearchFormStateUpdater.WEBSITE_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other'],
class ResearchDynamicFilter {
_setDynamicFilterDisableWebsitesEvent () {
$(this._getBody()).on('dynamicFilter:disableWebsites', (event, shouldDisableWebsites) => {
// απενεργοποίηση των φίλτρων ιστότοπων
});
}
}
Όπως μπορείτε να δείτε, το φίλτρο του ιστότοπου υποτίθεται ότι δεν είναι διαθέσιμο για το HEADERΠΡΟΣΦΟΡΑ, ΚΡΑΤΗΣΗ και ΑΛΛΑ κανάλια, έτσι ώστε να είναι διαθέσιμο μόνο για ADΚανάλι ΑΝΤΑΛΛΑΓΗΣ.
Το τελευταίο πράγμα που μπορείτε να πείτε για τον κώδικα είναι ότι είναι μόνιμος ή στατικός. Έτσι, έχουμε περισσότερα αιτήματα από τον πελάτη μας που κάνουν αυτές τις κλάσεις μεγαλύτερες και πιο πολύπλοκες.
Ανάπτυξη χαρακτηριστικών
Συναγερμός -> όταν ένα συστατικό είναι ανοιχτό για αλλαγές, θα αλλάξουν πολλά ονόματα στο μέλλον. Δεν θα δώσουμε σημασία σε αυτό στα επόμενα βήματα.
Προσθέστε ένα άλλο φίλτρο για το 'Προϊόν' (Προϊόν το σύστημα διαθεσιμότητας φίλτρων είναι το ίδιο με αυτό του ιστοτόπου)
- ResearchDynamicFilter η κλάση πρέπει να ελέγχει για μια ακόμη διάσταση κατά την απενεργοποίηση/ενεργοποίηση πεδίων
Ας πάμε πιο μακριά και ας προσθέσουμε κάποιο switcher πάνω από τα κανάλια -> 'Source'. Όλα τα κανάλια ζήτησης που είχαμε μέχρι τώρα βρίσκονται στην πηγή Ad Manager. Η νέα πηγή - SSP - δεν έχει κανάλια ζήτησης και το μόνο διαθέσιμο φίλτρο είναι η ιστοσελίδα.
Κανόνες:
- Υπάρχουν δύο καταστάσεις της πηγής: SSP.
- Όλα τα κανάλια ζήτησης είναι διαθέσιμα μόνο για την πηγή Ad Manager.
- Δεν υπάρχουν κανάλια ζήτησης για την πηγή SSP
- Το 'Website' είναι το μόνο διαθέσιμο φίλτρο για την πηγή SSP.
Εφαρμογή:
Προσθέστε ένα άλλο φίλτρο για την 'Πλατφόρμα'
Κανόνες:
- Η πλατφόρμα είναι διαθέσιμη μόνο όταν η πηγή είναι SSP
Δυσκολία:
- Τώρα έχουμε την 'Ιστοσελίδα', η οποία είναι διαθέσιμη για το κανάλι AD_EXCHANGE από το Ad Manager και για το Ssp και έχουμε την 'Πλατφόρμα', η οποία είναι διαθέσιμη για το Ssp αλλά όχι για το Ad Manager.
- Εναλλαγή η κατάσταση του εντύπου μπορεί να γίνει πραγματικά δύσκολη και συγκεχυμένη
Εφαρμογή με νέα λειτουργικότητα:
Σας παρουσιάζω το επόμενο απόσπασμα κυρίως για να δείξω την πολυπλοκότητα του κώδικα. Μπορείτε να το αφήσετε κρυφό.
class ResearchFormStateUpdater {
update () {
(...)
this._triggerCallbacks(),
}
_triggerCallbacks () {
// επιλέγουμε τα callbacks ανάλογα με την πηγή
}
_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) {
// ελέγχεται οποιοδήποτε από τα disablingDemandChannels
}
}
ResearchFormStateUpdater.AD_MANAGER_DISABLING_DEMAND_CHANNELS = ['header_bidding', 'reservation', 'other', 'ebda'],
class ResearchDynamicFilter {
// Δεν απλοποίησα το σώμα αυτών των δύο μεθόδων για να δείξω την πολυπλοκότητα της τρέχουσας υλοποίησης
_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) => {
// εναλλαγή της κατάστασης του φίλτρου ανάλογα με το 'shouldDisable'
});
}
}
ResearchDynamicFilter.NON_SSP_FILTERS_OPTIONS = ['ad_unit', 'creative_size', 'geo', 'device', 'product'],
Χρησιμοποιούμε ακόμα κάποια 'toggle' μηχανισμό. Είναι πραγματικά δύσκολο να αλλάξετε 4 μοχλούς και να φτάσετε στην αναμενόμενη κατάσταση και τώρα το DynamicFilter πρέπει να γνωρίζει, ποιες διαστάσεις δεν είναι για την πηγή ssp.
Έχουμε τον ResearchFormStateUpdater, γιατί να μην είναι αυτός υπεύθυνος;
Τελικό αίτημα
Προσθέστε ένα άλλο φίλτρο για το 'Yield partner'
Αυτή είναι η ακριβής στιγμή που αποφασίσαμε να αναδιαμορφώσουμε αυτές τις κλάσεις. Τα κανάλια και τα φίλτρα που αναλύονται είναι μόνο ένα μικρό μέρος του προβλήματος. Υπάρχουν πολλά τμήματα φόρμας εδώ και όλα τους έχουν το ίδιο πρόβλημα. Το refactor μας θα πρέπει να εξουδετερώνει την ανάγκη για αλλαγή εσωτερικών μεθόδων αυτών των κλάσεων *για* να προσθέσουμε κάποια νέα κανάλια ή διαστάσεις.
Στο επόμενο απόσπασμα, άφησα τις κύριες κλάσεις σχεδόν όπως είναι στον παραγωγικό μας κώδικα για να σας δείξω πόσο εύκολα κατανοητές είναι τώρα.
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(),
// Ενεργοποίηση των filtersToEnable
}
}
ResearchDynamicFilter.ALL_FILTERS = ['website', 'ad_unit', 'creative_size', 'geo', 'device', 'product', 'platform'],
Τα καταφέραμε! Τα καταφέραμε;
Τώρα το μόνο πράγμα που πρέπει να γνωρίζει το 'ResearchDynamicFilter' είναι μια λίστα με όλα τα φίλτρα - φαίνεται δίκαιο. Η υπόλοιπη λογική και ο έλεγχος έρχονται από πάνω - κάποιες ανώτερες μέθοδοι και σταθερές.
Ας δοκιμάσουμε λοιπόν τη νέα μας δομή προσθέτοντας ένα φίλτρο για το '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'],
Όπως μπορείτε να δείτε, πρόκειται για την προσθήκη κάποιων τιμών σε σταθερές και κάποιες πρόσθετες συνθήκες.
Χάρη στην "αρχή του ανοικτού-κλειστού" είμαστε σε θέση να αλλάξουμε την επιχειρησιακή λογική της φόρμας προσθέτοντας μόνο μερικές τιμές και συνθήκες σε ένα υψηλότερο επίπεδο αφαίρεσης. Δεν χρειάζεται να μπούμε στο εσωτερικό του συστατικού και να αλλάξουμε οτιδήποτε. Αυτό το refactor επηρέασε ολόκληρη τη φόρμα και υπήρχαν περισσότερα τμήματα και όλα υπακούουν πλέον στην αρχή open-closed.
Δεν μειώσαμε την ποσότητα του κώδικα - στην πραγματικότητα, την αυξήσαμε (πριν/μετά):
- ResearchFormStateUpdater - 211/282 γραμμές
- ResearchDynamicFilter - 267/256 γραμμές
Πρόκειται για τη συλλογή σε σταθερές -> είναι η δημόσια διεπαφή μας τώρα, η κονσόλα μας για τον έλεγχο της διαδικασίας χωρίς δεκάδες διακόπτες.
Διαβάστε επίσης: