|
|
Construction - Essential Concepts
This page outlines the essential aspects of software construction that every developer has to keep in mind while constructing applications. The goal here is to keep in mind that you are constructing an Application Framework, not just an application. This thought will go a log way.
It is recommended that the reader should read the Software Development Life Cycle article first, before delving into this page
(1) Reusability
|
Reusability is achieved by having a single implementation base of
common functionality, whether it is at the platform layer, or at the
business process layer.
Common tasks like Encryption, Transport, Logging, Database Access,
Security, etc need to be uniform across the enterprise in order to be
useful. The right place for these components is an Enterprise Class
Library that is available to all developers. The use of an Enterprise Class Library (ECL) that
incorporates these features should be made Mandatory, so every
developer does not have to come up with his own implementation and
strategy for these. Explore and identify the existence of an Enterprise
Class Library. If there isn’t one, inform your manager and
collaborate with other developers to come up with one. If there is an
ECL that already exists then get a copy of it on your computer and
explore it. If you find functionality that is inadequate or missing,
you should incorporate this into the ECL so not only you, but the
entire team can use it.
Cross Platform ECL will require some extra work and wrapping if you
work across a multitude of platforms, but that is where Service
Oriented Architecture (SOA) steps in. The goal is to drive Business
Process Productivity instead of spending time in Platform Engineering,
or re-engineering the same functionality on multiple platforms. For
example, an Asynchronous logging routine written in C# can be
encapsulated in a fire and forget Web Service, which uses the Logging
ECL. Now we could implement downloadable stubs for C, C++ and Java
applications that could then implement logging without having to
re-invent the wheel. Another benefit of this approach is that logging
becomes centralized.
|
(2) Componentization
|
Componentization is achieved not only through an ECL, but different
parts of the Business Processing Framework can be componentized as
well. This enables implementation of specific entity-driven or
process-driven functionality in one place.
For example, both the desktop store clerk application and the
e-commerce web site may require the use of a Customer class with a
(overloaded) Search function that enables the applications to retrieve
a customer by a combination first name, last name, email, member
number, Date Of Birth, phone number or even item last purchased. By
implementing the Customer Class in a Business
Class Library (BCL), we can enable Developer A and Developer B
working on the same use case in multiple applications to share one
implementation. Of course, this would also mean that when Impact
analysis is done on the BCL, changes might need to be propagated to
both applications. But that cost has almost always proven to be lower
than the cost of maintaining redundant implementations.
|
(3) Scalability
|
Scalability in a platform is defined as the ability of the platform to
handle an increased number of user requests and users themselves,
(Throughput, in simple words) without adverse degradation in
functionality. There are two ways to scale a platform.
Scaling Up is defined as the
ability to increase Hardware or Software parameters in the system
configuration to enable more throughput – this includes
activities like increasing the software configuration for thread count
or number of users, changing the network link to higher speeds or
changing the hardware configuration by adding more resources in terms
of Memory, Processor Speed, Number of Processors, Hard Drive Space,
Hard Drive Speed, etc.
Scaling Out is quite a
different animal. You can scale out by making copies of your
implementation and directing user traffic, and user request traffic to
these multiple copies. Many techniques exist for this –
Replication, Web Farming, DNS Round Robin, Network Load Balancing
(NLB), Server Load Balancing, Quota Management, etc. You will have to
do independent research to understand each of these, but the point of
bringing this up here is that in order for your application to support
scaling out, it has to be designed in a particular way. For example,
web servers that maintain a user session cannot afford to direct user
requests within a session to a different server in the farm. This is
where you need to come up with a loosely coupled architecture where
session data is stored in the Database instead of the web server. This
would also mean that the user needs to include a unique session
identifier in each request. These and many more are the reasons why
scalability planning requires a lot of thinking ahead and planning.
|
(4) Visibility
|
Logging is Key to obtaining
visibility into the application’s performance. Every application
should log in order to provide visibility into Application Execution.
Logging should be configurable at various levels – Informational,
Warning, Errors, Success Audits and Failure Audits. It is recommended
to use an ECL while logging from within applications, even across
platforms. While it is ideal to log to the Windows event log or a
database table, logging to a Text File is acceptable under exceptional
circumstances. Understand that file I/O happens in all cases, however,
directly logging to a file is an extremely single threaded activity.
In any event, the Fire-and Forget Model (Asynchronous Logging) should
be utilized or Logging will impede the primary thread of execution. The
Enterprise Instrumentation Framework (EIF) in Windows has a
Fire-And-Forget Event Logging Model. A good example is the
EventLog.WriteEntry method in the .NET Framework. An application can
initiate numerous logging requests in multiple threads. All the
requests are queued and control returns to the primary invoking thread
almost instantaneously, ensuring that logging will happen, but in a
lower priority thread, thus ensuring performance.
In any case, Logging does otherwise have a performance impact.
Therefore, informational message logging should only happen when the
application is run in Debug mode
during troubleshooting. Logging to a Database is also permitted, as
long as it is not performance intense (Append only – No Key
Lookup) and asynchronous.
Documentation is also key to visibility. It should be mandatory for
applications to use standard, well-defined error codes on a
per-application basis. These should be documented centrally and made
available to the Production Support team as it will aid them in both
identifying the error and route the corrective action accordingly.
|
(5) Extensibility
|
Extensibility is defined as the feature of an Application or Module to
accommodate increased functionality with minimal impact to the existing
codebase or functionality, and with increased re-usability.
Extensibility depends directly on Modularity.
Code should be designed in the form of individual blocks, each of which
performs its distinct atomic function. This simply means that we should
refrain from writing long procedural blocks of code that execute a
complex sequence with a lot of branching, looping and especially
control transfer. Instead, it should be broken into a set of individual
method invocations. That way, the code is readable and broken down into
segments and if there is a need to accommodate another step, or
different functionality, it is possible to do so by injecting another
method call and adding a new method as opposed to injecting a block of
code to a pre-existing block of code.
In addition to modularity, Inheritance
is critical to sharing code across a set of classes without having to
incorporate the shared code in each module. One should implement
functionality that is shared by numerous classes in a base class. It
may not be possible for every class to have multiple base classes, in
which case using of strongly typed interfaces is recommended.
For example, if we want each screen in our application to support
hibernation – the ability to persist the application to disk and
take off at any point, it becomes critical that the Screen classes
(Forms) in the application implement an interface that contains a
Serialize method (like ISerializable) which forces every screen to
implement the Serialize method, thus enforcing the ability to persist
to disk. It may be impossible to ensure that the implementation of
serialization in our example is going to work. The only way to ensure
that is to govern the Unit test cases and code itself using the
enforced SDLC Steps as we outlined earlier in this document.
We shall illustrate this with a real life example. Let us say that we
have a Customer class in our application which implements a Search()
method that returns an array of CustomerDetail class
instances based on valid search criteria. Now imagine that you
have extended your application to also enable search in a business
partner's database. Example being the automotive industry. Your web
site currently services GM customers. Now, all of a sudden BMW and
Mercedes want you to handle their customers also. You would create a
generic Customer base class and create three specialized classes -
GMCustomer, BMWCustomer and MercedesCustomer, all three implementing
the Search() method against the specific database. Again BMW may not
want to share the data with you, but might want you to access their web
service instead, while GM offers a data feed into your table. One
Behavior - Multiple implementations. With inheritance, you can make
this happen. Your application can continue using a reference to the
Customer class and not have to worry about the actual implementation
(extension).
The above example also illustrates Polymorphism
- The ability to implement multiple implementations of code and enable
as single invocation or instantiation to utilize different
implementations based on context. Polymorphism is illustrated in two
manners in the above class example.
Instantiation-driven: For one,
depending on the system context during construction, the application
can use a reference to the Customer class to either instantiate an
instance of the BMWCustomer, or an instance of the GMCustomer class.
This means that results can be obtained from a specific system with a
different search implementation based on system.
Note that you can also model classes to use these different
implementations of the Customer class to aggregate the results of the
customer search. What if your search screen wanted to search across
systems? This aggregation could not be achieved without the use of a
Factory Class or a specific CustomerSearch class, which in turn invoked
the search on each of the individual implementations and returned the
aggregated result in the form of an array of references of type
CustomerDetails.
Invocation-Driven: Understand
that the Search method itself can contain several specific
implementations depending on the order and type of arguments used -
Method overloading. It is obvious that the valid return type for all
these search implementations should be an array of references to the
type CustomerDetails, but depending on the arguments, different method
implementations will be invoked. This is the second type of
polymorphism that the above example demonstrates.
While there are multiple implementations of Customer, and there are
multiple ways to implement this search, based on parameters, some
criteria remain common to all the implementations by class and by
method. This is where inheritance promotes reusability. You can
implement common functionality like connecting to the database (ODBC),
retrieving the connection string, decrypting data, etc into the base
class Customer, thus enabling every implementation of the class to not
worry about implementing the base features. This can greatly increase
productivity.
|
(6) Data Access Strategy
|
Isolation: You could use
technologies like LINQ to loosely couple your implementation with the
actual database implementation, but there is a cost associated with
this level of isolation. The reasoning behind the approach of
refraining from implementing extensive logic in stored procedures,
especially triggers at the database layer is that doing so greatly
reduces visibility to program logic in the code thus making the task of
understanding logic a two-tiered task.
Abstraction: A better strategy is Abstraction. Irrespective of
database, certian functionality is very common to across all databases.
Methods like CheckDatabaseConnection(), RetrieveDatabaseQueryString(),
ConnectToDatabase(), ExecuteQuery(), ExecuteScalar(),
ExecuteSqlStatement() are used by applications irrespective of the
database. This is where it makes great sense to use a Database Access
Layer (DAL) component that ensures that the database connectivity is
always happening through a centralized unit of code. Therefore, if
there is ever a need to make changes to the database integration, for
example, if you wanted to log the time before invocation and after
result delivery to see if database access is a bottleneck this could be
implemented in the DAL component instead of changing every block of
code in your application that accesses the database.
|
|
|