Single Responsibility Principle
I denne artikel behandler vi Single Responsibility Principle fra S.O.L.I.D.
Måske har du set eller skrevet kode hvor flere ortogonale (ikke parallelle) ansvarsområder blev varetaget af een “mother-of-all” klasse? Sådanne monolitter er svære at vedligeholde fordi vi på et tidspunkt får behov for at udvide med præcist de features, som får kode-læsset til at tippe.
Kig fx på følgende klasser
public abstract class Statistics { public abstract void CollectData(); public abstract double CalculateMean(); } public class ValueStatistics : Statistics { private readonly IList values = new List(); public override void CollectData() { var done = false; values.Clear(); do { Console.Write("Enter value (int): "); var line = Console.ReadLine(); try { var number = Convert.ToInt32(line); values.Add(number); } catch (FormatException) { done = true; } } while (!done); } public override double CalculateMean() { return values.Average(); } }
Klassen Statistics slår her bro over to ansvarsområder: Et som har med indsamling af data at gøre og et andet som har med databehandling at gøre. De to ansvarsområder ser oveni købet umiddelbart ud til at være ortogonale, sådan at forstå at de kan variere uafhængigt uden at ændringer for det ene område påvirker det andet.
For den abstrakte Statistics er det ligegyldigt hvordan data er indsamlet når blot det giver mening efterfølgende at tale om beregningen af en middelværdi. Og præcis så langt som vi har tænkt nu giver det fuldt ud mening for en ValueStatistics. Vi kunne endda indføre nye nedarvede klasser, som deler de samme traits som ValueStatistics, fx en WordLengthStatistics, ToneCalibratorStatistics eller … find selv på flere.
Introduktion af ny feature vælter læs
Men, hvad hvis vi efter stort besvær har implementeret de øvrige statistikker og nu bliver mødt med kravet om at indføre en MonetaryAmountStatistics?
Jamen, det er da ikke noget problem, tænker vi. Vi kan da bare bede operatøren om at angive en møntfod i hver indtastning, fastlægge en intern enhed (f.eks. EURO) og så beregne middelværdien herefter! Jamen, hvordan angiver jeg så hvilken enhed middelværdien skal returneres i? Burde der i stedet beregnes en middelværdi for hver møntfod? Vi foranlediges måske til at begynde på at småreparere Statistics sådan at vi returnerer en række af middelværdier og ValueStatistics bliver blot en “special-case” hvor kun en middelværdi returneres, men vi er på vej ud over kanten, på vej mod rådden kode.
Vi forbryder os mod Single-Responsibility-Princippet [1]:
Enhver klasse bør kun have en grund til at ændres
Pludselig opstår der spørgsmål som komplicerer vores hidtidige implementation af statistikker, som udelukkende skyldtes en forsimplende antagelse om at indsamling af data kan foregå uafhængigt af middelværdi-beregningen, som angivet i Statistics, hvor de to begreber kobles sammen på en uhensigtsmæssig måde.
Mulig løsning ved at opdele funktionaliteter
Ved fx at splitte statistik klassen op i interfaces svarende til hver af dens hovedområder, lettes implementationen af de nye krav, idet kun MonetaryAmountStatistics klassen påvirkes heraf. De to begreber, indsamling og beregninger kan nu variere uafhængigt af hinanden.
public interface ICollectable { void CollectData(); } public interface IMeanSpreadable { double CalculateMean(); } public class ValueStatistics : ICollectable, IMeanSpreadable { ... public void CollectData() { ... } public double CalculateMean() { ... } } public class Amount { ... } public interface IMultipleMeansSpreadable { T[] CalculateMeans(); } public class MonetaryAmountStatistics : ICollectable, IMultipleMeansSpreadable { public void CollectData() { ... } public Amount[] CalculateMeans() { ... } }
Ovenstående kan også siges at være en anvendelse af ISP-princippet, mere om dette i en senere artikel. Se de andre artikler herunder.
Referencer
[1] “Agile, Principles, Patterns and Practices in C#”, Martin/Martin, s. 115-
[2] http://en.wikipedia.org/wiki/Single_responsibility_principle