//****************************************************************************//
//************ Metrics & Design Patterns - November 20th, 2017 **************//
//**************************************************************************//
- So, last week, we were finishing talking about the ACYCLIC DEPENDENCY PRINCIPLE
- Let's say that we're programming our application, and as we're working on the "Error" package we realize that we want all errors to display the error in the UI
- The LAZY way of doing this is to have the "Error" package inherit from the UI; but this means that the "Error" package is depending on the UI package, and vice-versa
- INSTEAD, we should create a new package ("Error Dialog", or something), and have it use stuff from BOTH "Error" and "UI"
- That way, the Error/UI packages can't break each other when they're changed!
- More commonly in student code, we have a package "1" with classes A/B and package "2" with classes "X/Y", and when we want to share functionality between them, we have "A" import "X" and "Y" import "B", or something like this
- This creates a cyclic dependency!
- So, to FIX this, we should instead have A/Y use an INTERFACE with the methods we need from the other package
- "Why are cyclic dependencies awful? Well, in Java, they're not too bad since the Java Compiler is smart about how it compiles changes...but in other lagnuages like C/C++, if you have cyclic dependencies, then it means every time ONE package in that cycle is changed, ALL the packages in the cycle have to be re-compiled"
- Versioning...there's a couple ways to do it, but the most common way:
[major update #].[minor update #].[patch #]
- e.g. "Version 4.6.19"
- On the whole, versioning isn't too complicated, though; many companies use alternate schemes just for the sake of it, but as long as it works, that's okay
- Stable Dependencies Principle
- How do we measure the "stability" of a class? Well, first of all...
- By STABLE, we mean that the class doesn't depend on other classes at all; we can change our other classes as much as we want, and it won't affect the package's functionality
- If ANY change in our code changes the functionality of the class, then it's 100% unstable
- To measure the instability numerically (lower is better/more stable):
Instability = Co / (Co + Ci)
- Ci = incoming dependencies (other packages that depend on me)
- Co = outgoing dependencies (other packages I depend on)
- Another "DESIGN METRIC" is "Abstractness" for a package, which we calculate as:
A = (# of abstract classes in package) / (# concrete classes)
- Ideally, something that is 100% abstract should have 0% instability, and something that's 100% concrete/unstable should be 0% abstract
- "Something that's 100% abstract and 100% unstable means that nothing is using that package and it isn't doing anything useful - this is the ZONE OF USELESSNESS"
- "Something that's 0% abstract and 0% unstable is the ZONE OF PAIN - it means the package is doing EVERYTHING on its own with no abstraction to organize it"
- Other useful design metrics we don't cover in this class: Method complexity, methods per class, lack of cohesion, coupling, ineritance tree depth, Halstead/McCabe measure, etc.
========================
// Design Patterns
========================
- Design patterns are part of a larger scheme we'll call "design expertise", which are common ways of solving problems; this includes:
- Architectural Styles
- Frameworks / Class library design
- "Sadly, we don't have time to go over most of this"
- Design patterns ( ways of solving common design problem)
- A good resource for this is a website called BALL OF MUD (or something like that)
- Language idioms (e.g. avoiding "magic numbers", clean coding practices, language-specific stuff like when to us lambda expressions, etc.)
- e.g. there are 2 reasons we use singletons: it allows global access to a model, and allows us to deal w/ classes we only want one instance of
- So, here, we're going to focus on some common DESIGN PATTERNS:
- Singleton/Monostate, we already talked about earlier
- FACTORY pattern
- Let's say we have an "Encryption" class with a few different kinds of encryption (RSA, DES, BlowFish, etc.); how do we determine which kind of encryption we should use?
- Well, instead of directly calling methods on Encryption, let's have an "IEncryptFactory" interface with 1 method on it that takes in a key, specifying which type to use; then, we can handle the appropriate logic for that
- "Factories are one of the few cases where switch statements are perfectly acceptable"
- Example code for the factory:
public class EncryptionFactory {
public Encryption createEncryption(Key encryptTypeKey) {
String algorithm = encryptTypeKey.getAlgorithm();
if (algorithm.equals("DES")) {
return new DESEncryption(encryptTypeKey);
} else if (algorithm.equals("RSA")) {
return new RSAEncryption(encryptTypeKey)
}
throw new Exception("Unknown encryption type specfied");
}
}
- Now, we might not always need a full class for the factory pattern; in that case, we can just create a static "Factory method" on the class; e.g.:
public class Person {
(...)
public static makeNewPerson(String line) {
(...) //parse the line and create the appropriate person
}
}
- This allows more descriptive naming
- BUILDER pattern
- This is another pattern that's meant to instantiate a new instance of a class
- Let's say we're creating a new User, where the name/age are mandatory and their address, email, and location are optional
- "We don't want to have a bunch of different constructors to handle all the different possible combinations, or to have something arbitrary like setting 'null' for optional parameters"
- In that case, we'd have a "UserBuilder" class with a bunch of method like this for EACH optional instance variable:
public UserBuilder email(String email) {
this.email = email;
return this;
}
(...)
- ...and the mandatory options as part of the constructor:
public UserBuilder(String name, int age) { ... }
- And THEN, finally, we'd have a "build" method like this on the class:
public User build() {
User u = new User(this.name, this.age);
u.setEmail(this.email);
(...)
}
- To use this, we'd then say something like:
User newUser = UserBuilder("Fred", 19).email("...").build();
- "This looks like a violation of the Law of Demeter...but again, here it's okay, since it's part of the design pattern and we're ONLY calling stuff within the class."
- "This has come up as a test question before: if it ends in 'build', it is NOT a violation of the Law of Demeter"