Chapter 9. Solutions to Bridge Domain Gaps

 

The idea has been to treat legacy systems as a black box and deal with them at more than arm’s length. All of this has made it possible to make many of these systems look like they were part of the 21st Century, leaving the tough stuff—the data and the business rules—untouched. If it ain’t broke, don’t fix it, right? Well, not exactly. Of course, old systems don’t get better by just being ignored. They get worse.

 
 --Ken Orr

One of the major goals for software engineering in the past decade has been to build software that promotes abstracted reusability. Because of this, developers witnessed the emergence of the object-oriented paradigm, which resulted in the introduction of reusable object-oriented frameworks. An object-oriented framework can be defined as a set of classes that embody an abstract design for solutions to a collection of related problems in a given problem domain.

The high reusability of frameworks is quite evident to software engineers, and it has solved many problems related to the goal of improved component reusability. With the emergence of new disciplines comes new issues and problems that must be addressed. Where single frameworks were originally used, we are now seeing a shift towards multiple frameworks that must communicate with each other in a cohesive fashion. Often there are problems communicating across dissimilar domain gaps, especially when each framework is being developed by a different development team. The source code to other frameworks is typically unavailable to developers who are not explicitly involved in its development, which generally leads to a number of integration issues between frameworks.

In this chapter, I will address the reasons behind the friction existing between multiple frameworks, and I offer some pragmatic approaches to building a cohesive design, even when you do not have access to the source code of other frameworks employed in your application. Throughout this chapter, I will refer to communication problems between frameworks in different problem domains as compositional friction.

Compositional Friction

There are numerous reasons why compositional friction can exist between two or more frameworks, and even on a solitary level between classes in a single framework. Although friction can exist between classes in a single framework, these issues can be solved through an iterative refactoring process with the availability of source code. This chapter is directed at eliminating friction between multiple frameworks, where one framework typically only has access to external frameworks through their public interfaces, and these interfaces cannot be modified or refactored.

Many software development issues can cause compositional friction, but a few of the most notable ones include domain coverage, design intentions, framework gap, entity overlap, and source code access.

Cause: Domain Coverage

The general purpose of a framework is to provide an abstract design for applications in a particular problem domain. It is important to realize that the framework does not need to cover the entire problem domain, but rather only a subset of relevant entities in the given problem domain. The amount of domain coverage to target with a framework is fairly subjective, though, because problem domains are not generally defined in extensive detail. Determining how much coverage to employ is up to the solution architect, and iterative refactoring helps to improve domain coverage.

When composing two frameworks, there are three levels of domain overlap that can occur, each with different implications and solutions. If no overlap occurs, there is no risk of integration issues when composing overlapping entities, but there may be a gap between frameworks that must be managed. If there is a relatively small amount of domain overlap between frameworks, the best solution is to evolve a few classes in both frameworks to communicate with each other with little to no compositional friction. However, when considerable domain overlap occurs, there are some important decisions to make. Sometimes it would be more advantageous to rewrite one or both of the frameworks from scratch when framework reuse is threatened. It can be more problematic to refactor communication between both frameworks when the expected lifetime of the product is quite long. If an application using the frameworks will be evolving over a long period of time, the frameworks must be continuously updated for each consecutive version of the application. Remember to make your decision based on the problem domain and the coverage of the framework.

Cause: Design Intentions

A well known design philosophy is that reusable software must be written for reuse through composition and adaptation. Generally, frameworks are designed to be reused through adaptation, but not through composition. Designing software reusability through composition is very important, and there are two composition directions that can occur. The first direction is parallel composition, which targets frameworks that exist on the same layer in the application. Parallel is the easiest composition direction because both frameworks do not rely directly on the services each other provides to properly operate. The other composition direction is perpendicular, which exists in a software application that supports a layered architecture where frameworks can depend on services provided by another framework in a different layer. One issue that is independent of the composition direction is the communication support, which can be either half-duplex (one-way, or simplex) or full-duplex (two-way). Half-duplex communication is fairly easy to design for, but full-duplex communication can present additional design problems when composing multiple frameworks.

Cause: Framework Gap

A framework gap occurs when multiple frameworks need to be composed to satisfy requirements, but both frameworks do not completely cover the requirements. This problem typically occurs because each framework does not have ample domain coverage, leading to domain gaps or overlaps.

There are a few solutions to this problem, the first one being the use of wrapper class that encapsulates the existing functionality and extends any missing functionality, also providing a uniform public interface so that clients are unaware of the internal architecture.

An alternative is to develop a software liaison, which is basically an application that exposes the public interface to clients and handles the communication and extension of functionality between the frameworks. This approach works great for situations where source code is inaccessible, or the base frameworks should not be modified.

Lastly, if source code is available, the cleanest solution is to bridge domain gaps by providing missing functionality, or remove domain overlaps through refactoring methods.

Cause: Entity Overlap

When more than one framework presents the same entity in a particular problem domain, each from a different perspective, the composition of these frameworks requires that the related entities be composed as well. This problem is known as entity overlap, and it occurs when the same problem and entities are modeled differently between multiple frameworks. Entity overlap is a common problem when composing multiple frameworks, and the solution can be fairly tricky due to the cohesive nature of entity classes and their need to sometimes notify the other frameworks when certain actions occur.

One solution to the problem of entity overlap is the use of multiple inheritance, but this method presents a problem when properties of an entity are not mutually exclusive. Multiple inheritance accomplishes the composition objective by handling the conversion between related entities in the frameworks and routing necessary events. It is possible to use this solution in development environments where source is inaccessible and cannot be modified.

An alternative solution is to use aggregation, where an aggregate class is used to represent a framework in parts. Each aggregate class is the entity definition for the application, but this approach requires that source code be available so that all references to a particular entity can be changed to point to the new aggregate classes. A drawback to this solution is that all interfaces for each representation of a certain entity must exist in the aggregate class, and there is a lot of additional overhead when using aggregation to bridge domain gaps.

A final solution is through the use of subclassing, where each framework is subclassed and each subclass handles the bidirectional communication of updates and conversion between other subclasses. Additionally, each subclass must override the operations in the superclass. This solution can also be used in situations where source code is inaccessible and cannot be modified. The major drawback of this solution is that the represented entity is partitioned across multiple frameworks. Another improvement to this solution is to use an aggregate class that contains all the subclasses and facilitates the communication and conversion between parts.

Cause: Legacy Components

There are times when the classes in a framework do not satisfy the problem domain solution, and the design warrants that a legacy component be used in conjunction with a framework to fill the gap. This situation can also arise if there is considerable time and expense invested in a legacy component (such as a game engine or utility library), and a business decision is made to reuse existing technology within a new framework (unmanaged and managed interoperability, for example). Using legacy components can cause severe compositional friction in your framework unless dealt with accordingly.

One method of removing the composition implications is to modify the framework to reference the legacy component instead of classes within the framework, though this solution requires access to the source code.

An alternative solution is to employ the adapter pattern and build a class that lies between the framework and the legacy component, acting as an interpreter so that both parts can communicate with each other. The latter part of this chapter focuses on this method in much greater detail.

Cause: Source Code Access

Quite often, multiple frameworks are developed by multiple teams, and development rules regulate that a certain team only has access to their own source code, and can only access functionality in the other frameworks using the public interfaces of compiled libraries or assemblies. This constraint is good in that it restricts each team from having a varying source code version of another team’s framework, but there is a problem as well. There are times when behavior must be added to another framework to allow for communication between the other frameworks. Without access to source code, each team must send numerous waves of change requests to the other teams, asking for modifications, and then other issues may arise when the public interfaces do not satisfy the needs of the team that requested them.

One solution to this problem is the use of wrappers encapsulating an external framework and attempting to build new functionality on top of the existing library. This approach has some problems though, such as considerable amounts of additional code and severe performance penalties. Additionally, if any logic is changed in the base framework, a change request must be sent to the development team of the framework.

The best solution is to either get the source code or establish a reliable and effective change request system where requests are dealt with almost immediately, and have a liaison from your team overseeing the modifications to make sure that requirements and needs are met correctly.

Relevant Design Patterns

Design patterns provide reusable solutions to commonly encountered programming problems, and there are a few that are applicable to this topic. The façade and adapter patterns are very beneficial to architectures that suffer from compositional friction, and can be employed to reduce the friction between multiple frameworks when used correctly.

Façade Design Pattern

This design pattern provides a unified high-level interface to a set of interfaces in a subsystem, thus making the subsystem easier to use. A subsystem can be defined as a set of classes or libraries that provide a solution to a given problem domain. A framework can be thought of as a subsystem in the context of this topic. A depiction of an architecture that is tightly coupled is shown in Figure 9.1. The façade pattern is depicted in Figure 9.2.

Depiction of tightly coupled architecture.

Figure 9.1. Depiction of tightly coupled architecture.

Depiction of the façade pattern.

Figure 9.2. Depiction of the façade pattern.

One benefit of this pattern is that classes in a subsystem are decoupled from the client interface, causing the architecture to be more portable and maintainable. Additionally, using the façade pattern reduces component dependencies, which can dramatically reduce compilation times of large software projects.

The following code shows how to implement the façade pattern in C#:

private class SubSystem1
{
    public void DoSomethingSpecific()
    {
        MessageBox.Show("Hello World #1");
    }
}

private class SubSystem2
{
    public void DoSomethingSpecific()
    {
        MessageBox.Show("Hello World #2");
    }
}
private class SubSystem3
{
    public void DoSomethingSpecific()
    {
        MessageBox.Show("Hello World #3");
    }
}

public class Facade
{
    private SubSystem1 _subSystem1 = new SubSystem1();
    private SubSystem2 _subSystem2 = new SubSystem2();
    private SubSystem3 _subSystem3 = new SubSystem3();

    public void DoSomething()
    {
        _subSystem1.DoSomethingSpecific();
        _subSystem2.DoSomethingSpecific();
    }

    public void DoAnotherThing()
    {
        _subSystem1.DoSomethingSpecific();
        _subSystem3.DoSomethingSpecific();
    }
}

public class Client
{
    public void Run(Facade facade)
    {
        facade.DoSomething();
        facade.DoAnotherThing();
    }
}

The façade pattern is quite useful when building new frameworks, but because this chapter is mainly addressing cohesion issues between existing frameworks, the adapter pattern is best suited for this problem.

Adapter Design Pattern

This design pattern is similar to the façade pattern, except the adapter pattern makes two existing interfaces work together instead of defining a new one. In order to fully understand the adapter pattern, there are some terms that should be defined. These terms are shown in Table 9.1.

Table 9.1. Adapter Pattern Elements

Name

Description

Target

The domain-specific interface that the client will use.

Adapter

An object that adapts the adaptee interface to the target interface.

Adaptee

An interface that needs adaptation to the target interface.

Client

The application that collaborates with the target interface.

It is possible to have the adapter class inherit from an adaptee, but doing so can lead to design problems when adapting to the target interface. A better approach is to store an instance of the adaptee inside the adapter class and access the instance explicitly. The adapter pattern is depicted in Figure 9.3.

Depiction of the adapter pattern.

Figure 9.3. Depiction of the adapter pattern.

The following code shows how to implement the adapter pattern in C#:

interface Target
{
    void DoSomething();
}

class Adaptee
{
    public void DoSomethingSpecific()
    {
        MessageBox.Show("Hello World!");
    }
}

class Adapter : Target
{
    private Adaptee _adaptee = new Adaptee();

    public void DoSomething()
    {
        _adaptee.DoSomethingSpecific();
    }
}

class Client
{
    public void Run(Target target)
    {
        target.DoSomething();
    }
}

The adapter pattern is not complicated to implement, but can be a great design move when you run into issues composing legacy components that do not support the required interface.

Even though there are a number of ways to reduce compositional friction and improve component cohesion, it was felt that extended coverage of adapters was important.

When dealing with unmanaged code (legacy components) that must interface with managed code, the adapter pattern is utilized often, whether explicitly or implicitly. If you think about it, exporting a COM interface from a managed application can be thought of as an implicit instance of the adapter pattern so that unmanaged applications can communicate with managed code. On a higher level, Windows Forms can also be thought of as an object-oriented adapter between any CLR-compliant language like C# and the traditional procedural controls available in the Win32 API. Lastly, you can find some adapters present in the .NET Class Framework itself. The database connection functionality employs adapters to interface with a variety of database engines. While each database engine is different, the base interfaces for dealing with them remain abstract and consistent.

It is important to note that there is increased performance overhead when using adapters because all methods called in the adaptee must first be called through the adapter methods. The best approach, disregarding any time, environment, or budget constraints, is to just refactor the code, but this is rarely the case when developing tools or games in general.

Part V, “Techniques for Legacy Interoperability,” covers interoperability between managed and unmanaged code, and specific real-world examples of using adapters are covered.

Conclusion

The introduction of object-oriented frameworks was a huge step forward in the area of software component reuse, but recently there has been a push towards the use of multiple frameworks within a single application. In this chapter, I discussed the issues behind using multiple frameworks, problems that occur from doing so, and some possible solutions to overcome these problems. Currently, there are some solutions to reduce compositional friction, such as the adapter pattern and employing wrapper objects, but these solutions only partially solve the problem when they too require a considerable amount of implementation effort. The best approach for framework reusability is to carefully study the problem domain to find the appropriate domain coverage, and build your frameworks from the ground up with a loosely coupled and maintainable architecture.

 

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.187.210