F# and Design Patterns for C# Developers
- 6/15/2013
- Using Object-Oriented Programming and Design Patterns
- Working with F# and Design Patterns
- Writing Design Patterns: Additional Notes
Writing Design Patterns: Additional Notes
As I mentioned in the beginning of this chapter, design patterns have been criticized since their birth. Many functional programmers believe that design patterns are not needed when programming in a functional style. Peter Norvig, in his paper “Design Patterns in Dynamic Languages,” claims that design patterns are just missing language features and demonstrates that design patterns can be simplified or eliminated completely when using a different language. I am not planning to be part of these discussions. Design patterns are a way to represent a system or idea. It is really a de facto and concise way for many computer professionals to describe system design. If the program is simple and small, design patterns are often unnecessary. For these scenarios, the use of basic data and flow-control structure is enough. However, when a program becomes large and complicated, a tested approach is needed to organize thinking and avoid possible design flaws or bugs. If the basic data structure is analogous to a word in a sentence, design patterns can be viewed as the idea to organize an article.
As a functional-first programming language, F# is adept at creating code with a functional style. For example, the pipeline and function composition operators make function operation much easier. Instead of being confined to a class, the function can be freely passed and processed like data in F#. If the design pattern is mainly about how to pass an action/operation or coordinate the flow of an operation, the pipeline and function composition operators can definitely simplify the implementation. The chain of responsibility pattern is an example. The biggest change from C# is that a function in F# is no longer auxiliary to the data; instead, it can be encapsulated, stored, and manipulated in a class. The data (field and property) in a class can actually be provided as a function or as method parameters and remain auxiliary to the function. Additionally, the presence of a class is optional if the class only serves as an operation container. The builder pattern demonstrates a way to eliminate the class while still implementing the same functionality.
Functional programming can still have a structure to encapsulate logic into a unit. Functions, which can be treated like data, can be encapsulated in a class or inside a closure and, more importantly, the application of object expressions provides an even simpler way to organize the code. Listing 3-33 shows different ways to encapsulate the data.
Listing 3-33 Data encapsulation
F# closure let myFunction () = let constValue = 100 let f () = [1..constValue] |> Seq.iter (printfn "%d") f() Object expression let obj = let ctor = printfn "constructor code" let field = ref 8 { new IA with member this.F() = printfn "%A" (!field) interface System.IDisposable with member this.Dispose() = ()}
Object expressions are great, because the type is created on the fly by the compiler. Instead of inventing a permanent boilerplate class to hold the function and data, you can use object expressions to quickly organize functions and data into a unit and get the job done. Imagine an investment bank with a bunch of mathematicians who lack a computer background: object expressions can let them quickly transform their knowledge into code without worrying about programmers complaining about their inability to implement complex inheritance hierarchies. The flattened structure from the object expression is a straightforward and suitable approach for quick prototyping and agile development. The command pattern is a good sample for demonstrating how to use object expressions to simplify the design.
Both functional programming and object-oriented programming have their own way of reusing the code. Object-oriented programming uses inheritance, while functional programming uses higher-order functions. Both approaches have loyal followers, and you might already be convinced that one is superior to the other. I say that both approaches have their own advantages under certain circumstances. Unfortunately, neither is a silver bullet that can be used to solve all problems. Using the right tool for the right job is the key. F#, which supports both OO and functional programming, provides both approaches, and this gives the developer the liberty to use the best way to perform the system design.
F# provides the alternative to encapsulation (object expressions) and inheritance (higher order functions): polymorphism. It can also be implemented by higher-order functions when given different parameters. This is yet another example of how F# provides a wide set of tools for developers to implement their components and systems.
In addition, the adapter pattern introduces the GI function, which breaks class encapsulation and makes possible communication between objects that do not share a common base class. It is not a recommended way to use the original object-oriented design; however, it is a feasible approach to wrap legacy code because of inaccessibility to the source code. It is not fair to blame a gun for causing crime and not blame the criminal. Likewise, F# provides this approach, but I’ll leave the decision to you regarding when and how to use it.
It is totally fine to copy a standard object-oriented approach when doing system design, especially when someone is new to a language. If you are motivated to use F# to write design patterns, here are some principles that I used to implement the design patterns in this chapter. If the design pattern is a behavior design pattern, its main focus is on how to organize the function, so consider using the function composition and pipeline operators. If the function needs to be organized into a unit, put the function into a module and use object expressions to organize the function. If the design pattern is a structural design pattern, I always question why extra structure is needed. If the extra structure is a placeholder for functionality, higher-order functions most likely will do the same job. If the extra structure is needed to make two unrelated objects work together, the GI function could be a good candidate to simplify the design.
F# is a young language and how to properly apply its language feature into the system design is still a new topic. Keep in mind that F# provides the OOP way of implementing class encapsulation, inheritance, and polymorphism. This chapter is only a small step to explore how to use F# in system design.