Title: Decoupling Component Dependencies
1Decoupling Component Dependencies
- Annatala Wolf
- 222 Lecture 2
2What is a dependency?
- Dependencies are relationships in code where a
change to one part of code necessitates a change
to another part. - Example you write code to add functionality to
Set_Kernel_1, then someone changes the code
inside Set_Kernel_1. If your code depended on
how Set_Kernel_1 was implemented, now it might be
broken
3Coupling
- When components work together they are said to be
coupled. - Loose coupling less dependency (good)
- Tight coupling more dependency (bad)
- Flexible code will feature components that are
loosely coupled, so changes will not require a
rewrite of the entire program.
4Types of dependencies
- Abstract-to-abstract loose coupling
- example specification for Fancy_Set which
extends (adds functionality to) specification for
Set_Kernel - Concrete-to-abstract loose coupling
- example a concrete implementation which
implements Set_Kernel - Concrete-to-concrete tight coupling
- example any time code depends on how another
concrete component is implemented
5Three situations where we might want to
decouple component dependencies
- Checking components check the requires clauses of
other components. They shouldnt depend on how
the component is implemented. - Extensions add new behavior to a component.
Often, they can be written without depending on
any concrete code that implements the base
component. - Utility classes contain static operations that we
might need to use inside a component. We want to
write components that use utility classes in a
way that allows the client to select the
functionality.
6Decoupling in Resolve/C
- To solve most of these problems in C,
programmers would typically use polymorphism. - In Resolve/C, we instead use C templates to
accomplish this. The benefits of this method are
that it produces code which is easy to read, and
it avoids the use of multiple-inheritance, which
is error-prone. - The downside is the cryptic error messages that
result from using templates in complex ways.
7Checking components for debugging
- Checking components are components that check the
requires clauses of other components. - Youve been using checking components since 221
(the ones that end in _C are the checking
versions). - These components give you violated assertion
messages when you break a precondition. - Theyre useful when you need to check code for
correctness, but they can be replaced with the
non-checking versions when the code works (for
speed). This is a common paradigm in program
design.
8Component Coupling Diagrams
- The diagrams we use in 222 are called component
coupling diagrams or CCD for short. CCD are
similar to diagrams used in industry, like UML
class diagrams. - CCD illustrate the way components depend on each
other. Each arrow represents a kind of
dependency. The component the arrow points from
depends on the one it points to.
9The checks dependency
- The checks dependency occurs when one component
checks the requires clauses of another
components operations.
This CCD illustrates a concrete-to-concrete
dependency, which wed like to avoid. We dont
want to write a separate checker for each
implementation of Sequence.
10How checking components work
- For every operation with a requires clause, a
checking component overrides the operation with
its own version. - First, it runs assertions that check the
preconditions. - If the assertions pass, the override operation
calls the original (unchecked) operation, so it
will do what the client expects it to do. - If an operation has no requires clause, it does
not appear in the checking component, so the
original operation is called automatically.
11Operations of Queue_Kernel_C
- procedure_body Dequeue (produces Item x)
-
- assert (self.Length () gt 0, "self /
empty_string") - self.Queue_BaseDequeue (x)
-
- function_body Item operator
- (preserves Accessor_Position current)
-
- assert (self.Length () gt 0, "self /
empty_string") - return self.Queue_Baseoperator
(current) -
- // We dont need any code for Enqueue() or
Size(), - // because their requires clause is always true.
12Generifying the process of checking
- Notice how the requires clause is specified in
the abstract component, not in the
implementation. - Since all implementations use the same abstract
component, we should only need to write a single
checking component for Set_Kernel, not one
component for each version of Set_Kernel. The
checker shouldnt need to know which
implementation of Set its checking.
13Decoupling checks
- Instead of checking a particular component, well
check a component whose type gets passed to the
checker as a template parameter (the black box).
- This parameter is named Sequence_Base. It can
be filled in with any implementation of Sequence.
The implementation to use is chosen by the
client.
14Clients choice
- In order to instantiate Sequence_C, the client
must plug two types in to fill out its template - Item (what type the Sequence will hold)
- Sequence_Base (which implementation of
Sequence_Kernel the client wants to use) - Once both of these parameters are filled in with
actual types, the client can make
automatically-checked Sequence objects.
15Partial instantiation
- The reason you dont see this is that we
partially instantiate most of the templates
youve been using. - This means we fill in part of the template for a
component like Queue_Kernel_C, for example, by
filling in Queue_Base with Queue_Kernel_2.
However, we leave other parts, such as Item, as
template parameters that still need to be filled
in. - We call this new concrete template (still
templated by Item) Queue_Kernel_2_C. You only
need to fill in a type for Item to instantiate
Queue_Kernel_2_C.
16Partial instantiation example( Item is kept as a
template parameter )
- concrete_template lt
- concrete_instance class Item
- gt
- class Sequence_Kernel_1a_C
- specializes
- concrete_instance Sequence_Kernel_C lt
- Item,
- Sequence_Kernel_1a ltItemgt
- gt
-
17Partial instantiation CCD
- Heres how the creation of Sequence_Kernel_1a_C
(through partial instantiation) is related to
other components.
18The specializes and uses arrows
- The specializes dependency just means this
depends on that, because it fills in some of that
components template parameters. It is only
used to denote partial instantiation. - The uses dependency is a generic dependency
that means, this needs that component for some
reason. In this case, we need
Sequence_Kernel_1a, because its the type were
plugging into Sequence_Base.
19Extensions adding functionality
- What if you want to add new behavior to a
component? Maybe you want to create a Fancy_Set
component that includes operations for Union,
Intersection, Set_Difference, etc. - Ideally, wed like to design Fancy_Set to work
with any implementation of Set_Kernel. If it
depended on a particular implementation, wed
only be able to use it with that one version of
Set_Kernel, and it might break if code changes.
20Extending the specification
- The behavior of a component is described by the
abstract component (here, Set_Kernel). This is
what specifications dothey describe the model,
and a contract for its behavior. - The behavior of an extended component will also
be described by an abstract model (here,
Fancy_Set_Kernel). The relationship here is an
abstract-to-abstract dependency (loose coupling,
which is good).
21The extends dependency
- The extends arrow means this adds
functionality to the component described in
that. (Note that extends is the only dependency
you will see between two abstract components.) - You can use extends between concrete components
too, but this illustrates a concrete-to-concrete
dependency. Wed like to avoid this if possible.
22Layered extensions
- Ideally, wed like to design an implementation of
Sequence_Reverse that will work with any version
(any particular implementation) of
Sequence_Kernel. - If we build Sequence_Reverse as a client of
Sequence (by simply calling the operations of
Sequence_Kernel), then we dont need to know how
its operations are implemented. - A component that works this way is called a
layered extension. Its a client of the component
beneath it, so its code will work with any
implementation.
23Decoupling extends
- To decouple the dependency, we can use templates
in a similar way to how we decoupled checking.
C-to-C Dependency
Decoupled!
24What to take from this
- Make sure you understand the component decoupling
example on the previous slide. - You should understand why we want to decouple
concrete-to-concrete dependencies in
component-based software development, and how we
accomplish it through templates. - Although the template paradigm is not the typical
way to do this in C, the need to limit
concrete-concrete dependencies is a very common
issue faced in systems programming.
25Note Item does not appear in CCD
- The black box is always a template parameter for
which implementation of an abstract class the
client wants to use. - It is not Item! Item is not shown as a
component in any CCD, except that templates
always have a bold outline, so Item is in some
cases the cause. - Here, the black box is a parameter for a
particular Sequence_Kernel implementation. - The name of the parameter is Sequence_Base.
26How to write a layered extension
- Inside a layered extension, the keyword self
refers to the base component (here, a Sequence of
Item), and you call operations on self to change
the Sequence that gets passed as the
distinguished parameter. - procedure_body Reverse ()
- object Integer frontIndex
- object Integer backIndex self.Length()-1
- while (frontIndex lt backIndex)
- selffrontIndex selfbackIndex
- frontIndex
- backIndex--
-
-
27How self is passed
- // My_Sequence is a Sequence_Reverse_1 type that
is - // instantiated by ltText, Sequence_Kernel_1altTextgt
gt - object My_Sequence names
- names.Add(name1)
- names.Add(name2)
- names.Reverse()
- Since Sequence_Reverse_1 is written as a layered
extension inside Reverse(), self (in this case)
is names passed by reference.
28Type of self in layered extensions
- In a layered extension, the type of self is the
same type as the component youre writing. You
can use it to make objects of the same type as
self, if you need to. - procedure_body Intersect( preserves Set_Intersect
ltItemgt s) - object catalyst Set_Intersect_1 temp
- while (self.Size () gt 0)
- object Item x
- self.Remove_Any (x)
- if (s.Is_Member (x))
- temp.Add (x)
-
-
- self temp
Set_Intersect is templated by Item. Inside
Intersect(), we can use Item just like any type.
The type of self here is Set_Intersect_1.
29Using Item
- Inside template-parameterized code, we can use a
parameter like Item just like any concrete type,
even though we dont know what it is. It will
have all four default operations, since clients
are required to use Resolve objects in
instantiations. - Some contracts may add a restriction about what
you can plug in for Item. Set_Kernel, for
example, requires any Item chosen for Set must
contain the Is_Equal_To() function. - So if youre writing something for Set, you can
assume Is_Equal_To() exists for Item, if needed.
30Default Resolve/C operations
- There are four default Resolve/C operations.
These exist for all Resolve/C objects (except
Pointer and Pointer_C). They even exist for
unknown, non-pointer objects, like Item. - (constructor) all objects created set to a
default value - (destructor) objects handle cleanup implicitly
- a b swaps objects a and b
- a.Clear() resets a to the default value for its
type - The existence of allows us to move Item
objects around without copying or using pointers.
31Non-layered extensions
- Sometimes, simply layering an extension on the
base component would give us bad performance. In
this case, we need to do extra work to get the
performance we need. - Writing a non-layered extension is just like
writing a Kernel, except that youre writing all
the Kernel operations, plus the new operations
from the extension. Youll do this for Lab 5.
32Kernels and non-layered extensions
- In Kernels and non-layered extensions, the
keyword self stands for something called the
Representation. This is a different use from
layered extensions, so dont get confused. Well
cover the different use of self in Kernels later. - (We also use the term layered with Kernels,
but it means something different. A Kernel is
layered if its built using other components,
such as using a Sequence to implement Queue. A
Kernel is non-layered if it uses pointers.)
33Utility classes code, but no data
- Utility classes are different from other
components, because they do not define a type.
You never create objects from them. - Utility classes can be used to
- Package a bunch of static operations together
- Package one or more operations in a class, so we
can plug the class into a template and let the
client select which version of an operation they
want to use as part of instantiation
34Utility class syntax
- Since you cant make objects of a utility class,
its operations are static, and are called by
using the class name as a prefix. Examples - // get the hash value for Text object name
- hash Text_HashHash(name)
- // compare two Foo objects, see which comes first
- flag Foo_Are_In_Order_2Are_In_Order(a, b)
35Is_Equal_To( ) syntax
- Is_Equal_To() uses a different syntax, partly
because there should only be one version of
equality for a given type, and partly because
its more efficient to implement it inside a
Kernel. - Because of this, when a spec tells you that you
have access to Is_Equal_To() for a type (like
Item), you call it as an instance operation. - // function returns true iff item1 item2
- answer item1.Is_Equal_To(item2)
36Equality with default types
- Note that the five basic default types in Resolve
(Boolean, Character, Integer, Real, and Text) all
have Is_Equal_To() implemented. In each case,
its identical to the operator for that
type. - Why do these types need Is_Equal_To() if they
have ? Well, say we instantiate Set_Kernel_1
with Text. The code in Set_Kernel_1 is designed
to work for any Item type that has
Is_Equal_To() written for it. So Is_Equal_To()
is the operation Set_Kernel_1 will call to test
equality for Item.
37Using utility classes Queue_Sort
- Queue_Sort is an extension of Queue. It needs to
know how to sort data, but Queue could be
instantiated with many different types of Item.
We could also want to sort the same type of Item
in different ways (ascending, descending, etc). - We can prevent the need to write a different
Queue_Sort for each type of data (and for each
way to order a type) by plugging in an
implementation of the utility class Are_In_Order
into an implementation of Queue_Sort.
38Utility Class CCD
- Note that Queue_Sort_1 is a template with three
parameters Queue_Base, Item, and a version of
Are_In_Order for Item.
39The meaning of parameters, and the version of
Queue_Sort selected
- Queue_Base tells us which concrete implementation
of Queue_Kernel we will use as the engine for
our new Queue_Sort type. - Item tells us what type of data our new
Queue_Sort type will hold. - Are_In_Order has to match the type for Item it
says what order the Items will be sorted in. - The version of Queue_Sort we choose to
instantiate will determine how the data is
sorted, in code (which sorting algorithm is used).
40Know your dependencies
- There are six dependencies (A ? B)
- checks A checks Bs requires clauses
- implements As code implements Bs specs
- extends A adds new functionality to B
- specializes A partially instantiates B
- uses A needs B for something (generic)
- encapsulates A contains Representation B(used
only with Kernels described later)
41Local operations(Implementer Perspective)
- We sometimes use private operations to assist us
in writing components. In Resolve, we call these
local operations. - Private means a local operation can only be
called from within the class were implementing.
It is written just like a global operation (from
a static context), but you can only call it from
within the class.
42Local operation context
- It is easier to reason about the behavior of
local operations if they never access self,
particularly inside Kernels (but also in layered
extensions). - This way, you dont have to worry about a local
operation messing up the object the class
represents. You can even write code for a local
operation without knowing what class youre in. - We write local operations statically everything
it needs will be passed as an operation
parameter. If it needs access to self, you can
just pass self (or in Kernels, a field of self)
as an argument.
43Self-discipline
- You can use self in local operations, but dont!
The way weve written them, you dont have to.
Just implement the specification, and pass self
to the operation if you need to. - One reason we have local operations is so you can
perform recursive steps there, since recursion is
not allowed in Kernel operations (its fine in
layered extensions). Well explain why later,
but this is another requirement where you can do
something, but shouldnt.
44Kinds of operations(implementers perspective)