├── LICENSE ├── README.md ├── csharp ├── .gitignore ├── .vscode │ ├── launch.json │ └── tasks.json ├── FunctionalRefactoring.Tests │ ├── AppTests.cs │ └── FunctionalRefactoring.Tests.csproj ├── FunctionalRefactoring.sln └── FunctionalRefactoring │ ├── App.cs │ ├── FunctionalRefactoring.csproj │ ├── IStorage.cs │ └── Models │ ├── Amount.cs │ ├── Cart.cs │ ├── CartId.cs │ ├── CustomerId.cs │ └── DiscountRule.cs ├── fsharp ├── FunctionalRefactoring.Tests │ ├── AppTests.cs │ ├── FunctionalRefactoring.Tests.csproj │ ├── Properties │ │ └── AssemblyInfo.cs │ └── packages.config ├── FunctionalRefactoring.sln └── FunctionalRefactoring │ ├── App.fs │ ├── AssemblyInfo.fs │ ├── FunctionalRefactoring.fsproj │ ├── Models.fs │ └── packages.config ├── java ├── .gitignore ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── functionalrefactoring │ │ ├── App.java │ │ └── models │ │ ├── Amount.java │ │ ├── Cart.java │ │ ├── CartId.java │ │ ├── CustomerId.java │ │ ├── DiscountRule.java │ │ └── Storage.java │ └── test │ └── java │ └── org │ └── functionalrefactoring │ └── AppTest.java ├── kotlin ├── .gitignore ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ ├── main │ └── kotlin │ │ └── org │ │ └── functionalrefactoring │ │ ├── App.kt │ │ └── models │ │ ├── Amount.kt │ │ ├── Cart.kt │ │ ├── CartId.kt │ │ ├── CustomerId.kt │ │ ├── DiscountRule.kt │ │ └── Storage.kt │ └── test │ └── kotlin │ └── org │ └── functionalrefactoring │ └── AppTest.kt ├── ruby ├── .gitignore ├── Gemfile ├── Rakefile ├── lib │ ├── app.rb │ └── models.rb └── test │ └── app_test.rb ├── scala ├── .gitignore ├── build.sbt ├── project │ ├── build.properties │ └── plugins.sbt └── src │ ├── main │ └── scala │ │ └── org │ │ └── fprefactoring │ │ ├── App.scala │ │ ├── Models.scala │ │ └── Storage.scala │ └── test │ └── scala │ └── org │ └── fprefactoring │ └── AppTests.scala └── typescript ├── .editorconfig ├── .gitignore ├── .vscode └── launch.json ├── package-lock.json ├── package.json ├── src ├── app.ts ├── models.ts └── storage.ts ├── test ├── stub │ └── storage.ts └── test.ts └── tsconfig.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Matteo Baglini 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Functional Structures Refactoring Kata 2 | 3 | This refactoring kata aims to let you practice with one of the main functional programming building block: **Functional Structures**. 4 | 5 | The starting code (provided in different languages) implements a use case with an imperative approach. Your mission is to **remove every effects** (aka computational context) within the ``applyDiscount`` function with the power of Functional Structures. 6 | 7 | # How to use this Kata 8 | 9 | The simplest way is to just clone the code and start hacking right away improving the design. The project includes some tests useful to make sure you don't break the code while you refactor. 10 | 11 | # Proposed Solution 12 | 13 | You can take a look at my refactored code (in Scala) wich you will find inside one of the [solution-* branchs](https://github.com/matteobaglini/functional-structures-refactoring-kata/branches). 14 | 15 | # Learning Materials 16 | 17 | - [Why Functional Programming? It’s the composition - Kailuo Wang](https://tech.iheart.com/why-fp-its-the-composition-f585d17b01d3) 18 | - [Functors, Applicatives, And Monads In Pictures - Aditya Bhargava](http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html) 19 | - [Functional Programming with Effects - Rob Norris](https://www.youtube.com/watch?v=po3wmq4S15A) 20 | - [Scala Typeclassopedia - John Kodumal](https://www.youtube.com/watch?v=IMGCDph1fNY) 21 | - [Functional Structures in Scala - Michael Pilquist](https://www.youtube.com/watch?v=Dsd4pc99FSY&list=PLFrwDVdSrYE6dy14XCmUtRAJuhCxuzJp0) 22 | - [A Pragmatic Introduction to Category Theory - Daniela Sfregola](https://www.youtube.com/watch?v=MvQxNm5gn8g) 23 | - [Category Theory for the Working Hacker - Philip Wadler](https://www.youtube.com/watch?v=V10hzjgoklA) 24 | 25 | ## Contributing 26 | 27 | Is your favorite language missing? Want to contribute? Awesome! Feel free to submit an Issue or a Pull Request. 28 | 29 | # License 30 | This project is licensed under the terms of the MIT license. 31 | -------------------------------------------------------------------------------- /csharp/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ### Visual Studio ### 3 | .vs/ 4 | *.suo 5 | *.user 6 | [Bb]in 7 | [Oo]bj 8 | 9 | ### NuGet ### 10 | **/packages/* 11 | -------------------------------------------------------------------------------- /csharp/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/FunctionalRefactoring.Tests/bin/Debug/netcoreapp2.2/FunctionalRefactoring.Tests.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/FunctionalRefactoring.Tests", 16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": ".NET Core Attach", 22 | "type": "coreclr", 23 | "request": "attach", 24 | "processId": "${command:pickProcess}" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /csharp/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/FunctionalRefactoring.Tests/FunctionalRefactoring.Tests.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/FunctionalRefactoring.Tests/FunctionalRefactoring.Tests.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/FunctionalRefactoring.Tests/FunctionalRefactoring.Tests.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /csharp/FunctionalRefactoring.Tests/AppTests.cs: -------------------------------------------------------------------------------- 1 | using FunctionalRefactoring.Models; 2 | using Xunit; 3 | 4 | namespace FunctionalRefactoring.Tests 5 | { 6 | public class AppTests 7 | { 8 | [Fact] 9 | public void HappyPath() 10 | { 11 | var cartId = new CartId("some-gold-cart"); 12 | var storage = new SpyStorage(); 13 | 14 | App.ApplyDiscount(cartId, storage); 15 | 16 | var expected = new Cart(new CartId("some-gold-cart"), new CustomerId("gold-customer"), new Amount(50)); 17 | 18 | Assert.Equal(expected, storage.Saved); 19 | } 20 | 21 | [Fact] 22 | public void NoDiscount() 23 | { 24 | var cartId = new CartId("some-normal-cart"); 25 | var storage = new SpyStorage(); 26 | 27 | App.ApplyDiscount(cartId, storage); 28 | 29 | Assert.Null(storage.Saved); 30 | } 31 | 32 | [Fact] 33 | public void MissingCart() 34 | { 35 | var cartId = new CartId("missing-cart"); 36 | var storage = new SpyStorage(); 37 | 38 | App.ApplyDiscount(cartId, storage); 39 | 40 | Assert.Null(storage.Saved); 41 | } 42 | 43 | class SpyStorage : IStorage 44 | { 45 | public Cart Saved { get; private set; } 46 | 47 | public void Flush(Cart item) 48 | { 49 | Saved = item; 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /csharp/FunctionalRefactoring.Tests/FunctionalRefactoring.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.2 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /csharp/FunctionalRefactoring.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FunctionalRefactoring", "FunctionalRefactoring\FunctionalRefactoring.csproj", "{6B36E4C9-F8CC-4F5E-8BA3-6487F0637ACD}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FunctionalRefactoring.Tests", "FunctionalRefactoring.Tests\FunctionalRefactoring.Tests.csproj", "{596EBEC4-7938-40B5-BFEC-0C0AF38664C5}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Debug|x64 = Debug|x64 14 | Debug|x86 = Debug|x86 15 | Release|Any CPU = Release|Any CPU 16 | Release|x64 = Release|x64 17 | Release|x86 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {6B36E4C9-F8CC-4F5E-8BA3-6487F0637ACD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {6B36E4C9-F8CC-4F5E-8BA3-6487F0637ACD}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {6B36E4C9-F8CC-4F5E-8BA3-6487F0637ACD}.Debug|x64.ActiveCfg = Debug|Any CPU 26 | {6B36E4C9-F8CC-4F5E-8BA3-6487F0637ACD}.Debug|x64.Build.0 = Debug|Any CPU 27 | {6B36E4C9-F8CC-4F5E-8BA3-6487F0637ACD}.Debug|x86.ActiveCfg = Debug|Any CPU 28 | {6B36E4C9-F8CC-4F5E-8BA3-6487F0637ACD}.Debug|x86.Build.0 = Debug|Any CPU 29 | {6B36E4C9-F8CC-4F5E-8BA3-6487F0637ACD}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {6B36E4C9-F8CC-4F5E-8BA3-6487F0637ACD}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {6B36E4C9-F8CC-4F5E-8BA3-6487F0637ACD}.Release|x64.ActiveCfg = Release|Any CPU 32 | {6B36E4C9-F8CC-4F5E-8BA3-6487F0637ACD}.Release|x64.Build.0 = Release|Any CPU 33 | {6B36E4C9-F8CC-4F5E-8BA3-6487F0637ACD}.Release|x86.ActiveCfg = Release|Any CPU 34 | {6B36E4C9-F8CC-4F5E-8BA3-6487F0637ACD}.Release|x86.Build.0 = Release|Any CPU 35 | {596EBEC4-7938-40B5-BFEC-0C0AF38664C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {596EBEC4-7938-40B5-BFEC-0C0AF38664C5}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {596EBEC4-7938-40B5-BFEC-0C0AF38664C5}.Debug|x64.ActiveCfg = Debug|Any CPU 38 | {596EBEC4-7938-40B5-BFEC-0C0AF38664C5}.Debug|x64.Build.0 = Debug|Any CPU 39 | {596EBEC4-7938-40B5-BFEC-0C0AF38664C5}.Debug|x86.ActiveCfg = Debug|Any CPU 40 | {596EBEC4-7938-40B5-BFEC-0C0AF38664C5}.Debug|x86.Build.0 = Debug|Any CPU 41 | {596EBEC4-7938-40B5-BFEC-0C0AF38664C5}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {596EBEC4-7938-40B5-BFEC-0C0AF38664C5}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {596EBEC4-7938-40B5-BFEC-0C0AF38664C5}.Release|x64.ActiveCfg = Release|Any CPU 44 | {596EBEC4-7938-40B5-BFEC-0C0AF38664C5}.Release|x64.Build.0 = Release|Any CPU 45 | {596EBEC4-7938-40B5-BFEC-0C0AF38664C5}.Release|x86.ActiveCfg = Release|Any CPU 46 | {596EBEC4-7938-40B5-BFEC-0C0AF38664C5}.Release|x86.Build.0 = Release|Any CPU 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /csharp/FunctionalRefactoring/App.cs: -------------------------------------------------------------------------------- 1 | using FunctionalRefactoring.Models; 2 | 3 | namespace FunctionalRefactoring 4 | { 5 | public static class App 6 | { 7 | public static void ApplyDiscount(CartId cartId, IStorage storage) 8 | { 9 | var cart = LoadCart(cartId); 10 | if (cart != Cart.MissingCart) 11 | { 12 | var rule = LookupDiscountRule(cart.CustomerId); 13 | if (rule != DiscountRule.NoDiscount) 14 | { 15 | var discount = rule.Compute(cart); 16 | var updatedCart = UpdateAmount(cart, discount); 17 | Save(updatedCart, storage); 18 | } 19 | } 20 | } 21 | 22 | static Cart LoadCart(CartId id) 23 | { 24 | if (id.Value.Contains("gold")) 25 | return new Cart(id, new CustomerId("gold-customer"), new Amount(100)); 26 | if (id.Value.Contains("normal")) 27 | return new Cart(id, new CustomerId("normal-customer"), new Amount(100)); 28 | return Cart.MissingCart; 29 | } 30 | 31 | static DiscountRule LookupDiscountRule(CustomerId id) 32 | { 33 | if (id.Value.Contains("gold")) return new DiscountRule(Half); 34 | return DiscountRule.NoDiscount; 35 | } 36 | 37 | static Cart UpdateAmount(Cart cart, Amount discount) 38 | { 39 | return new Cart(cart.Id, cart.CustomerId, new Amount(cart.Amount.Value - discount.Value)); 40 | } 41 | 42 | static void Save(Cart cart, IStorage storage) 43 | { 44 | storage.Flush(cart); 45 | } 46 | 47 | static Amount Half(Cart cart) => 48 | new Amount(cart.Amount.Value / 2); 49 | } 50 | } -------------------------------------------------------------------------------- /csharp/FunctionalRefactoring/FunctionalRefactoring.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /csharp/FunctionalRefactoring/IStorage.cs: -------------------------------------------------------------------------------- 1 | namespace FunctionalRefactoring 2 | { 3 | public interface IStorage 4 | { 5 | void Flush(T item); 6 | } 7 | } -------------------------------------------------------------------------------- /csharp/FunctionalRefactoring/Models/Amount.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FunctionalRefactoring.Models 4 | { 5 | public class Amount 6 | { 7 | public Decimal Value { get; } 8 | 9 | public Amount(Decimal value) => Value = value; 10 | 11 | Boolean Equals(Amount other) => Value == other.Value; 12 | 13 | public override Boolean Equals(Object obj) 14 | { 15 | if (ReferenceEquals(null, obj)) return false; 16 | if (ReferenceEquals(this, obj)) return true; 17 | if (obj.GetType() != typeof(Amount)) return false; 18 | return Equals((Amount) obj); 19 | } 20 | 21 | public override Int32 GetHashCode() => Value.GetHashCode(); 22 | 23 | public static Boolean operator ==(Amount left, Amount right) => Equals(left, right); 24 | 25 | public static Boolean operator !=(Amount left, Amount right) => !Equals(left, right); 26 | } 27 | } -------------------------------------------------------------------------------- /csharp/FunctionalRefactoring/Models/Cart.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FunctionalRefactoring.Models 4 | { 5 | public class Cart 6 | { 7 | public static readonly Cart MissingCart = new Cart(new CartId(""), new CustomerId(""), new Amount(0)); 8 | 9 | public CartId Id { get; } 10 | public CustomerId CustomerId { get; } 11 | public Amount Amount { get; } 12 | 13 | public Cart(CartId id, CustomerId customerId, Amount amount) 14 | { 15 | Id = id; 16 | CustomerId = customerId; 17 | Amount = amount; 18 | } 19 | 20 | Boolean Equals(Cart other) => Equals(Id, other.Id) 21 | && Equals(CustomerId, other.CustomerId) 22 | && Equals(Amount, other.Amount); 23 | 24 | public override Boolean Equals(Object obj) 25 | { 26 | if (ReferenceEquals(null, obj)) return false; 27 | if (ReferenceEquals(this, obj)) return true; 28 | if (obj.GetType() != typeof(Cart)) return false; 29 | return Equals((Cart)obj); 30 | } 31 | 32 | public override Int32 GetHashCode() 33 | { 34 | unchecked 35 | { 36 | var hashCode = Id != null ? Id.GetHashCode() : 0; 37 | hashCode = (hashCode * 397) ^ (CustomerId != null ? CustomerId.GetHashCode() : 0); 38 | hashCode = (hashCode * 397) ^ (Amount != null ? Amount.GetHashCode() : 0); 39 | return hashCode; 40 | } 41 | } 42 | 43 | public static Boolean operator ==(Cart left, Cart right) => Equals(left, right); 44 | 45 | public static Boolean operator !=(Cart left, Cart right) => !Equals(left, right); 46 | } 47 | } -------------------------------------------------------------------------------- /csharp/FunctionalRefactoring/Models/CartId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FunctionalRefactoring.Models 4 | { 5 | public class CartId 6 | { 7 | public String Value { get; } 8 | 9 | public CartId(String value) => Value = value; 10 | 11 | Boolean Equals(CartId other) => String.Equals(Value, other.Value); 12 | 13 | public override Boolean Equals(Object obj) 14 | { 15 | if (ReferenceEquals(null, obj)) return false; 16 | if (ReferenceEquals(this, obj)) return true; 17 | if (obj.GetType() != typeof(CartId)) return false; 18 | return Equals((CartId)obj); 19 | } 20 | 21 | public override Int32 GetHashCode() => Value != null ? Value.GetHashCode() : 0; 22 | 23 | public static Boolean operator ==(CartId left, CartId right) => Equals(left, right); 24 | 25 | public static Boolean operator !=(CartId left, CartId right) => !Equals(left, right); 26 | } 27 | } -------------------------------------------------------------------------------- /csharp/FunctionalRefactoring/Models/CustomerId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FunctionalRefactoring.Models 4 | { 5 | public class CustomerId 6 | { 7 | public String Value { get; } 8 | 9 | public CustomerId(String value) => Value = value; 10 | 11 | Boolean Equals(CustomerId other) => String.Equals(Value, other.Value); 12 | 13 | public override Boolean Equals(Object obj) 14 | { 15 | if (ReferenceEquals(null, obj)) return false; 16 | if (ReferenceEquals(this, obj)) return true; 17 | if (obj.GetType() != typeof(CustomerId)) return false; 18 | return Equals((CustomerId)obj); 19 | } 20 | 21 | public override Int32 GetHashCode() => Value != null ? Value.GetHashCode() : 0; 22 | 23 | public static Boolean operator ==(CustomerId left, CustomerId right) => Equals(left, right); 24 | 25 | public static Boolean operator !=(CustomerId left, CustomerId right) => !Equals(left, right); 26 | } 27 | } -------------------------------------------------------------------------------- /csharp/FunctionalRefactoring/Models/DiscountRule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FunctionalRefactoring.Models 4 | { 5 | public class DiscountRule 6 | { 7 | public static readonly DiscountRule NoDiscount = new DiscountRule(c => throw new InvalidOperationException("no discount")); 8 | 9 | public Func Compute { get; } 10 | 11 | public DiscountRule(Func compute) => Compute = compute; 12 | 13 | Boolean Equals(DiscountRule other) => Equals(Compute, other.Compute); 14 | 15 | public override Boolean Equals(Object obj) 16 | { 17 | if (ReferenceEquals(null, obj)) return false; 18 | if (ReferenceEquals(this, obj)) return true; 19 | if (obj.GetType() != typeof(DiscountRule)) return false; 20 | return Equals((DiscountRule)obj); 21 | } 22 | 23 | public override Int32 GetHashCode() 24 | { 25 | return Compute != null ? Compute.GetHashCode() : 0; 26 | } 27 | 28 | public static Boolean operator ==(DiscountRule left, DiscountRule right) => Equals(left, right); 29 | 30 | public static Boolean operator !=(DiscountRule left, DiscountRule right) => !Equals(left, right); 31 | } 32 | } -------------------------------------------------------------------------------- /fsharp/FunctionalRefactoring.Tests/AppTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | using static FunctionalRefactoring.Models; 3 | 4 | namespace FunctionalRefactoring.Tests 5 | { 6 | public class AppTests 7 | { 8 | [Fact] 9 | public void HappyPath() 10 | { 11 | var cartId = CartId.NewCartId("some-gold-cart"); 12 | var storage = new SpyStorage(); 13 | 14 | App.ApplyDiscount(cartId, storage); 15 | 16 | var expected = new Cart( 17 | CartId.NewCartId("some-gold-cart"), 18 | CustomerId.NewCustomerId("gold-customer"), 19 | Amount.NewAmount(50)); 20 | 21 | Assert.Equal(expected, storage.Saved); 22 | } 23 | 24 | [Fact] 25 | public void NoDiscount() 26 | { 27 | var cartId = CartId.NewCartId("some-normal-cart"); 28 | var storage = new SpyStorage(); 29 | 30 | App.ApplyDiscount(cartId, storage); 31 | 32 | Assert.Null(storage.Saved); 33 | } 34 | 35 | [Fact] 36 | public void MissingCart() 37 | { 38 | var cartId = CartId.NewCartId("missing-cart"); 39 | var storage = new SpyStorage(); 40 | 41 | App.ApplyDiscount(cartId, storage); 42 | 43 | Assert.Null(storage.Saved); 44 | } 45 | 46 | class SpyStorage : IStorage 47 | { 48 | public Cart Saved { get; private set; } 49 | 50 | public void Flush(Cart item) 51 | { 52 | Saved = item; 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /fsharp/FunctionalRefactoring.Tests/FunctionalRefactoring.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Debug 7 | AnyCPU 8 | {5BB7E114-EFD4-462D-82EC-8CA6D782432A} 9 | Library 10 | Properties 11 | FunctionalRefactoring.Tests 12 | FunctionalRefactoring.Tests 13 | v4.6.1 14 | 512 15 | 16 | 17 | 18 | 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | 27 | 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ..\packages\xunit.abstractions.2.0.1\lib\net35\xunit.abstractions.dll 46 | 47 | 48 | ..\packages\xunit.assert.2.3.1\lib\netstandard1.1\xunit.assert.dll 49 | 50 | 51 | ..\packages\xunit.extensibility.core.2.3.1\lib\netstandard1.1\xunit.core.dll 52 | 53 | 54 | ..\packages\xunit.extensibility.execution.2.3.1\lib\net452\xunit.execution.desktop.dll 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | {ccfc6382-4ea5-46ca-a317-d8ba51272445} 70 | FunctionalRefactoring 71 | 72 | 73 | 74 | 75 | 76 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /fsharp/FunctionalRefactoring.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("FunctionalRefactoring.Tests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("FunctionalRefactoring.Tests")] 13 | [assembly: AssemblyCopyright("Copyright © 2017")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("5bb7e114-efd4-462d-82ec-8ca6d782432a")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /fsharp/FunctionalRefactoring.Tests/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /fsharp/FunctionalRefactoring.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27004.2006 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FunctionalRefactoring.Tests", "FunctionalRefactoring.Tests\FunctionalRefactoring.Tests.csproj", "{5BB7E114-EFD4-462D-82EC-8CA6D782432A}" 7 | EndProject 8 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FunctionalRefactoring", "FunctionalRefactoring\FunctionalRefactoring.fsproj", "{CCFC6382-4EA5-46CA-A317-D8BA51272445}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {5BB7E114-EFD4-462D-82EC-8CA6D782432A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {5BB7E114-EFD4-462D-82EC-8CA6D782432A}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {5BB7E114-EFD4-462D-82EC-8CA6D782432A}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {5BB7E114-EFD4-462D-82EC-8CA6D782432A}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {CCFC6382-4EA5-46CA-A317-D8BA51272445}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {CCFC6382-4EA5-46CA-A317-D8BA51272445}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {CCFC6382-4EA5-46CA-A317-D8BA51272445}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {CCFC6382-4EA5-46CA-A317-D8BA51272445}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {FE5044EA-1BEE-4B3E-86E0-494013B2DB03} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /fsharp/FunctionalRefactoring/App.fs: -------------------------------------------------------------------------------- 1 | module FunctionalRefactoring.App 2 | 3 | open Models 4 | 5 | let loadCart cartId = 6 | match cartId with 7 | | CartId id when id.Contains "gold" -> 8 | { cartId = cartId; customerId = CustomerId "gold-customer"; amount = Amount 100m } 9 | | CartId id when id.Contains "normal" -> 10 | { cartId = cartId; customerId = CustomerId "normal-customer"; amount = Amount 100m } 11 | | CartId _ -> missingCart 12 | 13 | let half cart = 14 | match cart.amount with 15 | | Amount x -> Amount (x / 2m) 16 | 17 | let lookupDiscountRule (CustomerId id) = 18 | if id.Contains "gold" 19 | then DiscountRule half 20 | else noDiscount 21 | 22 | let updateAmount(cart, Amount discount) = 23 | let (Amount full) = cart.amount 24 | { cart with amount = Amount (full - discount) } 25 | 26 | let save (storage: IStorage<_>) = storage.Flush 27 | 28 | let ApplyDiscount (cartId, storage) = 29 | let cart = loadCart cartId 30 | if cart <> missingCart 31 | then 32 | let rule = lookupDiscountRule cart.customerId 33 | if rule <> noDiscount 34 | then 35 | let (DiscountRule f) = rule 36 | let discount = f cart 37 | let updatedCart = updateAmount(cart, discount) 38 | save storage updatedCart -------------------------------------------------------------------------------- /fsharp/FunctionalRefactoring/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | namespace FunctionalRefactoring.AssemblyInfo 2 | 3 | open System.Reflection 4 | open System.Runtime.CompilerServices 5 | open System.Runtime.InteropServices 6 | 7 | // General Information about an assembly is controlled through the following 8 | // set of attributes. Change these attribute values to modify the information 9 | // associated with an assembly. 10 | [] 11 | [] 12 | [] 13 | [] 14 | [] 15 | [] 16 | [] 17 | [] 18 | 19 | // Setting ComVisible to false makes the types in this assembly not visible 20 | // to COM components. If you need to access a type in this assembly from 21 | // COM, set the ComVisible attribute to true on that type. 22 | [] 23 | 24 | // The following GUID is for the ID of the typelib if this project is exposed to COM 25 | [] 26 | 27 | // Version information for an assembly consists of the following four values: 28 | // 29 | // Major Version 30 | // Minor Version 31 | // Build Number 32 | // Revision 33 | // 34 | // You can specify all the values or you can default the Build and Revision Numbers 35 | // by using the '*' as shown below: 36 | // [] 37 | [] 38 | [] 39 | 40 | do 41 | () -------------------------------------------------------------------------------- /fsharp/FunctionalRefactoring/FunctionalRefactoring.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | 2.0 8 | ccfc6382-4ea5-46ca-a317-d8ba51272445 9 | Library 10 | FunctionalRefactoring 11 | FunctionalRefactoring 12 | v4.6.1 13 | 4.4.1.0 14 | true 15 | FunctionalRefactoring 16 | 17 | 18 | true 19 | full 20 | false 21 | false 22 | bin\$(Configuration)\ 23 | DEBUG;TRACE 24 | 3 25 | bin\$(Configuration)\$(AssemblyName).XML 26 | 27 | 28 | pdbonly 29 | true 30 | true 31 | bin\$(Configuration)\ 32 | TRACE 33 | 3 34 | bin\$(Configuration)\$(AssemblyName).XML 35 | 36 | 37 | 11 38 | 39 | 40 | 41 | 42 | $(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets 43 | 44 | 45 | 46 | 47 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | FSharp.Core 62 | FSharp.Core.dll 63 | $(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\FSharp\.NETFramework\v4.0\$(TargetFSharpCoreVersion)\FSharp.Core.dll 64 | 65 | 66 | 67 | 68 | 69 | ..\packages\System.ValueTuple.4.3.1\lib\netstandard1.0\System.ValueTuple.dll 70 | 71 | 72 | 79 | -------------------------------------------------------------------------------- /fsharp/FunctionalRefactoring/Models.fs: -------------------------------------------------------------------------------- 1 | module FunctionalRefactoring.Models 2 | 3 | type IStorage<'a> = 4 | abstract Flush: 'a -> unit 5 | 6 | type Amount = Amount of decimal 7 | type CartId = CartId of string 8 | type CustomerId = CustomerId of string 9 | 10 | type Cart = { cartId: CartId; customerId: CustomerId; amount: Amount } 11 | 12 | [] 13 | type DiscountRule = DiscountRule of (Cart -> Amount) 14 | 15 | let noDiscount = DiscountRule (fun _ -> invalidOp "no discount") 16 | 17 | let missingCart = { cartId = CartId ""; customerId = CustomerId ""; amount = Amount 0m } -------------------------------------------------------------------------------- /fsharp/FunctionalRefactoring/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /java/.gitignore: -------------------------------------------------------------------------------- 1 | ### Java ### 2 | target/* 3 | 4 | ### Intellij ### 5 | .idea/* 6 | *.iml -------------------------------------------------------------------------------- /java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.refactoring 8 | functional-refactoring-kata 9 | 1.0-SNAPSHOT 10 | 11 | 12 | UTF-8 13 | UTF-8 14 | 15 | 1.8 16 | 1.8 17 | 18 | 1.8 19 | 20 | 21 | 22 | 23 | junit 24 | junit 25 | 4.12 26 | test 27 | 28 | 29 | org.hamcrest 30 | hamcrest-all 31 | 1.3 32 | test 33 | 34 | 35 | -------------------------------------------------------------------------------- /java/src/main/java/org/functionalrefactoring/App.java: -------------------------------------------------------------------------------- 1 | package org.functionalrefactoring; 2 | 3 | import org.functionalrefactoring.models.*; 4 | 5 | import java.math.BigDecimal; 6 | 7 | public class App { 8 | public static void applyDiscount(CartId cartId, Storage storage) { 9 | Cart cart = loadCart(cartId); 10 | if (cart != Cart.MissingCart) { 11 | DiscountRule rule = lookupDiscountRule(cart.customerId); 12 | if (rule != DiscountRule.NoDiscount) { 13 | Amount discount = rule.apply(cart); 14 | Cart updatedCart = updateAmount(cart, discount); 15 | save(updatedCart, storage); 16 | } 17 | } 18 | } 19 | 20 | private static Cart loadCart(CartId id) { 21 | if (id.value.contains("gold")) 22 | return new Cart(id, new CustomerId("gold-customer"), new Amount(new BigDecimal(100))); 23 | if (id.value.contains("normal")) 24 | return new Cart(id, new CustomerId("normal-customer"), new Amount(new BigDecimal(100))); 25 | return Cart.MissingCart; 26 | } 27 | 28 | private static DiscountRule lookupDiscountRule(CustomerId id) { 29 | if (id.value.contains("gold")) return new DiscountRule(App::half); 30 | return DiscountRule.NoDiscount; 31 | } 32 | 33 | private static Cart updateAmount(Cart cart, Amount discount) { 34 | return new Cart(cart.id, cart.customerId, new Amount(cart.amount.value.subtract(discount.value))); 35 | } 36 | 37 | private static void save(Cart cart, Storage storage) { 38 | storage.flush(cart); 39 | } 40 | 41 | private static Amount half(Cart cart) { 42 | return new Amount(cart.amount.value.divide(new BigDecimal(2))); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /java/src/main/java/org/functionalrefactoring/models/Amount.java: -------------------------------------------------------------------------------- 1 | package org.functionalrefactoring.models; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.Objects; 5 | 6 | public class Amount { 7 | public final BigDecimal value; 8 | 9 | public Amount(BigDecimal value) { 10 | this.value = value; 11 | } 12 | 13 | @Override 14 | public boolean equals(Object o) { 15 | if (this == o) return true; 16 | if (o == null || getClass() != o.getClass()) return false; 17 | Amount amount = (Amount) o; 18 | return Objects.equals(value, amount.value); 19 | } 20 | 21 | @Override 22 | public int hashCode() { 23 | return Objects.hash(value); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /java/src/main/java/org/functionalrefactoring/models/Cart.java: -------------------------------------------------------------------------------- 1 | package org.functionalrefactoring.models; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.Objects; 5 | 6 | public class Cart { 7 | public static final Cart MissingCart = new Cart(new CartId(""), new CustomerId(""), new Amount(new BigDecimal(0))); 8 | 9 | public final CartId id; 10 | public final CustomerId customerId; 11 | public final Amount amount; 12 | 13 | public Cart(CartId id, CustomerId customerId, Amount amount) { 14 | this.id = id; 15 | this.customerId = customerId; 16 | this.amount = amount; 17 | } 18 | 19 | @Override 20 | public boolean equals(Object o) { 21 | if (this == o) return true; 22 | if (o == null || getClass() != o.getClass()) return false; 23 | Cart cart = (Cart) o; 24 | return Objects.equals(id, cart.id) && 25 | Objects.equals(customerId, cart.customerId) && 26 | Objects.equals(amount, cart.amount); 27 | } 28 | 29 | @Override 30 | public int hashCode() { 31 | return Objects.hash(id, customerId, amount); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /java/src/main/java/org/functionalrefactoring/models/CartId.java: -------------------------------------------------------------------------------- 1 | package org.functionalrefactoring.models; 2 | 3 | import java.util.Objects; 4 | 5 | public class CartId { 6 | public final String value; 7 | 8 | public CartId(String value) { 9 | this.value = value; 10 | } 11 | 12 | @Override 13 | public boolean equals(Object o) { 14 | if (this == o) return true; 15 | if (o == null || getClass() != o.getClass()) return false; 16 | CartId cartId = (CartId) o; 17 | return Objects.equals(value, cartId.value); 18 | } 19 | 20 | @Override 21 | public int hashCode() { 22 | return Objects.hash(value); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /java/src/main/java/org/functionalrefactoring/models/CustomerId.java: -------------------------------------------------------------------------------- 1 | package org.functionalrefactoring.models; 2 | 3 | import java.util.Objects; 4 | 5 | public class CustomerId { 6 | public final String value; 7 | 8 | public CustomerId(String value) { 9 | this.value = value; 10 | } 11 | 12 | @Override 13 | public boolean equals(Object o) { 14 | if (this == o) return true; 15 | if (o == null || getClass() != o.getClass()) return false; 16 | CustomerId cartId = (CustomerId) o; 17 | return Objects.equals(value, cartId.value); 18 | } 19 | 20 | @Override 21 | public int hashCode() { 22 | return Objects.hash(value); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /java/src/main/java/org/functionalrefactoring/models/DiscountRule.java: -------------------------------------------------------------------------------- 1 | package org.functionalrefactoring.models; 2 | 3 | import java.util.Objects; 4 | import java.util.function.Function; 5 | 6 | public class DiscountRule implements Function { 7 | public static final DiscountRule NoDiscount = new DiscountRule(c -> { 8 | throw new IllegalStateException(); 9 | }); 10 | 11 | private final Function f; 12 | 13 | public DiscountRule(Function f) { 14 | this.f = f; 15 | } 16 | 17 | @Override 18 | public Amount apply(Cart cart) { 19 | return f.apply(cart); 20 | } 21 | 22 | @Override 23 | public boolean equals(Object o) { 24 | if (this == o) return true; 25 | if (o == null || getClass() != o.getClass()) return false; 26 | DiscountRule that = (DiscountRule) o; 27 | return Objects.equals(f, that.f); 28 | } 29 | 30 | @Override 31 | public int hashCode() { 32 | return Objects.hash(f); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /java/src/main/java/org/functionalrefactoring/models/Storage.java: -------------------------------------------------------------------------------- 1 | package org.functionalrefactoring.models; 2 | 3 | public interface Storage { 4 | void flush(T item); 5 | } 6 | -------------------------------------------------------------------------------- /java/src/test/java/org/functionalrefactoring/AppTest.java: -------------------------------------------------------------------------------- 1 | package org.functionalrefactoring; 2 | 3 | import org.functionalrefactoring.models.*; 4 | import org.junit.Test; 5 | 6 | import java.math.BigDecimal; 7 | 8 | import static org.hamcrest.Matchers.is; 9 | import static org.hamcrest.Matchers.nullValue; 10 | import static org.junit.Assert.assertThat; 11 | 12 | public class AppTest { 13 | 14 | @Test 15 | public void HappyPath() 16 | { 17 | CartId cartId = new CartId("some-gold-cart"); 18 | SpyStorage storage = new SpyStorage(); 19 | 20 | App.applyDiscount(cartId, storage); 21 | 22 | Cart expected = new Cart(new CartId("some-gold-cart"), new CustomerId("gold-customer"), new Amount(new BigDecimal(50))); 23 | assertThat(storage.saved, is(expected)); 24 | } 25 | 26 | @Test 27 | public void NoDiscount() 28 | { 29 | CartId cartId = new CartId("some-normal-cart"); 30 | SpyStorage storage = new SpyStorage(); 31 | 32 | App.applyDiscount(cartId, storage); 33 | 34 | assertThat(storage.saved, nullValue()); 35 | } 36 | 37 | @Test 38 | public void MissingCart() 39 | { 40 | CartId cartId = new CartId("missing-cart"); 41 | SpyStorage storage = new SpyStorage(); 42 | 43 | App.applyDiscount(cartId, storage); 44 | 45 | assertThat(storage.saved, nullValue()); 46 | } 47 | 48 | class SpyStorage implements Storage { 49 | public Cart saved; 50 | 51 | @Override 52 | public void flush(Cart item) { 53 | saved = item; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /kotlin/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gradle 3 | build 4 | -------------------------------------------------------------------------------- /kotlin/build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.refactoring' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | ext.kotlin_version = '1.1.61' 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | dependencies { 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | } 13 | } 14 | 15 | apply plugin: 'kotlin' 16 | 17 | repositories { 18 | mavenCentral() 19 | } 20 | 21 | dependencies { 22 | compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" 23 | testCompile 'junit:junit:4.12' 24 | } 25 | 26 | compileKotlin { 27 | kotlinOptions.jvmTarget = "1.8" 28 | } 29 | compileTestKotlin { 30 | kotlinOptions.jvmTarget = "1.8" 31 | } 32 | 33 | -------------------------------------------------------------------------------- /kotlin/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteobaglini/functional-structures-refactoring-kata/3376d397a6b1585d5673a01c7c6ea87de312dce0/kotlin/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /kotlin/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.3.1-bin.zip 6 | -------------------------------------------------------------------------------- /kotlin/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /kotlin/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /kotlin/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'functional-structures-refactoring-kata' 2 | -------------------------------------------------------------------------------- /kotlin/src/main/kotlin/org/functionalrefactoring/App.kt: -------------------------------------------------------------------------------- 1 | package org.functionalrefactoring 2 | 3 | import org.functionalrefactoring.models.* 4 | import java.math.BigDecimal 5 | 6 | object App { 7 | fun applyDiscount(cartId: CartId, storage: Storage) { 8 | val cart = loadCart(cartId) 9 | if (cart !== Cart.MissingCart) { 10 | val rule = lookupDiscountRule(cart.customerId) 11 | if (rule !== DiscountRule.NoDiscount) { 12 | val discount = rule.apply(cart) 13 | val updatedCart = updateAmount(cart, discount) 14 | save(updatedCart, storage) 15 | } 16 | } 17 | } 18 | 19 | private fun loadCart(id: CartId): Cart { 20 | if (id.value.contains("gold")) 21 | return Cart(id, CustomerId("gold-customer"), Amount(BigDecimal(100))) 22 | return if (id.value.contains("normal")) Cart(id, CustomerId("normal-customer"), Amount(BigDecimal(100))) else Cart.MissingCart 23 | } 24 | 25 | private fun lookupDiscountRule(id: CustomerId): DiscountRule { 26 | return if (id.value.contains("gold")) DiscountRule({ cart -> half(cart) }) else DiscountRule.NoDiscount 27 | } 28 | 29 | private fun updateAmount(cart: Cart, discount: Amount): Cart { 30 | return Cart(cart.id, cart.customerId, Amount(cart.amount.value.subtract(discount.value))) 31 | } 32 | 33 | private fun save(cart: Cart, storage: Storage) { 34 | storage.flush(cart) 35 | } 36 | 37 | private fun half(cart: Cart): Amount { 38 | return Amount(cart.amount.value.divide(BigDecimal(2))) 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /kotlin/src/main/kotlin/org/functionalrefactoring/models/Amount.kt: -------------------------------------------------------------------------------- 1 | package org.functionalrefactoring.models 2 | 3 | import java.math.BigDecimal 4 | 5 | data class Amount(val value: BigDecimal = BigDecimal(0)) 6 | 7 | -------------------------------------------------------------------------------- /kotlin/src/main/kotlin/org/functionalrefactoring/models/Cart.kt: -------------------------------------------------------------------------------- 1 | package org.functionalrefactoring.models 2 | 3 | data class Cart(val id: CartId, val customerId: CustomerId, val amount: Amount) { 4 | companion object { 5 | val MissingCart = Cart(CartId(), CustomerId(), Amount()) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /kotlin/src/main/kotlin/org/functionalrefactoring/models/CartId.kt: -------------------------------------------------------------------------------- 1 | package org.functionalrefactoring.models 2 | 3 | data class CartId(val value: String = "") 4 | -------------------------------------------------------------------------------- /kotlin/src/main/kotlin/org/functionalrefactoring/models/CustomerId.kt: -------------------------------------------------------------------------------- 1 | package org.functionalrefactoring.models 2 | 3 | data class CustomerId(val value: String = "") 4 | -------------------------------------------------------------------------------- /kotlin/src/main/kotlin/org/functionalrefactoring/models/DiscountRule.kt: -------------------------------------------------------------------------------- 1 | package org.functionalrefactoring.models 2 | 3 | data class DiscountRule(private val f: (Cart) -> (Amount)) { 4 | 5 | fun apply(cart: Cart): Amount { 6 | return f(cart) 7 | } 8 | 9 | companion object { 10 | val NoDiscount = DiscountRule({ _ -> throw IllegalStateException() }) 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /kotlin/src/main/kotlin/org/functionalrefactoring/models/Storage.kt: -------------------------------------------------------------------------------- 1 | package org.functionalrefactoring.models 2 | 3 | interface Storage { 4 | fun flush(item: T) 5 | } 6 | 7 | -------------------------------------------------------------------------------- /kotlin/src/test/kotlin/org/functionalrefactoring/AppTest.kt: -------------------------------------------------------------------------------- 1 | package org.functionalrefactoring 2 | 3 | import org.functionalrefactoring.models.* 4 | import org.hamcrest.CoreMatchers.`is` 5 | import org.hamcrest.CoreMatchers.nullValue 6 | import org.junit.Assert.assertThat 7 | import org.junit.Test 8 | import java.math.BigDecimal 9 | 10 | class AppTest { 11 | 12 | @Test 13 | fun happyPath() { 14 | val cartId = CartId("some-gold-cart") 15 | val storage = SpyStorage() 16 | 17 | App.applyDiscount(cartId, storage) 18 | 19 | val expected = Cart(CartId("some-gold-cart"), CustomerId("gold-customer"), Amount(BigDecimal(50))) 20 | assertThat(storage.saved, `is`(expected)) 21 | } 22 | 23 | @Test 24 | fun noDiscount() { 25 | val cartId = CartId("some-normal-cart") 26 | val storage = SpyStorage() 27 | 28 | App.applyDiscount(cartId, storage) 29 | 30 | assertThat(storage.saved, nullValue()) 31 | } 32 | 33 | @Test 34 | fun missingCart() { 35 | val cartId = CartId("missing-cart") 36 | val storage = SpyStorage() 37 | 38 | App.applyDiscount(cartId, storage) 39 | 40 | assertThat(storage.saved, nullValue()) 41 | } 42 | 43 | internal inner class SpyStorage : Storage { 44 | var saved: Cart? = null 45 | 46 | override fun flush(item: Cart) { 47 | saved = item 48 | } 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /ruby/.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock -------------------------------------------------------------------------------- /ruby/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "minitest" -------------------------------------------------------------------------------- /ruby/Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | 3 | Rake::TestTask.new do |t| 4 | t.pattern = "test/*_test.rb" 5 | end -------------------------------------------------------------------------------- /ruby/lib/app.rb: -------------------------------------------------------------------------------- 1 | class App 2 | def apply_discount(cartId, storage) 3 | cart = load_cart(cartId) 4 | if (cart != Cart.missing_cart) 5 | rule = lookup_discount_rule(cart.customerId) 6 | if (rule != DiscountRule.no_discount) 7 | discount = rule.call(cart) 8 | updated_cart = update_amount(cart, discount) 9 | save(updated_cart, storage) 10 | end 11 | end 12 | end 13 | 14 | private 15 | 16 | def load_cart(id) 17 | if(id.value.include?("gold")) 18 | Cart.new(id, CustomerId.new("gold-customer"), Amount.new(100)) 19 | elsif(id.value.include?("normal")) 20 | Cart.new(id, CustomerId.new("normal-customer"), Amount.new(100)) 21 | else 22 | Cart.missing_cart 23 | end 24 | end 25 | 26 | def lookup_discount_rule(customerId) 27 | if (customerId.value.include?("gold")) 28 | DiscountRule.new(&method(:half)) 29 | else 30 | DiscountRule.no_discount 31 | end 32 | end 33 | 34 | def half(cart) 35 | Amount.new(cart.amount.value / 2) 36 | end 37 | 38 | def update_amount(cart, discount) 39 | Cart.new(cart.id, cart.customerId, Amount.new(cart.amount.value - discount.value)) 40 | end 41 | 42 | def save(cart, storage) 43 | storage.flush(cart) 44 | end 45 | end -------------------------------------------------------------------------------- /ruby/lib/models.rb: -------------------------------------------------------------------------------- 1 | 2 | class Amount 3 | attr_reader :value 4 | 5 | def initialize(value) 6 | @value = value 7 | end 8 | 9 | def ==(o) 10 | o.class == self.class && o.value == value 11 | end 12 | end 13 | 14 | class CustomerId 15 | attr_reader :value 16 | 17 | def initialize(value) 18 | @value = value 19 | end 20 | 21 | def ==(o) 22 | o.class == self.class && o.value == value 23 | end 24 | end 25 | 26 | class CartId 27 | attr_reader :value 28 | 29 | def initialize(value) 30 | @value = value 31 | end 32 | 33 | def ==(o) 34 | o.class == self.class && o.value == value 35 | end 36 | end 37 | 38 | class Cart 39 | attr_reader :id 40 | attr_reader :customerId 41 | attr_reader :amount 42 | 43 | def self.missing_cart 44 | @@missing_cart ||= Cart.new(CartId.new(""), CustomerId.new(""), Amount.new(0)) 45 | end 46 | 47 | def initialize(id, customerId, amount) 48 | @id = id 49 | @customerId = customerId 50 | @amount = amount 51 | end 52 | 53 | def ==(o) 54 | o.class == self.class && o.state == state 55 | end 56 | 57 | protected 58 | 59 | def state 60 | [@id, @customerId, @amount] 61 | end 62 | end 63 | 64 | class DiscountRule < Proc 65 | def self.no_discount 66 | @@no_discount ||= DiscountRule.new { raise "no discount" } 67 | end 68 | end -------------------------------------------------------------------------------- /ruby/test/app_test.rb: -------------------------------------------------------------------------------- 1 | require "minitest/autorun" 2 | require "app" 3 | 4 | class ModelsTest < Minitest::Test 5 | def test_happy_path 6 | cartId = CartId.new("some-gold-cart") 7 | storage = SpyStorage.new 8 | 9 | App.new.apply_discount(cartId, storage) 10 | 11 | expected = Cart.new(CartId.new("some-gold-cart"), CustomerId.new("gold-customer"), Amount.new(50)) 12 | assert_equal expected, storage.saved 13 | end 14 | 15 | def test_missing_cart 16 | cartId = CartId.new("missing-cart") 17 | storage = SpyStorage.new 18 | 19 | App.new.apply_discount(cartId, storage) 20 | 21 | assert_nil storage.saved 22 | end 23 | 24 | def test_no_discount 25 | cartId = CartId.new("some-normal-cart") 26 | storage = SpyStorage.new 27 | 28 | App.new.apply_discount(cartId, storage) 29 | 30 | assert_nil storage.saved 31 | end 32 | 33 | class SpyStorage 34 | attr_reader :saved 35 | 36 | def flush(cart) 37 | @saved = cart 38 | end 39 | end 40 | end -------------------------------------------------------------------------------- /scala/.gitignore: -------------------------------------------------------------------------------- 1 | ### Scala ### 2 | target/* 3 | project/project/* 4 | project/target/* 5 | 6 | ### Intellij ### 7 | .idea/* -------------------------------------------------------------------------------- /scala/build.sbt: -------------------------------------------------------------------------------- 1 | name := "functional-structures-refactoring-kata" 2 | version := "1.0" 3 | scalaVersion := "2.12.2" 4 | libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.1" % "test" -------------------------------------------------------------------------------- /scala/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 0.13.15 -------------------------------------------------------------------------------- /scala/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | logLevel := Level.Warn -------------------------------------------------------------------------------- /scala/src/main/scala/org/fprefactoring/App.scala: -------------------------------------------------------------------------------- 1 | package org.fprefactoring 2 | 3 | import org.fprefactoring.Models._ 4 | 5 | object App { 6 | 7 | def applyDiscount(cartId: CartId, storage: Storage[Cart]): Unit = { 8 | val cart = loadCart(cartId) 9 | if (cart != Cart.missingCart) { 10 | val rule = lookupDiscountRule(cart.customerId) 11 | if (rule != DiscountRule.noDiscount) { 12 | val discount = rule(cart) 13 | val updatedCart = updateAmount(cart, discount) 14 | save(updatedCart, storage) 15 | } 16 | } 17 | } 18 | 19 | def loadCart(id: CartId): Cart = 20 | if (id.value.contains("gold")) Cart(id, CustomerId("gold-customer"), Amount(100)) 21 | else if (id.value.contains("normal")) Cart(id, CustomerId("normal-customer"), Amount(100)) 22 | else Cart.missingCart 23 | 24 | def lookupDiscountRule(id: CustomerId): DiscountRule = 25 | if (id.value.contains("gold")) DiscountRule(half) 26 | else DiscountRule.noDiscount 27 | 28 | def half(cart: Cart): Amount = 29 | Amount(cart.amount.value / 2) 30 | 31 | def updateAmount(cart: Cart, discount: Amount): Cart = 32 | cart.copy(id = cart.id, customerId = cart.customerId, amount = Amount(cart.amount.value - discount.value)) 33 | 34 | def save(cart: Cart, storage: Storage[Cart]): Unit = 35 | storage.flush(cart) 36 | } 37 | -------------------------------------------------------------------------------- /scala/src/main/scala/org/fprefactoring/Models.scala: -------------------------------------------------------------------------------- 1 | package org.fprefactoring 2 | 3 | object Models { 4 | case class Amount(value: BigDecimal) 5 | 6 | case class CustomerId(value: String) 7 | case class CartId(value: String) 8 | case class Cart(id: CartId, customerId: CustomerId, amount: Amount) 9 | object Cart { 10 | val missingCart = new Cart(CartId(""), CustomerId(""), Amount(0)) 11 | } 12 | 13 | case class DiscountRule(f: Cart => Amount) extends (Cart => Amount) { 14 | def apply(c: Cart): Amount = f(c) 15 | } 16 | object DiscountRule { 17 | val noDiscount = DiscountRule(_ => throw new RuntimeException("no discount")) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /scala/src/main/scala/org/fprefactoring/Storage.scala: -------------------------------------------------------------------------------- 1 | package org.fprefactoring 2 | 3 | trait Storage[A] { 4 | def flush(a: A): Unit 5 | } 6 | -------------------------------------------------------------------------------- /scala/src/test/scala/org/fprefactoring/AppTests.scala: -------------------------------------------------------------------------------- 1 | package org.fprefactoring 2 | 3 | import org.fprefactoring.App._ 4 | import org.fprefactoring.Models._ 5 | import org.scalatest.FunSuite 6 | 7 | class AppTests extends FunSuite { 8 | 9 | test("happy path") { 10 | val cartId = CartId("some-gold-cart") 11 | val storage = new SpyStorage 12 | 13 | applyDiscount(cartId, storage) 14 | 15 | val expected = Cart(CartId("some-gold-cart"), CustomerId("gold-customer"), Amount(50)) 16 | assert(storage.saved.get == expected) 17 | } 18 | 19 | test("no discount") { 20 | val cartId = CartId("some-normal-cart") 21 | val storage = new SpyStorage 22 | 23 | applyDiscount(cartId, storage) 24 | 25 | assert(storage.saved.isEmpty) 26 | } 27 | 28 | test("missing cart") { 29 | val cartId = CartId("missing-cart") 30 | val storage = new SpyStorage 31 | 32 | applyDiscount(cartId, storage) 33 | 34 | assert(storage.saved.isEmpty) 35 | } 36 | 37 | class SpyStorage extends Storage[Cart] { 38 | var saved: Option[Cart] = None 39 | override def flush(value: Cart): Unit = saved = Some(value) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /typescript/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,ts,json}] 2 | charset = utf-8 3 | indent_style = space 4 | indent_size = 2 -------------------------------------------------------------------------------- /typescript/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /typescript/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Mocha Tests", 11 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 12 | "args": [ 13 | "-u", 14 | "tdd", 15 | "--timeout", 16 | "999999", 17 | "--colors", 18 | "--require", 19 | "ts-node/register", 20 | "${workspaceFolder}/test/**/*.ts" 21 | ], 22 | "internalConsoleOptions": "openOnSessionStart" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /typescript/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functional-structures-refactoring-kata", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/chai": { 8 | "version": "4.0.6", 9 | "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.0.6.tgz", 10 | "integrity": "sha512-IzRWv/7IpaMm41KLLJcaaD/UKit/MrHu4rWs61oWiVjuk4aKWe2eopx3XyhAHhSnMyB5EeCMRr2AsJtuQ8COWA==", 11 | "dev": true 12 | }, 13 | "@types/lodash": { 14 | "version": "4.14.86", 15 | "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.86.tgz", 16 | "integrity": "sha512-DIiC7xZkI+iqwb6A28+JDfrioxcFRHAUXl+AEZ9lULQppiArWRfex4ugVUAJKZHxcgqZJ1w2de2DTahVyrEp4Q==" 17 | }, 18 | "@types/mocha": { 19 | "version": "2.2.44", 20 | "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.44.tgz", 21 | "integrity": "sha512-k2tWTQU8G4+iSMvqKi0Q9IIsWAp/n8xzdZS4Q4YVIltApoMA00wFBFdlJnmoaK1/z7B0Cy0yPe6GgXteSmdUNw==", 22 | "dev": true 23 | }, 24 | "ansi-styles": { 25 | "version": "3.2.0", 26 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", 27 | "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", 28 | "dev": true, 29 | "requires": { 30 | "color-convert": "1.9.1" 31 | } 32 | }, 33 | "arrify": { 34 | "version": "1.0.1", 35 | "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", 36 | "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", 37 | "dev": true 38 | }, 39 | "assertion-error": { 40 | "version": "1.0.2", 41 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", 42 | "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", 43 | "dev": true 44 | }, 45 | "balanced-match": { 46 | "version": "1.0.0", 47 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 48 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 49 | "dev": true 50 | }, 51 | "brace-expansion": { 52 | "version": "1.1.8", 53 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", 54 | "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", 55 | "dev": true, 56 | "requires": { 57 | "balanced-match": "1.0.0", 58 | "concat-map": "0.0.1" 59 | } 60 | }, 61 | "browser-stdout": { 62 | "version": "1.3.0", 63 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", 64 | "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", 65 | "dev": true 66 | }, 67 | "chai": { 68 | "version": "4.1.2", 69 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", 70 | "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", 71 | "dev": true, 72 | "requires": { 73 | "assertion-error": "1.0.2", 74 | "check-error": "1.0.2", 75 | "deep-eql": "3.0.1", 76 | "get-func-name": "2.0.0", 77 | "pathval": "1.1.0", 78 | "type-detect": "4.0.5" 79 | } 80 | }, 81 | "chalk": { 82 | "version": "2.3.0", 83 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", 84 | "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", 85 | "dev": true, 86 | "requires": { 87 | "ansi-styles": "3.2.0", 88 | "escape-string-regexp": "1.0.5", 89 | "supports-color": "4.4.0" 90 | } 91 | }, 92 | "check-error": { 93 | "version": "1.0.2", 94 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", 95 | "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", 96 | "dev": true 97 | }, 98 | "color-convert": { 99 | "version": "1.9.1", 100 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", 101 | "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", 102 | "dev": true, 103 | "requires": { 104 | "color-name": "1.1.3" 105 | } 106 | }, 107 | "color-name": { 108 | "version": "1.1.3", 109 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 110 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 111 | "dev": true 112 | }, 113 | "commander": { 114 | "version": "2.11.0", 115 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", 116 | "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", 117 | "dev": true 118 | }, 119 | "concat-map": { 120 | "version": "0.0.1", 121 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 122 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 123 | "dev": true 124 | }, 125 | "debug": { 126 | "version": "3.1.0", 127 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 128 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 129 | "dev": true, 130 | "requires": { 131 | "ms": "2.0.0" 132 | } 133 | }, 134 | "deep-eql": { 135 | "version": "3.0.1", 136 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", 137 | "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", 138 | "dev": true, 139 | "requires": { 140 | "type-detect": "4.0.5" 141 | } 142 | }, 143 | "diff": { 144 | "version": "3.3.1", 145 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", 146 | "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==", 147 | "dev": true 148 | }, 149 | "escape-string-regexp": { 150 | "version": "1.0.5", 151 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 152 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 153 | "dev": true 154 | }, 155 | "fs.realpath": { 156 | "version": "1.0.0", 157 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 158 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 159 | "dev": true 160 | }, 161 | "get-func-name": { 162 | "version": "2.0.0", 163 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", 164 | "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", 165 | "dev": true 166 | }, 167 | "glob": { 168 | "version": "7.1.2", 169 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 170 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 171 | "dev": true, 172 | "requires": { 173 | "fs.realpath": "1.0.0", 174 | "inflight": "1.0.6", 175 | "inherits": "2.0.3", 176 | "minimatch": "3.0.4", 177 | "once": "1.4.0", 178 | "path-is-absolute": "1.0.1" 179 | } 180 | }, 181 | "growl": { 182 | "version": "1.10.3", 183 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", 184 | "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", 185 | "dev": true 186 | }, 187 | "has-flag": { 188 | "version": "2.0.0", 189 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", 190 | "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", 191 | "dev": true 192 | }, 193 | "he": { 194 | "version": "1.1.1", 195 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", 196 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", 197 | "dev": true 198 | }, 199 | "homedir-polyfill": { 200 | "version": "1.0.1", 201 | "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", 202 | "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", 203 | "dev": true, 204 | "requires": { 205 | "parse-passwd": "1.0.0" 206 | } 207 | }, 208 | "inflight": { 209 | "version": "1.0.6", 210 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 211 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 212 | "dev": true, 213 | "requires": { 214 | "once": "1.4.0", 215 | "wrappy": "1.0.2" 216 | } 217 | }, 218 | "inherits": { 219 | "version": "2.0.3", 220 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 221 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 222 | "dev": true 223 | }, 224 | "lodash": { 225 | "version": "4.17.4", 226 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", 227 | "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" 228 | }, 229 | "make-error": { 230 | "version": "1.3.0", 231 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.0.tgz", 232 | "integrity": "sha1-Uq06M5zPEM5itAQLcI/nByRLi5Y=", 233 | "dev": true 234 | }, 235 | "minimatch": { 236 | "version": "3.0.4", 237 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 238 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 239 | "dev": true, 240 | "requires": { 241 | "brace-expansion": "1.1.8" 242 | } 243 | }, 244 | "minimist": { 245 | "version": "1.2.0", 246 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 247 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", 248 | "dev": true 249 | }, 250 | "mkdirp": { 251 | "version": "0.5.1", 252 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 253 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 254 | "dev": true, 255 | "requires": { 256 | "minimist": "0.0.8" 257 | }, 258 | "dependencies": { 259 | "minimist": { 260 | "version": "0.0.8", 261 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 262 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 263 | "dev": true 264 | } 265 | } 266 | }, 267 | "mocha": { 268 | "version": "4.0.1", 269 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.0.1.tgz", 270 | "integrity": "sha512-evDmhkoA+cBNiQQQdSKZa2b9+W2mpLoj50367lhy+Klnx9OV8XlCIhigUnn1gaTFLQCa0kdNhEGDr0hCXOQFDw==", 271 | "dev": true, 272 | "requires": { 273 | "browser-stdout": "1.3.0", 274 | "commander": "2.11.0", 275 | "debug": "3.1.0", 276 | "diff": "3.3.1", 277 | "escape-string-regexp": "1.0.5", 278 | "glob": "7.1.2", 279 | "growl": "1.10.3", 280 | "he": "1.1.1", 281 | "mkdirp": "0.5.1", 282 | "supports-color": "4.4.0" 283 | } 284 | }, 285 | "ms": { 286 | "version": "2.0.0", 287 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 288 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 289 | "dev": true 290 | }, 291 | "once": { 292 | "version": "1.4.0", 293 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 294 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 295 | "dev": true, 296 | "requires": { 297 | "wrappy": "1.0.2" 298 | } 299 | }, 300 | "parse-passwd": { 301 | "version": "1.0.0", 302 | "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", 303 | "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", 304 | "dev": true 305 | }, 306 | "path-is-absolute": { 307 | "version": "1.0.1", 308 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 309 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 310 | "dev": true 311 | }, 312 | "pathval": { 313 | "version": "1.1.0", 314 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", 315 | "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", 316 | "dev": true 317 | }, 318 | "source-map": { 319 | "version": "0.5.7", 320 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", 321 | "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", 322 | "dev": true 323 | }, 324 | "source-map-support": { 325 | "version": "0.4.18", 326 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", 327 | "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", 328 | "dev": true, 329 | "requires": { 330 | "source-map": "0.5.7" 331 | } 332 | }, 333 | "strip-bom": { 334 | "version": "3.0.0", 335 | "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", 336 | "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", 337 | "dev": true 338 | }, 339 | "strip-json-comments": { 340 | "version": "2.0.1", 341 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 342 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", 343 | "dev": true 344 | }, 345 | "supports-color": { 346 | "version": "4.4.0", 347 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", 348 | "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", 349 | "dev": true, 350 | "requires": { 351 | "has-flag": "2.0.0" 352 | } 353 | }, 354 | "ts-node": { 355 | "version": "3.3.0", 356 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-3.3.0.tgz", 357 | "integrity": "sha1-wTxqMCTjC+EYDdUwOPwgkonUv2k=", 358 | "dev": true, 359 | "requires": { 360 | "arrify": "1.0.1", 361 | "chalk": "2.3.0", 362 | "diff": "3.3.1", 363 | "make-error": "1.3.0", 364 | "minimist": "1.2.0", 365 | "mkdirp": "0.5.1", 366 | "source-map-support": "0.4.18", 367 | "tsconfig": "6.0.0", 368 | "v8flags": "3.0.1", 369 | "yn": "2.0.0" 370 | } 371 | }, 372 | "tsconfig": { 373 | "version": "6.0.0", 374 | "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-6.0.0.tgz", 375 | "integrity": "sha1-aw6DdgA9evGGT434+J3QBZ/80DI=", 376 | "dev": true, 377 | "requires": { 378 | "strip-bom": "3.0.0", 379 | "strip-json-comments": "2.0.1" 380 | } 381 | }, 382 | "type-detect": { 383 | "version": "4.0.5", 384 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.5.tgz", 385 | "integrity": "sha512-N9IvkQslUGYGC24RkJk1ba99foK6TkwC2FHAEBlQFBP0RxQZS8ZpJuAZcwiY/w9ZJHFQb1aOXBI60OdxhTrwEQ==", 386 | "dev": true 387 | }, 388 | "typescript": { 389 | "version": "2.6.2", 390 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.2.tgz", 391 | "integrity": "sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q=", 392 | "dev": true 393 | }, 394 | "v8flags": { 395 | "version": "3.0.1", 396 | "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.0.1.tgz", 397 | "integrity": "sha1-3Oj8N5wX2fLJ6e142JzgAFKxt2s=", 398 | "dev": true, 399 | "requires": { 400 | "homedir-polyfill": "1.0.1" 401 | } 402 | }, 403 | "wrappy": { 404 | "version": "1.0.2", 405 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 406 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 407 | "dev": true 408 | }, 409 | "yn": { 410 | "version": "2.0.0", 411 | "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", 412 | "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", 413 | "dev": true 414 | } 415 | } 416 | } 417 | -------------------------------------------------------------------------------- /typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functional-structures-refactoring-kata", 3 | "version": "1.0.0", 4 | "description": "This refactoring kata aims to let you practice with one of the main functional programming building block: Functional Structures.", 5 | "scripts": { 6 | "test": "mocha --require ts-node/register test/**/*.ts", 7 | "test:auto": "npm test -- --watch-extensions ts,tsx --watch" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/matteobaglini/functional-structures-refactoring-kata.git" 12 | }, 13 | "keywords": [ 14 | "refactoring", 15 | "functional", 16 | "kata" 17 | ], 18 | "author": "Claudio Bartoli ", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/matteobaglini/functional-structures-refactoring-kata/issues" 22 | }, 23 | "homepage": "https://github.com/matteobaglini/functional-structures-refactoring-kata#readme", 24 | "devDependencies": { 25 | "@types/lodash": "^4.14.86", 26 | "@types/chai": "^4.0.6", 27 | "@types/mocha": "^2.2.44", 28 | "chai": "^4.1.2", 29 | "mocha": "^4.0.1", 30 | "ts-node": "^3.3.0", 31 | "typescript": "^2.6.2" 32 | }, 33 | "dependencies": { 34 | "lodash": "^4.17.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /typescript/src/app.ts: -------------------------------------------------------------------------------- 1 | import { isEqual } from 'lodash' 2 | 3 | import { Storage } from './storage' 4 | import { CartId, MissingCart, NoDiscount, Cart, CustomerId, DiscountRule, Amount } from './models' 5 | 6 | export const applyDiscount = (cartId: CartId, storage: Storage): void => { 7 | 8 | const cart = loadCart(cartId) 9 | 10 | if (!isEqual(cart, MissingCart)) { 11 | const rule = lookupDiscountRule(cart.customerId) 12 | if (!isEqual(rule, NoDiscount)) { 13 | const discount = rule(cart) 14 | const updatedCart = updateAmount(cart, discount) 15 | save(updatedCart, storage) 16 | } 17 | } 18 | } 19 | 20 | const loadCart = (id: CartId): Cart => { 21 | if (id.includes('gold')) 22 | return { 23 | id: id, 24 | customerId: 'gold-customer', 25 | amount: 100 26 | } 27 | if (id.includes('normal')) 28 | return { 29 | id: id, 30 | customerId: 'normal-customer', 31 | amount: 100 32 | } 33 | return MissingCart 34 | } 35 | 36 | const lookupDiscountRule = (id: CustomerId): DiscountRule => { 37 | if (id.includes('gold')) return half 38 | return NoDiscount 39 | } 40 | 41 | const updateAmount = (cart: Cart, discount: Amount): Cart => { 42 | return { 43 | ...cart, 44 | amount: cart.amount - discount 45 | } 46 | } 47 | 48 | const save = (cart: Cart, storage: Storage) => { 49 | storage.flush(cart) 50 | } 51 | 52 | const half = (cart: Cart): Amount => { 53 | return cart.amount / 2.0 54 | } 55 | -------------------------------------------------------------------------------- /typescript/src/models.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export type Amount = number 4 | 5 | export type CartId = string 6 | 7 | export type CustomerId = string 8 | 9 | export type DiscountRule = (Cart) => Amount 10 | 11 | export interface Cart { 12 | id: CartId, 13 | customerId: CustomerId, 14 | amount: Amount 15 | } 16 | 17 | export const MissingCart: Cart = { 18 | id: "", 19 | customerId: "", 20 | amount: 0 21 | } 22 | 23 | export const NoDiscount: DiscountRule = (c: Cart): Amount => { 24 | throw 'IllegalStateException' 25 | } 26 | -------------------------------------------------------------------------------- /typescript/src/storage.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Storage { 3 | flush(item: T): void 4 | } 5 | -------------------------------------------------------------------------------- /typescript/test/stub/storage.ts: -------------------------------------------------------------------------------- 1 | import { Storage } from '../../src/storage' 2 | import { Cart } from '../../src/models' 3 | 4 | export class SpyStorage implements Storage{ 5 | 6 | public saved: Cart 7 | 8 | flush(item: Cart): void { 9 | this.saved = item 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /typescript/test/test.ts: -------------------------------------------------------------------------------- 1 | import 'mocha' 2 | import { assert } from 'chai' 3 | 4 | import { CartId, Cart } from '../src/models' 5 | import { applyDiscount } from '../src/app' 6 | import { SpyStorage } from './stub/storage' 7 | 8 | describe('Functional Refactoring', () => { 9 | 10 | it('Happy Path', () => { 11 | const cartId: CartId = 'some-gold-cart' 12 | const storage = new SpyStorage() 13 | 14 | applyDiscount(cartId, storage) 15 | 16 | const expected: Cart = { 17 | id: 'some-gold-cart', 18 | customerId: 'gold-customer', 19 | amount: 50 20 | } 21 | 22 | assert.deepEqual(storage.saved, expected) 23 | 24 | }) 25 | 26 | 27 | it('No discount', () => { 28 | const cartId: CartId = 'some-normal-cart' 29 | const storage = new SpyStorage() 30 | 31 | applyDiscount(cartId, storage) 32 | 33 | assert.isUndefined(storage.saved) 34 | 35 | }) 36 | 37 | it('Missing Cart', () => { 38 | const cartId: CartId = 'missing-cart' 39 | const storage = new SpyStorage() 40 | 41 | applyDiscount(cartId, storage) 42 | 43 | assert.isUndefined(storage.saved) 44 | 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "types": [ 5 | "mocha" 6 | ] 7 | } 8 | } --------------------------------------------------------------------------------