One mistake a think is happening many times is to treat all those patterns equally, when some of them are used way more often than others. My recommendation is to learnt them that way: start with something you may actually use, and add more when needed, in time. Other mistake is to consider only those listed in that book or websites mentioned, forgetting others described in other sources (see the list on Wikipedia is much longer).
However, when comes to learn those patterns my suggestion is to focus on UML class diagrams provided.
Let's take an example of a behavioural pattern that's very likely to be used in complex architectures: Mediator. Here's the UML diagram from Wikipedia:
(Actually the other image is much better because has more information and uses the correct relationship arrows instead of these non-standard, but also include sequence diagram which a tried to avoid for now, for simplicity)
You also have the C# code, as an example, but may question to you is: what's easier to read, diagram or C# code? I'm pretty sure many will answer the latter, but why? UML has many objectives in scope, some of them are obsolete these days like using CASE tools to generate code, but don't forget the main objective - to draw a picture that replace 1000 words. Just seeing that diagram should be clear for many developers, because that's the meaning of it, to simplify communications between developers.
I was looking for a good UML class diagram refresher with significant code example in C# and because I couldn't find one, here's my attempt to do one. Some inspiration comes from Agile Principles, Patterns, and Practices in C# by Robert C. Martin and Micah Martin (see chapters 13 and 19).
Class diagrams (made on draw.io) have a name, attributes (fields or properties) and messages (methods):
Which in C# it may be:
public class ThingA {
public int attribute1 {get; set;}
private string attribute2;
public int Message1() { /* ... */ }
protected bool Message2(string s) { /* ... */ }
private void Message3(IEnumerable<int>) { /* ... */ }
}
public abstract class AbstractThingA {
public virtual int attribute1 {get; set;}
public abstract int Message1();
}
public interface IThingA {
public int attribute1 {get; set;}
public int Message1();
}
Notice here:
- Access modifiers (visibility in UML): '+' means 'public', '-' means 'private' and '#' means protected
- Abstract classes are denoted by italics font (used on DOFactory.com, for instance), or may use '<<abstract>>' stereotype;
- For interfaces you can use '<<interface>>' stereotype (above interface name) or just prefix it it 'I' (my preferred method);
- Constants may be uppercase and static elements underlined (not used here);
There may be different preferences when implementing class diagrams, but in C# in think best option is to implement private attributes as class fields and public attributes as properties. Also, up to you if in abstract classes you implement a method (or property / field) as abstract or virtual (be aware of the difference), which is just an implementation detail.
This is easy, right? Now let's continue with relationships between classes and interfaces which needs to be clearly understood in order to 'read' effectively UML class diagrams. Here we have a few options:
- Inheritance (----|>) - ThingA class inherits from AbstractThingA:
public abstract class AbstractThingA { /* .. */ }
public class ThingA: AbstractThingA { /* .. */ }
- Implementation or realization (- - - -|>) - ThingA class implements interface IThingA:
public interface IThingA { /* .. */ }
public class ThingA: IThingA { /* .. */ }
- Association (---->) - class ThingA uses class ThingB. There are various ways to 'use' a class like :
- A local variable in a method in an object of type ThingA:
public class ThingA {
public int Message1() {
ThingB b = new ThingB();
return b.MessageB1();
}
}
- An object of type ThingB being parameter in a method in ThingB object:
public class ThingA {
public int Message1(ThingB b) {
return b.MessageB1();
}
}
- Other option may be returning an object of type ThingB from a method in ThingA. These differences may be specified with stereotypes above the arrow, like respectively "<<local>>", "<<parameter>>", or "<<create>>".
- Aggregation ( <>----->) is a special kind of association when class ThingA "has an" object of ThingB, but ThingB object may exists on its own:
public class ThingA {
private ThingB b;
public ThingA(ThingB b) {
this.b = b;
}
// then use b.MessageB1()
}
- Composition ( <|>----->) is yet another kind of association when class ThingA "has an" object of ThingB, but ThingB object may not exists on its own (like the part may only make sense in the context of a whole object):
public class ThingA {
private ThingB b = new ThingB();
// then use b.MessageB1()
}
- Dependency ( - - - - >) - usually means "everything else", when an object of type ThingB is used somehow by an object of type ThingA, other than the relationships described above. It's a weaker relationship, but still included in the class diagram.
Note these implementations are not mandatory this way, you may choose similar solutions with the same results.
Other important details:
- Multiplicity - like 0 (when no instance of the dependent object may exists), 1 (exactly 1 instance of dependent object exists), 0..1 (no or 1 instance exists), * or 0..* (any number of instances may exists, usually means some sort of collection):
class ThingA {
private ThingB objB;
} - Attribute Name - on the arrow we can specify the name of the property holding the dependent class ("collB" here):
class ThingA {
private List<ThingB> collB;
} - Association Qualifier - on the source class we have some key or collection to keep track of dependent object ("keyB" here):
class ThingA {
private string keyB;
public ThingB {
get { return new ThingB(keyB);}
}
}
After all these details let's see the Mediator pattern as described on DOFactory.com website:
Is it now a bit clearer that Mediator is an abstract class having ConcreteMediator as derived class, and Colleague abstract class has a property called 'mediator' (of type Mediator) having 2 derived classes, ConcreteCollegue1 and ConcreteCollegue2, both used by ConcreteMediator? So, we can translate this into C# code as:
abstract class Mediator;
class ConcreteMediator: Mediator {
private ConcreteCollegue1 collegue1;
public ConcreteCollegue1 Collegue1 {
set {collegue1 = value;}
}
private ConcreteCollegue2 collegue2;
public ConcreteCollegue2 Collegue2 {
set {collegue2 = value;}
}
}
abstract class Collegue {
protected Mediator mediator;
public Collegue(Mediator mediator) {
this.mediator = mediator;
}
}
class ConcreteCollegue1: Collegue {
public ConcreteCollegue1(Mediator mediator): base(mediator){}
}
class ConcreteCollegue2: Collegue {
public ConcreteCollegue2(Mediator m): base(mediator){}
}
main() {
ConcreteMediator m = new ConcreteMediator();
ConcreteCollegue1 c1 = new ConcreteCollegue1(m);
ConcreteCollegue1 c2 = new ConcreteCollegue2(m);
m.Collegue1 = c1;
m.Collegue2 = c2;
}
That's all information we can extract from this diagram, for some other details we'd need some other diagrams, like a sequence diagram. Also, we considered some best practices for C#, like creating only public setters. In ConcreteMediator we have access to collegue1 & collegue2 private properties which can be used from whatever functionality we implement in that mediator class.
On the website you can see a full C# implementation.
Getting used to "read" class diagrams may help understanding and implementing design patterns, because I think it's easier to remember differences and details of various patterns graphically instead of the implementation code. Also, in many cases is more important to understand the classes, interfaces and relationships between them instead of some kind of implementation, which can slightly vary from project to project. For instance, you can use public properties or fields, or constructor injection on how the objects are created and passed around. Even for mediator example above there may be other solutions that does similar functionality, but the pattern itself is described by the class diagram, not by implementation details.
No comments:
Post a Comment